diff --git a/dev/architecture/weaken-destroy.md b/dev/architecture/weaken-destroy.md index f5e09d6df..0c727ade2 100644 --- a/dev/architecture/weaken-destroy.md +++ b/dev/architecture/weaken-destroy.md @@ -974,6 +974,33 @@ the `classHasDestroy()` check at bless time, which is cached per class. Code that actually uses DESTROY classes pays the full tracking cost (increment/ decrement per reference assignment), but this is by design. +### Tracked-store optimization (2026-07-27) + +`RuntimeScalar.setLargeRefCounted()` registered each newly owned reference +twice in `ScalarRefRegistry`. Once `weaken()` had activated the registry, each +registration was a synchronized `WeakHashMap.put()`, so tracked stores paid for +two monitor/map operations even though the second put replaced the same entry. + +The optimized path: + +- registers the scalar once, after DESTROY-rescue handling; +- uses the registry without a synchronization wrapper, consistent with + PerlOnJava's documented single-threaded runtime model; +- removes empty reverse weak-reference buckets during `unweaken()`; +- includes `dev/bench/benchmark_refcount_store.pl` for repeatable measurement. + +Median CPU time over three fresh JVM runs of the tracked-store benchmark: + +| Revision | CPU time | Change | +|----------|----------|--------| +| `master` (`168466619`) | 2.71 s | baseline | +| optimized branch | 2.41 s | **-11.1%** | + +The ordinary method benchmark was unchanged at approximately 2.35 CPU seconds, +showing no measurable regression before weak-reference bookkeeping is active. +Correctness validation completed with `make` and the full DBIx::Class suite: +314 files, 13,121 tests, all successful in 679 wall-clock seconds. + ### Memory Overhead - **Per-referent:** `refCount` (int, 4 bytes) and `blessId` (int, 4 bytes) on @@ -983,7 +1010,8 @@ decrement per reference assignment), but this is by design. 4 bytes), on `RuntimeScalar`. Always present; the JVM may pack the booleans into existing object-alignment padding. - **WeakRefRegistry:** External identity maps. Only allocated when `weaken()` - is called. Zero memory when no weak refs exist. + is called. Zero memory when no weak refs exist. Empty per-referent reverse + buckets are removed by overwrite, clear, and `unweaken()` paths. - **DestroyDispatch caches:** `BitSet` + `ConcurrentHashMap`. Negligible. --- diff --git a/dev/bench/README.md b/dev/bench/README.md index c33f1567f..c51be143e 100644 --- a/dev/bench/README.md +++ b/dev/bench/README.md @@ -23,6 +23,7 @@ under PerlOnJava (and optionally system Perl for comparison). Use them to: | `benchmark_memory_delta.pl` | Memory growth / leak detection | | `benchmark_method.pl` | Method dispatch (class, inheritance) | | `benchmark_regex.pl` | Regex compilation and matching | +| `benchmark_refcount_store.pl` | Tracked reference stores after `weaken()` activates lifecycle bookkeeping | | `benchmark_string.pl` | String operations (concat, substr, etc.) | ## Running diff --git a/dev/bench/benchmark_refcount_store.pl b/dev/bench/benchmark_refcount_store.pl new file mode 100644 index 000000000..d13772940 --- /dev/null +++ b/dev/bench/benchmark_refcount_store.pl @@ -0,0 +1,38 @@ +use strict; +use warnings; +use Benchmark; +use Scalar::Util qw(weaken); + +{ + package RefcountStoreBench; + + sub DESTROY { + $main::destroyed++; + } +} + +our $destroyed = 0; +my $object = bless { value => 1 }, 'RefcountStoreBench'; +my $weak = $object; +weaken($weak); + +# Keep enough container slots active to exercise tracked overwrite ownership +# without measuring allocation of a fresh aggregate on every iteration. +my @slots = map { $object } 0 .. 1023; +my $sink = 0; + +sub loop_refcount_stores { + for my $i (0 .. 999_999) { + $slots[$i & 1023] = $object; + $sink += $slots[$i & 1023]{value}; + } +} + +timethis(30, sub { + $sink = 0; + loop_refcount_stores(); +}); + +die "tracked object destroyed during benchmark\n" + if $destroyed || !defined $weak; +print "done $sink\n"; diff --git a/dev/modules/dbix_class.md b/dev/modules/dbix_class.md index ed05e240f..bb794d4f8 100644 --- a/dev/modules/dbix_class.md +++ b/dev/modules/dbix_class.md @@ -543,9 +543,21 @@ can't ship a regression behind that flag. --- -## Non-Bug Warnings (informational) +## Environment Warning Fixes (2026-07-27) + +- **Generated Exception::Class version mismatches**: Exception::Class 1.45 + gives generated subclasses version 1.1 while registering + `Exception/Class.pm` (version 1.45) as their `%INC` source. The bundled CPAN + patch now aligns generated subclass versions with the registered file. +- **Optional Getopt::Long::Descriptive base classes**: `base::import` still + throws the normal catchable "Base class package ... is empty" exception, but + no longer prints a duplicate message directly to stderr before throwing. +- **DynaLoader and bytes version mismatches**: bundled module files now expose + the same versions as their Java runtime implementations. +- Verified `t/00describe_environment.t` has none of these warnings. + +## Remaining Non-Bug Warnings (informational) -- **`Mismatch of versions '1.1' and '1.45'`** in `t/00describe_environment.t` for `Params::ValidationCompiler::Exception::Named::Required`: Not a PerlOnJava bug. `Exception::Class` deliberately sets `$INC{$subclass.pm} = __FILE__` on every generated subclass. - **`Subroutine is_bool redefined at Cpanel::JSON::XS line 2429`**: Triggered when Cpanel::JSON::XS loads through `@ISA` fallback. Cosmetic only. --- diff --git a/dev/modules/moo.md b/dev/modules/moo.md index 7a936b25f..b2692f27b 100644 --- a/dev/modules/moo.md +++ b/dev/modules/moo.md @@ -16,6 +16,22 @@ All 841/841 Moo subtests now pass across all 71 test files. ## Completed Fixes +### Readonly Pad Constants Passed Through Constructors — FIXED (2026-07-27) + +**2 test files, 6 subtests fixed (tests 17-19 in each accessor-weaken file).** + +Root cause: a readonly scalar created by `\ "literal"` is strongly owned by +the defining subroutine's pad until that subroutine is replaced. Selective +refcounting cannot see that pad edge. When Moo passed the reference through +its constructor and weakened the attribute, the constructor temporary reached +zero and mortal processing cleared the weak reference immediately. + +Fix: detect readonly scalars present in the pad constants of currently +installed named subroutines. Both immediate `weaken()` processing and deferred +mortal cleanup preserve those references. Replacing the subroutine continues +to clear them through the existing `clearPadConstantWeakRefs()` optree-reaping +hook. + ### Category C: Optree Reaping — FIXED (2025-04-09) **2 test files, 2 subtests fixed (test 19 in each accessor-weaken file).** diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java index 2e8ed6759..28408e2ef 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java @@ -126,7 +126,6 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) { // Java bridge stubs for IO::Handle::_sync, or DBIC's // eval-created classes). if (!GlobalVariable.isPackageLoaded(baseClassName)) { - System.err.println("Base class package \"" + baseClassName + "\" is empty."); throw new PerlCompilerException("Base class package \"" + baseClassName + "\" is empty."); } } else { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DynaLoader.java b/src/main/java/org/perlonjava/runtime/perlmodule/DynaLoader.java index da5cb605c..49c18c7f2 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/DynaLoader.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DynaLoader.java @@ -16,8 +16,6 @@ public static void initialize() { DynaLoader dynaLoader = new DynaLoader(); dynaLoader.initializeExporter(); dynaLoader.defineExport("EXPORT", "bootstrap"); - // Set $DynaLoader::VERSION so CPAN dependency checks are satisfied - GlobalVariable.getGlobalVariable("DynaLoader::VERSION").set("1.54"); try { dynaLoader.registerMethod("bootstrap", null); dynaLoader.registerMethod("boot_DynaLoader", null); @@ -36,7 +34,8 @@ public static void initialize() { dynaLoader.registerMethod("dl_undef_symbols", "dl_empty", null); dynaLoader.registerMethod("dl_error", "dl_error", null); - // Set $DynaLoader::VERSION so CPAN dependency checking works + // Match the bundled DynaLoader.pm so runtime and file-based + // dependency/version checks report the same value. GlobalVariable.getGlobalVariable("DynaLoader::VERSION").set("1.56"); } catch (NoSuchMethodException e) { System.err.println("Warning: Missing DynaLoader method: " + e.getMessage()); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java index 41c52763f..3f95dd546 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java @@ -985,6 +985,16 @@ private static void processDeferredBase(RuntimeBase base, boolean clearWeakRefsF // generation is wiring Moo metadata. Clearing then makes // nested dispatch call undefer_sub(undef). base.refCount = 1; + } else if (base instanceof RuntimeScalarReadOnly + && hasWeakRefs + && RuntimeCode.isInstalledPadConstant(base)) { + // A readonly scalar referenced through \("literal") belongs to + // its defining subroutine's pad. Constructor/call temporaries + // are selectively counted, but that pad ownership is not. + // Preserve the weak ref while the sub remains installed; + // RuntimeCode.clearPadConstantWeakRefs() clears it when the + // sub's glob is replaced, matching Perl optree reaping. + base.refCount = 1; } else if (base.blessId == 0 && hasWeakRefs && (ReachabilityWalker.isReachableFromLiveCodeCaptures(base) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 3bce86fbf..133c13c7d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -890,6 +890,30 @@ public void clearPadConstantWeakRefs() { } } + /** + * Return whether a readonly scalar is owned by the optree/pad of a + * currently installed named subroutine. + * + *
Pad constants are JVM references that selective refcounting cannot + * observe. This targeted lookup lets mortal processing preserve a weak + * reference until the owning glob is replaced, when + * {@link #clearPadConstantWeakRefs()} performs Perl-compatible optree + * reaping.
+ */ + public static boolean isInstalledPadConstant(RuntimeBase target) { + if (target == null) return false; + for (RuntimeScalar codeScalar : GlobalVariable.globalCodeRefs.values()) { + if (codeScalar == null || !(codeScalar.value instanceof RuntimeCode code) + || code.padConstants == null) { + continue; + } + for (RuntimeBase constant : code.padConstants) { + if (constant == target) return true; + } + } + return false; + } + /** * Release captured variable references. Called when this closure is being * discarded (scope exit, undef, or reassignment of the variable holding diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index ab3493d69..0904570e2 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -1540,12 +1540,6 @@ private RuntimeScalar setLargeRefCounted(RuntimeScalar value) { } } - // Phase B1 (refcount_alignment_52leaks_plan.md): track this - // scalar so the reachability walker can enumerate live lexicals. - if (newOwned) { - ScalarRefRegistry.registerRef(this); - } - int preAssignType = this.type; Object preAssignValue = this.value; @@ -1597,10 +1591,10 @@ private RuntimeScalar setLargeRefCounted(RuntimeScalar value) { newOwned = true; } - // Phase B1 (refcount_alignment_52leaks_plan.md): register this - // scalar in ScalarRefRegistry so the reachability walker can - // enumerate live ref-holding RuntimeScalars on demand. No-op - // when no weaken() has ever been called. + // Phase B1 (refcount_alignment_52leaks_plan.md): register this scalar + // once, after rescue handling has had a chance to acquire ownership. + // This used to run both before and after the assignment, causing two + // WeakHashMap.put() calls for every ordinary tracked reference store. if (newOwned) { ScalarRefRegistry.registerRef(this); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarRefRegistry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarRefRegistry.java index c061e2721..2ed2c4fa9 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarRefRegistry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarRefRegistry.java @@ -1,10 +1,7 @@ package org.perlonjava.runtime.runtimetypes; import java.lang.ref.WeakReference; -import java.util.Collections; -import java.util.IdentityHashMap; import java.util.Map; -import java.util.Set; import java.util.WeakHashMap; /** @@ -31,7 +28,7 @@ public class ScalarRefRegistry { // hashCode/equals — RuntimeScalar uses Object's defaults, so this // is effectively IdentityHashMap-with-weak-keys. private static final Map