Skip to content

Commit f453083

Browse files
perf(walker-gate): cache reachable set per flush invocation
The property-based walker gate (commit ce94475) replaced a tiny class-name allowlist (Class::MOP/Moose/Moo) with a per-object flag storedInPackageGlobal. This semantically correct change widened the gate to fire on far more blessed objects -- DBIx::Class stores many blessed instances (Schema, ResultSource, Storage::DBI) into package- global hashes and uses weaken() liberally for back-refs, so most of its objects now satisfy the gate at MortalList.flush(). Each gate fire ran ReachabilityWalker.isReachableFromRoots(target), an O(globals) BFS. With N targets per flush, total cost was O(N x globals) -- quadratic. Surfaced as a 100% CPU spin in DBI- generated SQL dispatch code under HARNESS_OPTIONS=j4 parallel runs (t/100populate.t, t/101populate_rs.t, t/100extra_source.t etc. never completed; standalone runs stayed fast because their flush queues are smaller). Fix: compute the reachable set once per flush via the existing ReachabilityWalker.walk() and reuse for O(1) membership lookups. Cleared in the finally block so subsequent flushes recompute against fresh global state. Verified: - DBIx::Class parallel-harness batch that previously hung now passes in 47s (16 tests, 327 subtests). - DBIx::Class t/52leaks.t still passes (the leak-detection test the walker gate exists to protect). - Moo 841/841, Moose 5/5, Template Toolkit 2660/2660 unchanged. - make unit tests pass. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent a86a1ca commit f453083

2 files changed

Lines changed: 21 additions & 3 deletions

File tree

src/main/java/org/perlonjava/core/Configuration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class Configuration {
3333
* Automatically populated by Gradle/Maven during build.
3434
* DO NOT EDIT MANUALLY - this value is replaced at build time.
3535
*/
36-
public static final String gitCommitId = "6fe03a5c0";
36+
public static final String gitCommitId = "0cdf8633e";
3737

3838
/**
3939
* Git commit date of the build (ISO format: YYYY-MM-DD).
@@ -48,7 +48,7 @@ public final class Configuration {
4848
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
4949
* DO NOT EDIT MANUALLY - this value is replaced at build time.
5050
*/
51-
public static final String buildTimestamp = "Apr 29 2026 13:54:22";
51+
public static final String buildTimestamp = "Apr 29 2026 15:54:11";
5252

5353
// Prevent instantiation
5454
private Configuration() {

src/main/java/org/perlonjava/runtime/runtimetypes/MortalList.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,23 @@ public static boolean suppressFlush(boolean suppress) {
562562
System.getenv("JPERL_GC_DEBUG") != null;
563563
private static boolean inAutoSweep = false;
564564

565+
// D-W6.18 perf: cached reachable-set, valid for the duration of a
566+
// single flush() invocation. The walker BFS is O(globals); without
567+
// this cache, calling it once per pending target turns flush into
568+
// O(targets × globals). DBIC stresses this hard because many
569+
// blessed objects are stored-in-package-global AND have weak refs
570+
// (Schema/ResultSource back-refs) — every flush would re-walk the
571+
// full global graph for each one. Computed lazily on first need.
572+
private static Set<RuntimeBase> flushReachableCache = null;
573+
574+
private static boolean isReachableCached(RuntimeBase base) {
575+
if (flushReachableCache == null) {
576+
ReachabilityWalker w = new ReachabilityWalker();
577+
flushReachableCache = w.walk();
578+
}
579+
return flushReachableCache.contains(base);
580+
}
581+
565582
public static void flush() {
566583
if (!active || pending.isEmpty() || flushing) return;
567584
flushing = true;
@@ -591,7 +608,7 @@ public static void flush() {
591608
} else if (base.blessId != 0
592609
&& base.storedInPackageGlobal
593610
&& WeakRefRegistry.hasWeakRefsTo(base)
594-
&& ReachabilityWalker.isReachableFromRoots(base)) {
611+
&& isReachableCached(base)) {
595612
// D-W6.18: property-based walker gate.
596613
// Replaces the class-name heuristic
597614
// (classNeedsWalkerGate). Object's lifetime is
@@ -616,6 +633,7 @@ public static void flush() {
616633
marks.clear(); // All entries drained; marks are meaningless now
617634
} finally {
618635
flushing = false;
636+
flushReachableCache = null;
619637
}
620638
// Phase B2a: guarded auto-sweep.
621639
maybeAutoSweep();

0 commit comments

Comments
 (0)