Files
scriptc/scripts/llvm-helper-inputs.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

115 lines
4.3 KiB
JavaScript

/* Inputs of the LLVM helper, captured once and replayed with any helper:
* scripts/refresh-llvm-profile.mjs trains the helper's profile on them and
* scripts/check-llvm-dist.mjs compares helpers on them. Each input is a
* program's LLVM text from `scriptc build --emit llvm` and the `emit`
* arguments the compiler passes for an executable build of it
* (native-codegen.ts), partitions included. The partition cache is left out,
* so every replay compiles every partition. */
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("..", import.meta.url));
const EFFECT_FIXTURES = [
"async-sleep",
"async-promise-typed",
"async-timeout",
"async-resume-timer",
"async-yield",
];
/** The programs: tsc-ts (when a checkout is given), five Effect fixtures,
* the runtime benchmark workloads, every 50th numbered corpus program, and
* `extra` directories of standalone programs. */
export function helperInputPrograms({ tscTs, extra = [] } = {}) {
const programs = [];
if (tscTs !== undefined)
programs.push({ name: "tsc-ts", cwd: join(tscTs, "packages/tsc-ts"), entry: "src/main.ts" });
for (const fixture of EFFECT_FIXTURES)
programs.push({
name: `effect-${fixture}`,
cwd: root,
entry: `tests/fixtures/effect/${fixture}.ts`,
args: ["--npm-static", "effect,fast-check,pure-rand"],
});
const suite = join(root, "benchmarks/runtime");
for (const file of readdirSync(join(suite, "workloads"))
.filter((f) => f.endsWith(".json"))
.sort()) {
const { entry } = JSON.parse(readFileSync(join(suite, "workloads", file), "utf8"));
// Variants of a workload share its entry and differ only in arguments.
if (!programs.some((program) => program.cwd === suite && program.entry === entry))
programs.push({ name: `wl-${file.slice(0, -5)}`, cwd: suite, entry });
}
const corpus = join(root, "tests/corpus");
for (const file of readdirSync(corpus).sort()) {
const number = /^(\d+)-.*\.ts$/.exec(file)?.[1];
if (number !== undefined && Number(number) % 50 === 0)
programs.push({ name: `corpus-${file.slice(0, -3)}`, cwd: corpus, entry: file });
}
for (const dir of extra)
for (const file of readdirSync(dir)
.filter((f) => f.endsWith(".ts"))
.sort())
programs.push({ name: `extra-${file.slice(0, -3)}`, cwd: dir, entry: file });
return programs;
}
/** Writes `<dir>/<name>.ll` for each program with this checkout's compiler
* and returns the inputs that compiled: `{ name, input, outputs, args(out) }`,
* where `args(out)` gives the helper's arguments writing into `out`. */
export async function captureHelperInputs(programs, dir) {
const { nativeProgramPartitions } = await import(
join(root, "packages/compiler/dist/backend/native-codegen-core.js")
);
const cli = join(root, "packages/cli/dist/main.js");
const target =
process.arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu";
mkdirSync(dir, { recursive: true });
const inputs = [];
for (const program of programs) {
const input = join(dir, `${program.name}.ll`);
if (!existsSync(input)) {
const built = spawnSync(
process.execPath,
[cli, "build", program.entry, "--emit", "llvm", "-o", input, ...(program.args ?? [])],
{ cwd: program.cwd, encoding: "utf8" },
);
if (built.status !== 0) {
console.log(`skipped ${program.name}: the compiler refused it`);
continue;
}
}
const partitions = nativeProgramPartitions(statSync(input).size);
const source = join(program.cwd, program.entry);
inputs.push({
name: program.name,
input,
partitions,
args: (out) => [
"emit",
"--input",
input,
...Array.from({ length: partitions }, (_, i) => [
"--output",
join(out, `${program.name}.part${i}.o`),
]).flat(),
"--filetype",
"obj",
"--target",
target,
"--opt-level",
"2",
"--relocation-model",
"pic",
"--diagnostic-format",
"json",
"--source-path",
source,
],
});
}
return inputs;
}