Files
scriptc/tests/harness/bench-dynamic-tier.test.ts
T
Malte UblandClaude Opus 5.5 9a0a3f1562 Load Node.js in-process on first use: libnode bridge and runtime loader
Scope-B milestone NI-1, runtime side. Programs whose IR has a
`nodeIsland.*` operation link scr_node_island.c and the SCR_NODE_ISLANDS
build of scr_async.c; every other program links neither, byte for byte.

- scr_node_island.c dlopens the bridge of a libnode pack on the first call
  (RTLD_NOW | RTLD_GLOBAL), checks its bridge ABI and that it links exactly
  the Node version recorded at build time, starts one Node environment and
  runs the embedded bootstrap (scr_node_island_host.cjs). A run that never
  calls into Node never maps libnode. The pack directory is SCRIPTC_LIBNODE
  at run time, else the recorded path; a `$ORIGIN/` path resolves against
  the executable's directory (the beside packaging, and the hook for
  shipping Node with the program).
- scr_node_bridge.h is the C ABI between the runtime and the bridge; the
  runtime includes no Node or V8 header. The bridge
  (packages/libnode-common/bridge/scr_node_bridge.cc) initializes Node once
  with no default signal handling, no debug-signal handler or inspector,
  no stdio reinitialization and one platform worker, creates the
  environment from the built-in snapshot, routes Node's process exit to
  the runtime's exit sequence, promotes libnode to the global scope for
  native addons, and runs calls in a CallbackScope with a TryCatch.
  build-pack.mjs assembles a pack (libnode, the bridge, manifest.json) from
  a Node --shared build and the matching headers.
- The SCR_NODE_ISLANDS build of scr_async.c adds a loop slot through which
  the runtime's loop drives Node's until NI-3 hosts the runtime's queues on
  Node's libuv: it keeps the program alive while Node has ref'd work,
  sleeps on Node's backend fd up to its next timer, and runs one
  non-blocking turn per loop turn.
- The `nodeIsland` runtime feature (rule, link features, unit table, pack
  matrix row and scr_async.c variant), the nodeIsland.call/callSync
  libCalls, the install call in main, and the build-time record of the
  pack (node-islands/libnode.ts: --libnode=<pack>|beside or
  SCRIPTC_LIBNODE, SC8005 when none is usable, Node >=24.13.1).
- The zero-cost gate and the bench nm test require that no good-code
  executable defines a Node island symbol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:54:47 -07:00

68 lines
2.8 KiB
TypeScript

/* Two-tier invariant I3 for the runtime benchmark workloads, in CI: the
* release executable of a fully typed workload defines no dynamic-tier
* runtime symbol (DYNAMIC_TIER_PREFIXES), and every workload the zero-cost
* gate lists as dynamic (DYNAMIC_WORKLOADS) still defines one, so the list
* shrinks as leaks close. The gate (scripts/zero-cost-gate.mjs) checks the
* same contract, but only when someone runs it. The contract is the Linux
* release binary the gate measures, so other hosts and the sanitized lane
* skip the builds. */
import { execFile } from "node:child_process";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
import {
DYNAMIC_WORKLOADS,
definedFunctionNames,
dynamicTierSymbols,
nodeIslandSymbols,
} from "../../scripts/zero-cost-compare.mjs";
import { readWorkloads } from "../../benchmarks/runtime/workloads.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = join(import.meta.dirname, "../..");
const suite = join(repoRoot, "benchmarks/runtime");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/bench-dynamic-tier");
const skip = process.platform !== "linux" || process.env["SCRIPTC_SAN"] === "1";
interface Workload {
name: string;
entry: string;
}
// One program per entry file, named after its first workload, as the gate
// names them.
const programs = new Map<string, string>();
for (const w of readWorkloads(suite) as Workload[])
if (![...programs.values()].includes(w.entry)) programs.set(`bench/${w.name}`, w.entry);
test("every workload the gate lists as dynamic is a bench program", () => {
for (const id of DYNAMIC_WORKLOADS) expect(programs.has(id), id).toBe(true);
});
test.skipIf(skip).each([...programs])(
"%s links the dynamic tier only if listed",
async (id, entry) => {
const outDir = join(cacheDir, id.slice("bench/".length));
mkdirSync(outDir, { recursive: true });
const result = await compile(join(suite, entry), { outPath: join(outDir, "program"), outDir });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const { stdout } = await execFileAsync("nm", [result.binaryPath], {
encoding: "utf8",
maxBuffer: 1 << 28,
});
const defined = definedFunctionNames(stdout);
// No workload calls into Node.js: none links the Node island runtime.
expect(nodeIslandSymbols(defined), `${id} links Node island symbols`).toEqual([]);
const linked = dynamicTierSymbols(defined);
if (DYNAMIC_WORKLOADS.has(id))
expect(
linked.length,
`${id} links no dynamic-tier symbol: drop it from DYNAMIC_WORKLOADS`,
).toBeGreaterThan(0);
else expect(linked, `fully typed ${id} links dynamic-tier symbols`).toEqual([]);
},
);