mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
206 lines
7.5 KiB
JavaScript
206 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/* Refreshes packages/compiler/native/cli.profdata.gz, the profile that release
|
|
* builds of the native compiler optimize with (scripts/build-native-cli.mts).
|
|
*
|
|
* Builds an instrumented native compiler for this Linux host, runs it on the
|
|
* training builds below, and merges the raw profiles it writes into one
|
|
* sparse indexed profile. The training builds are tsc-ts (a cold `speed`
|
|
* build, then a rebuild after an edit), every seventh corpus program at the
|
|
* default optimization and a further slice in `dev`, and the runtime
|
|
* benchmark workloads. Run it on a quiet Linux x64 host after changes that
|
|
* move much compiler code; a stale profile only loses part of its gain. A
|
|
* refresh takes about 11 minutes on 16 cores.
|
|
*
|
|
* node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout> [--jobs 8]
|
|
*/
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import {
|
|
appendFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { cpus, tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
import { constants, gzipSync } from "node:zlib";
|
|
|
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
const { values } = parseArgs({
|
|
options: {
|
|
"tsc-ts": { type: "string" },
|
|
jobs: { type: "string", default: String(Math.max(1, Math.floor(cpus().length / 2))) },
|
|
output: { type: "string", default: join(root, "packages/compiler/native/cli.profdata.gz") },
|
|
work: { type: "string" },
|
|
instrumented: { type: "string" },
|
|
help: { type: "boolean", default: false },
|
|
},
|
|
});
|
|
if (values.help || values["tsc-ts"] === undefined) {
|
|
console.log(
|
|
"Usage: node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout> [--jobs N] [--output <profdata.gz>] [--work <dir>] [--instrumented <compiler dir>]",
|
|
);
|
|
process.exit(values.help ? 0 : 2);
|
|
}
|
|
if (process.platform !== "linux" || process.arch !== "x64")
|
|
throw new Error("the compiler profile is collected on Linux x64");
|
|
const tscTs = resolve(values["tsc-ts"]);
|
|
const tscTsPackage = join(tscTs, "packages/tsc-ts");
|
|
if (!existsSync(join(tscTsPackage, "src/main.ts")))
|
|
throw new Error(`${tscTs} is not a tsc-ts checkout with installed dependencies`);
|
|
const jobs = Number(values.jobs);
|
|
const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-cli-profile-")));
|
|
const raw = join(work, "raw");
|
|
const builds = join(work, "builds");
|
|
rmSync(raw, { recursive: true, force: true });
|
|
for (const dir of [raw, builds]) mkdirSync(dir, { recursive: true });
|
|
const log = join(work, "training.log");
|
|
|
|
// Every run merges its counters into one of four pooled raw profiles.
|
|
const profileFile = join(raw, "cli-%4m.profraw");
|
|
// --instrumented reuses a compiler an earlier run built in <work>/cli.
|
|
const cli = resolve(values.instrumented ?? join(work, "cli"));
|
|
if (values.instrumented === undefined) {
|
|
console.log(`building the instrumented compiler in ${cli}`);
|
|
const built = spawnSync(
|
|
process.execPath,
|
|
[
|
|
"--max-old-space-size=8192",
|
|
"--import",
|
|
"tsx",
|
|
join(root, "scripts/build-native-cli.mts"),
|
|
cli,
|
|
],
|
|
{
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
env: { ...process.env, SCRIPTC_NATIVE_PROFILE_GENERATE: profileFile },
|
|
},
|
|
);
|
|
if (built.status !== 0) throw new Error("the instrumented compiler did not build");
|
|
}
|
|
const scriptc = join(cli, "bin/scriptc");
|
|
// The compiler trains with the helper it bundles, so a helper override in
|
|
// the environment does not apply.
|
|
const trainingEnv = { ...process.env, LLVM_PROFILE_FILE: profileFile };
|
|
delete trainingEnv["SCRIPTC_LLVM_HELPER"];
|
|
|
|
/** Runs the instrumented compiler; a failing build still profiles the
|
|
* paths it took. */
|
|
function run(args, options) {
|
|
return new Promise((done) => {
|
|
const child = spawn(scriptc, args, {
|
|
cwd: options.cwd,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: { ...trainingEnv, SCRIPTC_CACHE_DIR: options.cache },
|
|
});
|
|
let output = "";
|
|
child.stdout.on("data", (chunk) => (output += chunk));
|
|
child.stderr.on("data", (chunk) => (output += chunk));
|
|
child.on("close", (code) => {
|
|
appendFileSync(log, `$ scriptc ${args.join(" ")}\n${output}exit ${code}\n`);
|
|
done(code === 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function pool(tasks) {
|
|
let next = 0;
|
|
let passed = 0;
|
|
await Promise.all(
|
|
Array.from({ length: jobs }, async () => {
|
|
while (next < tasks.length) if (await tasks[next++]()) passed++;
|
|
}),
|
|
);
|
|
return passed;
|
|
}
|
|
|
|
const programNumber = (name) => Number.parseInt(name, 10);
|
|
const corpus = readdirSync(join(root, "tests/corpus"))
|
|
.filter((name) => /^\d+-.*\.ts$/.test(name))
|
|
.sort((a, b) => programNumber(a) - programNumber(b));
|
|
const training = [
|
|
...corpus
|
|
.filter((name) => programNumber(name) % 7 === 0)
|
|
.map((name) => ({ file: join(root, "tests/corpus", name), optimization: "release" })),
|
|
...corpus
|
|
.filter((name) => programNumber(name) % 14 === 7)
|
|
.map((name) => ({ file: join(root, "tests/corpus", name), optimization: "dev" })),
|
|
...readdirSync(join(root, "benchmarks/runtime/workloads"))
|
|
.filter((name) => name.endsWith(".ts"))
|
|
.map((name) => ({
|
|
file: join(root, "benchmarks/runtime/workloads", name),
|
|
optimization: "release",
|
|
})),
|
|
];
|
|
|
|
const started = Date.now();
|
|
console.log("training: tsc-ts, cold and after an edit");
|
|
const tscCache = join(work, "cache-tsc-ts");
|
|
const tscArgs = [
|
|
"build",
|
|
"src/main.ts",
|
|
"--optimization=speed",
|
|
"--detached-exit",
|
|
"-o",
|
|
join(builds, "tsc"),
|
|
];
|
|
await run(tscArgs, { cwd: tscTsPackage, cache: tscCache });
|
|
const edited = join(tscTsPackage, "src/main.ts");
|
|
const source = readFileSync(edited, "utf8");
|
|
try {
|
|
writeFileSync(edited, source + "\n// profile training edit\n");
|
|
await run(tscArgs, { cwd: tscTsPackage, cache: tscCache });
|
|
} finally {
|
|
writeFileSync(edited, source);
|
|
}
|
|
|
|
console.log(`training: ${training.length} programs, ${jobs} at a time`);
|
|
const passed = await pool(
|
|
training.map((program, index) => () => {
|
|
const cache = join(work, `cache-${index}`);
|
|
return run(
|
|
[
|
|
"build",
|
|
program.file,
|
|
`--optimization=${program.optimization}`,
|
|
"-o",
|
|
join(builds, `program-${index}`),
|
|
],
|
|
{ cwd: root, cache },
|
|
).then((ok) => {
|
|
rmSync(cache, { recursive: true, force: true });
|
|
rmSync(join(builds, `program-${index}`), { force: true });
|
|
return ok;
|
|
});
|
|
}),
|
|
);
|
|
console.log(
|
|
`trained in ${Math.round((Date.now() - started) / 1000)} s; ${passed} of ${training.length} programs built (failures still profile; see ${log})`,
|
|
);
|
|
|
|
const profiles = readdirSync(raw)
|
|
.filter((name) => name.endsWith(".profraw"))
|
|
.map((name) => join(raw, name));
|
|
rmSync(builds, { recursive: true, force: true });
|
|
const indexed = join(work, "cli.profdata");
|
|
const merged = spawnSync(scriptc, ["merge-profiles", "--sparse", "-o", indexed, ...profiles], {
|
|
stdio: "inherit",
|
|
env: { ...trainingEnv, LLVM_PROFILE_FILE: join(work, "merge.profraw") },
|
|
});
|
|
if (merged.status !== 0) throw new Error("merge-profiles failed");
|
|
// The indexed profile compresses to about an eighth of its size.
|
|
const output = resolve(values.output);
|
|
writeFileSync(output, gzipSync(readFileSync(indexed), { level: constants.Z_BEST_COMPRESSION }));
|
|
console.log(
|
|
`wrote ${output} (${statSync(output).size} bytes; ${statSync(indexed).size} uncompressed)`,
|
|
);
|
|
// A work directory the caller named stays for inspection.
|
|
if (values.work === undefined) rmSync(work, { recursive: true, force: true });
|