Files
scriptc/packages/runtime/test/cycle.test.ts
Malte Ubl 73a99b2393 Let the cycle collector's growth fraction widen to 16
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%).
2026-10-10 14:44:14 -07:00

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`);
}
});