@@ -381,6 +381,71 @@ export { isExtractConfigPath, isMetadataFormModulePath };
381381
382382const 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 */
15401605export 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 */
23672432export 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 */
29613026export 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 */
52365301export 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. */
53965461export 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$/;
57175782const MANIFEST_EXPORTS_READ = /(?<!\bmodule)\.exports\b|\[\s*(['"`])exports\1\s*\]|\bexports\s*[:?]/;
57185783
57195784export 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