Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9db37ce
feat: add PerlRuntime with ThreadLocal isolation (multiplicity phases…
fglock Apr 10, 2026
e647098
feat: migrate InheritanceResolver caches to PerlRuntime (multiplicity…
fglock Apr 10, 2026
23a5bf0
feat: migrate GlobalVariable symbol tables to PerlRuntime (multiplici…
fglock Apr 10, 2026
6ebcd79
docs: update concurrency design doc with multiplicity progress tracking
fglock Apr 10, 2026
1e1cd6a
feat: migrate regex match state to PerlRuntime (multiplicity phase 4 …
fglock Apr 10, 2026
fcde615
feat: migrate RuntimeCode caches and eval state to PerlRuntime (multi…
fglock Apr 10, 2026
7650f18
feat: add multiplicity demo - 3 independent Perl interpreters in one JVM
fglock Apr 10, 2026
404b685
docs: add git commit -F workaround for single quotes in AGENTS.md
fglock Apr 10, 2026
4826d78
docs: add multiplicity demo to concurrency design doc
fglock Apr 10, 2026
fd8b0ca
docs: add Phase 0 compilation thread safety plan to concurrency.md
fglock Apr 10, 2026
c3915cf
feat: add compilation thread safety (multiplicity Phase 0)
fglock Apr 10, 2026
317ea15
docs: mark Phase 0 compilation thread safety as complete
fglock Apr 10, 2026
7bba136
feat: add compile lock to RuntimeCode eval paths (Phase 0 completion)
fglock Apr 10, 2026
510106c
fix: use boolean flag instead of isHeldByCurrentThread() in evalStrin…
fglock Apr 10, 2026
9c00815
docs: add reentrancy analysis and boolean flag fix to concurrency des…
fglock Apr 10, 2026
7f2e3db
fix: per-runtime globalInitialized + COMPILE_LOCK in executePerlCode
fglock Apr 10, 2026
e2f16ec
fix(multiplicity): migrate 16 shared static local-save/restore stacks…
fglock Apr 10, 2026
4c6b5c8
docs: update concurrency design doc with local stack fix and remainin…
fglock Apr 10, 2026
c30eeb4
fix(multiplicity): per-runtime CWD isolation
fglock Apr 10, 2026
0179c88
fix(multiplicity): per-runtime PID for $$ and pipe thread runtime bin…
fglock Apr 10, 2026
ce84472
docs: update concurrency design doc with CWD isolation and PID/pipe f…
fglock Apr 10, 2026
5b68145
docs: move multiplicity demo to dev/sandbox/multiplicity/
fglock Apr 10, 2026
97b6b96
docs: clarify Runtime Pool as optimization in concurrency next steps
fglock Apr 10, 2026
94a5323
docs: add performance baseline and optimization plan for multiplicity
fglock Apr 10, 2026
4b581bf
docs: expand optimization plan with test methodology and revert criteria
fglock Apr 10, 2026
25bbed0
docs: add git workflow and failed-attempts log to optimization plan
fglock Apr 10, 2026
67dce07
docs: add git push to failure path so findings are preserved
fglock Apr 10, 2026
886e749
perf: cache PerlRuntime.current() in local variables (Tier 1)
fglock Apr 10, 2026
d070812
perf: migrate ThreadLocal stacks to PerlRuntime instance fields (Tier 2)
fglock Apr 10, 2026
4a3b072
perf: batch push/pop caller state to reduce ThreadLocal lookups
fglock Apr 10, 2026
c33d7c8
perf: batch RegexState save/restore to single PerlRuntime.current() call
fglock Apr 10, 2026
97a4762
docs: add JFR profiling results and optimization summary to concurren…
fglock Apr 10, 2026
b84ee49
perf: skip RegexState save/restore for subroutines without regex ops
fglock Apr 10, 2026
c445676
docs: update concurrency.md with final optimization results
fglock Apr 10, 2026
85dafca
feat: migrate remaining shared static state to per-PerlRuntime
fglock Apr 10, 2026
d4ddb70
perf: JVM-compile anonymous subs inside eval STRING
fglock Apr 10, 2026
d7b8bc7
Merge pull request #486 from fglock/feature/multiplicity-opt
fglock Apr 11, 2026
148b54e
Merge remote-tracking branch 'origin/master' into feature/multiplicity
fglock Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ The perl_test_runner.pl sets these automatically based on the test file being ru

### Commits

- **Single quotes in commit messages:** The `$(cat <<'EOF' ... EOF)` heredoc pattern breaks when the message body contains single quotes (e.g., Perl's `$_`, `don't`). Write the message to a temp file and use `git commit -F` instead:
```bash
cat > /tmp/commit_msg.txt << 'ENDMSG'
feat: description here

Body with single quotes like $_ and don't is fine.

Generated with [TOOL_NAME](TOOL_DOCS_URL)

Co-Authored-By: TOOL_NAME <TOOL_BOT_EMAIL>
ENDMSG
git commit -F /tmp/commit_msg.txt
```
Replace `TOOL_NAME`, `TOOL_DOCS_URL`, and `TOOL_BOT_EMAIL` as described in [AI_POLICY.md](AI_POLICY.md).
- Reference the design doc or issue in commit messages when relevant
- Use conventional commit format when possible
- **Write commit messages to a file** to avoid shell quoting issues (apostrophes, backticks, special characters). Use `git commit -F /tmp/commit_msg.txt` instead of `-m`:
Expand Down
4 changes: 2 additions & 2 deletions dev/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ When adding new design documents:

For the most important architectural decisions and current work, check:

- **multiplicity.md** - Multiple independent Perl runtimes (enables fork/threads/web concurrency)
- **concurrency.md** - Unified concurrency design (supersedes multiplicity.md, fork.md, threads.md). Includes multiplicity demo and progress tracking.
- **jsr223-perlonjava-web.md** - JSR-223 compliance and web server integration
- **fork.md** / **threads.md** - Concurrency model and limitations
- **multiplicity.md** / **fork.md** / **threads.md** - Superseded by concurrency.md

These represent major architectural directions for the project.

Expand Down
680 changes: 680 additions & 0 deletions dev/design/concurrency.md

Large diffs are not rendered by default.

136 changes: 136 additions & 0 deletions dev/sandbox/multiplicity/MultiplicityDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.perlonjava.demo;

import org.perlonjava.app.cli.CompilerOptions;
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import org.perlonjava.runtime.io.StandardIO;
import org.perlonjava.runtime.runtimetypes.*;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
* Demonstrates PerlOnJava's "multiplicity" feature: multiple independent Perl
* interpreters running concurrently within a single JVM process.
*
* Each interpreter has its own global variables, regex state, @INC, %ENV, etc.
* They share the JVM heap; generated classes are loaded into each runtime's own
* ClassLoader and become eligible for GC once the runtime is discarded.
*
* Usage:
* java -cp target/perlonjava-5.42.0.jar \
* org.perlonjava.demo.MultiplicityDemo script1.pl script2.pl ...
*
* Or with the helper script:
* ./dev/sandbox/multiplicity/run_multiplicity_demo.sh script1.pl script2.pl ...
*
* See also: dev/design/concurrency.md (Multiplicity Demo section)
*/
public class MultiplicityDemo {

public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.err.println("Usage: MultiplicityDemo <script1.pl> [script2.pl] ...");
System.err.println("Runs each Perl script in its own interpreter, concurrently.");
System.exit(1);
}

// Read all script files upfront
List<String> scriptNames = new ArrayList<>();
List<String> scriptSources = new ArrayList<>();
for (String arg : args) {
Path p = Path.of(arg);
if (!Files.isRegularFile(p)) {
System.err.println("Error: not a file: " + arg);
System.exit(1);
}
scriptNames.add(p.getFileName().toString());
scriptSources.add(Files.readString(p));
}

int n = scriptNames.size();
System.out.println("=== PerlOnJava Multiplicity Demo ===");
System.out.println("Starting " + n + " independent Perl interpreter(s)...\n");

// Per-thread output capture
ByteArrayOutputStream[] outputs = new ByteArrayOutputStream[n];
long[] durations = new long[n];
Throwable[] errors = new Throwable[n];

Thread[] threads = new Thread[n];
for (int i = 0; i < n; i++) {
final int idx = i;
final String name = scriptNames.get(i);
final String source = scriptSources.get(i);
outputs[idx] = new ByteArrayOutputStream();

threads[i] = new Thread(() -> {
try {
// --- Create an independent PerlRuntime for this thread ---
PerlRuntime.initialize();

// Redirect this interpreter's STDOUT to a private buffer.
// RuntimeIO.setStdout() operates on the current thread's PerlRuntime.
PrintStream ps = new PrintStream(outputs[idx], true);
RuntimeIO customOut = new RuntimeIO(new StandardIO(ps, true));
RuntimeIO.setStdout(customOut);
RuntimeIO.setSelectedHandle(customOut);
RuntimeIO.setLastWrittenHandle(customOut);

// Set up compiler options
CompilerOptions options = new CompilerOptions();
options.code = source;
options.fileName = name;

// Use executePerlCode() which handles the full lifecycle:
// - GlobalContext.initializeGlobals() (per-runtime, under COMPILE_LOCK)
// - Tokenize, parse, compile (under COMPILE_LOCK — serialized)
// - Run UNITCHECK, CHECK, INIT blocks
// - Execute the main code (no lock — concurrent)
// - Run END blocks
long t0 = System.nanoTime();
PerlLanguageProvider.executePerlCode(options, true);
durations[idx] = System.nanoTime() - t0;

// Flush buffered output
RuntimeIO.flushFileHandles();

} catch (Throwable t) {
errors[idx] = t;
}
}, "perl-" + name);
}

// Start all threads
long wallStart = System.nanoTime();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join(60_000); // 60s safety timeout
long wallElapsed = System.nanoTime() - wallStart;

// --- Print results ---
System.out.println("=== Output from each interpreter ===\n");
for (int i = 0; i < n; i++) {
System.out.println("--- " + scriptNames.get(i) + " ---");
if (errors[i] != null) {
System.out.println(" ERROR: " + errors[i].getMessage());
errors[i].printStackTrace(System.out);
} else {
// Indent each line for readability
String out = outputs[i].toString().stripTrailing();
for (String line : out.split("\n")) {
System.out.println(" " + line);
}
System.out.printf(" (executed in %.1f ms)%n", durations[i] / 1_000_000.0);
}
System.out.println();
}

System.out.printf("=== All %d interpreters finished (wall time: %.1f ms) ===%n",
n, wallElapsed / 1_000_000.0);
}
}
45 changes: 45 additions & 0 deletions dev/sandbox/multiplicity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Multiplicity Demo

Demonstrates PerlOnJava's "multiplicity" feature: multiple independent Perl
interpreters running concurrently within a single JVM process.

Each interpreter has its own global variables, regex state, `@INC`, `%ENV`,
current working directory, process ID (`$$`), etc. They share the JVM heap;
generated classes are loaded into each runtime's own ClassLoader and become
eligible for GC once the runtime is discarded.

## Quick Start

```bash
# Build the project first
make dev

# Run with the bundled demo scripts
./dev/sandbox/multiplicity/run_multiplicity_demo.sh

# Run with custom scripts
./dev/sandbox/multiplicity/run_multiplicity_demo.sh script1.pl script2.pl

# Run all unit tests concurrently (stress test)
./dev/sandbox/multiplicity/run_multiplicity_demo.sh src/test/resources/unit/*.t
```

## Files

| File | Description |
|------|-------------|
| `MultiplicityDemo.java` | Java driver that creates N threads, each with its own `PerlRuntime` |
| `run_multiplicity_demo.sh` | Shell wrapper that compiles and runs the demo |
| `multiplicity_script1.pl` | Demo script: basic variable isolation |
| `multiplicity_script2.pl` | Demo script: regex state isolation |
| `multiplicity_script3.pl` | Demo script: module loading isolation |

## Current Status

- **122/126** unit tests pass with 126 concurrent interpreters
- Only 4 failures remain: `tie_*.t` (pre-existing `DESTROY` not implemented)

## Design Document

See [dev/design/concurrency.md](../../design/concurrency.md) for the full
concurrency design, implementation phases, and progress tracking.
26 changes: 26 additions & 0 deletions dev/sandbox/multiplicity/multiplicity_script1.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# multiplicity_script1.pl — Demonstrates isolated state in interpreter 1
use strict;
use warnings;

my $id = "Interpreter-1";
$_ = "Hello from $id";

# Set a global variable
our $shared_test = 42;

# Regex match — regex state ($1, $&, etc.) should be isolated
"The quick brown fox" =~ /(\w+)\s+(\w+)/;
my $match = "$1 $2";

# Simulate some work
my $sum = 0;
for my $i (1..1000) {
$sum += $i;
}

print "[$id] \$_ = $_\n";
print "[$id] Regex match: $match\n";
print "[$id] \$shared_test = $shared_test\n";
print "[$id] Sum 1..1000 = $sum\n";
print "[$id] \@INC has " . scalar(@INC) . " entries\n";
print "[$id] Done!\n";
26 changes: 26 additions & 0 deletions dev/sandbox/multiplicity/multiplicity_script2.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# multiplicity_script2.pl — Demonstrates isolated state in interpreter 2
use strict;
use warnings;

my $id = "Interpreter-2";
$_ = "Greetings from $id";

# This variable should NOT see interpreter 1's value
our $shared_test = 99;

# Different regex — state should not leak from interpreter 1
"2025-04-10" =~ /(\d{4})-(\d{2})-(\d{2})/;
my $match = "$1/$2/$3";

# Different computation
my $product = 1;
for my $i (1..10) {
$product *= $i;
}

print "[$id] \$_ = $_\n";
print "[$id] Regex match: $match\n";
print "[$id] \$shared_test = $shared_test\n";
print "[$id] 10! = $product\n";
print "[$id] \@INC has " . scalar(@INC) . " entries\n";
print "[$id] Done!\n";
26 changes: 26 additions & 0 deletions dev/sandbox/multiplicity/multiplicity_script3.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# multiplicity_script3.pl — Demonstrates isolated state in interpreter 3
use strict;
use warnings;

my $id = "Interpreter-3";
$_ = "Bonjour from $id";

# Independent global
our $shared_test = -1;

# Yet another regex
"foo\@bar.com" =~ /(\w+)\@(\w+)\.(\w+)/;
my $match = "user=$1 domain=$2 tld=$3";

# Fibonacci
my @fib = (0, 1);
for my $i (2..20) {
push @fib, $fib[-1] + $fib[-2];
}

print "[$id] \$_ = $_\n";
print "[$id] Regex match: $match\n";
print "[$id] \$shared_test = $shared_test\n";
print "[$id] Fib(20) = $fib[20]\n";
print "[$id] \@INC has " . scalar(@INC) . " entries\n";
print "[$id] Done!\n";
41 changes: 41 additions & 0 deletions dev/sandbox/multiplicity/run_multiplicity_demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
#
# Compile and run the Multiplicity Demo.
#
# Usage:
# ./dev/sandbox/multiplicity/run_multiplicity_demo.sh [script1.pl script2.pl ...]
#
# If no scripts are given, runs the three bundled demo scripts.
#
# See also: dev/design/concurrency.md (Multiplicity Demo section)
#
set -euo pipefail
cd "$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"

DEMO_SRC="dev/sandbox/multiplicity/MultiplicityDemo.java"
DEMO_DIR="dev/sandbox/multiplicity"

# Find the fat JAR the same way jperl does
if [ -f "target/perlonjava-5.42.0.jar" ]; then
JAR="target/perlonjava-5.42.0.jar"
elif [ -f "perlonjava-5.42.0.jar" ]; then
JAR="perlonjava-5.42.0.jar"
else
echo "Fat JAR not found. Run 'make dev' first."
exit 1
fi

# Compile the demo against the fat JAR
echo "Compiling MultiplicityDemo.java..."
javac -d "$DEMO_DIR" -cp "$JAR" "$DEMO_SRC"

# Default scripts if none provided
if [ $# -eq 0 ]; then
set -- dev/sandbox/multiplicity/multiplicity_script1.pl \
dev/sandbox/multiplicity/multiplicity_script2.pl \
dev/sandbox/multiplicity/multiplicity_script3.pl
fi

echo ""
# Run with the demo class prepended to the classpath
java -cp "$DEMO_DIR:$JAR" org.perlonjava.demo.MultiplicityDemo "$@"
4 changes: 4 additions & 0 deletions src/main/java/org/perlonjava/app/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.PerlExitException;
import org.perlonjava.runtime.runtimetypes.PerlRuntime;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;

import java.util.Locale;
Expand All @@ -26,6 +27,9 @@ public class Main {
* @param args Command-line arguments.
*/
public static void main(String[] args) {
// Initialize the PerlRuntime for the main thread
PerlRuntime.initialize();

CompilerOptions parsedArgs = ArgumentParser.parseArguments(args);

if (parsedArgs.code == null) {
Expand Down
Loading
Loading