Uh oh!
There was an error while loading. Please reload this page.
perf(pm): memoise the source maskers dispatch-gates re-runs per family (843s to 303s under the verify lock) - #14584
Merged
baozhoutao merged 2 commits intoSep 2, 2026
Conversation
`discoverFamilies` hands the same source string to six analysers in one pass of its per-family loop, and each re-derives the masked body from scratch — two of them twice, since they mask and then hand the masked text to `anchoredReadTargets`, which masks again. One source pays `maskComments` about seven times and `maskSelfTests` about five, per discovery, for bytes that cannot have changed in between. A V8 CPU profile of one `discoverFamilies()` call (201 families, 196 distinct gate sources, 11.8 MB) spent 14.3 s, of which `maskSelfTests` was 4.5 s of self time (31.7%) and the `maskComments` inside those six analysers most of another 5.2 s — the largest entry in the profile, and everything above the first pass is repetition. Both maskers are pure functions of their input string, so they are memoised on it, behind a byte-bounded cache. Nothing about what is masked, scanned or discovered changes: same bytes in, same bytes out, the same derivation run once instead of a dozen times. The set of families and the verdict of every self-test case are held byte-identical. Measured on this tree: one discovery 13.6 s -> 4.9 s cold, 13.4 s -> 2.6 s on a repeat within the same process.
…spatch-gates-selftest-profile
baozhoutao
marked this pull request as ready for review
September 2, 2026 13:52
baozhoutao
enabled auto-merge
September 2, 2026 13:52
Uh oh!
There was an error while loading. Please reload this page.
baozhoutao
deleted the
claude/issue-14521-dispatch-gates-selftest-profile
branch
September 2, 2026 14:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#14521
check:pm-dispatch-gateswas the lock holder for five consecutiveos-verify-lock.shqueue timeouts on the card that filed this. Measured first, as the triage asked, and the
answer is one line: discovery masks each gate source about twelve times per pass, and
the battery pays for that ninety-plus times over. The maskers are now memoised on their
input string. No family is dropped, no self-test case is dropped, no assertion is
weakened, and the 540 s budget in
os-verify-lock.shis untouched.BEFORE / AFTER — the profile
Both columns are the same battery, on the same box, through the same entry point. Every
timing run was detached, per
check-dispatch-gates.mjs's own header.Lock hold time — the way the problem was found (triage rule 5)
Shared-box seconds on a 4-core container; the load average is the one printed at the end
of each run. The last two rows are the pair to read: same base commit, same box, only
the patch differs.
53d368921, unpatched (profiling preload attached)53d368921, unpatched, clean7286dd58e, unpatched, clean7286dd58e+ this patch (profiling preload attached)e5fa9d54a(merged) + this patch, viapnpm check:pm-dispatch-gates843 s to 303 s on the identical base — 2.78x — and the faster run was the one taken
under the heavier load. Two BEFORE readings on the previous base bracket it at 804 s and
816 s, and the 804 s vs 816 s pair also says the profiling preload costs nothing readable:
the instrumented run was the faster of the two.
never idle. What is above is what the box gave, with its load figures attached, rather than
an invented idle number.
The fleet already recorded the BEFORE population
/tmp/os-heavy-verify.lock.ledger— 445 records over 26 h 48 m — names this family in 25rows, and it is the ledger's largest single consumer of lock-seconds by a factor of six:
Of those 25 rows, 23 acquired and 2 are
queue-timeoutat the 540 s budget — the card'ssymptom, in the ledger. The 23 holds run 517 s to 842 s, and they are ordered: the four
earliest are 517-521 s, the eight latest are 770-842 s. So the card's "500-900 s" is right,
and the drift inside that range is real rather than noise.
One correction to the triage's baseline
The triage reads 534.5 s out of
os-verify-lock.sh:133-134. That figure is rank 1 plusrank 2;
docs/audits/2026-08-verify-lock-gate-routing-measurement.md:51measurescheck:pm-dispatch-gatesalone at 305.5 s. Against that, the observed 800 s-plus is~2.7x the documented baseline for this one family, not ~1.6x. Nothing in the scope rules
moves — the correction makes the growth larger, not smaller. That audit is a dated reading
and is left exactly as it is; this is a new reading and it lives here.
Where the 803.9 s went — per case, not a total
From the instrumented
before1run. A case's cost is the wall time between the previousverdict line and its own, i.e. everything the battery did to decide it; that is the only
attribution a stream of verdict timestamps supports. The AFTER column is the same case in
after1.--commandsanswers--commandsoutput copied verbatim reconciles GREEN and exits 0--jsonputs a single parseable document on stdoutOver the 1216 cases present in both profiled runs: 760.2 s to 285.7 s. The top 20 cases
carry 73.3% of the BEFORE total, and every one of them is a case that either spawns the
tool's own CLI or calls
discoverFamilies().Child processes — 58% of the run, and the count does not move
spawnSynccallsgit(fixture repos,ls-files)The counts are identical on both sides. That is the point: nothing was removed, each of the
28 self-spawns just stopped paying for the same masking a dozen times. Each one costs
16.6 s before and 7.0 s after.
Inside one
discoverFamilies()call — the V8 profile that names the defect201 families, 196 distinct gate sources, 11.8 MB of source. Self time, attributed to the
nearest frame declared in
dispatch-gates.mjs:maskSelfTestsanchoredReadTargetsspawnedProgramTargetsextractWatchHintsselfTestOnlyCallablesfirstPartyImportTargetspayloadEnvDependencepackageManifestTargetsSix of those frames are the six analysers
discoverFamiliescalls on the same sourcestring, in one pass of its per-family loop — and every one of them re-derives the masked
body from scratch. Two of them do it twice (they mask, then hand the masked text to
anchoredReadTargets, which masks again). One source therefore paysmaskCommentsaboutseven times and
maskSelfTestsabout five, per discovery, for bytes that cannot havechanged in between.
That is the triage's own target, verbatim: repeated filesystem walks and work that belongs
outside a per-family loop.
The change
scripts/pm/dispatch-gates.mjsonly — 74 insertions, 9 deletions, one new module-bodyblock and nine call sites rewritten to go through it.
Both maskers are pure functions of their input string, so they are memoised on it, behind a
cache bounded by bytes rather than entries (the gate set is ~12 MB and fits whole, so
repeated discoveries in one process reuse it; a tracked-corpus sweep evicts oldest-first
instead of growing without limit). A memo over a pure function is observationally identical
to calling through, and JavaScript strings are immutable, so one caller cannot edit the
shared result under another. A non-string argument bypasses the cache entirely, so a memo
never becomes the thing that decides what a masker does with one.
Unit cost of a single
discoverFamilies()call on this tree, three consecutive calls eachway:
The warm number is why the in-process half improves at all: the battery calls
discoverFamilies()about fifteen times.⛔ Not done, deliberately, and each is a triage rule: no family removed, no case removed, no
assertion relaxed (rule 3);
scripts/pm/os-verify-lock.shnot touched and the 540 s budgetnot raised (rule 4);
docs/audits/2026-08-verify-lock-gate-routing-measurement.mdnotrewritten; no workflow changed. Cases were not parallelised — the profile did not need
it once the repetition was gone, and a flaky parallel battery is worse than a slow serial
one.
Byte-identical proof
Both trees at base
7286dd58e, the only difference being this patch.basecmpis adetached worktree at that commit; the run is the same battery through the same lock.
dispatch-gates self-test: 1240 cases pass.Control, so the empty diffs mean something: two BEFORE runs of the unpatched battery on the
same tree (
before1,before2) also produce a byte-identical verdict set, so thecomparison above is measuring the patch and not run-to-run drift.
Gates
Derived on the real diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(15 families; the tool read the change set from git itself), and the union was run at
e5fa9d54a— the merged head this branch now carries. Fourteen exit 0; the fifteenth,node scripts/check-test-completeness.mjs, exits 3 — PREREQUISITE NOT MET, which itsown text calls NOT MEASURED and not a finding: it grades a saved
turbo run testlog andthere is none locally.
--ranreconciles the union: 15 derived, 15 run, 0 UNRUN, exit 0.Per-gate exit codes are in the report comment on #14521.
Declared: the fourteen scripted gates were run off the verify lock. That is what the
lock does today rather than a narrowing — its own header records that routing gate runs
through it was measured one-directional and refused, and
--statusprintscheck:*gatescripts among the work it "does NOT see". The battery itself, which is genuinely heavy, went
through the lock every time, which is where the hold-time table above comes from.
Scripts-only, nothing published from any package, so no changeset —
skip-changeset.Generated by Claude Code
Generated by Claude Code