mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Run the frontend of a first executable build in a child process (#1649)
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js";
|
||||
|
||||
// The first optimized executable build of an invocation prepares its input in a frontend process
|
||||
// (prepareExecutableInputInChild in the compiler's index.ts) when the compiler runs from its built
|
||||
// files, as the CLI's bootstrap does. Later builds of the invocation, and builds with
|
||||
// SCRIPTC_FRONTEND_PROCESS=0, prepare it in the compiler's own process.
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const bootstrap = join(repoRoot, "packages/cli/dist/bootstrap.js");
|
||||
const compilerApi = pathToFileURL(join(repoRoot, "packages/compiler/dist/index.js")).href;
|
||||
const helperTarget = precompiledRuntimePackTarget();
|
||||
|
||||
interface Build {
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
async function build(
|
||||
entry: string,
|
||||
outPath: string,
|
||||
cacheRoot: string,
|
||||
inProcess: boolean,
|
||||
extra: { args?: string[]; env?: NodeJS.ProcessEnv } = {},
|
||||
): Promise<Build> {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
SCRIPTC_CACHE_DIR: cacheRoot,
|
||||
SCRIPTC_TIMING: "1",
|
||||
...extra.env,
|
||||
};
|
||||
delete env["SCRIPTC_NO_CACHE"];
|
||||
if (inProcess) env["SCRIPTC_FRONTEND_PROCESS"] = "0";
|
||||
else delete env["SCRIPTC_FRONTEND_PROCESS"];
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
[bootstrap, "build", entry, "-o", outPath, ...(extra.args ?? [])],
|
||||
{ env, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
return { code: 0, stdout, stderr };
|
||||
} catch (error) {
|
||||
const failed = error as { code?: number; stdout?: string; stderr?: string };
|
||||
return { code: failed.code ?? 1, stdout: failed.stdout ?? "", stderr: failed.stderr ?? "" };
|
||||
}
|
||||
}
|
||||
|
||||
/** The build's output without its timing lines, whose times differ between runs. */
|
||||
const withoutTiming = (text: string): string =>
|
||||
text
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith("scriptc timing") && !line.startsWith("scriptc lowering"))
|
||||
.join("\n");
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"the frontend process builds the executable, diagnostics and cache entries the in-process build does",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-"));
|
||||
try {
|
||||
const src = join(dir, "src");
|
||||
await mkdir(src);
|
||||
await writeFile(
|
||||
join(src, "shapes.ts"),
|
||||
"export const area = (w: number, h: number): number => w * h;\n",
|
||||
);
|
||||
await writeFile(
|
||||
join(src, "main.ts"),
|
||||
'import { area } from "./shapes.js";\nconsole.log(area(3, 4), [1, 2, 3].map((x) => x * 2).join(","));\n',
|
||||
);
|
||||
const entry = join(src, "main.ts");
|
||||
const out = (name: string): string => join(dir, name, "program");
|
||||
await mkdir(join(dir, "child"));
|
||||
await mkdir(join(dir, "inline"));
|
||||
|
||||
const child = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
const inline = await build(entry, out("inline"), join(dir, "cache-inline"), true);
|
||||
expect(child.code).toBe(0);
|
||||
expect(inline.code).toBe(0);
|
||||
expect(child.stderr).toContain('"phase":"frontend-process"');
|
||||
expect(inline.stderr).not.toContain('"phase":"frontend-process"');
|
||||
expect(await readFile(out("child"))).toEqual(await readFile(out("inline")));
|
||||
expect(await readFile(join(dir, "child", "main.ll"), "utf8")).toBe(
|
||||
await readFile(join(dir, "inline", "main.ll"), "utf8"),
|
||||
);
|
||||
expect((await execFileAsync(out("child"))).stdout).toBe("12 2,4,6\n");
|
||||
// The request and result files are removed.
|
||||
expect(
|
||||
(await readdir(join(dir, "child"))).filter((name) => name.includes("frontend")),
|
||||
).toEqual([]);
|
||||
|
||||
// A repeat finds the executable the frontend process's build published. An edit rebuilds in
|
||||
// the compiler's own process.
|
||||
const repeat = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
expect(repeat.code).toBe(0);
|
||||
expect(repeat.stderr).not.toContain("scriptc lowering");
|
||||
expect(repeat.stderr).not.toContain('"phase":"frontend-process"');
|
||||
await writeFile(
|
||||
join(src, "shapes.ts"),
|
||||
"export const area = (w: number, h: number): number => w * h + 1;\n",
|
||||
);
|
||||
const edited = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
expect(edited.code).toBe(0);
|
||||
expect(edited.stderr).toContain("scriptc lowering");
|
||||
expect(edited.stderr).not.toContain('"phase":"frontend-process"');
|
||||
expect((await execFileAsync(out("child"))).stdout).toBe("13 2,4,6\n");
|
||||
|
||||
// Without the executable cache entries (as for a program whose frontend inputs cannot be
|
||||
// cached), the rebuild still runs in the compiler's own process: the first build recorded
|
||||
// its native program object.
|
||||
await rm(join(dir, "cache-child", "early-exe"), { recursive: true, force: true });
|
||||
await rm(join(dir, "cache-child", "early-exe-route"), { recursive: true, force: true });
|
||||
const uncached = await build(entry, out("child"), join(dir, "cache-child"), false);
|
||||
expect(uncached.code).toBe(0);
|
||||
expect(uncached.stderr).toContain("scriptc lowering");
|
||||
expect(uncached.stderr).not.toContain('"phase":"frontend-process"');
|
||||
expect((await execFileAsync(out("child"))).stdout).toBe("13 2,4,6\n");
|
||||
|
||||
// A program the frontend rejects reports the same diagnostics either way. The first build of
|
||||
// the invocation in an empty cache runs the frontend process.
|
||||
await writeFile(join(src, "main.ts"), 'const n: number = "not a number";\nconsole.log(n);\n');
|
||||
const childError = await build(entry, out("child"), join(dir, "cache-child-error"), false);
|
||||
const inlineError = await build(entry, out("inline"), join(dir, "cache-inline"), true);
|
||||
expect(childError.stderr).toContain('"phase":"frontend-process"');
|
||||
expect(childError.code).not.toBe(0);
|
||||
expect(childError.code).toBe(inlineError.code);
|
||||
expect(withoutTiming(childError.stderr)).toBe(withoutTiming(inlineError.stderr));
|
||||
expect(withoutTiming(childError.stderr)).toContain("not a number");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"a compile called from evaluated code runs its frontend process once",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-eval-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, 'console.log("from eval");\n');
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`import { compile } from ${JSON.stringify(compilerApi)};
|
||||
const result = await compile(process.argv[1], { outDir: process.argv[2], outPath: process.argv[3] });
|
||||
console.log("compiled", result.ok);`,
|
||||
entry,
|
||||
dir,
|
||||
join(dir, "program"),
|
||||
],
|
||||
{
|
||||
env: { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache"), SCRIPTC_TIMING: "1" },
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
expect(stdout).toBe("compiled true\n");
|
||||
expect(stderr).toContain('"phase":"frontend-process"');
|
||||
expect((await execFileAsync(join(dir, "program"))).stdout).toBe("from eval\n");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"a build with --provenance-sources compiles the attested sources in the frontend process",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-provenance-"));
|
||||
try {
|
||||
const fixture = join(repoRoot, "tests/fixtures/provenance");
|
||||
const entry = join(fixture, "cases/greet/main.ts");
|
||||
const extra = {
|
||||
args: ["--provenance-sources"],
|
||||
env: { SCRIPTC_PROVENANCE_MANIFEST: join(fixture, "manifest.json") },
|
||||
};
|
||||
await mkdir(join(dir, "child"));
|
||||
await mkdir(join(dir, "inline"));
|
||||
const child = await build(
|
||||
entry,
|
||||
join(dir, "child", "greet"),
|
||||
join(dir, "cache"),
|
||||
false,
|
||||
extra,
|
||||
);
|
||||
const inline = await build(
|
||||
entry,
|
||||
join(dir, "inline", "greet"),
|
||||
join(dir, "cache"),
|
||||
true,
|
||||
extra,
|
||||
);
|
||||
expect(child.code).toBe(0);
|
||||
expect(inline.code).toBe(0);
|
||||
expect(child.stderr).toContain('"phase":"frontend-process"');
|
||||
const childLlvm = await readFile(join(dir, "child", "main.ll"), "utf8");
|
||||
expect(childLlvm).toBe(await readFile(join(dir, "inline", "main.ll"), "utf8"));
|
||||
// Without the flag, the program compiles the published package instead.
|
||||
await mkdir(join(dir, "published"));
|
||||
const published = await build(
|
||||
entry,
|
||||
join(dir, "published", "greet"),
|
||||
join(dir, "cache"),
|
||||
true,
|
||||
);
|
||||
expect(published.code).toBe(0);
|
||||
expect(await readFile(join(dir, "published", "main.ll"), "utf8")).not.toBe(childLlvm);
|
||||
expect((await execFileAsync(join(dir, "child", "greet"))).stdout).toBe(
|
||||
"hello, world\nHELLO, COMPILER!\nhello, chain!\n",
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
|
||||
test.skipIf(helperTarget === null)(
|
||||
"concurrent first builds of entries with the same name in one output directory keep their own LLVM",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-frontend-process-stem-"));
|
||||
try {
|
||||
const out = join(dir, "out");
|
||||
await mkdir(out);
|
||||
const entries = ["first", "second"].map((name) => join(dir, name, "main.ts"));
|
||||
for (const [index, entry] of entries.entries()) {
|
||||
await mkdir(join(entry, ".."));
|
||||
// Different programs with the same stem write the same public main.ll.
|
||||
const lines = Array.from({ length: 200 }, (_, line) => `console.log("${index}-${line}");`);
|
||||
await writeFile(entry, `${lines.join("\n")}\n`);
|
||||
}
|
||||
const builds = await Promise.all(
|
||||
entries.map((entry, index) =>
|
||||
build(entry, join(out, `program-${index}`), join(dir, `cache-${index}`), false),
|
||||
),
|
||||
);
|
||||
for (const [index, result] of builds.entries()) {
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stderr).toContain('"phase":"frontend-process"');
|
||||
const { stdout } = await execFileAsync(join(out, `program-${index}`));
|
||||
expect(stdout.split("\n")[0]).toBe(`${index}-0`);
|
||||
}
|
||||
// The frontend process's private copies of the LLVM text are removed.
|
||||
expect((await readdir(out)).filter((name) => name.includes("frontend"))).toEqual([]);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { createHash, type Hash } from "node:crypto";
|
||||
import { closeSync, constants, openSync, readSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { access, chmod, mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { access, chmod, copyFile, mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
@@ -73,9 +73,31 @@ export interface NativeCodegenArtifact {
|
||||
|
||||
const resolvedHelperCache = new Map<string, Promise<ResolvedHelper>>();
|
||||
|
||||
/** LLVM text already written to `file`, of `bytes` bytes, which a build passes on without reading it. */
|
||||
export interface LlvmFile {
|
||||
readonly file: string;
|
||||
readonly bytes: number;
|
||||
}
|
||||
|
||||
/** LLVM text as a string, as parts to concatenate, or as a file. */
|
||||
export type LlvmInput = string | readonly string[] | LlvmFile;
|
||||
|
||||
export function isLlvmFile(llvm: LlvmInput): llvm is LlvmFile {
|
||||
return typeof llvm === "object" && !Array.isArray(llvm);
|
||||
}
|
||||
|
||||
/** The size of `llvm` in bytes. */
|
||||
export function llvmInputBytes(llvm: LlvmInput): number {
|
||||
if (isLlvmFile(llvm)) return llvm.bytes;
|
||||
return (typeof llvm === "string" ? [llvm] : llvm).reduce(
|
||||
(bytes, part) => bytes + Buffer.byteLength(part),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
export interface NativeCodegenOptions {
|
||||
outputPath: string;
|
||||
llvm: string | readonly string[];
|
||||
llvm: LlvmInput;
|
||||
outputKind: NativeCodegenOutputKind;
|
||||
sourcePath: string;
|
||||
optimization?: "0" | "1" | "2" | "3" | "s" | "z";
|
||||
@@ -278,14 +300,31 @@ async function resolveHelper(
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** Adds the bytes of `file` to `hash`, as `hash.update` of the text written there would. */
|
||||
function updateHashFromFile(hash: Hash, file: string): void {
|
||||
const fd = openSync(file, "r");
|
||||
try {
|
||||
const buffer = Buffer.allocUnsafe(4 * 1024 * 1024);
|
||||
for (;;) {
|
||||
const read = readSync(fd, buffer, 0, buffer.length, null);
|
||||
if (read === 0) break;
|
||||
hash.update(buffer.subarray(0, read));
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheKey(
|
||||
options: NativeCodegenOptions,
|
||||
target: NativeTargetSpec,
|
||||
helper: HelperIdentity,
|
||||
): string {
|
||||
const hash = createHash("sha256").update("scriptc-native-codegen-v1\0");
|
||||
for (const part of typeof options.llvm === "string" ? [options.llvm] : options.llvm)
|
||||
hash.update(part);
|
||||
if (isLlvmFile(options.llvm)) updateHashFromFile(hash, options.llvm.file);
|
||||
else
|
||||
for (const part of typeof options.llvm === "string" ? [options.llvm] : options.llvm)
|
||||
hash.update(part);
|
||||
return hash
|
||||
.update("\0")
|
||||
.update(JSON.stringify(target))
|
||||
@@ -424,7 +463,8 @@ export async function emitNativeArtifact(
|
||||
// concurrent build of another entry that has the same output stem.
|
||||
const input = privateSiblingPath(options.outputPath, "native-input");
|
||||
try {
|
||||
await writeLlvmSource(input, options.llvm, 0o600);
|
||||
if (isLlvmFile(options.llvm)) await copyFile(options.llvm.file, input);
|
||||
else await writeLlvmSource(input, options.llvm, 0o600);
|
||||
await invoke(helper.binaryPath, [
|
||||
"emit",
|
||||
"--input",
|
||||
|
||||
@@ -13,9 +13,11 @@ import {
|
||||
} from "../backend/native-toolchain.js";
|
||||
import { installDarwinDebugSymbols, readDarwinDebugSymbols } from "../backend/debug-symbols.js";
|
||||
import {
|
||||
hasEarlyExecutableNativeBuild,
|
||||
publishEarlyExecutableCache,
|
||||
readEarlyExecutableCache,
|
||||
readRoutedExecutableCache,
|
||||
recordEarlyExecutableNativeBuild,
|
||||
type EarlyExecutableCacheOptions,
|
||||
type EarlyExecutableNativeFeatures,
|
||||
} from "./executable-cache.js";
|
||||
@@ -596,3 +598,38 @@ test("the route index still isolates eager and deferred native environments", as
|
||||
)?.executableRestored,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a recorded native build is found by its invocation", async () => {
|
||||
const f = await fixture();
|
||||
expect(await hasEarlyExecutableNativeBuild(null, f.options)).toBe(false);
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, f.options)).toBe(false);
|
||||
await recordEarlyExecutableNativeBuild(null, f.options);
|
||||
await recordEarlyExecutableNativeBuild(f.root, f.options);
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, f.options)).toBe(true);
|
||||
// The record does not depend on the compiler implementation or the native environment.
|
||||
const rebuilt: EarlyExecutableCacheOptions = {
|
||||
...f.options,
|
||||
implementation: "b".repeat(64),
|
||||
nativeEnvironment: "another driver",
|
||||
};
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, rebuilt)).toBe(true);
|
||||
expect(
|
||||
await hasEarlyExecutableNativeBuild(f.root, {
|
||||
...f.options,
|
||||
outPath: join(f.options.outDir, "other"),
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, { ...f.options, sanitize: true })).toBe(false);
|
||||
// Publishing an executable does not record a native build.
|
||||
const other = { ...f.options, outPath: join(f.options.outDir, "published") };
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
await publishEarlyExecutableCache(f.root, other, {
|
||||
llvmPath: f.llvmPath,
|
||||
irPath: f.irPath,
|
||||
native,
|
||||
executableRestored: false,
|
||||
frontend: tracker.snapshot(),
|
||||
});
|
||||
expect(await hasEarlyExecutableNativeBuild(f.root, other)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -332,6 +332,39 @@ function routePath(
|
||||
return join(root, "early-exe-route", routeKey(options));
|
||||
}
|
||||
|
||||
function nativeBuildMarkerPath(
|
||||
root: string,
|
||||
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
||||
): string {
|
||||
return join(root, "early-exe-native-build", routeKey(options));
|
||||
}
|
||||
|
||||
/** Records that a build of the invocation `options` emitted its native program object, so the
|
||||
* native helper's partition cache holds its partitions. Unlike the route, the record does not
|
||||
* depend on the build publishing its executable. */
|
||||
export async function recordEarlyExecutableNativeBuild(
|
||||
root: string | null,
|
||||
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
||||
): Promise<void> {
|
||||
if (root === null) return;
|
||||
const marker = nativeBuildMarkerPath(root, options);
|
||||
await mkdir(dirname(marker), { recursive: true, mode: 0o700 });
|
||||
await writeFile(marker, "", { mode: 0o600 });
|
||||
}
|
||||
|
||||
/** Whether an earlier build of the invocation `options` recorded its native program object
|
||||
* (`recordEarlyExecutableNativeBuild`). False when there is no cache. */
|
||||
export async function hasEarlyExecutableNativeBuild(
|
||||
root: string | null,
|
||||
options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment">,
|
||||
): Promise<boolean> {
|
||||
if (root === null) return false;
|
||||
return stat(nativeBuildMarkerPath(root, options)).then(
|
||||
(info) => info.isFile(),
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
function routeIntegrity(stamp: Omit<EarlyExecutableRouteStamp, "integrity">): string {
|
||||
return createHash("sha256")
|
||||
.update("early-executable-route-stamp-v2\0")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { frontendProcessExecArgv } from "./frontend-process-args.js";
|
||||
|
||||
test("the frontend process keeps heap limits and loaders", () => {
|
||||
expect(
|
||||
frontendProcessExecArgv([
|
||||
"--max-old-space-size=8192",
|
||||
"--stack-size=4000",
|
||||
"--import",
|
||||
"tsx",
|
||||
"-r",
|
||||
"./setup.cjs",
|
||||
"--conditions=development",
|
||||
"--enable-source-maps",
|
||||
]),
|
||||
).toEqual([
|
||||
"--max-old-space-size=8192",
|
||||
"--stack-size=4000",
|
||||
"--import",
|
||||
"tsx",
|
||||
"-r",
|
||||
"./setup.cjs",
|
||||
"--conditions=development",
|
||||
"--enable-source-maps",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the frontend process drops evaluated code, inspectors, watch and test runner options", () => {
|
||||
expect(
|
||||
frontendProcessExecArgv([
|
||||
"--max-old-space-size=8192",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
"await compile(process.argv[1], {})",
|
||||
]),
|
||||
).toEqual(["--max-old-space-size=8192"]);
|
||||
expect(frontendProcessExecArgv(["-e", "code", "-p", "code", "-pe", "code"])).toEqual([]);
|
||||
expect(
|
||||
frontendProcessExecArgv(["--eval=code", "--print=code", "--input-type", "module"]),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
frontendProcessExecArgv([
|
||||
"--inspect",
|
||||
"--inspect-brk=9229",
|
||||
"--inspect-port",
|
||||
"9230",
|
||||
"--watch",
|
||||
"--watch-path=src",
|
||||
"--test",
|
||||
"--test-name-pattern",
|
||||
"build",
|
||||
"--test-reporter=spec",
|
||||
"--expose-gc",
|
||||
]),
|
||||
).toEqual(["--expose-gc"]);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/** Options of the compiler's own process that the frontend process (frontend-process.ts) must not
|
||||
* receive, with the options among them that take their value as the next argument: the code of
|
||||
* --eval and --print would run in place of the frontend process's entry, and the inspector, watch
|
||||
* and test runner options belong to the process that was started with them. */
|
||||
const OPTIONS_WITH_VALUES = new Set([
|
||||
"-e",
|
||||
"--eval",
|
||||
"-p",
|
||||
"--print",
|
||||
"--input-type",
|
||||
"--inspect-port",
|
||||
"--debug-port",
|
||||
"--watch-path",
|
||||
"--test-concurrency",
|
||||
"--test-coverage-branches",
|
||||
"--test-coverage-exclude",
|
||||
"--test-coverage-functions",
|
||||
"--test-coverage-include",
|
||||
"--test-coverage-lines",
|
||||
"--test-global-setup",
|
||||
"--test-isolation",
|
||||
"--test-name-pattern",
|
||||
"--test-reporter",
|
||||
"--test-reporter-destination",
|
||||
"--test-shard",
|
||||
"--test-skip-pattern",
|
||||
"--test-timeout",
|
||||
]);
|
||||
const OPTIONS = new Set(["-i", "--interactive", "-c", "--check", "--test"]);
|
||||
const OPTION_PREFIXES = ["--inspect", "--debug-port", "--watch", "--test-", "--input-type="];
|
||||
|
||||
/** The Node.js options of the frontend process: `execArgv` without the options that run code
|
||||
* (--eval, --print), open an inspector, or watch or test, so that its own entry runs with the
|
||||
* compiler process's remaining options, such as heap limits and loaders. */
|
||||
export function frontendProcessExecArgv(execArgv: readonly string[]): string[] {
|
||||
const kept: string[] = [];
|
||||
for (let index = 0; index < execArgv.length; index++) {
|
||||
const option = execArgv[index]!;
|
||||
// A short option group such as -pe takes the code that follows it.
|
||||
if (OPTIONS_WITH_VALUES.has(option) || /^-[a-z]*[ep][a-z]*$/i.test(option)) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
OPTIONS.has(option) ||
|
||||
option.startsWith("--eval=") ||
|
||||
option.startsWith("--print=") ||
|
||||
OPTION_PREFIXES.some((prefix) => option.startsWith(prefix))
|
||||
)
|
||||
continue;
|
||||
kept.push(option);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// The frontend process of an executable build (FrontendProcess and runFrontendProcess in index.ts):
|
||||
// loads the compiler, then reads the paths of its request and result from stdin. Without them it
|
||||
// exits: the build found its executable without it.
|
||||
import { enableCompileCache } from "node:module";
|
||||
|
||||
try {
|
||||
enableCompileCache();
|
||||
} catch {
|
||||
// Bytecode caching only speeds up loading.
|
||||
}
|
||||
const { runFrontendProcess } = await import("./index.js");
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += String(chunk);
|
||||
const [requestPath, resultPath] = input.split("\n");
|
||||
if (requestPath !== undefined && requestPath !== "" && resultPath !== undefined)
|
||||
await runFrontendProcess(requestPath, resultPath);
|
||||
// The frontend's own helpers (the checker process, timers) must not keep this process alive.
|
||||
process.exit(0);
|
||||
+310
-32
@@ -1,5 +1,5 @@
|
||||
import { optimizationField, type NativeOptimization } from "./backend/optimization.js";
|
||||
import { compilationTiming, type CompilationTiming } from "./timing.js";
|
||||
import { compilationTiming, type CompilationTiming, type TimingClock } from "./timing.js";
|
||||
import { prepareExecutableModule } from "./executable/prepare.js";
|
||||
import {
|
||||
prepareLibrary,
|
||||
@@ -45,9 +45,13 @@ export type {
|
||||
} from "./compile-types.js";
|
||||
import { compilePackedLibrary } from "./backend/library-pack.js";
|
||||
import { InternalCompilerError } from "./errors.js";
|
||||
import { frontendProcessExecArgv } from "./frontend-process-args.js";
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
clearCcCaches,
|
||||
configuredTargetPlatform,
|
||||
@@ -67,6 +71,9 @@ import { emitLlvmModuleSource, LlvmUnsupportedError } from "./backend/llvm/emitt
|
||||
import { inlineRcOption } from "./backend/llvm/shapes.js";
|
||||
import {
|
||||
emitNativeArtifact,
|
||||
isLlvmFile,
|
||||
llvmInputBytes,
|
||||
type LlvmInput,
|
||||
NativeCodegenError,
|
||||
writeLlvmSource,
|
||||
} from "./backend/native-codegen.js";
|
||||
@@ -116,12 +123,20 @@ 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";
|
||||
import { provenanceSources } from "./frontend/provenance-registry.js";
|
||||
import {
|
||||
type ProvenanceSources,
|
||||
provenanceSources,
|
||||
setProvenanceSources,
|
||||
} from "./frontend/provenance-registry.js";
|
||||
import { clearResolveCaches } from "./frontend/resolve.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js";
|
||||
import { executableLinkFeatures } from "./backend/executable-features.js";
|
||||
import { youngNurseryRequested } from "./backend/young-nursery.js";
|
||||
import { FrontendInputTracker, trackedReadFile } from "./frontend/input-tracker.js";
|
||||
import {
|
||||
type FrontendInputSnapshot,
|
||||
FrontendInputTracker,
|
||||
trackedReadFile,
|
||||
} from "./frontend/input-tracker.js";
|
||||
import {
|
||||
libraryFrontendImplementationFingerprint,
|
||||
publishEarlyLibraryCache,
|
||||
@@ -133,9 +148,11 @@ import {
|
||||
type SemanticLibraryCacheHit,
|
||||
} from "./library/library-cache.js";
|
||||
import {
|
||||
hasEarlyExecutableNativeBuild,
|
||||
publishEarlyExecutableCache,
|
||||
publishEarlyExecutableRoute,
|
||||
readEarlyExecutableCache,
|
||||
recordEarlyExecutableNativeBuild,
|
||||
type EarlyExecutableCacheOptions,
|
||||
type EarlyExecutableNativeFeatures,
|
||||
} from "./executable/executable-cache.js";
|
||||
@@ -417,10 +434,16 @@ export async function compile(
|
||||
clearCompileSessionCaches();
|
||||
const frontendInputs = new FrontendInputTracker();
|
||||
const warnings: CompileWarningSink = { warnings: [], sourceTexts: null };
|
||||
const result = await frontendInputs.run(() =>
|
||||
compileTracked(entryPath, opts, frontendInputs, warnings),
|
||||
);
|
||||
return withCompileWarnings(result, warnings);
|
||||
// compileTracked starts it for a build with no earlier build of the same invocation.
|
||||
const frontendProcess = preparesInChildProcess(opts) ? new FrontendProcess() : undefined;
|
||||
try {
|
||||
const result = await frontendInputs.run(() =>
|
||||
compileTracked(entryPath, opts, frontendInputs, warnings, frontendProcess),
|
||||
);
|
||||
return withCompileWarnings(result, warnings);
|
||||
} finally {
|
||||
frontendProcess?.release();
|
||||
}
|
||||
}
|
||||
|
||||
/** Divergence warnings collected while a compile lowers (none when a cache
|
||||
@@ -610,7 +633,7 @@ interface NativeProgramObject {
|
||||
async function emitNativeProgramObject(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
llvm: string | readonly string[],
|
||||
llvm: LlvmInput,
|
||||
features: EarlyExecutableNativeFeatures,
|
||||
profile: NativeProfileOptions | undefined,
|
||||
): Promise<NativeProgramObject> {
|
||||
@@ -622,10 +645,7 @@ async function emitNativeProgramObject(
|
||||
const linkPath = `${privateSiblingPath(artifactPath, "native-program-object")}.o`;
|
||||
// The validated program-object artifact is a single object by contract.
|
||||
const target = nativeCodegenTarget();
|
||||
const llvmBytes = (typeof llvm === "string" ? [llvm] : llvm).reduce(
|
||||
(bytes, part) => bytes + Buffer.byteLength(part),
|
||||
0,
|
||||
);
|
||||
const llvmBytes = llvmInputBytes(llvm);
|
||||
const partitions =
|
||||
opts.nativeProgramObject === true || target === null ? 1 : nativeProgramPartitions(llvmBytes);
|
||||
// Optimized programs import small runtime functions from the bitcode of
|
||||
@@ -715,7 +735,7 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD
|
||||
}
|
||||
|
||||
interface PreparedExecutable {
|
||||
llvmSource: string | readonly string[];
|
||||
llvmSource: LlvmInput;
|
||||
llvmPath: string;
|
||||
irPath: string | undefined;
|
||||
nativeFeatures: EarlyExecutableNativeFeatures;
|
||||
@@ -870,13 +890,247 @@ async function prepareExecutableInput(
|
||||
return { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts };
|
||||
}
|
||||
|
||||
/**
|
||||
* The child process that prepares an executable build's input: frontend, lowering and LLVM emission
|
||||
* (`runFrontendProcess`). Node's heap keeps the pages a build's frontend used for the life of the
|
||||
* process, so running them in a process of their own leaves the native helper the memory they took.
|
||||
*/
|
||||
const frontendProcessEntry = fileURLToPath(new URL("./frontend-process.js", import.meta.url));
|
||||
|
||||
/**
|
||||
* Whether `prepareExecutableInputInChild` may prepare this build: optimized executables, when the
|
||||
* compiler runs from its built files. Development builds split the LLVM text in this process, and
|
||||
* SCRIPTC_FRONTEND_PROCESS=0 keeps the frontend in-process.
|
||||
*
|
||||
* compileTracked starts the process only when no earlier build of the same invocation emitted its
|
||||
* native program object (`hasEarlyExecutableNativeBuild`). A rebuild finds most native partitions
|
||||
* in the helper's cache, so its memory peaks in the frontend, and the parent process would add to
|
||||
* that peak.
|
||||
*/
|
||||
function preparesInChildProcess(opts: CompileRequestOptions): boolean {
|
||||
return (
|
||||
process.env["SCRIPTC_FRONTEND_PROCESS"] !== "0" &&
|
||||
(opts.outputKind ?? "exe") === "exe" &&
|
||||
(opts.optimization ?? "release") !== "dev" &&
|
||||
existsSync(frontendProcessEntry)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontend process (frontend-process.ts) of one compile. `start` launches it; it then waits on
|
||||
* its stdin for the paths of a request and its result (`run`), and exits once its stdin closes
|
||||
* without them (`release`).
|
||||
*/
|
||||
class FrontendProcess {
|
||||
private child: ChildProcess | undefined;
|
||||
private exited: Promise<number | null> | undefined;
|
||||
private sent = false;
|
||||
|
||||
/** Whether `start` launched the process. */
|
||||
get started(): boolean {
|
||||
return this.child !== undefined;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.child !== undefined) return;
|
||||
const execArgv = frontendProcessExecArgv(process.execArgv);
|
||||
const child = spawn(process.execPath, [...execArgv, frontendProcessEntry], {
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
});
|
||||
this.exited = new Promise<number | null>((resolveExit) => {
|
||||
child.on("error", () => resolveExit(null));
|
||||
child.on("exit", (code) => resolveExit(code));
|
||||
});
|
||||
// A failed start surfaces as a failed run; the build then prepares its input in-process.
|
||||
child.stdin!.on("error", () => undefined);
|
||||
this.child = child;
|
||||
}
|
||||
|
||||
/** Sends the request at `requestPath`; resolves with the process's exit code. */
|
||||
run(requestPath: string, resultPath: string): Promise<number | null> {
|
||||
if (this.child === undefined || this.exited === undefined || this.sent)
|
||||
return Promise.resolve(null);
|
||||
this.sent = true;
|
||||
this.child.stdin!.end(`${requestPath}\n${resultPath}\n`);
|
||||
return this.exited;
|
||||
}
|
||||
|
||||
/** Lets the process exit if no request was sent. */
|
||||
release(): void {
|
||||
if (this.child === undefined || this.sent) return;
|
||||
this.sent = true;
|
||||
this.child.stdin!.end();
|
||||
// The build is done with it: this process need not wait for it to exit.
|
||||
this.child.unref();
|
||||
(this.child.stdin as { unref?: () => void }).unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
interface FrontendProcessRequest {
|
||||
entryPath: string;
|
||||
opts: CompileRequestOptions;
|
||||
buildPlatform: string;
|
||||
/** The compile's SCRIPTC_TIMING clock, which the frontend process's phases continue. */
|
||||
timing: TimingClock;
|
||||
/** The compile's --provenance-sources mappings, which the frontend process installs. */
|
||||
provenance: ProvenanceSources | null;
|
||||
}
|
||||
|
||||
/** What the frontend process sends back. A thrown error comes back as `failed`. */
|
||||
type FrontendProcessResult =
|
||||
| {
|
||||
kind: "prepared";
|
||||
llvmPath: string;
|
||||
/** A private copy of the LLVM text, which the native helper reads (`nativeInputPath`). */
|
||||
nativeInputPath: string;
|
||||
llvmBytes: number;
|
||||
irPath: string | undefined;
|
||||
nativeFeatures: EarlyExecutableNativeFeatures;
|
||||
sourceTexts: [string, string][];
|
||||
warnings: ScrDiagnostic[];
|
||||
frontend: FrontendInputSnapshot;
|
||||
}
|
||||
| {
|
||||
kind: "diagnostics";
|
||||
diagnostics: ScrDiagnostic[];
|
||||
sourceTexts: [string, string][];
|
||||
warnings: ScrDiagnostic[];
|
||||
frontend: FrontendInputSnapshot;
|
||||
}
|
||||
| { kind: "failed" };
|
||||
|
||||
/**
|
||||
* `prepareExecutableInput` in the frontend process, with the LLVM text passed on as its file. The
|
||||
* process's frontend inputs join `frontendInputs` and its warnings `warningSink`. If it fails, the
|
||||
* input is prepared here, so the build reports the same errors.
|
||||
*/
|
||||
async function prepareExecutableInputInChild(
|
||||
frontendProcess: FrontendProcess,
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
ffi: FfiProfile | null,
|
||||
buildPlatform: string,
|
||||
timing: CompilationTiming,
|
||||
timingClock: TimingClock,
|
||||
frontendInputs: FrontendInputTracker,
|
||||
warningSink?: CompileWarningSink,
|
||||
): Promise<PreparedExecutable | CompileRequestResult> {
|
||||
await mkdir(opts.outDir, { recursive: true });
|
||||
const requestPath = privateSiblingPath(join(opts.outDir, "frontend-request.json"), "frontend");
|
||||
const resultPath = privateSiblingPath(join(opts.outDir, "frontend-result.json"), "frontend");
|
||||
let result: FrontendProcessResult = { kind: "failed" };
|
||||
try {
|
||||
const request: FrontendProcessRequest = {
|
||||
entryPath,
|
||||
opts,
|
||||
buildPlatform,
|
||||
timing: timingClock,
|
||||
provenance: provenanceSources(),
|
||||
};
|
||||
await writeFile(requestPath, JSON.stringify(request), { mode: 0o600 });
|
||||
const exitCode = await frontendProcess.run(requestPath, resultPath);
|
||||
if (exitCode === 0)
|
||||
result = JSON.parse(await readFile(resultPath, "utf8")) as FrontendProcessResult;
|
||||
} catch {
|
||||
result = { kind: "failed" };
|
||||
} finally {
|
||||
await rm(requestPath, { force: true });
|
||||
await rm(resultPath, { force: true });
|
||||
}
|
||||
if (result.kind === "failed")
|
||||
return prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink);
|
||||
timing("frontend-process");
|
||||
for (const probe of result.frontend.probes) frontendInputs.record(probe);
|
||||
if (!result.frontend.stable) frontendInputs.markUnstable();
|
||||
const sourceTexts = new Map(result.sourceTexts);
|
||||
if (warningSink !== undefined && result.warnings.length > 0) {
|
||||
warningSink.warnings = result.warnings;
|
||||
warningSink.sourceTexts = sourceTexts;
|
||||
}
|
||||
if (result.kind === "diagnostics")
|
||||
return { ok: false, diagnostics: result.diagnostics, sourceTexts };
|
||||
return {
|
||||
llvmSource: { file: result.nativeInputPath, bytes: result.llvmBytes },
|
||||
llvmPath: result.llvmPath,
|
||||
irPath: result.irPath,
|
||||
nativeFeatures: result.nativeFeatures,
|
||||
programSplit: null,
|
||||
sourceTexts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The frontend process (frontend-process.ts): prepares the executable input of the request at
|
||||
* `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM
|
||||
* text, to `resultPath`.
|
||||
*/
|
||||
export async function runFrontendProcess(requestPath: string, resultPath: string): Promise<void> {
|
||||
const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest;
|
||||
clearCompileSessionCaches();
|
||||
setProvenanceSources(request.provenance);
|
||||
const frontendInputs = new FrontendInputTracker();
|
||||
const warnings: CompileWarningSink = { warnings: [], sourceTexts: null };
|
||||
let result: FrontendProcessResult = { kind: "failed" };
|
||||
try {
|
||||
let ffi: FfiProfile | null = null;
|
||||
if (request.opts.ffiProfilePath !== undefined) {
|
||||
const loaded = loadFfiProfile(resolve(request.opts.ffiProfilePath));
|
||||
if (!loaded.ok) throw new InternalCompilerError("the FFI profile could not be reloaded");
|
||||
ffi = loaded.profile;
|
||||
}
|
||||
const prepared = await frontendInputs.run(() =>
|
||||
prepareExecutableInput(
|
||||
request.entryPath,
|
||||
request.opts,
|
||||
ffi,
|
||||
request.buildPlatform,
|
||||
compilationTiming(request.timing),
|
||||
warnings,
|
||||
),
|
||||
);
|
||||
const frontend = frontendInputs.snapshot();
|
||||
if (!("ok" in prepared)) {
|
||||
// The public .ll file may be rewritten by a concurrent build of another entry with the
|
||||
// same output stem before the native helper reads it (see emitNativeArtifact).
|
||||
if (isLlvmFile(prepared.llvmSource))
|
||||
throw new InternalCompilerError("in-process preparation returned LLVM text as a file");
|
||||
const nativeInputPath = privateSiblingPath(prepared.llvmPath, "frontend-llvm");
|
||||
await writeLlvmSource(nativeInputPath, prepared.llvmSource, 0o600);
|
||||
result = {
|
||||
kind: "prepared",
|
||||
llvmPath: prepared.llvmPath,
|
||||
nativeInputPath,
|
||||
llvmBytes: llvmInputBytes(prepared.llvmSource),
|
||||
irPath: prepared.irPath,
|
||||
nativeFeatures: prepared.nativeFeatures,
|
||||
sourceTexts: [...prepared.sourceTexts],
|
||||
warnings: warnings.warnings,
|
||||
frontend,
|
||||
};
|
||||
} else if (!prepared.ok) {
|
||||
result = {
|
||||
kind: "diagnostics",
|
||||
diagnostics: prepared.diagnostics,
|
||||
sourceTexts: [...prepared.sourceTexts],
|
||||
warnings: warnings.warnings,
|
||||
frontend,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
result = { kind: "failed" };
|
||||
}
|
||||
await writeFile(resultPath, JSON.stringify(result), { mode: 0o600 });
|
||||
}
|
||||
|
||||
async function compileTracked(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
frontendInputs: FrontendInputTracker,
|
||||
warningSink?: CompileWarningSink,
|
||||
frontendProcess?: FrontendProcess,
|
||||
): Promise<CompileRequestResult> {
|
||||
const timing = compilationTiming();
|
||||
const timingStart = Date.now();
|
||||
entryPath = resolve(entryPath);
|
||||
const outputKind = opts.outputKind ?? "exe";
|
||||
if (
|
||||
@@ -1105,16 +1359,10 @@ async function compileTracked(
|
||||
if (outputKind === "exe") {
|
||||
const helperObjectRoute =
|
||||
opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, "llvm");
|
||||
// Package content and native driver discovery are independent inputs to
|
||||
// the same cache key. Start both before waiting so edits do not pay their
|
||||
// filesystem and subprocess latency serially.
|
||||
const [implementation, nativeEnvironment] = await Promise.all([
|
||||
compilerImplementationIdentity(),
|
||||
helperObjectRoute
|
||||
? executableLinkerEnvironmentFingerprint(process.env, nativeCodegenTarget()?.defaultLinker)
|
||||
: executableNativeEnvironmentFingerprint(),
|
||||
]);
|
||||
earlyCacheOptions = {
|
||||
const routeOptions: Omit<
|
||||
EarlyExecutableCacheOptions,
|
||||
"implementation" | "implementationDependencies" | "nativeEnvironment"
|
||||
> = {
|
||||
entryPath,
|
||||
outDir: opts.outDir,
|
||||
outPath: opts.outPath,
|
||||
@@ -1145,8 +1393,27 @@ async function compileTracked(
|
||||
? resolvePlatformLinker(process.env, nativeCodegenTarget()?.defaultLinker)
|
||||
: (process.env["SCRIPTC_CC"] ?? "clang"),
|
||||
],
|
||||
nativeEnvironment,
|
||||
nodeVersion: process.version,
|
||||
};
|
||||
// The frontend process prepares the first build of an invocation (preparesInChildProcess).
|
||||
// Started here, it loads the compiler while this process computes the rest of the cache key.
|
||||
if (
|
||||
frontendProcess !== undefined &&
|
||||
!(await hasEarlyExecutableNativeBuild(cacheRoot, routeOptions))
|
||||
)
|
||||
frontendProcess.start();
|
||||
// Package content and native driver discovery are independent inputs to
|
||||
// the same cache key. Start both before waiting so edits do not pay their
|
||||
// filesystem and subprocess latency serially.
|
||||
const [implementation, nativeEnvironment] = await Promise.all([
|
||||
compilerImplementationIdentity(),
|
||||
helperObjectRoute
|
||||
? executableLinkerEnvironmentFingerprint(process.env, nativeCodegenTarget()?.defaultLinker)
|
||||
: executableNativeEnvironmentFingerprint(),
|
||||
]);
|
||||
earlyCacheOptions = {
|
||||
...routeOptions,
|
||||
nativeEnvironment,
|
||||
implementation: implementation.digest,
|
||||
implementationDependencies: implementation.dependencies,
|
||||
};
|
||||
@@ -1282,14 +1549,20 @@ async function compileTracked(
|
||||
};
|
||||
}
|
||||
timing("executable-cache-miss");
|
||||
const prepared = await prepareExecutableInput(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
timing,
|
||||
warningSink,
|
||||
);
|
||||
const prepared =
|
||||
frontendProcess?.started === true
|
||||
? await prepareExecutableInputInChild(
|
||||
frontendProcess,
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
timing,
|
||||
{ start: timingStart, previous: Date.now() },
|
||||
frontendInputs,
|
||||
warningSink,
|
||||
)
|
||||
: await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink);
|
||||
if ("ok" in prepared) return prepared;
|
||||
const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared;
|
||||
const backend = "llvm" as const;
|
||||
@@ -1318,6 +1591,9 @@ async function compileTracked(
|
||||
profile,
|
||||
);
|
||||
timing("native-object");
|
||||
await recordEarlyExecutableNativeBuild(cacheRoot, executableCacheOptions).catch(
|
||||
() => undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
if (!(err instanceof NativeCodegenError)) throw err;
|
||||
return {
|
||||
@@ -1378,6 +1654,8 @@ async function compileTracked(
|
||||
nativeProgramObject.partitionPaths,
|
||||
);
|
||||
}
|
||||
// The frontend process's private copy of the LLVM text.
|
||||
if (isLlvmFile(llvmSource)) await rm(llvmSource.file, { force: true });
|
||||
}
|
||||
if (!publishedExecutable) {
|
||||
await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, {
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
export type CompilationTiming = (phase: string, detail?: Record<string, unknown>) => void;
|
||||
|
||||
/** The start of a timing and its latest phase, as `Date.now()` values. */
|
||||
export interface TimingClock {
|
||||
start: number;
|
||||
previous: number;
|
||||
}
|
||||
|
||||
/** Opt-in phase durations shared by the Node seed and native compiler.
|
||||
* The disabled path does not read the clock or write compiler output. */
|
||||
export function compilationTiming(): CompilationTiming {
|
||||
* The disabled path does not read the clock or write compiler output.
|
||||
* With `clock`, the phases continue a timing that started in another
|
||||
* process, so a frontend process reports them as its parent compile would. */
|
||||
export function compilationTiming(clock?: TimingClock): CompilationTiming {
|
||||
if (process.env["SCRIPTC_TIMING"] !== "1") return () => {};
|
||||
const start = performance.now();
|
||||
let previous = start;
|
||||
const opened = performance.now();
|
||||
const start = clock === undefined ? opened : opened - (Date.now() - clock.start);
|
||||
let previous = clock === undefined ? start : opened - (Date.now() - clock.previous);
|
||||
return (phase, detail = {}) => {
|
||||
const now = performance.now();
|
||||
process.stderr.write(
|
||||
|
||||
Reference in New Issue
Block a user