Skip to content

Commit 3bca6a4

Browse files
claude[bot]claude
andauthored
perf(pm): memoise the source maskers dispatch-gates re-runs per family (#14584)
`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. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 996cb1d commit 3bca6a4

1 file changed

Lines changed: 74 additions & 9 deletions

File tree

scripts/pm/dispatch-gates.mjs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,71 @@ export { isExtractConfigPath, isMetadataFormModulePath };
381381

382382
const ROOT = new URL('../..', import.meta.url).pathname;
383383

384+
// ── The source maskers are memoised, because discovery masks each file ~12x ──
385+
//
386+
// PROFILED, not guessed. `discoverFamilies` hands the SAME source string to six
387+
// analysers in one pass of its per-family loop — `extractWatchHints`,
388+
// `readProgramTargetsInSource`, `payloadEnvDependence`, `firstPartyImportTargets`,
389+
// `spawnedProgramTargets` and `packageManifestTargets` — and every one of them
390+
// re-derives the masked body from scratch, two of them twice (they mask, then
391+
// hand the masked text to `anchoredReadTargets`, which masks again). One source
392+
// therefore pays `maskComments` about seven times and `maskSelfTests` about five,
393+
// per discovery, for bytes that cannot have changed in between.
394+
//
395+
// The cost that buys: a V8 CPU profile of ONE `discoverFamilies()` call on this
396+
// tree — 201 families, 196 distinct gate sources, 11.8 MB of them — spent 14.3 s,
397+
// of which `maskSelfTests` was 4.5 s of self time (31.7%) and the `maskComments`
398+
// inside those six analysers most of another 5.2 s. That is the largest single
399+
// entry in the profile, and all of it above the first pass is repetition.
400+
//
401+
// So the maskers are memoised on their INPUT STRING. Both are pure functions of
402+
// that string, and JavaScript strings are immutable, so a memo is
403+
// observationally identical to calling through: same bytes in, same bytes out,
404+
// and no caller can edit the shared result under another. ⛔ This changes
405+
// nothing about WHAT is masked, scanned or discovered — it is the same
406+
// derivation run once instead of a dozen times, which is the only kind of
407+
// speed-up this tool may take.
408+
//
409+
// The bound is in BYTES rather than entries because the corpora these run over
410+
// differ by three orders of magnitude: the gate set is ~12 MB and fits whole, so
411+
// repeated discoveries in one process reuse it, while a tracked-corpus sweep
412+
// would otherwise grow the cache without limit. Eviction is oldest-first, and a
413+
// miss after eviction is a recomputation — never a different answer.
414+
const MASK_MEMO_BYTE_BUDGET = 32 * 1024 * 1024;
415+
416+
function memoiseMask(compute) {
417+
const cache = new Map();
418+
let bytes = 0;
419+
return (source) => {
420+
// A non-string argument is passed straight through: today's behaviour is
421+
// whatever the masker does with it, and a memo must not be the thing that
422+
// decides otherwise.
423+
if (typeof source !== 'string') return compute(source);
424+
const hit = cache.get(source);
425+
if (hit !== undefined) return hit;
426+
const value = compute(source);
427+
cache.set(source, value);
428+
bytes += source.length + value.length;
429+
// `Map` iterates in insertion order, so the first key is the oldest.
430+
while (bytes > MASK_MEMO_BYTE_BUDGET && cache.size > 1) {
431+
const oldest = cache.keys().next().value;
432+
bytes -= oldest.length + cache.get(oldest).length;
433+
cache.delete(oldest);
434+
}
435+
return value;
436+
};
437+
}
438+
439+
/** `maskComments`, memoised — see the block above. */
440+
const maskedComments = memoiseMask((source) => maskComments(source));
441+
442+
/**
443+
* `maskSelfTests(maskComments(source))`, memoised — see the block above. It
444+
* composes through `maskedComments` rather than calling `maskComments` again, so
445+
* the comment mask is derived once for the callers that want each half.
446+
*/
447+
const maskedModuleBody = memoiseMask((source) => maskSelfTests(maskedComments(source)));
448+
384449
// ── What a gate that IMPORTS this module inherits (#11556) ─────────────────
385450
//
386451
// This module is importable and is NOT a discovered gate file — `check:pm-dispatch-gates`
@@ -1538,7 +1603,7 @@ const PAYLOAD_ENV_ACCESS = new RegExp(
15381603
* the difference between classifying a gate and classifying its docblock.
15391604
*/
15401605
export function payloadEnvDependence(scriptSource) {
1541-
const body = maskSelfTests(maskComments(String(scriptSource)));
1606+
const body = maskedModuleBody(String(scriptSource));
15421607
return PAYLOAD_ENV_ACCESS.test(body) ? WORKFLOW_PAYLOAD_ENV : null;
15431608
}
15441609

@@ -2366,7 +2431,7 @@ export const COMPOUND_ANCHOR_KEYS = new Map(
23662431
*/
23672432
export function compoundAnchorDecls(source) {
23682433
const scan = scanSource(source);
2369-
const decommented = maskComments(source);
2434+
const decommented = maskedComments(source);
23702435
const out = [];
23712436
for (const m of decommented.matchAll(SELF_TEST_DECL)) {
23722437
if (scan.comment[m.index] || scan.literal[m.index]) continue;
@@ -2959,7 +3024,7 @@ export function packageRootAnchoredHint(hint, base, tree, files) {
29593024
* have to remember to do it.
29603025
*/
29613026
export function extractWatchHints(scriptSource, scriptPath = null, { tree = null } = {}) {
2962-
const moduleBody = maskSelfTests(maskComments(scriptSource));
3027+
const moduleBody = maskedModuleBody(scriptSource);
29633028
const hints = new Set();
29643029
for (const m of moduleBody.matchAll(/['"`]([^'"`\n]{2,120})['"`]/g)) {
29653030
const raw = m[1];
@@ -3175,7 +3240,7 @@ export function firstPartyImportTargets(scriptPath, source, { root = ROOT } = {}
31753240
// The same masking hint extraction uses, for the same reason: an import
31763241
// written out in a docblock, or one inside a self-test fixture, is a
31773242
// specifier this script NAMES rather than one it loads.
3178-
const body = maskSelfTests(maskComments(String(source)));
3243+
const body = maskedModuleBody(String(source));
31793244
const specifiers = new Set();
31803245
for (const m of body.matchAll(IMPORT_FROM_SPECIFIER)) specifiers.add(m[2]);
31813246
for (const m of body.matchAll(SIDE_EFFECT_IMPORT)) specifiers.add(m[2]);
@@ -5234,7 +5299,7 @@ function combineReadings(readings, name) {
52345299
* own repo-relative path — the anchor spellings resolve against it.
52355300
*/
52365301
export function scratchDirSitesInSource(rel, source) {
5237-
const masked = maskComments(String(source));
5302+
const masked = maskedComments(String(source));
52385303
// A call spelled inside a STRING is a fixture, not a call — this module's own
52395304
// self-test plants fixture sources as string literals, and read as code they
52405305
// reported four sites in a file that creates none of them. Comments are
@@ -5394,7 +5459,7 @@ export function readProgramTargetsInSource(rel, source, isTracked) {
53945459

53955460
/** Every TRACKED file the source opens at a path anchored to its own location. */
53965461
export function anchoredReadTargets(rel, source, isTracked) {
5397-
const masked = maskComments(String(source));
5462+
const masked = maskedComments(String(source));
53985463
const { literal } = scanSource(masked);
53995464
const ctx = {
54005465
fileSegs: rel.split('/'),
@@ -5533,7 +5598,7 @@ export function spawnedProgramTargets(rel, source, isTracked) {
55335598
// follow inherits a POPULATION, and a spawn written inside a self-test body
55345599
// is a fixture the self-test drives rather than the gate's work. The read
55355600
// scan next door wants the opposite from the same bytes, and says so.
5536-
const masked = maskSelfTests(maskComments(String(source)));
5601+
const masked = maskedModuleBody(String(source));
55375602
const { literal } = scanSource(masked);
55385603
const ctx = {
55395604
fileSegs: rel.split('/'),
@@ -5717,7 +5782,7 @@ const PACKAGE_MANIFEST_TARGET = /(?:^|\/)package\.json$/;
57175782
const MANIFEST_EXPORTS_READ = /(?<!\bmodule)\.exports\b|\[\s*(['"`])exports\1\s*\]|\bexports\s*[:?]/;
57185783

57195784
export function packageManifestTargets(rel, source, isTracked) {
5720-
const masked = maskSelfTests(maskComments(String(source)));
5785+
const masked = maskedModuleBody(String(source));
57215786
if (!MANIFEST_EXPORTS_READ.test(masked)) return [];
57225787
return anchoredReadTargets(rel, masked, isTracked).filter((t) => PACKAGE_MANIFEST_TARGET.test(t));
57235788
}
@@ -6635,7 +6700,7 @@ export function stampsAnErrorCodeLiteral(path, readSource = readTrackedSource) {
66356700
// Comments are masked for the reason the gate masks them: a code DISCUSSED in
66366701
// prose is not a code stamped in source. This narrows nothing the gate would
66376702
// have reported, so it costs no recall in the expensive direction.
6638-
const masked = maskComments(source);
6703+
const masked = maskedComments(source);
66396704
return CODE_STAMP_POSITION.test(masked) || CODE_CONSTANT_BINDING.test(masked);
66406705
}
66416706

0 commit comments

Comments
 (0)