Files
scriptc/scripts/check-llvm-dist.mjs
T
Chris Tate 4c3a1d9c98 Link the Linux release helpers against LLVM built with ThinLTO and a helper profile
The Linux helpers linked LLVM 22.1.8's release archive. The helper's code
generation is LLVM's code, so building that LLVM from its release sources
with ThinLTO and with a profile of the helper's own work makes code
generation 5-9% faster than the helper as released, with identical objects.

- native/llvm-codegen/llvm.profdata.gz is the profile, collected by
  scripts/refresh-llvm-profile.mjs: it builds LLVM with IR instrumentation
  and an instrumented helper, and runs that helper on tsc-ts, five Effect
  fixtures, the runtime benchmark workloads and every 50th corpus program
  (scripts/llvm-helper-inputs.mjs). It follows LLVM's sources, so it needs
  a refresh when LLVM is bumped.
- scripts/build-llvm-dist.mjs builds LLVM with that profile and ThinLTO,
  and packs it with a checksum. The llvm-dist workflow, run by hand, builds
  it for x64 and arm64, requires a helper linked against it to give the
  same objects as one linked against the release archive
  (scripts/check-llvm-dist.mjs), prints both helpers' times and the
  profile's mismatch count, and attaches the archive to the LLVM_DIST_TAG
  release of this repository.
- The release workflow's Linux jobs, and the wasm32-wasi job, which also
  uploads the x64 GNU helper, download that archive and build the helper
  with SCRIPTC_LLVM_THINLTO=1: clang, lld and ThinLTO, and the profile for
  the helper's own sources. Without the archive, or if that build fails,
  the helper links the release archive as before.
- The four Linux helper packages share their build in
  native/llvm-codegen/linux-helper.mjs. Without SCRIPTC_LLVM_THINLTO it
  builds the same executable as before.
2026-10-25 18:53:25 -05:00

106 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
/* Compares two LLVM helpers on the helper inputs of this checkout
* (scripts/llvm-helper-inputs.mjs): every input must give identical objects
* with both, and the script prints each input group's median wall time per
* helper over the rounds, with the candidate/baseline ratio. The llvm-dist
* workflow runs it with the helper linked against LLVM's release archive as
* the baseline and the one linked against the profiled ThinLTO distribution
* as the candidate, on each architecture it builds. Exits with status 1 when
* an object differs.
*
* node scripts/check-llvm-dist.mjs --baseline <helper> --candidate <helper>
* [--rounds N] [--work <dir>] [--tsc-ts <checkout>] [--extra <dir of .ts programs>]
*/
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { performance } from "node:perf_hooks";
import { parseArgs } from "node:util";
import { captureHelperInputs, helperInputPrograms } from "./llvm-helper-inputs.mjs";
const { values } = parseArgs({
options: {
baseline: { type: "string" },
candidate: { type: "string" },
rounds: { type: "string", default: "3" },
work: { type: "string" },
"tsc-ts": { type: "string" },
extra: { type: "string", multiple: true, default: [] },
help: { type: "boolean", default: false },
},
});
if (values.help || values.baseline === undefined || values.candidate === undefined) {
console.log(
"Usage: node scripts/check-llvm-dist.mjs --baseline <helper> --candidate <helper> [--rounds N] [--work <dir>] [--tsc-ts <checkout>] [--extra <dir>]",
);
process.exit(values.help ? 0 : 2);
}
const helpers = { baseline: resolve(values.baseline), candidate: resolve(values.candidate) };
const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-llvm-check-")));
const inputs = await captureHelperInputs(
helperInputPrograms({
tscTs: values["tsc-ts"] === undefined ? undefined : resolve(values["tsc-ts"]),
extra: values.extra.map((dir) => resolve(dir)),
}),
join(work, "inputs"),
);
function emit(helper, input, out) {
rmSync(out, { recursive: true, force: true });
mkdirSync(out, { recursive: true });
const start = performance.now();
const result = spawnSync(helper, input.args(out), { encoding: "utf8" });
const ms = performance.now() - start;
if (result.status !== 0)
throw new Error(`${helper} failed on ${input.name}: ${result.stdout}${result.stderr}`);
return ms;
}
const times = new Map(); // `${group}|${helper}` -> per-round totals
const group = (name) => (name === "tsc-ts" ? name : name.split("-")[0]);
const rounds = Number(values.rounds);
let differing = 0;
for (let round = 0; round < rounds; round++) {
const order = round % 2 === 0 ? ["baseline", "candidate"] : ["candidate", "baseline"];
for (const input of inputs) {
for (const which of order) {
const ms = emit(helpers[which], input, join(work, which));
const key = `${group(input.name)}|${which}`;
const totals = times.get(key) ?? new Array(rounds).fill(0);
totals[round] += ms;
times.set(key, totals);
}
if (round === 0) {
for (const file of readdirSync(join(work, "baseline"))) {
const a = readFileSync(join(work, "baseline", file));
const b = readFileSync(join(work, "candidate", file));
if (!a.equals(b)) {
differing++;
console.log(`${input.name}: ${file} differs between the helpers`);
}
}
}
}
}
const median = (xs) => {
const s = [...xs].sort((x, y) => x - y);
return s.length % 2 ? s[s.length >> 1] : (s[s.length / 2 - 1] + s[s.length / 2]) / 2;
};
console.log(`${inputs.length} inputs, ${rounds} rounds; median wall time per group`);
console.log("| group | baseline | candidate | candidate/baseline |");
console.log("| --- | ---: | ---: | ---: |");
for (const name of [...new Set(inputs.map((input) => group(input.name)))]) {
const b = median(times.get(`${name}|baseline`)),
c = median(times.get(`${name}|candidate`));
console.log(
`| ${name} | ${(b / 1000).toFixed(2)} s | ${(c / 1000).toFixed(2)} s | ${(c / b).toFixed(3)} |`,
);
}
if (differing > 0) {
console.log(`${differing} objects differ between the helpers`);
process.exit(1);
}
console.log("every object is identical between the helpers");