mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Read cached LLVM in parts on warm builds
A warm build that finds the program's LLVM in the cache read the file into one string before emitting the program object. A program whose LLVM is past the engine's string limit (three.js nodes, 816 MB) failed there with `RangeError: Invalid string length`, though its cold build, which keeps the LLVM in parts, succeeds. The cache hit now reads the file with readTextParts: parts of at most 64 Mi UTF-16 units, decoded across reads, none ending inside a surrogate pair, which the emitter takes as a cold build hands them over.
This commit is contained in:
@@ -112,7 +112,7 @@ import {
|
||||
import { type IrModule, type SrcLoc } from "./ir/ir.js";
|
||||
import { moduleRuntimeFeatures } from "./ir/runtime-features.js";
|
||||
import { serializeModuleParts } from "./ir/serialize.js";
|
||||
import { writeTextParts } from "./text-parts.js";
|
||||
import { readTextParts, writeTextParts } from "./text-parts.js";
|
||||
import { validateModule } from "./ir/validate.js";
|
||||
import { loadProgram } from "./frontend/program-node.js";
|
||||
import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js";
|
||||
@@ -1197,7 +1197,9 @@ async function compileTracked(
|
||||
nativeProgramObject = await emitNativeProgramObject(
|
||||
entryPath,
|
||||
opts,
|
||||
await readFile(earlyHit.llvmPath, "utf8"),
|
||||
// A large program's LLVM exceeds the engine's string limit as one
|
||||
// string; the emitter takes it in parts, as on a cold build.
|
||||
await readTextParts(earlyHit.llvmPath),
|
||||
earlyHit.native,
|
||||
profile,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { constants } from "node:buffer";
|
||||
import { mkdtempSync, rmSync, truncateSync, writeFileSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { readTextParts, writeTextParts } from "./text-parts.js";
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), "scriptc-text-parts-"));
|
||||
afterAll(() => rmSync(scratch, { recursive: true, force: true }));
|
||||
|
||||
test("parts join to the file's text, each within the part length", async () => {
|
||||
// Multi-byte sequences and surrogate pairs straddle the 1 MiB reads and
|
||||
// the 8-unit parts.
|
||||
const unit = "abéc中\u{1f600}d";
|
||||
const text = unit.repeat(90_000);
|
||||
const path = join(scratch, "mixed.ll");
|
||||
writeFileSync(path, text);
|
||||
const parts = await readTextParts(path, 8);
|
||||
expect(parts.join("")).toBe(text);
|
||||
for (const part of parts) {
|
||||
expect(part.length).toBeLessThanOrEqual(8);
|
||||
const last = part.charCodeAt(part.length - 1);
|
||||
expect(last >= 0xd800 && last <= 0xdbff).toBe(false);
|
||||
}
|
||||
// Each part encodes on its own, so writing them back reproduces the file.
|
||||
const copy = join(scratch, "copy.ll");
|
||||
await writeTextParts(copy, parts);
|
||||
expect(await readFile(copy, "utf8")).toBe(text);
|
||||
});
|
||||
|
||||
test("an empty file has no parts", async () => {
|
||||
const path = join(scratch, "empty.ll");
|
||||
writeFileSync(path, "");
|
||||
expect(await readTextParts(path)).toEqual([]);
|
||||
});
|
||||
|
||||
test("a file past the engine's string limit reads in parts", async () => {
|
||||
// A sparse file: its bytes are NUL characters, valid UTF-8, and cost no
|
||||
// disk space.
|
||||
const path = join(scratch, "huge.ll");
|
||||
const size = constants.MAX_STRING_LENGTH + 1;
|
||||
writeFileSync(path, "");
|
||||
truncateSync(path, size);
|
||||
await expect(readFile(path, "utf8")).rejects.toThrow(RangeError);
|
||||
const parts = await readTextParts(path);
|
||||
expect(parts.length).toBeGreaterThan(1);
|
||||
expect(parts.reduce((length, part) => length + part.length, 0)).toBe(size);
|
||||
}, 120_000);
|
||||
@@ -1,4 +1,10 @@
|
||||
import { createReadStream } from "node:fs";
|
||||
import { open } from "node:fs/promises";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
|
||||
/** The longest part readTextParts returns, far below the engine's string
|
||||
* limit. */
|
||||
const TEXT_PART_LENGTH = 64 * 1024 * 1024;
|
||||
|
||||
/** Writes compiler text artifacts (LLVM, serialized IR) in large batches.
|
||||
* Programs past the engine's string limit arrive as many parts, and writing
|
||||
@@ -35,3 +41,43 @@ export async function writeTextParts(
|
||||
await file.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a text artifact as parts of at most `partLength` UTF-16 units:
|
||||
* cached LLVM and serialized IR of large programs exceed the engine's
|
||||
* string limit as one string. The decoder carries a multi-byte sequence
|
||||
* split across reads into the next part, and no part ends inside a
|
||||
* surrogate pair, so the parts join to the file's text and each encodes on
|
||||
* its own. */
|
||||
export async function readTextParts(
|
||||
path: string,
|
||||
partLength = TEXT_PART_LENGTH,
|
||||
): Promise<string[]> {
|
||||
const decoder = new StringDecoder("utf8");
|
||||
const parts: string[] = [];
|
||||
let pending: string[] = [];
|
||||
let pendingLength = 0;
|
||||
const flush = (): void => {
|
||||
if (pendingLength === 0) return;
|
||||
parts.push(pending.join(""));
|
||||
pending = [];
|
||||
pendingLength = 0;
|
||||
};
|
||||
const add = (text: string): void => {
|
||||
for (let offset = 0; offset < text.length;) {
|
||||
let take = Math.min(text.length - offset, partLength - pendingLength);
|
||||
// A part never ends inside a surrogate pair: writeTextParts encodes
|
||||
// its batches separately.
|
||||
const last = text.charCodeAt(offset + take - 1);
|
||||
if (take > 1 && last >= 0xd800 && last <= 0xdbff && offset + take < text.length) take--;
|
||||
pending.push(text.slice(offset, offset + take));
|
||||
pendingLength += take;
|
||||
offset += take;
|
||||
if (pendingLength >= partLength - 1) flush();
|
||||
}
|
||||
};
|
||||
for await (const chunk of createReadStream(path, { highWaterMark: 1 << 20 }))
|
||||
add(decoder.write(chunk as Buffer));
|
||||
add(decoder.end());
|
||||
flush();
|
||||
return parts;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user