mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Keep the native compiler's LLVM text as the emitter's parts (#1919)
This commit is contained in:
@@ -483,7 +483,8 @@ function versionParameters(mod: IrModule): IrModule {
|
||||
return versionLoopBoundParameters(mod, mangleFunction);
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
/** The emitter for `input` after the module passes every emission runs. */
|
||||
function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
@@ -515,7 +516,19 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
).emit();
|
||||
);
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
return moduleEmitter(input, options).emit();
|
||||
}
|
||||
|
||||
/** emitLlvmModule's text as parts that concatenate to it, without the copy
|
||||
* that joining them makes: a large program's text is 150 MB. */
|
||||
export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] {
|
||||
return moduleEmitter(input, options)
|
||||
.emitParts()
|
||||
.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
}
|
||||
|
||||
/** Keep large translation units below the host's single-string limit. */
|
||||
@@ -525,35 +538,7 @@ export function emitLlvmModuleSource(
|
||||
): string | readonly string[] {
|
||||
// IR validation, contracts and the frontend's disposal run after lowering without checks.
|
||||
relieveHeapPressure(true);
|
||||
const mod = unshareNodes(input);
|
||||
const parts = new LlEmitter(
|
||||
options.debugSources === undefined
|
||||
? versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: mod,
|
||||
options,
|
||||
).emitParts();
|
||||
const parts = moduleEmitter(input, options).emitParts();
|
||||
const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
|
||||
if (length <= 256 * 1024 * 1024) return parts.join("\n");
|
||||
return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveBuildCacheRoot } from "../backend/cache-root.js";
|
||||
import { writeTextGeneratedSync } from "./text-file.js";
|
||||
import { joinedParts, writeTextGeneratedSync } from "./text-file.js";
|
||||
|
||||
/** UTF-8 bytes one native compiler string can hold (the runtime's
|
||||
* SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads
|
||||
@@ -28,6 +28,13 @@ export function contentDigest(value: string | Uint8Array): string {
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** contentDigest of the text the parts concatenate to. */
|
||||
export function partsDigest(parts: readonly string[]): string {
|
||||
const hash = createHash("sha256");
|
||||
for (const part of joinedParts(parts)) hash.update(part);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/** contentDigest of a file's bytes, read in 1 MiB blocks so a large file
|
||||
* (a module's LLVM text) is never held whole. */
|
||||
export function fileDigest(path: string): string {
|
||||
|
||||
@@ -362,7 +362,7 @@ export class NativeCompiler {
|
||||
mkdirSync(inputDirectory);
|
||||
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
|
||||
// In slices: the module text is never encoded whole.
|
||||
writeTextPartsSync(llvmInput, [llvm]);
|
||||
writeTextPartsSync(llvmInput, llvm);
|
||||
timing("llvm-write");
|
||||
if (outputKind === "llvm") {
|
||||
renameSync(stagedOutput, output);
|
||||
@@ -496,7 +496,7 @@ export class NativeCompiler {
|
||||
renameSync(stagedOutput + ".dSYM", output + ".dSYM");
|
||||
}
|
||||
mkdirSync(options.outDir, { recursive: true });
|
||||
writeTextPartsSync(llvmPath, [llvm]);
|
||||
writeTextPartsSync(llvmPath, llvm);
|
||||
let irPath: string | undefined;
|
||||
if (options.emitIr) {
|
||||
irPath = join(options.outDir, `${stem}.ir.json`);
|
||||
@@ -585,7 +585,7 @@ export class NativeCompiler {
|
||||
const llvm = prepared.llvm;
|
||||
const llvmInput = join(stage, "program.ll");
|
||||
// In slices: the module text is never encoded whole.
|
||||
writeTextPartsSync(llvmInput, [llvm]);
|
||||
writeTextPartsSync(llvmInput, llvm);
|
||||
timing("llvm-write");
|
||||
const features = prepared.features;
|
||||
const pack = selectNativeRuntimePack(
|
||||
@@ -648,7 +648,7 @@ export class NativeCompiler {
|
||||
});
|
||||
}
|
||||
timing("native-archive");
|
||||
writeTextPartsSync(llvmPath, [llvm]);
|
||||
writeTextPartsSync(llvmPath, llvm);
|
||||
let irPath: string | undefined;
|
||||
if (options.emitIr) {
|
||||
irPath = join(options.outDir, `${stem}.lib.ir.json`);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { runNativeTool } from "../backend/native-tools.js";
|
||||
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { contentDigest, NativeCache } from "./cache.js";
|
||||
import { contentDigest, NativeCache, partsDigest } from "./cache.js";
|
||||
|
||||
interface InputIdentity {
|
||||
path: string;
|
||||
@@ -254,7 +254,7 @@ export class NativeExecutableCache {
|
||||
export function openNativeExecutableCache(
|
||||
cache: NativeCache | null,
|
||||
toolchain: NativeToolchain,
|
||||
llvm: string,
|
||||
llvm: readonly string[],
|
||||
options: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
pack: NativeRuntimeSelection,
|
||||
@@ -299,7 +299,7 @@ export function openNativeExecutableCache(
|
||||
const key = contentDigest(
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
llvm: contentDigest(llvm),
|
||||
llvm: partsDigest(llvm),
|
||||
options,
|
||||
toolchain,
|
||||
runtimeIdentity: pack.packageText,
|
||||
|
||||
@@ -11,6 +11,12 @@ import { NativeCache } from "./cache.js";
|
||||
import { prepareNativeExecutable } from "./prepare.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
|
||||
/** A prepared input with its LLVM parts joined: a cache hit reads the text
|
||||
* back as one part. */
|
||||
function text(input: ReturnType<ReturnType<typeof fixture>["prepare"]>) {
|
||||
return input.ok ? { ...input, llvm: input.llvm.join("") } : input;
|
||||
}
|
||||
|
||||
const scratch: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true });
|
||||
@@ -68,14 +74,14 @@ test("warm frontend reuse survives output creation and invalidates imported sour
|
||||
if (!first.ok) return;
|
||||
mkdirSync(f.options.outDir);
|
||||
writeFileSync(f.options.outPath, "binary placeholder");
|
||||
writeFileSync(join(f.options.outDir, "main.ll"), first.llvm);
|
||||
writeFileSync(join(f.options.outDir, "main.ll"), first.llvm.join(""));
|
||||
const warm = f.prepare();
|
||||
expect(warm).toEqual(first);
|
||||
expect(text(warm)).toEqual(text(first));
|
||||
expect(f.loads()).toBe(1);
|
||||
writeFileSync(f.dependency, 'export const value = "second";\n');
|
||||
const edited = f.prepare();
|
||||
expect(edited.ok).toBe(true);
|
||||
if (edited.ok) expect(edited.llvm).not.toBe(first.llvm);
|
||||
if (edited.ok) expect(edited.llvm.join("")).not.toBe(first.llvm.join(""));
|
||||
expect(f.loads()).toBe(2);
|
||||
});
|
||||
|
||||
@@ -101,13 +107,13 @@ test("serialized IR travels as bounded parts through the frontend cache", () =>
|
||||
expect(mod.functions.length).toBeGreaterThan(0);
|
||||
const warm = f.prepare({ emitIr: true });
|
||||
expect(f.loads()).toBe(1);
|
||||
expect(warm).toEqual(first);
|
||||
expect(text(warm)).toEqual(text(first));
|
||||
const family = join(f.cache.root, "frontend");
|
||||
const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!;
|
||||
const payload = JSON.parse(readFileSync(join(family, key), "utf8"));
|
||||
expect(payload.schema).toBe("scriptc.native-frontend.v3");
|
||||
expect(payload.input.llvm).toBe("");
|
||||
expect(payload.input.ir).toBeNull();
|
||||
expect(payload.llvm).toBe(first.llvm);
|
||||
expect(payload.llvm).toBe(first.llvm.join(""));
|
||||
expect(payload.ir).toEqual(first.ir);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ import type {
|
||||
CompileLibraryOptions,
|
||||
CompileRequestOptions,
|
||||
} from "../compile-types.js";
|
||||
import { emitLlvmModule, type ForkPrimitives, lowRegionTarget } from "../backend/llvm/emitter.js";
|
||||
import {
|
||||
emitLlvmModuleParts,
|
||||
type ForkPrimitives,
|
||||
lowRegionTarget,
|
||||
} from "../backend/llvm/emitter.js";
|
||||
import { inlineRcOption } from "../backend/llvm/shapes.js";
|
||||
import { executableLinkFeatures } from "../backend/executable-features.js";
|
||||
import type { NativeLinkFeatures } from "../backend/native-link-info-core.js";
|
||||
@@ -29,13 +33,15 @@ import type { LibraryProfile } from "../library/library-profile.js";
|
||||
import type { IrModule } from "../ir/ir.js";
|
||||
import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js";
|
||||
import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js";
|
||||
import { batchTextParts, textSlices } from "./text-file.js";
|
||||
import { batchTextParts, joinedParts, textSlices } from "./text-file.js";
|
||||
import type { NativeToolchain } from "./toolchain.js";
|
||||
import { nativeFileIdentity } from "./file-identity.js";
|
||||
|
||||
export interface NativeExecutableInput {
|
||||
ok: true;
|
||||
llvm: string;
|
||||
/** The module's LLVM text as parts that concatenate to it
|
||||
* (emitLlvmModuleParts), never joined: a large program's is 150 MB. */
|
||||
llvm: readonly string[];
|
||||
/** Compact serialized IR as bounded parts (`batchTextParts`): compiler-scale
|
||||
* modules exceed one native string. */
|
||||
ir: string[] | null;
|
||||
@@ -71,7 +77,8 @@ function* frontendCacheParts(
|
||||
input: { ...input, llvm: "", ir: null },
|
||||
});
|
||||
yield metadata.slice(0, metadata.length - 1) + ',"llvm":"';
|
||||
for (const slice of textSlices(input.llvm)) yield JSON.stringify(slice).slice(1, -1);
|
||||
for (const part of joinedParts(input.llvm))
|
||||
for (const slice of textSlices(part)) yield JSON.stringify(slice).slice(1, -1);
|
||||
yield '"';
|
||||
if (input.ir === null) yield ',"ir":null}';
|
||||
else {
|
||||
@@ -122,7 +129,7 @@ function readFrontendCache(
|
||||
const cached: unknown = JSON.parse(bytes.toString("utf8"));
|
||||
if (!validCachedInput(cached) || !frontendInputsStillMatch(cached.probes, exclusions))
|
||||
return null;
|
||||
return { ...cached.input, llvm: cached.llvm, ir: cached.ir };
|
||||
return { ...cached.input, llvm: [cached.llvm], ir: cached.ir };
|
||||
}
|
||||
|
||||
function implementationIdentity(toolchain: NativeToolchain): string {
|
||||
@@ -310,8 +317,8 @@ export function prepareNativeExecutable(
|
||||
timing("ir-serialize");
|
||||
const llvm =
|
||||
outputKind === "ir"
|
||||
? ""
|
||||
: emitLlvmModule(prepared.mod, {
|
||||
? []
|
||||
: emitLlvmModuleParts(prepared.mod, {
|
||||
targetTriple: toolchain.target.llvmTriple,
|
||||
pointerBits: toolchain.target.pointerBits,
|
||||
wasi: toolchain.target.platform === "wasi",
|
||||
@@ -427,7 +434,7 @@ export function prepareNativeLibrary(
|
||||
);
|
||||
});
|
||||
if (!prepared.ok) return prepared;
|
||||
const llvm = emitLlvmModule(prepared.mod, {
|
||||
const llvm = emitLlvmModuleParts(prepared.mod, {
|
||||
targetTriple: toolchain.target.llvmTriple,
|
||||
pointerBits: toolchain.target.pointerBits,
|
||||
wasi: toolchain.target.platform === "wasi",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { textSlices, writeTextPartsSync } from "./text-file.js";
|
||||
import { joinedParts, textSlices, writeTextPartsSync } from "./text-file.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
@@ -40,3 +40,13 @@ test("a part larger than one slice is written as its UTF-8 bytes", () => {
|
||||
expect(seen.join("")).toBe("head," + large + "tail");
|
||||
expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024);
|
||||
});
|
||||
|
||||
test("small parts join into batches and a long part passes through whole", () => {
|
||||
const small = Array.from({ length: 3000 }, (_, index) => `line ${index}\n`);
|
||||
const long = "x".repeat(2 * 1024 * 1024);
|
||||
const parts = [...small, long, "tail"];
|
||||
const joined = [...joinedParts(parts)];
|
||||
expect(joined.join("")).toBe(parts.join(""));
|
||||
expect(joined.length).toBeLessThan(5);
|
||||
expect(joined).toContain("tail");
|
||||
});
|
||||
|
||||
@@ -42,6 +42,24 @@ export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator
|
||||
}
|
||||
}
|
||||
|
||||
/** `parts` with consecutive parts joined into strings of about
|
||||
* TEXT_WRITE_LENGTH UTF-16 units (a longer part passes through whole), so
|
||||
* writing or escaping many small parts takes few calls. */
|
||||
export function* joinedParts(parts: readonly string[]): Generator<string> {
|
||||
let batch: string[] = [];
|
||||
let length = 0;
|
||||
for (const part of parts) {
|
||||
batch.push(part);
|
||||
length += part.length;
|
||||
if (length >= TEXT_WRITE_LENGTH) {
|
||||
yield batch.length === 1 ? batch[0]! : batch.join("");
|
||||
batch = [];
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
if (batch.length > 0) yield batch.join("");
|
||||
}
|
||||
|
||||
/** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large
|
||||
* part is never encoded whole. `written` sees each slice after it is
|
||||
* written. */
|
||||
@@ -55,8 +73,8 @@ function writeTextPart(fd: number, part: string, written?: (slice: string) => vo
|
||||
}
|
||||
}
|
||||
|
||||
/** Write text parts to `path` in order without joining them first; pass
|
||||
* batched parts (`batchTextParts`) for many small ones. */
|
||||
/** Write text parts to `path` in order without joining them whole: small
|
||||
* parts are written in batches (`joinedParts`). */
|
||||
export function writeTextPartsSync(
|
||||
path: string,
|
||||
parts: readonly string[],
|
||||
@@ -64,7 +82,7 @@ export function writeTextPartsSync(
|
||||
): void {
|
||||
const fd = openSync(path, "w");
|
||||
try {
|
||||
for (const part of parts) writeTextPart(fd, part, written);
|
||||
for (const part of joinedParts(parts)) writeTextPart(fd, part, written);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user