mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
Release executables now import small runtime functions from runtime bitcode, as speed executables do, so LLVM can inline them into program code. Release units keep the runtime compiler's own objects, unchanged, and additionally ship the bitcode of the same compilation reduced by the helper's prune-import. Their unit-local symbols stay local, so only bodies that need none of them can be imported, which keeps the import small: about 1.3 MB per program. Only unsanitized programs small enough for one codegen partition import. Every partition reads the bitcode again; on the Effect fixtures (64 partitions) import cost +2..18% compile time and +4.3..4.9% size, so partitioned release builds stay exactly as before. Speed is unchanged. Measured against main in Linux Vercel Sandboxes, same-VM interleaved medians (bench-runtime --layouts=4 --runs=16), on two VMs: - bench-runtime (31 workloads): -1.3% (8 faster, 0 slower) and -1.0% (1 slower, typed-array-scan-workers +3.2%); size +0.0% - js-compiled suite (22 programs): -2.2% and -1.8%; size -0.1% - cold builds of all 53 programs, interleaved: +3.1%; helper time per build +40..235 ms - Effect fixtures: byte-identical to main
126 lines
4.8 KiB
JavaScript
126 lines
4.8 KiB
JavaScript
import { execFile } from "node:child_process";
|
|
import { access, constants, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
const run = promisify(execFile);
|
|
|
|
/* Executable runtime units ship the bitcode the LLVM helper imports small
|
|
* runtime functions from (available_externally, so program code can inline
|
|
* them while the unit's object stays the only linked definition).
|
|
*
|
|
* Speed units produce both artifacts from one optimized LLVM module: the
|
|
* runtime compiler optimizes the C source to bitcode, and the host helper's
|
|
* `runtime-unit` command promotes translation-unit-local symbols to hidden
|
|
* unit-qualified globals and emits the object and the bitcode. Release
|
|
* units keep the runtime compiler's own objects; their bitcode is the same
|
|
* compilation with -emit-llvm, which the helper's `prune-import` reduces to
|
|
* the bodies that need none of the unit's local state. Both bitcode forms
|
|
* keep only what a program build can import.
|
|
*
|
|
* Every host helper supports every scriptc target, so packs keep building
|
|
* on their usual hosts. Without a runnable host helper (or with
|
|
* SCRIPTC_RUNTIME_BITCODE=0) units are compiled to objects directly and
|
|
* ship no bitcode; program builds then simply import nothing. */
|
|
|
|
function hostHelperPackage() {
|
|
if (process.platform === "darwin") return `llvm-darwin-${process.arch}`;
|
|
if (process.platform === "win32") return `llvm-win32-${process.arch}-msvc`;
|
|
if (process.platform === "linux") {
|
|
const musl = process.report?.getReport?.().header?.glibcVersionRuntime === undefined;
|
|
return `llvm-linux-${process.arch}-${musl ? "musl" : "gnu"}`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function resolveRuntimeUnitHelper(repoRoot, triple) {
|
|
if (process.env.SCRIPTC_RUNTIME_BITCODE === "0") return null;
|
|
const explicit = process.env.SCRIPTC_LLVM_CODEGEN;
|
|
const pkg = hostHelperPackage();
|
|
const helper =
|
|
explicit ??
|
|
(pkg === null
|
|
? null
|
|
: join(
|
|
repoRoot,
|
|
"packages",
|
|
pkg,
|
|
"bin",
|
|
process.platform === "win32" ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen",
|
|
));
|
|
if (helper === null) return null;
|
|
try {
|
|
await access(helper, constants.X_OK);
|
|
const { stdout } = await run(helper, ["version", "--format=json"]);
|
|
const version = JSON.parse(stdout);
|
|
if (!version.supported_targets?.includes(triple)) return null;
|
|
return { path: helper, identity: `scriptc-llvm-codegen ${version.llvm_version}` };
|
|
} catch {
|
|
if (explicit !== undefined) throw new Error(`SCRIPTC_LLVM_CODEGEN is not usable: ${helper}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Compile one unit through the helper. `compileBitcode(output)` must run
|
|
* the runtime compiler with the unit's exact flags plus -emit-llvm.
|
|
* Returns false, leaving no artifacts behind, when the helper cannot emit
|
|
* this unit; the caller then compiles the object directly without bitcode,
|
|
* so a pack never fails to build because of the import path. */
|
|
export async function buildRuntimeUnit({
|
|
helper,
|
|
compileBitcode,
|
|
object,
|
|
bitcode,
|
|
triple,
|
|
tag,
|
|
sections,
|
|
}) {
|
|
const optimized = object.replace(/\.o$/, ".opt.bc");
|
|
// A runtime compiler failure is a real build error and surfaces as one.
|
|
await compileBitcode(optimized);
|
|
try {
|
|
await run(helper.path, [
|
|
"runtime-unit",
|
|
"--input",
|
|
optimized,
|
|
"--object",
|
|
object,
|
|
"--bitcode",
|
|
bitcode,
|
|
"--target",
|
|
triple,
|
|
"--tag",
|
|
tag,
|
|
...(sections ? ["--function-sections", "--data-sections"] : []),
|
|
]);
|
|
return true;
|
|
} catch (error) {
|
|
const detail = (error.stderr || error.message || String(error)).trim().split("\n")[0];
|
|
process.stderr.write(`runtime-unit: ${tag}: emitting without import bitcode (${detail})\n`);
|
|
await Promise.all([rm(object, { force: true }), rm(bitcode, { force: true })]);
|
|
return false;
|
|
} finally {
|
|
await rm(optimized, { force: true });
|
|
}
|
|
}
|
|
|
|
/** Import bitcode for a unit whose object the runtime compiler emits
|
|
* itself (the release flavor). `compileBitcode(output)` must run the
|
|
* runtime compiler with the unit's exact flags plus -emit-llvm. A helper
|
|
* failure only drops the bitcode; the object is unaffected. */
|
|
export async function buildImportBitcode({ helper, compileBitcode, bitcode, tag }) {
|
|
const optimized = bitcode.replace(/\.bc$/, ".opt.bc");
|
|
await compileBitcode(optimized);
|
|
try {
|
|
await run(helper.path, ["prune-import", "--input", optimized, "--output", bitcode]);
|
|
return true;
|
|
} catch (error) {
|
|
const detail = (error.stderr || error.message || String(error)).trim().split("\n")[0];
|
|
process.stderr.write(`prune-import: ${tag}: shipping without import bitcode (${detail})\n`);
|
|
await rm(bitcode, { force: true });
|
|
return false;
|
|
} finally {
|
|
await rm(optimized, { force: true });
|
|
}
|
|
}
|