mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Mature and old passes are scheduled by heap growth. While growth-triggered passes at a level keep finding nothing, the fraction they wait for widens by doublings from 1/4, and it used to stop at 1, so a large live heap that keeps growing was walked in full once per doubling. On tsc-ts those walks are most of the collector's time: in single-threaded Excalidraw, 12 growth-triggered full passes cost 2.0 s of 14.7 s, the last two walking 2.6M and 5.2M objects to free 1.4%. The cap is now 16. That is a memory trade-off: once passes stop finding garbage, the cycle-headered heap may grow to 17 times its size after the last pass at a level before the next (it was 2 times), so a program that accumulates dead cyclic garbage while passes find nothing can hold up to 16 times more of it. Yield-driven narrowing is unchanged: a pass that finds old garbage moves the fraction straight back to what its yield asks for. SCR_CYCLE_GROWTH_CAP lowers the cap for memory-sensitive programs, read like SCR_CYCLE_THRESHOLD: a power of two from 1 to 16 (other positive values round down, larger ones read as 16); 1 restores the previous schedule. test_cycle pins the cap on a heap at the growth floor: a dead cycle through the old generation must go exactly when the heap has grown the configured cap past the floor (not one doubling earlier, not later), and a pass that finds old garbage must narrow the fraction at once. cycle.test.ts runs it at the default cap and at SCR_CYCLE_GROWTH_CAP=1, 3 and 64. The large-chain widening check keeps its walk bound but no longer waits out the cap on a 266k-node structure, and now runs after the tests that assume the base schedule. Measured with tsc-ts origin/parallel-check e469e33, built with this runtime vs the parent commit, interleaved in one Linux Vercel Sandbox (8 vCPU; 5 rounds, medians; --checkers 8 repeated with 10 rounds). Diagnostics identical. "cap 1" is this build with SCR_CYCLE_GROWTH_CAP=1: single-threaded wall RSS cap 1 Excalidraw -10.1% 1283 -> 1263 +0.5% Playwright -11.0% 1446 -> 1457 +0.3% --checkers 8 Excalidraw -8.7% / -4.5% 2110 -> 2132 / 2115 -> 2125 +2.6% (10 rounds) Playwright -7.8% / -7.7% 2616 -> 2634 / 2589 -> 2678 -1.0% (10 rounds) RSS in MB (max of two runs). The runtime benchmark suite's allocation-heavy workloads (alloc-trees, word-graph, ast-interp, json-records, records-sort, functional-pipeline; --layouts=4 --runs=16) are neutral in time and peak RSS (-0.3% to +1.7%).
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect, test } from "vitest";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const testDir = import.meta.dirname;
|
|
|
|
test("cycle traversal preserves counts, deep graphs, configured thresholds and growth caps", async () => {
|
|
const buildDir = join(testDir, "build");
|
|
await mkdir(buildDir, { recursive: true });
|
|
const bin = join(buildDir, "test_cycle");
|
|
await execFileAsync("clang", [
|
|
"-std=c11",
|
|
"-O1",
|
|
"-Wall",
|
|
"-Wextra",
|
|
"-fsanitize=address,undefined",
|
|
"-o",
|
|
bin,
|
|
join(testDir, "test_cycle.c"),
|
|
join(testDir, "../src/scr_cycle.c"),
|
|
join(testDir, "../src/scr_alloc.c"),
|
|
]);
|
|
const baseEnv = {
|
|
...process.env,
|
|
ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1",
|
|
UBSAN_OPTIONS: "halt_on_error=1",
|
|
};
|
|
delete baseEnv.SCR_CYCLE_THRESHOLD;
|
|
delete baseEnv.SCR_CYCLE_GROWTH_CAP;
|
|
|
|
for (const [threshold, cap, expected] of [
|
|
[undefined, undefined, "threshold=256 growth_cap=16"],
|
|
["1", undefined, "threshold=1 growth_cap=16"],
|
|
["2", undefined, "threshold=2 growth_cap=16"],
|
|
["7", undefined, "threshold=7 growth_cap=16"],
|
|
[undefined, "1", "threshold=256 growth_cap=1"],
|
|
["1", "1", "threshold=1 growth_cap=1"],
|
|
[undefined, "3", "threshold=256 growth_cap=2"],
|
|
[undefined, "64", "threshold=256 growth_cap=16"],
|
|
] as const) {
|
|
const env = { ...baseEnv };
|
|
if (threshold !== undefined) env.SCR_CYCLE_THRESHOLD = threshold;
|
|
if (cap !== undefined) env.SCR_CYCLE_GROWTH_CAP = cap;
|
|
const run = await execFileAsync(bin, [], { env });
|
|
expect(run.stdout).toBe(`cycle collection checks passed: ${expected}\n`);
|
|
}
|
|
});
|