Key baseline cache entries by their program's inputs

A recorded baseline entry now holds a hash of its program's inputs: the
entry file and its relative imports (paths and contents) and the
optimization level. --baseline-cache uses an entry only when the program
still hashes the same, so a program or an import that changed since the
record builds its baseline, instead of a recorded build error or artifact
hash of the old sources settling the comparison as both-fail or identical.
Records take schema 2; older records are ignored.
This commit is contained in:
Chris Tate
2026-10-26 19:53:49 -05:00
parent d0047c813f
commit cd606e96ff
4 changed files with 76 additions and 8 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ A change that must not cost good code anything, such as a compatibility fix that
node scripts/zero-cost-gate.mjs --baseline=/vercel/sandbox/prev --candidate=. --allow='\barguments\b'
```
The whole gate takes 30 to 75 minutes on one 8-vCPU Sandbox, so shard it across Sandboxes. `--shard=i/n` splits the bench and corpus programs; the self-hosted compiler runs in shard 1, or alone with `--set=self`, and its candidate-source build overlaps its comparison build on hosts with 40 GB or more. Each shard writes `--json`, and `node scripts/zero-cost-merge.mjs <shard.json>...` merges them into one verdict, recomputing the compile-time intervals over every shard. `--record-baseline-cache=<file>` builds only the baseline and records each program's artifact hashes; `--baseline-cache=<file>` then skips the baseline build of every program whose candidate artifacts hash the same, which halves the work for a change that leaves most programs identical. A record applies only to the built baseline checkout, toolchain, and paths it was made with, and is ignored otherwise.
The whole gate takes 30 to 75 minutes on one 8-vCPU Sandbox, so shard it across Sandboxes. `--shard=i/n` splits the bench and corpus programs; the self-hosted compiler runs in shard 1, or alone with `--set=self`, and its candidate-source build overlaps its comparison build on hosts with 40 GB or more. Each shard writes `--json`, and `node scripts/zero-cost-merge.mjs <shard.json>...` merges them into one verdict, recomputing the compile-time intervals over every shard. `--record-baseline-cache=<file>` builds only the baseline and records each program's artifact hashes; `--baseline-cache=<file>` then skips the baseline build of every program whose candidate artifacts hash the same, which halves the work for a change that leaves most programs identical. A record applies only to the built baseline checkout, toolchain, and paths it was made with, and is ignored otherwise; a program whose entry, relative imports, or optimization level changed since the record builds its baseline.
Test location follows scope:
+22
View File
@@ -629,6 +629,28 @@ export function timingGroups(rows) {
/** A content hash of one artifact file, as the baseline cache records it. */
export const artifactHash = hash;
/** The hash of what a program's build reads, as the baseline cache records
* it: `files` (the entry and its relative imports) by path relative to
* `root` and content, read through `read`, and the optimization level. */
export function programInputsHash(root, files, optimization, read) {
const h = createHash("sha1").update(`${optimization}\0`);
for (const file of [...files].sort())
h.update(`${file.startsWith(root + "/") ? file.slice(root.length + 1) : file}\0`)
.update(read(file))
.update("\0");
return h.digest("hex");
}
/** The baseline cache entry recorded for program `id`, or undefined when the
* cache has none or recorded it for other inputs (`programInputsHash`): a
* program or an import that changed since the record must build its
* baseline, or a recorded build error or artifact hash of the old sources
* would settle the new sources' comparison. */
export function recordedBaseline(cache, id, inputs) {
const entry = cache?.programs[id];
return entry !== undefined && entry.inputs === inputs ? entry : undefined;
}
/** The comparison a cached baseline entry settles without building the
* baseline, or null when the baseline must be built: identical artifacts
* (every recorded hash equals the candidate's) or the same build error on
+27 -7
View File
@@ -41,8 +41,10 @@ import {
dynamicTierSymbols,
median,
nmOutput,
programInputsHash,
programLines,
programVerdict,
recordedBaseline,
timingGroups,
timingVerdict,
} from "./zero-cost-compare.mjs";
@@ -117,11 +119,14 @@ and compares instruction counts per function of the executable's own
symbols, excluding the allocator's prefault thread.
--record-baseline-cache=<file> builds only the baseline and records each
program's artifact hashes (or build error) in <file>, then exits.
program's artifact hashes (or build error) in <file>, with a hash of the
program's inputs (its entry, its relative imports and its optimization
level), then exits.
--baseline-cache=<file> reads such a record: a program whose candidate
artifacts hash the same as the recorded baseline's is identical without a
baseline build, and the same build error on both sides is both-fail; any
other program builds the baseline as usual. The record applies only to the
other program builds the baseline as usual, and so does a program whose
inputs changed since the record. The record applies only to the
baseline revision and toolchain it was made with; otherwise it is ignored.
The self program always builds both sides, since its builds are timed.
@@ -473,6 +478,16 @@ function toolchainFingerprint(root) {
return h.digest("hex");
}
/** The hash of `program`'s inputs, which its baseline cache entry records. */
function inputsHash(program) {
return programInputsHash(
programsRoot,
programInputs(program.entry),
program.kind === "source" ? "source" : (program.optimization ?? "release"),
(file) => readFileSync(file),
);
}
/** The content hashes of one build's artifacts (null for a missing one). */
function buildHashes(build) {
const hashes = {};
@@ -488,7 +503,7 @@ if (recordPath === undefined && values["baseline-cache"] !== undefined) {
const file = resolve(values["baseline-cache"]);
const cache = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
const fingerprint = cache === null ? null : toolchainFingerprint(trees.baseline);
if (cache?.schema === 1 && cache.fingerprint === fingerprint) baselineCache = cache;
if (cache?.schema === 2 && cache.fingerprint === fingerprint) baselineCache = cache;
else
console.log(
`baseline cache ${file} ${cache === null ? "is missing" : "was made with another baseline or toolchain"}; building every baseline`,
@@ -527,7 +542,7 @@ const started = Date.now();
if (recordPath !== undefined) {
const record = {
schema: 1,
schema: 2,
date: report.date,
fingerprint: toolchainFingerprint(trees.baseline),
baseline: report.trees.baseline,
@@ -537,14 +552,16 @@ if (recordPath !== undefined) {
programs.map((program) => ({
run: async () => {
const build = await buildProgram(program, "baseline");
const inputs = inputsHash(program);
record.programs[program.id] = build.ok
? {
inputs,
ok: true,
ms: build.ms,
hashes: buildHashes(build),
...(build.exe ? { exeBytes: statSync(build.exe).size } : {}),
}
: { ok: false, ms: build.ms ?? null, error: build.error };
: { inputs, ok: false, ms: build.ms ?? null, error: build.error };
if (build.dir) rmSync(build.dir, { recursive: true, force: true });
},
})),
@@ -581,7 +598,10 @@ await pool(
programs.map((program) => ({
heavy: program.kind === "source",
run: async () => {
const recorded = program.kind === "source" ? undefined : baselineCache?.programs[program.id];
const recorded =
program.kind === "source" || baselineCache === null
? undefined
: recordedBaseline(baselineCache, program.id, inputsHash(program));
let baseline;
let candidate;
let settled = null;
@@ -593,7 +613,7 @@ await pool(
else {
candidate = await buildProgram(program, "candidate");
settled = cachedComparison(recorded, candidate, candidate.ok ? buildHashes(candidate) : {});
// A cached identical baseline is byte for byte the candidate's build.
// A cached identical baseline has the candidate's artifacts.
baseline =
settled === null
? await buildProgram(program, "baseline")
+26
View File
@@ -10,8 +10,10 @@ import {
dynamicTierSymbols,
mergeReports,
nmOutput,
programInputsHash,
programLines,
programVerdict,
recordedBaseline,
timingVerdict,
} from "../../scripts/zero-cost-compare.mjs";
@@ -223,6 +225,30 @@ test("a recorded baseline settles identical programs and shared build errors", (
expect(cachedComparison(failed, { ok: true }, hashes)).toBeNull();
});
test("a recorded baseline applies only to the program inputs it was recorded for", () => {
const files: Record<string, string> = {
"/p/tests/corpus/a/main.ts": 'import { f } from "./lib.ts";',
"/p/tests/corpus/a/lib.ts": "export const f = 1;",
};
const read = (file: string) => Buffer.from(files[file]!);
const paths = Object.keys(files);
const inputs = programInputsHash("/p", paths, "release", read);
expect(programInputsHash("/p", [...paths].reverse(), "release", read)).toBe(inputs);
expect(programInputsHash("/p", paths, "speed", read)).not.toBe(inputs);
expect(programInputsHash("/q", paths, "release", read)).not.toBe(inputs);
// An edit to an import changes the hash as an edit to the entry does.
files["/p/tests/corpus/a/lib.ts"] = "export const f = 2;";
const edited = programInputsHash("/p", paths, "release", read);
expect(edited).not.toBe(inputs);
const entry = { inputs, ok: false, ms: 3, error: "SC1090" };
const cache = { programs: { "corpus/a/main.ts": entry } };
expect(recordedBaseline(cache, "corpus/a/main.ts", inputs)).toBe(entry);
// A record of the old sources must not settle the new sources as both-fail or identical.
expect(recordedBaseline(cache, "corpus/a/main.ts", edited)).toBeUndefined();
expect(recordedBaseline(cache, "corpus/b/main.ts", inputs)).toBeUndefined();
expect(recordedBaseline(null, "corpus/a/main.ts", inputs)).toBeUndefined();
});
test("merged gate shards judge compile time and comparisons over every shard", () => {
const trees = { baseline: { revision: "a" }, candidate: { revision: "b" } };
const row = (id: string, ratio: number) => ({