Skip to content

Commit 51b9990

Browse files
perf(spec): check:liveness reads each cited file once per run (#23535)
Part of #23180 Clause-②: no **This PR takes no row off the card.** Both of the card's dominant `@objectstack/spec` rows stay over the 10 s per-file budget after it, and the card stays open for both: - `scripts/liveness/check-liveness.test.ts` (`local` project) drops from about 190 to 255 s to about 87 to 101 s locally: 2.2x to 2.9x, still about 9x over. - `scripts/build-schemas-check-mode.test.ts` (`repo` project) is untouched, at about 900 s. The measured floors for both files are in the `os-dev-report` comment on the card, along with why neither can reach 10 s without weakening what it proves. The seat's decision on that report is to land this speed-up and send the budget question for this class of self-test to the maintainer. Dispatched by the `domain:spec` seat 4 PM (`sunhaolin`), claim comment 6175077909, branch `claude/issue-23180-spec-slowest-two`. ## What changed `packages/spec/scripts/liveness/check-liveness.mts` is the only file changed (+21/-6): - `lineCountOf` and `contentOf` now read a cited file through one per-run memo, `readCited`. It is keyed by the repo-relative path, and a missing file is memoized as missing. - Before, every citation re-read its file. One gate run read 455.8 MB in 2,367 reads over 828 files; `packages/rest/src/rest-server.ts` alone was read 142 times. That was nearly half of each run's wall, and the self-test pays it on every one of its roughly 66 gate spawns. - After, one run reads 30.5 MB. - Nothing in the repo changes during one gate run, so the memo cannot change a verdict. The equivalence readings below measure that. ## Readings All readings are local, taken through `bash scripts/pm/os-verify-lock.sh -c` (each `VERDICT command-exit 0`). They are shared-box seconds: the lock excludes only other locked runs, so ratios are the robust part. **The self-test file**, as the vitest `default` reporter's module line (the number the full battery records). Command: ``` env -u AI_AGENT -u CLAUDECODE -u CLAUDE_CODE -u CURSOR_AGENT NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 scripts/liveness/check-liveness.test.ts --reporter=default ``` | tree | run 1 | run 2 | run 3 | cases | |:--|--:|--:|--:|:--| | before: the gate at `0be162c75d` | 190.5 s | 255.3 s | 251.3 s | 71/71 pass | | after: the memo at `f6bcfe9c91` | 87.1 s | 87.4 s | 90.0 s | 71/71 pass | | after: this head, merged with `origin/main` `b1cfa3d263` | 97.4 s | 93.8 s | 101.3 s | 71/71 pass | CI's reading before was 493.4 s, in run `38332198724`. The CI reading after is NOT MEASURED until the next refresh window lists this file again. **One `check-liveness.mts` run.** This is what `pnpm check:liveness` pays on every lint run, and what the self-test pays per spawn: | comparison | before | after | |:--|:--|:--| | the gate at `0be162c75d` vs the memo at `f6bcfe9c91` | 3.84 to 4.20 s | 1.23 to 1.34 s | | `origin/main`'s gate vs this head's gate, on this head's tree | 3.02 to 3.49 s | 1.37 to 1.45 s | **The same verdicts.** `origin/main`'s gate (a temporary untracked copy beside this one, deleted after) and this head's gate were run on the same tree. Their output is byte-identical: - default mode: 6,514 bytes, exit 0 on both; - `--json` mode: 55,633 bytes, exit 0 on both. **The same assertions.** `check-liveness.test.ts` is not edited. Every assertion is the one on `main`, and all 71 pass. ## Ablation: the self-test still catches a broken memo The mutation keys `readCited` by basename, so two different cited files collide, for example `packages/objectql/src/engine.ts` and `packages/services/service-automation/src/engine.ts`. It was applied with `node scripts/ablation-replace.mjs` and its `-- CMD` form, which arms the restore before it writes: - **Mutation:** anchor count 1 to 0, blob `141dc51f5a62` to `835c251a41e2`. On-disk counts: injected marker 1, removed `citedText.set(p, text);` 0. - **Mutant self-test:** exit 1, **24 of 71 cases red.** - The green controls exit 1 instead of 0. On the verbatim-copy control, the gate reports `✗ 57 anchored citation(s) name a symbol the cited file does not contain` and `✗ 22 ... citation(s) name a file that never names the property`. - The anchor case `FAILS when an anchored symbol is gone ...` misses its exact `✗ 1 anchored citation(s)` count. - **Restore:** `blob == HEAD` (`141dc51f5a62`), `git diff HEAD` empty, porcelain clean, marker count 0. - The source is run directly by tsx, so there is no `dist/` in the path. ## Why it stops here **`build-schemas-check-mode.test.ts`:** - Each of its 88 cases runs the real generator on its own fixture: about 96 runs at 8.5 to 9.0 s warm. - Most of each run is Zod `toJSONSchema` over about 1,700 schemas, plus git and an output rewrite. - The median case alone takes 9.07 s. **`check-liveness.test.ts`:** - Its floor is about 43 distinct one-cause fixtures times one gate run. - An in-process route measured about 7.4 s locally with zero margin, and it reverses the file's spawn-the-real-gate design. - Sharing the identical-state runs inside the test is left out: a case would read another case's run, and the file's own hook-budget block scans `runGate`'s reach to `spawnSync`. ## Changeset None, because nothing publishes. - `packages/spec`'s `files[]` does not list `scripts/`. - Measured on the packed tarball (`pnpm pack` after `pnpm --filter @objectstack/spec build`, 2,091 files): - no `scripts/` path; - 0 files contain `readCited`, `citedText` or the pre-existing gate-only `EVIDENCE_SCANNED_STATUSES`; - positive control: `listMetadataTypeSchemaTypes` is in 14 files. - Hence `skip-changeset`. ## Gates Derived at this head (`62adc640c1`) with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 55 commands. All 55 were run, with each exit code captured before any pipe. The `--ran` reconciliation reads `55 derived famil(ies) accounted for — 53 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, 0 UNRUN. - **53 exit 0.** These include `pnpm --filter @objectstack/spec run check:liveness`, `check:nul-bytes`, `check:issue-citations`, `check:doc-authoring`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check:published-files`, `check:closing-keyword-parity` and `check:pm-dispatch-gates`. - **NOT MEASURED: `pnpm check:dual-build-cjs-loads` and `pnpm check:lean-entry-closure`.** Reason: PREREQUISITE NOT MET. They load other packages' built `dist/` entries, and only `@objectstack/spec` was built locally. Neither can read an unshipped file under `packages/spec/scripts/`. CI runs both. - **Changeset readings beyond the derivation, all exit 0:** `pnpm check:changeset-gate-self-tests`, `node scripts/check-empty-changeset.mjs`, `node scripts/check-changeset-no-major.mjs --base origin/main` and `pnpm check:published-files`. ## Acceptance notes - Option D from the report, a ruling on the 10 s line for gate self-tests that spawn the real gate once per one-cause fixture, is the maintainer's. It is not part of this PR. - The merge commits from `origin/main` carry git's default message. --- _Generated by [Claude Code](https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent fba3712 commit 51b9990

1 file changed

Lines changed: 21 additions & 6 deletions

File tree

‎packages/spec/scripts/liveness/check-liveness.mts‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -793,16 +793,33 @@ const producerEntries: ProducerEntry[] = [];
793793

794794
const proofFs = { existsSync, readFileSync };
795795

796+
/**
797+
* One read per cited file per run (#23180). The walk cites the same consumers
798+
* from hundreds of rows — `packages/rest/src/rest-server.ts` alone 142 times —
799+
* and reading each citation afresh made one run read 455.8 MB to check 2,367
800+
* citations over 828 files: nearly half the run's wall, and the cost the
801+
* self-test pays on every one of its gate spawns. Nothing in the repo moves
802+
* under one run, so a memo keyed by the repo-relative path changes no verdict;
803+
* a missing file is memoized as missing for the same reason.
804+
*/
805+
const citedText = new Map<string, string | null>();
806+
function readCited(p: string): string | null {
807+
if (citedText.has(p)) return citedText.get(p) ?? null;
808+
const f = join(repoRoot, p);
809+
const text = existsSync(f) ? readFileSync(f, 'utf8') : null;
810+
citedText.set(p, text);
811+
return text;
812+
}
813+
796814
/**
797815
* How many lines the cited file has, or `null` when it cannot be read. `null`
798816
* is not a pass: it means the EXISTENCE check owns this path's verdict already
799817
* (a rotted `evidence` pointer would otherwise be reported twice, once per
800818
* check, which teaches a reader to discount both lists).
801819
*/
802820
function lineCountOf(p: string): number | null {
803-
const f = join(repoRoot, p);
804-
if (!existsSync(f)) return null;
805-
return countLines(readFileSync(f, 'utf8'));
821+
const text = readCited(p);
822+
return text === null ? null : countLines(text);
806823
}
807824

808825
/**
@@ -811,9 +828,7 @@ function lineCountOf(p: string): number | null {
811828
* from the existence check and must not be reported a second time here.
812829
*/
813830
function contentOf(p: string): string | null {
814-
const f = join(repoRoot, p);
815-
if (!existsSync(f)) return null;
816-
return readFileSync(f, 'utf8');
831+
return readCited(p);
817832
}
818833

819834
/** Unanchored (entry, cited path) pairs the walk observes, reconciled after it. */

0 commit comments

Comments
 (0)