mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
The LLVM helper's runtime-unit now optimizes a unit itself (--opt-level), with IR instrumentation (--profile-generate) or a profile (--profile-use), through the same pipeline as program code. The Linux x64 pack's speed units are compiled to unoptimized bitcode and optimized there with packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz, so one LLVM writes and reads every profile. Release and library flavors are unchanged. The pack manifest records how the profile matched the units. Code the runtime profile finds cold is optimized for speed, not size: a runtime function the training programs rarely call can be hot in another program. scr_message.c is optimized without the profile. scripts/refresh-runtime-profile.mjs trains the profile on the runtime benchmark workloads and, optionally, a tsc-ts checkout, and keeps only the records of functions the instrumented runtime units define (merge-profiles --keep-functions). Tests check that the profile still matches the runtime and names only runtime functions.
99 lines
4.5 KiB
TypeScript
99 lines
4.5 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { gunzipSync } from "node:zlib";
|
|
import { expect, test } from "vitest";
|
|
import { resolveRuntimeUnitHelper } from "../scripts/runtime-unit.mjs";
|
|
|
|
const repoRoot = fileURLToPath(new URL("../../..", import.meta.url));
|
|
const committedProfile = join(
|
|
repoRoot,
|
|
"packages/runtime-pack-common/profiles/linux-x64-gnu.profdata.gz",
|
|
);
|
|
const manifestPath = join(repoRoot, "packages/runtime-linux-x64-gnu/runtime-pack.json");
|
|
const runtimeRoot = join(repoRoot, "packages/runtime");
|
|
|
|
// Both tests read the committed profile through an LLVM helper new enough to
|
|
// apply it; the pack test also needs the Linux x64 pack built with it.
|
|
const helper =
|
|
process.platform === "linux" && process.arch === "x64" && existsSync(committedProfile)
|
|
? await resolveRuntimeUnitHelper(repoRoot, "x86_64-unknown-linux-gnu")
|
|
: null;
|
|
const built =
|
|
helper?.features.includes("runtime-unit-profile") === true && existsSync(manifestPath);
|
|
|
|
// A profile older than the runtime sources loses the counts of every function
|
|
// whose control flow changed since it was collected. More than this share of
|
|
// the functions it describes means it is due for scripts/refresh-runtime-profile.mjs.
|
|
const MAX_MISMATCHED_SHARE = 0.1;
|
|
|
|
test.skipIf(!built)("the committed runtime profile still matches the runtime's speed units", () => {
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
const match = manifest.flavors?.speed?.runtime_profile;
|
|
expect(match, "the speed flavor was built without the committed runtime profile").toBeDefined();
|
|
const described = match.annotated + match.mismatched;
|
|
expect(match.annotated).toBeGreaterThan(0);
|
|
expect(
|
|
match.mismatched / described,
|
|
`${match.mismatched} of the ${described} runtime functions the profile describes changed since it was collected; run node scripts/refresh-runtime-profile.mjs`,
|
|
).toBeLessThanOrEqual(MAX_MISMATCHED_SHARE);
|
|
});
|
|
|
|
// The committed profile holds the runtime's records only. Training programs'
|
|
// functions, and the source paths their names can carry, stay out of the
|
|
// repository: refresh-runtime-profile.mjs keeps the functions the
|
|
// instrumented runtime units define.
|
|
test.skipIf(helper?.features.includes("profile-functions") !== true)(
|
|
"the committed runtime profile names only runtime functions",
|
|
() => {
|
|
const profile = gunzipSync(readFileSync(committedProfile));
|
|
const scratch = mkdtempSync(join(tmpdir(), "scriptc-runtime-profile-"));
|
|
let functions: string[];
|
|
try {
|
|
const unpacked = join(scratch, "runtime.profdata");
|
|
writeFileSync(unpacked, profile);
|
|
({ functions } = JSON.parse(
|
|
execFileSync(helper!.path, ["profile-functions", "--input", unpacked], {
|
|
encoding: "utf8",
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
}),
|
|
));
|
|
} finally {
|
|
rmSync(scratch, { recursive: true, force: true });
|
|
}
|
|
expect(functions.length).toBeGreaterThan(0);
|
|
|
|
const sources = new Set(readdirSync(join(runtimeRoot, "src")));
|
|
const identifiers = new Set<string>();
|
|
const collect = (dir: string) => {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name);
|
|
if (entry.isDirectory()) collect(path);
|
|
else if (/\.(c|h|inc|def)$/.test(entry.name))
|
|
for (const name of readFileSync(path, "latin1").match(/[A-Za-z_]\w*/g) ?? [])
|
|
identifiers.add(name);
|
|
}
|
|
};
|
|
collect(join(runtimeRoot, "src"));
|
|
collect(join(runtimeRoot, "vendor"));
|
|
|
|
// A unit's internal functions are named "<unit>.c;<name>" after its
|
|
// runtime source, and its external ones by their C names, which are never
|
|
// a program's `main` or generated `sc_` symbols.
|
|
const foreign = functions.filter((name) => {
|
|
if (/[/\\]/.test(name)) return true;
|
|
const local = /^([\w-]+\.c);(.+)$/.exec(name);
|
|
if (local !== null) return !sources.has(local[1]!);
|
|
return name === "main" || name.startsWith("sc_") || !identifiers.has(name);
|
|
});
|
|
expect(foreign, "records that are not runtime functions").toEqual([]);
|
|
|
|
// Nor does any other part of the file hold a path, such as a checkout's.
|
|
const text = profile.toString("latin1");
|
|
const paths = text.match(/(?:\/[\w.-]+){3,}|[A-Za-z]:\\[\w.-]+\\|\.tsx?;/g) ?? [];
|
|
expect(paths, "path strings in the committed profile").toEqual([]);
|
|
},
|
|
);
|