mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Lay out the native compiler's functions by the code each kind of run executes (#2265)
This commit is contained in:
@@ -101,11 +101,11 @@ jobs:
|
||||
if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`);
|
||||
NODE
|
||||
|
||||
# Collects the native compiler's profile on this commit
|
||||
# (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes
|
||||
# with a profile of the code it compiles. The committed profile falls behind
|
||||
# main within a day or two. If this job fails, the CLI builds use the
|
||||
# committed profile.
|
||||
# Collects the native compiler's profile and function layout on this commit
|
||||
# (scripts/refresh-cli-profile.mjs, scripts/refresh-cli-layout.mjs), so every
|
||||
# platform's CLI build optimizes and lays out the code it compiles. The
|
||||
# committed ones fall behind main within a day or two. If this job fails,
|
||||
# the CLI builds use the committed profile and layout.
|
||||
cli-profile:
|
||||
name: Collect the native compiler profile
|
||||
needs: check-release
|
||||
@@ -139,11 +139,17 @@ jobs:
|
||||
- name: Collect the profile
|
||||
run: >-
|
||||
node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4
|
||||
--output "$RUNNER_TEMP/cli-profile/cli.profdata.gz"
|
||||
--output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" --work "$RUNNER_TEMP/cli-profile-work"
|
||||
- name: Collect the function layout
|
||||
run: >-
|
||||
node scripts/refresh-cli-layout.mjs --jobs 4 --instrumented "$RUNNER_TEMP/cli-profile-work/cli"
|
||||
--output "$RUNNER_TEMP/cli-profile/cli.layout.gz"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile/cli.profdata.gz
|
||||
path: |
|
||||
${{ runner.temp }}/cli-profile/cli.profdata.gz
|
||||
${{ runner.temp }}/cli-profile/cli.layout.gz
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -336,8 +342,8 @@ jobs:
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile
|
||||
# Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and
|
||||
# build-native-cli.mts uses the committed profile.
|
||||
# Without the artifact, SCRIPTC_NATIVE_PROFILE_USE and SCRIPTC_NATIVE_LAYOUT
|
||||
# stay unset and build-native-cli.mts uses the committed profile and layout.
|
||||
- name: Select the native compiler profile
|
||||
if: matrix.cli != ''
|
||||
shell: bash
|
||||
@@ -347,6 +353,11 @@ jobs:
|
||||
if [ -n "$COLLECTED" ]; then
|
||||
echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV"
|
||||
echo "The native CLI builds with the profile collected on $GITHUB_SHA"
|
||||
layout="$(dirname "$COLLECTED")/cli.layout.gz"
|
||||
if [ -f "$layout" ]; then
|
||||
echo "SCRIPTC_NATIVE_LAYOUT=$layout" >> "$GITHUB_ENV"
|
||||
echo "The native CLI builds with the function layout collected on $GITHUB_SHA"
|
||||
fi
|
||||
else
|
||||
echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile"
|
||||
fi
|
||||
|
||||
@@ -18,6 +18,8 @@ The release job builds each pinned LLVM helper, precompiled runtime pack, and na
|
||||
|
||||
Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc-<version>-cli.profdata.gz`. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile `packages/compiler/native/cli.profdata.gz`, which local and CI builds of the native compiler also use. Refresh the committed profile when compiler code has changed substantially since the last refresh: run `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` on a quiet Linux x64 host and commit the new profile on its own.
|
||||
|
||||
Builds of the native compiler also order its functions by the committed layout `packages/compiler/native/cli.layout.gz`: the functions that `--version` and rebuilds of unchanged programs run come first, then the ones other builds run, so a run maps fewer pages of the executable. Functions the layout does not list keep their usual order. Function names of closures and generated helpers change with nearby edits, so the `cli-profile` job also collects a layout on the release commit (`scripts/refresh-cli-layout.mjs`, reusing the job's instrumented compiler), and the CLI builds use it (`SCRIPTC_NATIVE_LAYOUT`). Refresh the committed layout the same way as the profile, with `node scripts/refresh-cli-layout.mjs`, and commit it on its own.
|
||||
|
||||
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret.
|
||||
Each published package must have a GitHub Actions trusted publisher for
|
||||
`release.yml` and the `Release` environment. A missing configuration fails
|
||||
|
||||
Binary file not shown.
@@ -82,9 +82,30 @@ void scriptc_native_unlock(void) {}
|
||||
#include <sys/file.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
int32_t scriptc_native_fork(void) { return (int32_t)fork(); }
|
||||
#if defined(__linux__)
|
||||
/* LLVM's profile runtime, linked into instrumented builds of the compiler
|
||||
* (SCRIPTC_NATIVE_PROFILE_GENERATE); other builds leave these null. A worker
|
||||
* starts with zeroed counters and writes its profile before _exit, which
|
||||
* skips the runtime's atexit writer, so the profile counts the worker's work
|
||||
* once. */
|
||||
extern void __llvm_profile_reset_counters(void) __attribute__((weak));
|
||||
extern int __llvm_profile_write_file(void) __attribute__((weak));
|
||||
#endif
|
||||
|
||||
void scriptc_native_exit(int32_t code) { _exit(code); }
|
||||
int32_t scriptc_native_fork(void) {
|
||||
pid_t pid = fork();
|
||||
#if defined(__linux__)
|
||||
if (pid == 0 && __llvm_profile_reset_counters != NULL) __llvm_profile_reset_counters();
|
||||
#endif
|
||||
return (int32_t)pid;
|
||||
}
|
||||
|
||||
void scriptc_native_exit(int32_t code) {
|
||||
#if defined(__linux__)
|
||||
if (__llvm_profile_write_file != NULL) (void)__llvm_profile_write_file();
|
||||
#endif
|
||||
_exit(code);
|
||||
}
|
||||
|
||||
/* The worker's exit status, 128 plus the signal for a worker a signal
|
||||
* ended, or -1 when waitpid fails. */
|
||||
|
||||
@@ -67,6 +67,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
import { CPU_LEVEL_INIT_CALL, cpuDispatchAttributes, cpuLevelDefs } from "./cpu-dispatch.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
import { type FunctionLayout, layoutTierOf } from "./function-layout.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
|
||||
@@ -233,6 +234,61 @@ function partitionGroupLines(functions: readonly IrFunction[]): string[] {
|
||||
/** Ends the group of the definitions that follow. */
|
||||
const PARTITION_GROUP_END = "; scriptc-group";
|
||||
|
||||
/** Writes the generated helpers, then the function definitions with their
|
||||
* group lines. With a layout, the definitions it lists, helpers included,
|
||||
* move after the unlisted helpers and before the other functions, by tier,
|
||||
* each in its tier's group (`layout-<tier>`). The helper compiles each
|
||||
* partition's definitions in text order, so listed ones come first in their
|
||||
* partitions, and it places a group's definitions in the same partitions.
|
||||
* Everything else keeps its order and its source-file group. */
|
||||
function writeDefinitions(
|
||||
out: string[],
|
||||
layout: FunctionLayout | undefined,
|
||||
helperTexts: readonly (readonly string[])[],
|
||||
fnDefs: readonly string[],
|
||||
groups: readonly string[],
|
||||
): void {
|
||||
const tierLine = (tier: number) => `${PARTITION_GROUP_END} layout-${tier}`;
|
||||
const listed = new Map<number, string[]>();
|
||||
const listIn = (tier: number, lines: readonly string[]) => {
|
||||
const list = listed.get(tier) ?? [];
|
||||
if (!listed.has(tier)) listed.set(tier, list);
|
||||
for (const line of lines) list.push(line);
|
||||
};
|
||||
if (layout === undefined || layout.size === 0) {
|
||||
for (const texts of helperTexts) for (const text of texts) out.push(text);
|
||||
} else {
|
||||
// A helper's text can span several entries (one per line), so the
|
||||
// definitions are found in the joined text. A listed definition moves
|
||||
// through its closing brace; what follows it stays in place.
|
||||
const joined: string[] = [];
|
||||
for (const texts of helperTexts) for (const text of texts) joined.push(text);
|
||||
for (const part of joined.join("\n").split(/\n(?=define )/)) {
|
||||
const tier = part.startsWith("define ") ? layoutTierOf(layout, part) : undefined;
|
||||
const close = tier === undefined ? -1 : part.search(/\n}(\n|$)/);
|
||||
if (tier === undefined || close < 0) {
|
||||
out.push(part);
|
||||
continue;
|
||||
}
|
||||
listIn(tier, [tierLine(tier), part.slice(0, close + 2)]);
|
||||
const rest = part.slice(close + 3);
|
||||
if (rest !== "") out.push(rest);
|
||||
}
|
||||
}
|
||||
const unlisted: number[] = [];
|
||||
fnDefs.forEach((text, i) => {
|
||||
const tier = layout === undefined || layout.size === 0 ? undefined : layoutTierOf(layout, text);
|
||||
if (tier === undefined) unlisted.push(i);
|
||||
else listIn(tier, ["", tierLine(tier), text]);
|
||||
});
|
||||
for (const tier of [...listed.keys()].sort((a, b) => a - b))
|
||||
for (const line of listed.get(tier)!) out.push(line);
|
||||
for (const [k, i] of unlisted.entries()) {
|
||||
if (k > 0 || listed.size > 0) out.push("");
|
||||
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
|
||||
}
|
||||
}
|
||||
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
@@ -948,21 +1004,24 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
);
|
||||
}
|
||||
if (globals.length > 0) out.push(``);
|
||||
for (const line of helpers) out.push(line);
|
||||
for (const line of ffiCallbacks.defs) out.push(line);
|
||||
for (const line of shapes.defs) out.push(line);
|
||||
for (const line of classShapes.defs) out.push(line);
|
||||
for (const line of classObjDefs) out.push(line);
|
||||
for (const line of emitter.walkers.defs) out.push(line);
|
||||
for (const line of emitter.dyn.defs) out.push(line);
|
||||
for (const line of wrappers) out.push(line);
|
||||
for (const line of asyncDefs) out.push(line);
|
||||
for (const line of emitter.resolveThunkDefs) out.push(line);
|
||||
const groups = partitionGroupLines(emitter.mod.functions);
|
||||
for (let i = 0; i < fnDefs.length; i++) {
|
||||
if (i > 0) out.push("");
|
||||
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
|
||||
}
|
||||
writeDefinitions(
|
||||
out,
|
||||
emitter.targetOptions.functionLayout,
|
||||
[
|
||||
helpers,
|
||||
ffiCallbacks.defs,
|
||||
shapes.defs,
|
||||
classShapes.defs,
|
||||
classObjDefs,
|
||||
emitter.walkers.defs,
|
||||
emitter.dyn.defs,
|
||||
wrappers,
|
||||
asyncDefs,
|
||||
emitter.resolveThunkDefs,
|
||||
],
|
||||
fnDefs,
|
||||
partitionGroupLines(emitter.mod.functions),
|
||||
);
|
||||
if (fnDefs.length === 0) out.push("");
|
||||
out.push(PARTITION_GROUP_END, "");
|
||||
// WebAssembly modules translated into this program, and the registry.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TargetCpu } from "../target-cpu.js";
|
||||
import type { FunctionLayout } from "./function-layout.js";
|
||||
import { relieveHeapPressure } from "../../heap-pressure.js";
|
||||
import { wasmBoundsChecks } from "./wasm-modules.js";
|
||||
import { jsTruthy } from "./jsval.js";
|
||||
@@ -499,6 +500,8 @@ export interface LlvmTargetOptions {
|
||||
/** The host's process primitives, where it can fork (the native
|
||||
* compiler's). */
|
||||
forkPrimitives?: ForkPrimitives | undefined;
|
||||
/** Functions to write first, by tier (function-layout.ts). */
|
||||
functionLayout?: FunctionLayout;
|
||||
/** Exact frontend sources for dev-build locations and variable metadata. */
|
||||
debugSources?: ReadonlyMap<string, string>;
|
||||
/** Pointer width of the target C ABI. Native targets are 64-bit today. */
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/** Function layouts: the functions that runs of a program execute, in tiers
|
||||
* (scripts/refresh-cli-layout.mjs writes the native compiler's). The emitter
|
||||
* writes a listed function before the others, in a partition group of its
|
||||
* tier, so the code that runs together sits together in the executable. */
|
||||
|
||||
/** Layout key → tier (1 first). */
|
||||
export type FunctionLayout = ReadonlyMap<string, number>;
|
||||
|
||||
/** The layout key of an LLVM symbol: module numbers (`%m12.`, `%init.12`),
|
||||
* which change when modules are added, are left out. */
|
||||
export function functionLayoutKey(symbol: string): string {
|
||||
return symbol.replace(/_x25_m\d+_/g, "_x25_m_").replace(/_x25_init_\d+$/, "_x25_init_");
|
||||
}
|
||||
|
||||
/** Reads a layout's lines, each `<tier> <symbol>`. A function listed in two
|
||||
* tiers keeps the first. */
|
||||
export function parseFunctionLayout(text: string): FunctionLayout {
|
||||
const layout = new Map<string, number>();
|
||||
for (const line of text.split("\n")) {
|
||||
const space = line.indexOf(" ");
|
||||
if (space < 1) continue;
|
||||
const tier = Number(line.slice(0, space));
|
||||
const key = functionLayoutKey(line.slice(space + 1).trim());
|
||||
if (Number.isInteger(tier) && tier > 0 && !layout.has(key)) layout.set(key, tier);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
/** The lowest tier of the functions `text` defines, or undefined when the
|
||||
* layout lists none of them. */
|
||||
export function layoutTierOf(layout: FunctionLayout, text: string): number | undefined {
|
||||
let tier: number | undefined;
|
||||
for (let at = text.indexOf("define "); at >= 0; at = text.indexOf("\ndefine ", at + 1)) {
|
||||
const name = text.indexOf("@", at);
|
||||
const open = text.indexOf("(", name);
|
||||
if (name < 0 || open < 0) break;
|
||||
const listed = layout.get(functionLayoutKey(text.slice(name + 1, open)));
|
||||
if (listed !== undefined && (tier === undefined || listed < tier)) tier = listed;
|
||||
}
|
||||
return tier;
|
||||
}
|
||||
@@ -59,6 +59,11 @@ export interface CompileBaseOptions {
|
||||
* merge-profiles`). Code the profile does not describe, or describes
|
||||
* from an older version of the program, is optimized as without one. */
|
||||
profileUse?: string;
|
||||
/** A function layout (scripts/refresh-cli-layout.mjs; gzip-compressed when
|
||||
* the name ends in .gz): the listed functions are written first, by tier,
|
||||
* so the code that runs together sits together in the executable. The
|
||||
* native compiler's build uses it. Executables and LLVM output. */
|
||||
functionLayout?: string;
|
||||
|
||||
/** Eligible packages compile automatically. Explicit names override automatic selection heuristics. */
|
||||
npmStatic?: readonly string[] | "auto";
|
||||
|
||||
@@ -220,7 +220,7 @@ test("early executable cache misses on source and resolution changes", async ()
|
||||
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
|
||||
});
|
||||
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, and FFI manifests", async () => {
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, function layouts, and FFI manifests", async () => {
|
||||
const f = await fixture();
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
@@ -242,6 +242,7 @@ test("early executable keys isolate compile modes, paths, implementation, checke
|
||||
{ ...f.options, cpuDispatch: true },
|
||||
{ ...f.options, profile: "profile-generate:/tmp/%p.profraw" },
|
||||
{ ...f.options, profile: "profile-use:digest" },
|
||||
{ ...f.options, functionLayout: "layout-digest" },
|
||||
{ ...f.options, checker: "tsc-ts-executable-identity" },
|
||||
{ ...f.options, npmStatic: "auto" },
|
||||
{ ...f.options, npmStatic: ["commander"] },
|
||||
|
||||
@@ -157,6 +157,8 @@ export interface EarlyExecutableCacheOptions {
|
||||
/** Omitted without --profile-generate/--profile-use; otherwise the
|
||||
* pattern, or the digest of the profile's bytes. */
|
||||
profile?: string;
|
||||
/** Omitted without a function layout; otherwise the digest of its bytes. */
|
||||
functionLayout?: string;
|
||||
/** Omitted for the default tsgo checker; with SCRIPTC_CHECKER=tsc-ts, the
|
||||
* tsc-ts executable's file identity. */
|
||||
checker?: string;
|
||||
@@ -311,6 +313,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...targetCpuKeyParts(options.targetCpu),
|
||||
...cpuDispatchKeyParts(options.cpuDispatch),
|
||||
...(options.profile !== undefined ? [options.profile] : []),
|
||||
...(options.functionLayout !== undefined ? [`layout:${options.functionLayout}`] : []),
|
||||
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
|
||||
...(options.youngNursery ? ["young-nursery"] : []),
|
||||
options.npmStatic === null
|
||||
@@ -355,6 +358,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
for (const part of targetCpuKeyParts(options.targetCpu)) hash.update(`${part}\0`);
|
||||
for (const part of cpuDispatchKeyParts(options.cpuDispatch)) hash.update(`${part}\0`);
|
||||
if (options.profile !== undefined) hash.update(`${options.profile}\0`);
|
||||
if (options.functionLayout !== undefined) hash.update(`layout:${options.functionLayout}\0`);
|
||||
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
|
||||
if (options.youngNursery) hash.update("young-nursery\0");
|
||||
hash
|
||||
|
||||
@@ -54,6 +54,7 @@ export type {
|
||||
import { compilePackedLibrary } from "./backend/library-pack.js";
|
||||
import { node24FetchCompatProfile } from "./compat/fetch-profile.js";
|
||||
import { libFnSignatures } from "./ir/builtin-signatures.js";
|
||||
import { type FunctionLayout, parseFunctionLayout } from "./backend/llvm/function-layout.js";
|
||||
import { InternalCompilerError } from "./errors.js";
|
||||
import { frontendProcessExecArgv } from "./frontend-process-args.js";
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
@@ -63,6 +64,7 @@ import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import {
|
||||
clearCcCaches,
|
||||
configuredTargetPlatform,
|
||||
@@ -801,6 +803,37 @@ async function readNativeProfile(
|
||||
return { use: { path, digest: createHash("sha256").update(bytes).digest("hex") } };
|
||||
}
|
||||
|
||||
/** The digest of the request's function layout file, which keys the
|
||||
* executable cache: the layout changes the code's order, not its frontend
|
||||
* input. */
|
||||
async function functionLayoutKey(opts: CompileRequestOptions): Promise<string | undefined> {
|
||||
if (opts.functionLayout === undefined) return undefined;
|
||||
const path = resolve(opts.functionLayout);
|
||||
let bytes: Buffer;
|
||||
try {
|
||||
bytes = await readFile(path);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`cannot read function layout ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
/** The request's function layout. */
|
||||
async function readFunctionLayout(
|
||||
opts: CompileRequestOptions,
|
||||
): Promise<{ functionLayout?: FunctionLayout }> {
|
||||
if (opts.functionLayout === undefined) return {};
|
||||
const path = resolve(opts.functionLayout);
|
||||
const bytes = await readFile(path);
|
||||
return {
|
||||
functionLayout: parseFunctionLayout(
|
||||
(path.endsWith(".gz") ? gunzipSync(bytes) : bytes).toString("utf8"),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function usesPrecompiledRuntimePack(opts: CompileRequestOptions, backend: "llvm"): boolean {
|
||||
if (backend !== "llvm" || opts.sanitize === true) return false;
|
||||
return nativeCodegenTarget() !== null;
|
||||
@@ -874,6 +907,7 @@ async function prepareExecutableInput(
|
||||
llvm = emitLlvmModuleSource(mod, {
|
||||
targetTriple: process.env["SCRIPTC_TARGET"] ?? "",
|
||||
...debugOptions,
|
||||
...(await readFunctionLayout(opts)),
|
||||
pointerBits: buildPlatform === "wasi" ? 32 : 64,
|
||||
wasi: buildPlatform === "wasi",
|
||||
runtimeAbiMarker: outputKind === "obj",
|
||||
@@ -948,6 +982,7 @@ async function prepareExecutableInput(
|
||||
llvmSource = emitLlvmModuleSource(mod, {
|
||||
targetTriple: process.env["SCRIPTC_TARGET"] ?? "",
|
||||
...debugOptions,
|
||||
...(await readFunctionLayout(opts)),
|
||||
pointerBits: buildPlatform === "wasi" ? 32 : 64,
|
||||
wasi: buildPlatform === "wasi",
|
||||
runtimeAbiMarker: useRuntimePack,
|
||||
@@ -1571,9 +1606,11 @@ async function compileTracked(
|
||||
platform: buildPlatform,
|
||||
});
|
||||
let profile: NativeProfileOptions | undefined;
|
||||
let functionLayoutDigest: string | undefined;
|
||||
try {
|
||||
if (profileRefusal !== null) throw new Error(profileRefusal);
|
||||
profile = await readNativeProfile(opts);
|
||||
functionLayoutDigest = await functionLayoutKey(opts);
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -1609,6 +1646,7 @@ async function compileTracked(
|
||||
...targetCpuField(opts.targetCpu),
|
||||
...(opts.cpuDispatch === true ? { cpuDispatch: true as const } : {}),
|
||||
...(profile === undefined ? {} : { profile: nativeProfileKey(profile) }),
|
||||
...(functionLayoutDigest === undefined ? {} : { functionLayout: functionLayoutDigest }),
|
||||
...checkerField(),
|
||||
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
|
||||
npmStatic: opts.npmStatic ?? null,
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { compile } from "../src/index.js";
|
||||
import {
|
||||
functionLayoutKey,
|
||||
layoutTierOf,
|
||||
parseFunctionLayout,
|
||||
} from "../src/backend/llvm/function-layout.js";
|
||||
|
||||
const source = `
|
||||
function first(n: number): number { return n + 1; }
|
||||
function second(n: number): number { return n * 2; }
|
||||
function third(n: number): number { return n - 3; }
|
||||
console.log(first(Number(process.argv.length)), second(4), third(5));
|
||||
`;
|
||||
|
||||
async function emitLlvm(layout?: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-function-layout-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ll");
|
||||
await writeFile(entry, source);
|
||||
let functionLayout: string | undefined;
|
||||
if (layout !== undefined) {
|
||||
functionLayout = join(dir, "layout.gz");
|
||||
await writeFile(functionLayout, gzipSync(layout));
|
||||
}
|
||||
const result = await compile(entry, {
|
||||
outDir: dir,
|
||||
outPath,
|
||||
outputKind: "llvm",
|
||||
...(functionLayout === undefined ? {} : { functionLayout }),
|
||||
});
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
return await readFile(outPath, "utf8");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Each defined function's name with the partition group line it follows. */
|
||||
function definitions(llvm: string): { name: string; group: string }[] {
|
||||
const out: { name: string; group: string }[] = [];
|
||||
let group = "";
|
||||
for (const line of llvm.split("\n")) {
|
||||
if (line.startsWith("; scriptc-group")) group = line.slice("; scriptc-group".length).trim();
|
||||
const name = /^define [^@]*@([\w.$]+)\(/.exec(line)?.[1];
|
||||
if (name !== undefined) out.push({ name, group });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test("a layout writes the functions it lists first, by tier, in their tier's group", async () => {
|
||||
const plain = definitions(await emitLlvm()).filter((d) => /_(first|second|third)$/.test(d.name));
|
||||
expect(plain.map((d) => d.name.replace(/^sc_b?f_/, ""))).toEqual(["first", "second", "third"]);
|
||||
expect(plain.every((d) => !d.group.startsWith("layout-"))).toBe(true);
|
||||
|
||||
const names = new Map(plain.map((d) => [d.name.replace(/^sc_b?f_/, ""), d.name]));
|
||||
const laid = definitions(await emitLlvm(`2 ${names.get("first")}\n1 ${names.get("third")}\n`));
|
||||
const index = (fn: string) => laid.findIndex((d) => d.name === names.get(fn));
|
||||
expect(index("third")).toBeLessThan(index("first"));
|
||||
expect(index("first")).toBeLessThan(index("second"));
|
||||
expect(laid[index("third")]!.group).toBe("layout-1");
|
||||
expect(laid[index("first")]!.group).toBe("layout-2");
|
||||
expect(laid[index("second")]!.group).not.toMatch(/^layout-/);
|
||||
// Listed functions precede every unlisted definition in the function section.
|
||||
const firstUnlisted = laid.findIndex(
|
||||
(d, i) => i > index("third") && !d.group.startsWith("layout-"),
|
||||
);
|
||||
expect(firstUnlisted).toBeGreaterThan(index("first"));
|
||||
});
|
||||
|
||||
test("layout keys leave out module numbers", () => {
|
||||
expect(functionLayoutKey("sc_bf__x25_m447_lowerCall")).toBe(
|
||||
functionLayoutKey("sc_bf__x25_m12_lowerCall"),
|
||||
);
|
||||
expect(functionLayoutKey("sc_f__x25_init_253")).toBe(functionLayoutKey("sc_f__x25_init_7"));
|
||||
expect(functionLayoutKey("sc_bf__x25_m3_Lexer_scan")).not.toBe(
|
||||
functionLayoutKey("sc_bf__x25_m3_Lexer_peek"),
|
||||
);
|
||||
});
|
||||
|
||||
test("a function listed twice keeps its first tier, and malformed lines are skipped", () => {
|
||||
const layout = parseFunctionLayout("1 sc_f_a\n2 sc_f_a\n2 sc_f_b\nnot a line\n0 sc_f_c\n\n");
|
||||
expect([...layout]).toEqual([
|
||||
["sc_f_a", 1],
|
||||
["sc_f_b", 2],
|
||||
]);
|
||||
const text =
|
||||
"define internal void @sc_f_b() {\n ret void\n}\n\ndefine void @sc_f_a() {\n ret void\n}";
|
||||
expect(layoutTierOf(layout, text)).toBe(1);
|
||||
expect(layoutTierOf(layout, "define void @sc_f_z() {\n ret void\n}")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("an executable built with a layout that lists helpers and functions runs unchanged", async () => {
|
||||
const program = `
|
||||
class Point { constructor(public x: number, public y: number) {} }
|
||||
function norm(p: Point): number { return Math.sqrt(p.x * p.x + p.y * p.y); }
|
||||
const points = [new Point(3, 4), new Point(6, 8)];
|
||||
console.log(points.map(norm).join(","), points[0]!.x === 3 ? "three" : "other");
|
||||
`;
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-function-layout-exe-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, program);
|
||||
const llvmPath = join(dir, "main.ll");
|
||||
const emitted = await compile(entry, { outDir: dir, outPath: llvmPath, outputKind: "llvm" });
|
||||
expect(emitted.ok, JSON.stringify(emitted.diagnostics)).toBe(true);
|
||||
// The generated helpers written before the functions, and one function,
|
||||
// in two tiers.
|
||||
const names = definitions(await readFile(llvmPath, "utf8")).map((d) => d.name);
|
||||
const helpers = names.slice(
|
||||
0,
|
||||
names.findIndex((name) => /^sc_b?f_/.test(name)),
|
||||
);
|
||||
expect(helpers.length).toBeGreaterThan(0);
|
||||
const norm = names.find((name) => /^sc_b?f_norm$/.test(name))!;
|
||||
const layout = join(dir, "layout");
|
||||
await writeFile(layout, [`1 ${norm}`, ...helpers.map((name) => `2 ${name}`)].join("\n"));
|
||||
const laid = join(dir, "laid.ll");
|
||||
const laidOut = await compile(entry, {
|
||||
outDir: dir,
|
||||
outPath: laid,
|
||||
outputKind: "llvm",
|
||||
functionLayout: layout,
|
||||
});
|
||||
expect(laidOut.ok, JSON.stringify(laidOut.diagnostics)).toBe(true);
|
||||
const order = definitions(await readFile(laid, "utf8"));
|
||||
expect(order.filter((d) => d.group === "layout-2").map((d) => d.name)).toEqual(helpers);
|
||||
expect(order.find((d) => d.name === norm)!.group).toBe("layout-1");
|
||||
const exe = join(dir, "main");
|
||||
const built = await compile(entry, {
|
||||
outputKind: "exe",
|
||||
outDir: dir,
|
||||
outPath: exe,
|
||||
functionLayout: layout,
|
||||
});
|
||||
expect(built.ok, JSON.stringify(built.diagnostics)).toBe(true);
|
||||
expect(execFileSync(exe, { encoding: "utf8" })).toBe("5,10 three\n");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a changed function layout misses the executable cache", async () => {
|
||||
vi.stubEnv("SCRIPTC_NO_CACHE", undefined);
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-function-layout-cache-"));
|
||||
try {
|
||||
vi.stubEnv("SCRIPTC_CACHE_DIR", join(dir, "cache"));
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, source);
|
||||
const exe = join(dir, "main");
|
||||
const llvm = join(dir, "main.ll");
|
||||
const build = async (functionLayout?: string) => {
|
||||
const built = await compile(entry, {
|
||||
outputKind: "exe",
|
||||
outDir: dir,
|
||||
outPath: exe,
|
||||
...(functionLayout === undefined ? {} : { functionLayout }),
|
||||
});
|
||||
expect(built.ok, JSON.stringify(built.diagnostics)).toBe(true);
|
||||
return definitions(await readFile(llvm, "utf8"));
|
||||
};
|
||||
const plain = await build();
|
||||
const third = plain.find((d) => /^sc_b?f_third$/.test(d.name))!.name;
|
||||
const first = plain.find((d) => /^sc_b?f_first$/.test(d.name))!.name;
|
||||
const layout = join(dir, "layout");
|
||||
await writeFile(layout, `1 ${third}\n`);
|
||||
expect((await build(layout)).find((d) => d.name === third)!.group).toBe("layout-1");
|
||||
// The same path with other contents is another layout.
|
||||
await writeFile(layout, `1 ${first}\n`);
|
||||
const edited = await build(layout);
|
||||
expect(edited.find((d) => d.name === first)!.group).toBe("layout-1");
|
||||
expect(edited.find((d) => d.name === third)!.group).not.toMatch(/^layout-/);
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -25,6 +25,19 @@ export function verifyBootstrapSeed(directory, revision, sanitize) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The environment a bootstrap seed builds with, over the inherited one. The
|
||||
* bootstrap compares the seed's LLVM with the native compiler's own emission,
|
||||
* which has no function layout. The layout only orders the functions, so the
|
||||
* seed is built without it. */
|
||||
export function seedBuildEnvironment(sanitize) {
|
||||
return {
|
||||
SCRIPTC_SAN: sanitize ? "1" : "",
|
||||
SCRIPTC_NATIVE_EMIT_IR: "1",
|
||||
SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release",
|
||||
SCRIPTC_NATIVE_LAYOUT: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const directory = process.env.SCRIPTC_BOOTSTRAP_SEED_DIRECTORY;
|
||||
const revision = process.env.GITHUB_SHA;
|
||||
@@ -47,11 +60,7 @@ async function main() {
|
||||
{
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
SCRIPTC_NATIVE_EMIT_IR: "1",
|
||||
SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release",
|
||||
},
|
||||
env: { ...process.env, ...seedBuildEnvironment(sanitize) },
|
||||
},
|
||||
);
|
||||
writeFileSync(
|
||||
|
||||
@@ -37,6 +37,15 @@ const useProfile =
|
||||
!sanitize &&
|
||||
requestedProfile !== "" &&
|
||||
(requestedProfile !== undefined || existsSync(committedProfile));
|
||||
// Builds lay out the compiler's functions with the committed layout
|
||||
// (scripts/refresh-cli-layout.mjs), or the one SCRIPTC_NATIVE_LAYOUT names;
|
||||
// an empty SCRIPTC_NATIVE_LAYOUT, and instrumented builds, use none.
|
||||
const committedLayout = join(root, "packages/compiler/native/cli.layout.gz");
|
||||
const requestedLayout = process.env["SCRIPTC_NATIVE_LAYOUT"];
|
||||
const functionLayout =
|
||||
profileGenerate !== undefined || requestedLayout === ""
|
||||
? undefined
|
||||
: (requestedLayout ?? (existsSync(committedLayout) ? committedLayout : undefined));
|
||||
let target = nativeCodegenTarget();
|
||||
if (target === null || target.platform === "wasi")
|
||||
throw new Error("a supported native host is required to build the compiler");
|
||||
@@ -126,6 +135,7 @@ const result = await compile(join(root, "packages/compiler/src/native/cli.ts"),
|
||||
sanitize,
|
||||
...(profileGenerate === undefined ? {} : { profileGenerate }),
|
||||
...(profileUse === undefined ? {} : { profileUse }),
|
||||
...(functionLayout === undefined ? {} : { functionLayout }),
|
||||
emitIr: process.env["SCRIPTC_NATIVE_EMIT_IR"] === "1",
|
||||
});
|
||||
if (!result.ok)
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env node
|
||||
/* Refreshes packages/compiler/native/cli.layout.gz, the function layout that
|
||||
* builds of the native compiler use (scripts/build-native-cli.mts). The
|
||||
* layout lists, in two tiers, the functions the compiler runs: tier 1 for
|
||||
* `--version` and rebuilds of unchanged programs, tier 2 for the other
|
||||
* builds. The emitter writes the listed functions first, so the code a build
|
||||
* runs sits together in the compiler's executable and a run maps fewer of its
|
||||
* pages.
|
||||
*
|
||||
* Builds an instrumented native compiler for this Linux host (or reuses one
|
||||
* with --instrumented), builds each training program cold and then again
|
||||
* unchanged, and reads the functions each kind of run executed from the raw
|
||||
* profiles. The training programs are the runtime benchmark workloads at
|
||||
* `speed` and every seventh corpus program at the default optimization. A
|
||||
* refresh takes about 15 minutes on 16 cores, most of it for the instrumented
|
||||
* build. A stale layout lists some functions that no longer exist and misses
|
||||
* new ones, which then keep their usual order.
|
||||
*
|
||||
* node scripts/refresh-cli-layout.mjs [--jobs 8] [--instrumented <compiler dir>]
|
||||
*/
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
appendFileSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { cpus, tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { constants, gzipSync, inflateSync } from "node:zlib";
|
||||
|
||||
const root = fileURLToPath(new URL("..", import.meta.url));
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
jobs: { type: "string", default: String(Math.max(1, Math.floor(cpus().length / 2))) },
|
||||
output: { type: "string", default: join(root, "packages/compiler/native/cli.layout.gz") },
|
||||
work: { type: "string" },
|
||||
instrumented: { type: "string" },
|
||||
help: { type: "boolean", default: false },
|
||||
},
|
||||
});
|
||||
if (values.help) {
|
||||
console.log(
|
||||
"Usage: node scripts/refresh-cli-layout.mjs [--jobs N] [--output <layout.gz>] [--work <dir>] [--instrumented <compiler dir>]",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
if (process.platform !== "linux" || process.arch !== "x64")
|
||||
throw new Error("the compiler layout is collected on Linux x64");
|
||||
const jobs = Number(values.jobs);
|
||||
const output = resolve(values.output);
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-cli-layout-")));
|
||||
const raw = join(work, "raw");
|
||||
const builds = join(work, "builds");
|
||||
rmSync(raw, { recursive: true, force: true });
|
||||
for (const dir of [raw, builds]) mkdirSync(dir, { recursive: true });
|
||||
const log = join(work, "training.log");
|
||||
|
||||
const cli = resolve(values.instrumented ?? join(work, "cli"));
|
||||
if (values.instrumented === undefined) {
|
||||
console.log(`building the instrumented compiler in ${cli}`);
|
||||
const built = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--max-old-space-size=8192",
|
||||
"--import",
|
||||
"tsx",
|
||||
join(root, "scripts/build-native-cli.mts"),
|
||||
cli,
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, SCRIPTC_NATIVE_PROFILE_GENERATE: join(raw, "build-%4m.profraw") },
|
||||
},
|
||||
);
|
||||
if (built.status !== 0) throw new Error("the instrumented compiler did not build");
|
||||
}
|
||||
const scriptc = join(cli, "bin/scriptc");
|
||||
// Phase timing shows whether a rebuild hit both caches. It is on for every
|
||||
// run, so the rebuild's environment matches the first build's.
|
||||
const baseEnv = { ...process.env, SCRIPTC_TIMING: "1" };
|
||||
delete baseEnv["SCRIPTC_LLVM_HELPER"];
|
||||
|
||||
/** Runs the instrumented compiler, writing its profile to `options.profile`. */
|
||||
function run(args, options) {
|
||||
return new Promise((done) => {
|
||||
const child = spawn(scriptc, args, {
|
||||
cwd: options.cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...baseEnv,
|
||||
SCRIPTC_CACHE_DIR: options.cache,
|
||||
LLVM_PROFILE_FILE: options.profile,
|
||||
},
|
||||
});
|
||||
let text = "";
|
||||
child.stdout.on("data", (chunk) => (text += chunk));
|
||||
child.stderr.on("data", (chunk) => (text += chunk));
|
||||
child.on("close", (code) => {
|
||||
appendFileSync(log, `$ scriptc ${args.join(" ")}\n${text}exit ${code}\n`);
|
||||
done({ ok: code === 0, text });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function pool(tasks) {
|
||||
let next = 0;
|
||||
let passed = 0;
|
||||
await Promise.all(
|
||||
Array.from({ length: jobs }, async () => {
|
||||
while (next < tasks.length) if (await tasks[next++]()) passed++;
|
||||
}),
|
||||
);
|
||||
return passed;
|
||||
}
|
||||
|
||||
const programNumber = (name) => Number.parseInt(name, 10);
|
||||
const training = [
|
||||
...readdirSync(join(root, "benchmarks/runtime/workloads"))
|
||||
.filter((name) => name.endsWith(".ts"))
|
||||
.map((name) => ({
|
||||
file: join(root, "benchmarks/runtime/workloads", name),
|
||||
optimization: "speed",
|
||||
})),
|
||||
...readdirSync(join(root, "tests/corpus"))
|
||||
.filter((name) => /^\d+-.*\.ts$/.test(name) && programNumber(name) % 7 === 0)
|
||||
.map((name) => ({ file: join(root, "tests/corpus", name), optimization: "release" })),
|
||||
];
|
||||
|
||||
// Tier 1 collects each run that took the warm path from its own profile; a
|
||||
// rebuild that missed a cache (it shows no `frontend-cache-hit` or
|
||||
// `executable-cache-hit` phase) is left out. Tier 2 pools the first builds.
|
||||
const warm = new Set();
|
||||
const cold = new Set();
|
||||
let skipped = 0;
|
||||
async function warmRun(args, cache, name, cacheHits) {
|
||||
const profile = join(raw, `warm-${name}.profraw`);
|
||||
const { ok, text } = await run(args, { cwd: root, cache, profile });
|
||||
if (ok && cacheHits.every((phase) => text.includes(`"phase":"${phase}"`)))
|
||||
for (const fn of executedFunctions(profile)) warm.add(fn);
|
||||
else skipped++;
|
||||
rmSync(profile, { force: true });
|
||||
}
|
||||
|
||||
const started = Date.now();
|
||||
await warmRun(["--version"], join(work, "cache-version"), "version", []);
|
||||
console.log(`training: ${training.length} programs, cold then unchanged, ${jobs} at a time`);
|
||||
const passed = await pool(
|
||||
training.map((program, index) => async () => {
|
||||
const cache = join(work, `cache-${index}`);
|
||||
const out = join(builds, `program-${index}`);
|
||||
const args = ["build", program.file, `--optimization=${program.optimization}`, "-o", out];
|
||||
const { ok } = await run(args, { cwd: root, cache, profile: join(raw, "cold-%4m.profraw") });
|
||||
if (ok)
|
||||
await warmRun(args, cache, String(index), ["frontend-cache-hit", "executable-cache-hit"]);
|
||||
rmSync(cache, { recursive: true, force: true });
|
||||
rmSync(out, { force: true });
|
||||
return ok;
|
||||
}),
|
||||
);
|
||||
console.log(
|
||||
`trained in ${Math.round((Date.now() - started) / 1000)} s; ${passed} of ${training.length} programs built, ${skipped} rebuilds left out of tier 1 (see ${log})`,
|
||||
);
|
||||
|
||||
/** Functions a raw profile (LLVM's format version 10) records as run: the
|
||||
* names whose counters are not all zero, without the file prefix of
|
||||
* internal functions. */
|
||||
function executedFunctions(path) {
|
||||
const buf = readFileSync(path);
|
||||
const u64 = (offset) => Number(buf.readBigUInt64LE(offset));
|
||||
if (buf.readBigUInt64LE(0) !== 0xff6c70726f667281n)
|
||||
throw new Error(`${path} is not a 64-bit raw profile`);
|
||||
const version = Number(buf.readBigUInt64LE(8) & 0xffffffffn);
|
||||
if (version !== 10) throw new Error(`${path} has raw profile version ${version}, expected 10`);
|
||||
const [binaryIdsBytes, records, beforeCounters, counters, afterCounters] = [2, 3, 4, 5, 6].map(
|
||||
(field) => u64(field * 8),
|
||||
);
|
||||
const [bitmapBytes, afterBitmap, namesBytes] = [7, 8, 9].map((field) => u64(field * 8));
|
||||
const valueKinds = u64(15 * 8) + 1;
|
||||
const recordBytes = Math.ceil((6 * 8 + 4 + 2 * valueKinds + 4) / 8) * 8;
|
||||
const dataStart = 16 * 8 + binaryIdsBytes;
|
||||
const countersStart = dataStart + records * recordBytes + beforeCounters;
|
||||
const namesStart = countersStart + counters * 8 + afterCounters + bitmapBytes + afterBitmap;
|
||||
const countersDelta = buf.readBigInt64LE(10 * 8);
|
||||
const names = new Map();
|
||||
let at = namesStart;
|
||||
const uleb = () => {
|
||||
let value = 0;
|
||||
let scale = 1;
|
||||
for (;;) {
|
||||
const byte = buf[at++];
|
||||
value += (byte & 0x7f) * scale;
|
||||
if ((byte & 0x80) === 0) return value;
|
||||
scale *= 128;
|
||||
}
|
||||
};
|
||||
while (at < namesStart + namesBytes) {
|
||||
const plain = uleb();
|
||||
const packed = uleb();
|
||||
if (plain === 0) break;
|
||||
const bytes =
|
||||
packed === 0 ? buf.subarray(at, at + plain) : inflateSync(buf.subarray(at, at + packed));
|
||||
at += packed === 0 ? plain : packed;
|
||||
for (const name of bytes.toString("latin1").split("\x01"))
|
||||
names.set(createHash("md5").update(name, "latin1").digest().readBigUInt64LE(0), name);
|
||||
}
|
||||
const ran = [];
|
||||
for (let index = 0; index < records; index++) {
|
||||
const record = dataStart + index * recordBytes;
|
||||
const counterOffset = Number(
|
||||
buf.readBigInt64LE(record + 16) - (countersDelta - BigInt(index * recordBytes)),
|
||||
);
|
||||
const count = buf.readUInt32LE(record + 48);
|
||||
for (let c = 0; c < count; c++) {
|
||||
if (buf.readBigUInt64LE(countersStart + counterOffset + c * 8) !== 0n) {
|
||||
const name = names.get(buf.readBigUInt64LE(record));
|
||||
if (name !== undefined) ran.push(name.slice(name.indexOf(";") + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ran;
|
||||
}
|
||||
|
||||
for (const name of readdirSync(raw))
|
||||
if (name.startsWith("cold-")) for (const fn of executedFunctions(join(raw, name))) cold.add(fn);
|
||||
const lines = [
|
||||
...[...warm].sort().map((name) => `1 ${name}`),
|
||||
...[...cold]
|
||||
.filter((name) => !warm.has(name))
|
||||
.sort()
|
||||
.map((name) => `2 ${name}`),
|
||||
];
|
||||
writeFileSync(output, gzipSync(`${lines.join("\n")}\n`, { level: constants.Z_BEST_COMPRESSION }));
|
||||
console.log(
|
||||
`wrote ${output}: ${warm.size} tier-1 and ${lines.length - warm.size} tier-2 functions (${statSync(output).size} bytes)`,
|
||||
);
|
||||
rmSync(builds, { recursive: true, force: true });
|
||||
if (values.work === undefined) rmSync(work, { recursive: true, force: true });
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
|
||||
import { seedBuildEnvironment, verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
|
||||
|
||||
test("bootstrap artifacts require a complete seed from the same revision, lane, and path", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-seed-contract-"));
|
||||
@@ -36,6 +36,22 @@ test("bootstrap artifacts require a complete seed from the same revision, lane,
|
||||
}
|
||||
});
|
||||
|
||||
test("seeds build without the function layout in both lanes, shared or not", () => {
|
||||
for (const sanitize of [false, true])
|
||||
expect(seedBuildEnvironment(sanitize)).toEqual({
|
||||
SCRIPTC_SAN: sanitize ? "1" : "",
|
||||
SCRIPTC_NATIVE_EMIT_IR: "1",
|
||||
SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release",
|
||||
SCRIPTC_NATIVE_LAYOUT: "",
|
||||
});
|
||||
// The bootstrap test builds its own seed when CI shares none.
|
||||
const driver = readFileSync(
|
||||
new URL("./self-hosting-native-driver.test.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
expect(driver).toContain("...seedBuildEnvironment(sanitize)");
|
||||
});
|
||||
|
||||
test("CI restores each seed only into its matching instrumented bootstrap lane", () => {
|
||||
const workflow = load(
|
||||
readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
|
||||
import { seedBuildEnvironment, verifyBootstrapSeed } from "../../scripts/bootstrap-seed.mjs";
|
||||
import { execFile, spawnSync } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
@@ -89,12 +89,7 @@ test(
|
||||
// seed avoids optimizing the huge compiler module before ASan can
|
||||
// exercise its complete traversal, serialization and LLVM emission.
|
||||
...options,
|
||||
env: {
|
||||
...process.env,
|
||||
SCRIPTC_SAN: sanitize ? "1" : "",
|
||||
SCRIPTC_NATIVE_EMIT_IR: "1",
|
||||
SCRIPTC_NATIVE_OPTIMIZATION: sanitize ? "dev" : "release",
|
||||
},
|
||||
env: { ...process.env, ...seedBuildEnvironment(sanitize) },
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user