mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
The error identity cache (scr_dyn_from_error) keeps one checked-dynamic node per error so a crossing error compares reference-equal and keeps the properties set on it. It retained both sides for the whole process, so every error that crossed a checked-dynamic boundary (a caught error read through a cast, an error delivered to an untyped callback, a dns failure, a warning) stayed alive until exit: 100,000 caught AssertionErrors read through a cast grew the resident set by about 210 MiB. The cache now sweeps itself. An entry whose error and node are referenced only by the cache is unreachable (a node holds no reference to its error; a registered subclass box holds one, and a spawnSync error's `error` property refers to its own node), so a sweep releases both sides. Sweeps run when the cache doubles past the entries the last sweep kept, so they cost amortized constant time per crossing. The cache is per thread, and objects shared across threads are published (immortal), so a sweep never races another thread's reference. Live errors and live nodes keep their entries: identity, rethrowing the same instance, isNativeError and kind checks are unchanged. scr_error_set_prop sets an own property on an error's node; dns, assert, cipher, crypto, util.callbackify and warning errors use it for the properties they attach. util.inspect of a runtime error renders through its node: Node's own properties follow the bracket form in key order (a system error's errno, code, syscall and path, and the extra properties a throw site attached), and an AssertionError renders at depth 0 with long string actual/expected values shortened, as its custom inspect does. A node that contains itself renders as Node's <ref *1>/[Circular *1]. child_process errors carry Node's own properties: - execSync and execFileSync: status, signal, output, pid, stdout and stderr (Buffers, or strings with an encoding option); a spawn failure or timeout adds errno, syscall, path, spawnargs and error first. - execFile's callback and the promisified execFile/exec: code (the exit status, or null after a signal), killed, signal and cmd, with stdout and stderr when promisified; a spawn failure has errno, code, syscall, path, spawnargs and cmd. The sync forms echo the captured stderr before a timeout throw too, as Node's inheritStderr does. The cp.execSync libCall takes a flag for Buffer outputs. An error's own `stack` stays non-enumerable when assigned through its node. A worker's record-source table is freed when the worker exits.
39 lines
1.7 KiB
TypeScript
39 lines
1.7 KiB
TypeScript
/* Errors that cross into checked-dynamic code stay in the runtime's error
|
|
* identity cache only while something references them: a retry loop that
|
|
* reads 100,000 caught errors through a cast keeps the resident set flat,
|
|
* on the main thread and in a worker (each thread sweeps its own cache).
|
|
* Plain lane only: the sanitizer's allocator quarantine holds freed
|
|
* memory, so resident size says nothing there. */
|
|
import { execFile } from "node:child_process";
|
|
import { mkdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { describe, expect, test } from "vitest";
|
|
import { compile } from "@scriptc/compiler";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = join(import.meta.dirname, "../..");
|
|
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/error-cache-sweep");
|
|
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
|
|
|
describe.skipIf(sanitize)("error identity cache", () => {
|
|
test("a retry loop of crossing errors keeps memory flat", { timeout: 300_000 }, async () => {
|
|
mkdirSync(outDir, { recursive: true });
|
|
const result = await compile(join(repoRoot, "tests/fixtures/error-cache-sweep/retry-loop.ts"), {
|
|
outPath: join(outDir, "retry-loop"),
|
|
outDir,
|
|
backend: "llvm",
|
|
});
|
|
if (!result.ok) {
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
}
|
|
for (const args of [[], ["worker"]]) {
|
|
const run = await execFileAsync(result.binaryPath, args);
|
|
expect(run.stderr).toBe("");
|
|
expect(Number(run.stdout.trim()), `growth in MiB (${args.join(" ") || "main"})`).toBeLessThan(
|
|
16,
|
|
);
|
|
}
|
|
});
|
|
});
|