mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user