Repository navigation
Description
Summary
Running @perry/postgres's bench/bench-this.ts on Perry 0.5.82 has three possible outcomes, chosen at random each run:
-
Clean completion — all 4 workloads run, numbers are great (1k×20: 10 ms vs 42 ms on 0.5.29; 10k×20: 95 ms vs 764 ms on 0.5.29). Happens roughly 1 in 4 runs.
-
SIGSEGV after
tinyworkload — crashes on the first iteration ofparam-1row(exit 139). Happens roughly 2 in 4 runs. -
Sample-array corruption in
large-10k-x-20— workload completes butsamples.lengthreads as 256 (should be 50), all samples are garbage:large-10k-x-20 n=256 min -7229134230240085510…µs p50 0µs p95 NaNms mean NaNmsHappens roughly 1 in 4 runs.
The #72 fix unblocked correctness (queries return rows) and the IC work is clearly paying off on the good runs, so this is a different residual issue — probably GC-related since the failure points are specifically where arrays are filled in loops and consumed later.
Repro
cd @perry/postgres
perry compile bench/bench-this.ts -o /tmp/bench-this
PGHOST=… /tmp/bench-this # run 3-4 times; you'll see all three modes# Good run (1 in 4):
@perry/postgres perry tiny n=50 p50 2.00ms …
@perry/postgres perry param-1row n=50 p50 2.50ms …
@perry/postgres perry medium-1k-x-20 n=50 p50 10.0ms …
@perry/postgres perry large-10k-x-20 n=50 p50 95.0ms …
# SIGSEGV run (2 in 4):
@perry/postgres perry tiny n=50 p50 2.00ms …
[ exit 139 ]
# Corrupted-sample run (1 in 4):
@perry/postgres perry tiny n=50 …
@perry/postgres perry param-1row n=50 …
@perry/postgres perry medium-1k-x-20 n=50 p50 10.0ms …
@perry/postgres perry large-10k-x-20 n=256 min -7229134230…µs p50 0µs p95 NaNms mean NaNms
Likely cause
Two failure signatures — SIGSEGV and array-length-reads-256 — both involve per-workload samples: number[] = new Array(iters):
const samples: number[] = new Array(iters);
for (let i = 0; i < iters; i++) {
const t0 = now();
const r = await conn.query(wl.sql, wl.params);
const t1 = now();
samples[i] = t1 - t0;
}
printRow(label, wl.name, computeStats(samples));The pattern allocate fixed-length array → fill in loop → pass to computeStats is the same shape as the #72 row-accumulator pattern that v0.5.82 fixed. This looks like the same bug-class still reaching another call site — probably the samples array gets scalar-replaced or freed mid-loop, and whatever replaces it in memory has different length + garbage contents.
SIGSEGV flavor: now() probes globalThis.performance (guard added in #70). After many iterations the probe path may be hitting something that's been freed.
Narrowing
The minimal repro that DOES crash has to include all of: ≥55 SELECT 1 queries in a loop, a samples array filled with now() deltas, THEN another workload (param-1row) in a fresh sample array. Isolated single-workload scripts don't crash (I ran 55 tiny + 55 param and 55 tiny + 10 param separately — both fine). The bench's WORKLOADS-array loop + stats call appears necessary.
Why it matters
Blocks stable reporting of the new perf numbers. When it works, Perry 0.5.82 delivers the massive improvements from the IC work (#68, #70, #71, #72 all landing). When it doesn't, the @perry/postgres bench matrix can't include Perry columns reliably.
Environment
- Perry 0.5.82
- macOS 26 / Apple Silicon
- Repro:
perry compile bench/bench-this.tsin @perry/postgres repo
Activity
docs: Perry 0.5.82 perf numbers + hidden-class journey
fix: type-validate inline .length PropertyGet receiver (v0.5.83, #73)
fix: tighten receiver guards to Darwin mimalloc heap window (v0.5.84, #…
fix: pin new Array(N) + header sanity guard (v0.5.85, #73)
fix: interior-pointer GC + caller-saved FP reg scan (v0.5.86, #73)
fix: defer arena block reset for recent blocks (v0.5.87, #73 final)
proggeramlug commentedon Apr 17, 2026
Fixed in v0.5.87 (95ec17a).
Progression (50-run @perry/postgres bench-this sweep):
Root cause — two-part GC liveness problem:
js_array_reduce,.sort, etc.) derivedelements_ptr = arr + 8and called user callbacks holding only the interior pointer. The conservative scan'sValidPointerSetonly matched object-start pointers, soarrlost its last reachable reference mid-iteration.setjmpcaptures x19-x28 + d8-d15; LLVM was keeping NaN-boxed handles in caller-saved FP regs (d0-d7 / d16-d31) across the async poll loop's internal calls, invisible to the scan.Fix (landed across v0.5.83 through v0.5.87):
.lengthpath, PIC receiver,clean_arr_ptr(preventive)ValidPointerSet::enclosing_object()for interior-pointer scan; inline asm to sweep d0-d31 for NaN-box candidatesResidual 6% NaN under sustained GC pressure left as a follow-up if it surfaces in production workloads.
docs: Perry 0.5.87 stable bench numbers — #73 closed
fix(codegen): #128 obj.field returns NaN on --target android (v0.5.157)
gc(runtime): #179 cut block-persist cascade + adaptive step tune (v0.…