Repository navigation
Conversation
…side the pipe test
shell-blocking-pipe.test.ts called generateHeapSnapshot("v8") inside the
1 MiB pipe test. Inside bun test the heap holds the runner, preload and
harness, and under a debug + ASAN build walking it takes longer than the
test's 5 second budget, so the test timed out or passed with no margin.
The snapshot was there to exercise the memoryCost hooks of the shell's
native classes, so that coverage moves to a fixture spawned in its own
process, where the heap is small, and now asserts what those hooks report:
ParsedShellScript counts the script before the command starts,
ShellInterpreter counts it while the command runs, and neither does once
the command has finished. The pipe tests themselves no longer take a
snapshot.
|
Warning Review limit reached
Next review available in: 26 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 7:05 AM PT - Aug 15th, 2026
❌ @robobun, your commit 41d232c has some failures in 🧪 To try this PR locally: bunx bun-pr 38940That installs a local version of the PR into your bun-38940 --bun |
|
Status
|
There was a problem hiding this comment.
LGTM — test-only de-flake that moves the heap snapshot into a fixture process and replaces an assertion-free call with real memoryCost checks.
What was reviewed:
- Verified
Bun.generateHeapSnapshot()(default JSC path, BunObject.cpp:841) uses the Inspector format, so the fixture's hardcoded stride of 4 is correct; the siblingcommonjs-module-heap-snapshot-fixture.cjsbranches ontypeonly because other callers may pass a GCDebugging snapshot. - Confirmed the fixture pattern matches existing snapshot fixtures and the test follows harness conventions (
bunExe/bunEnv,await using, concurrent pipe drain, exitCode last). - Checked that a missing class name (
indexOf→ -1) yields size 0, which fails the>= scriptLengthassertions rather than passing vacuously. - The PR's mutation-test evidence (breaking
compute_estimated_size_for_gcfails the new test) satisfies the "fails for the right reason" bar.
Extended reasoning...
Overview
Test-only change to test/js/bun/shell/shell-blocking-pipe.test.ts plus a new fixture shell-heap-snapshot-fixture.ts. The original test called generateHeapSnapshot("v8") inline, which under debug+ASAN walks ~13.6k nodes and blows the 5s default timeout. The fix removes the snapshot from the two pipe tests (they now check only what their names claim) and adds a third test that spawns a small-heap fixture process to take three JSC-format snapshots, asserting that ParsedShellScript and ShellInterpreter report ≥ 1 MiB while holding the script and far less afterward. No src/ files touched.
Security risks
None. Test code only; spawns bunExe() on a local fixture with bunEnv, no network, no untrusted input.
Level of scrutiny
Low-to-moderate. This is a de-flake of a known-flaky test with an unusually thorough PR description: root cause traced to snapshot cost scaling with heap size, timings tabulated, and the new assertions mutation-tested against compute_estimated_size_for_gc. The one non-obvious detail — hardcoding node stride 4 — I verified is correct for Bun.generateHeapSnapshot()'s default path (BunObject.cpp:841 constructs HeapSnapshotBuilder with no snapshot-type override, i.e. Inspector format). The existing heap.ts and commonjs-module-heap-snapshot-fixture.cjs branch on type because they may receive GCDebugging snapshots from bun:jsc; this fixture always calls the Bun. variant.
Other factors
- REVIEW.md's "never silently weaken" rule is satisfied: the original snapshot call asserted nothing and (per the PR) ran before any
ShellInterpreterexisted; the replacement is strictly stronger coverage. - Harness conventions followed:
await using proc,Promise.all([stdout, stderr, exited]), stderr and stdout asserted before exitCode,skipIf(isWindows)matching sibling tests, no sleeps. - No CODEOWNERS entry covers
test/js/bun/shell/. - No prior reviewer comments on the PR.
There was a problem hiding this comment.
LGTM — test-only de-flake that moves the heap snapshot into a small-heap fixture process and replaces a no-op call with real assertions.
What was reviewed:
- Fixture's node layout (
nodes[i+1]= size,nodes[i+2]= classNameIdx) and theGCDebugging ? 7 : 4stride matchtest/js/bun/util/heap.tsandcommonjs-module-heap-snapshot-fixture.cjs. - Original snapshot call asserted nothing and ran before any
ShellInterpreterexisted; the new test assertsmemoryCostat three lifecycle points and the author verified it fails whencompute_estimated_size_for_gcstops counting the script. - Spawn follows harness conventions:
bunExe/bunEnv,await using, concurrent drain of stdout/stderr/exited, stderr and parsed values asserted beforeexitCode, fixture named*-fixture.ts, Windows skip consistent with sibling tests.
Extended reasoning...
Overview
Test-only change to test/js/bun/shell/shell-blocking-pipe.test.ts plus a new fixture shell-heap-snapshot-fixture.ts. The first pipe test was timing out under debug+ASAN because it called generateHeapSnapshot("v8") in-process, which walks the entire ~13.6k-node bun test heap (6-8s under debug+ASAN). The snapshot call asserted nothing and — because .text() starts the command on a later microtask — ran before any ShellInterpreter existed, so it wasn't exercising what it claimed to. The PR removes the snapshot from the pipe tests and adds a third test that spawns a small-heap fixture process, takes three JSC-format snapshots at the ParsedShellScript / ShellInterpreter lifecycle points, and asserts each reports (or stops reporting) the 1 MiB script via memoryCost().
Security risks
None. Test-only; no src/ changes, no network, no untrusted input.
Level of scrutiny
Low-to-medium. This is a de-flake of a debug-lane timeout that also strengthens coverage. I checked the snapshot-parsing details against the existing helpers: the node stride selection (type === "GCDebugging" ? 7 : 4) and the field offsets (id=0, size=1, classNameIdx=2) match NodeLayout in test/js/bun/util/heap.ts and the identical pattern in commonjs-module-heap-snapshot-fixture.cjs. The JSC and v8 snapshot formats reach the same estimatedSize → memoryCost hook, so switching to the JSC format for the assertions doesn't lose coverage. The fixture uses Bun.which("cat") to force a real child process into the pipeline so the command cannot complete before the synchronous "while running" snapshot — the same construction the sibling pipe tests already use.
Other factors
- Harness conventions are followed:
bunExe()/bunEnv,await using proc,Promise.allover stdout/stderr/exited, assertions on stderr and parsed stdout beforeexitCode, fixture file named*-fixture.ts,test.skipIf(isWindows)matching the sibling tests that need systemcat. - The new assertions are meaningful (
>= 1 MiBbefore/during,< 1 MiBafter finish) and the PR description documents that temporarily removing the arena fromcompute_estimated_size_for_gcon either class makes the test fail with clear values, so the test can fail for the right reason. USE_SYSTEM_BUN=1passing is expected here — this is a test refactor with no runtime behavior change, not a bugfix that must fail on the unfixed binary.- No prior reviews from me on this PR; no outstanding reviewer comments; the one earlier note about hardcoding stride 4 was already addressed in commit 41d232c.
|
This change also removes a CI flake, so it is worth a look for that reason too. The On an Intel Mac mini from the CI pool, the shard's batch reproduces it in three of four runs (232 s to 270 s). With this file's in-process snapshot taken out of the batch, the same test takes 2.6 s to 7 s. The measurements and the cause are in #34085 (comment). |
|
Data from a second symptom of the same retained snapshot.
The numbers and a replay on Linux are in oven-sh/WebKit#656 (comment) and in #46610. |
Test-only change.
Problem
bun bd test test/js/bun/shell/shell-blocking-pipe.test.tson a debug + ASAN build fails the first test ("writing > send buffer size doesn't block the main thread") withthis test timed out after 5000ms(8033ms on this machine; on main 87b26b5 it was seen failing at 5107ms and passing at 4879ms). The sibling test, which runs the same 1 MiBecho ... | catpipeline, takes about 100ms.bun bd testis the workflow CLAUDE.md prescribes, so any change that adds to this file (io: POSIX pipe writers hold a ref on their parent from on_error until close() returns #34697 does, for example) runs into it when verified with a debug build. CI itself does not fail:scripts/runner.node.mjspasses--timeout=90000(270000 on ASAN lanes) to every file, so there the ASAN lanes just spend the 6 to 8 seconds.generateHeapSnapshot("v8")call at line 17. A snapshot walks every live cell, and insidebun testthe heap also holds the runner, the preload and harness: about 13.6k nodes and 44k edges, which the v8 snapshot builder takes 6 to 8 seconds to walk under debug + ASAN here (about 10ms in a release build). The pipeline is irrelevant to the cost: a v8 snapshot of an otherwise idlebun testheap costs the same.memoryCosthooks of the shell's native classes (ParsedShellScript,ShellInterpreter). It asserted nothing, and because.text()only starts the command on a later microtask, it ran before anyShellInterpreterexisted.Fix
memoryCostcoverage moves totest/js/bun/shell/shell-heap-snapshot-fixture.ts, spawned from the same test file, which is howv8-heap-snapshot.test.tsalready takes its snapshots. The fixture's heap is about 2k nodes, so a snapshot costs about 0.2s under debug + ASAN; the new test takes 1.1 to 1.5s underbun bd testand about 60ms in release, independent of what the test runner has loaded.ParsedShellScriptreports at least the 1 MiB script; while it runs,ShellInterpreterreports at least the script (the snapshot is taken synchronously after.then()starts the command, and a pipeline with a child process in it cannot finish until the event loop runs); after it finishes, whateverShellInterpretercells remain report far less than the script, becausefinish()resets the parse arena. It also checks the command's output survived the full GC a snapshot runs.bun testmeasured 1.3s, 1.7s, 2.1s, 3.9s, 5.2s as each one's output stays in the heap the next one walks, so three of them are back over the budget.bun:internal-for-testingand callingestimateShallowMemoryUsageOfon them would reach the same hook in microseconds and also run on Windows, but needs runtime changes plus a test that rebuilds the interpreter's construction protocol by hand; the fixture uses only the public$API and the snapshot, like the native-size checks inheap-snapshot.test.ts. Happy to do the binding version instead if that is preferred.JSCell::estimatedSizeInBytes-> generatedestimatedSize->memoryCost-> Rustmemory_cost()), so the JSC format used by the fixture exercises the same shell code the v8 call did, and its output can be read directly.bun bd test test/js/bun/shell/shell-blocking-pipe.test.ts: 3 pass, new test 1.1 to 1.5s. The unmodified file still times out under the same build on this machine.USE_SYSTEM_BUN=1 bun test test/js/bun/shell/shell-blocking-pipe.test.ts: 3 pass.Interpreter::compute_estimated_size_for_gctemporarily not countingargs, the new test fails withExpected: >= 1048576, Received: 3162; with the same change inParsedShellScript::compute_estimated_size_for_gcit fails withReceived: 1868. Both experiments were reverted; this PR touches nosrc/file.Background
memoryCost/estimatedSize: a class declared in a*.classes.tsfile withmemoryCost: truegets a generatedestimatedSize(cell, vm)entry in its JSC method table which calls the wrapped Rust object'smemory_cost(). Both heap snapshot builders (andestimateShallowMemoryUsageOf) record that value as the cell's size. The GC itself reads a separately cachedestimated_size(), so in the test suite a heap snapshot is what callsmemory_cost().ParsedShellScript/ShellInterpreter:$`...`parses the script right away into aParsedShellScriptowned by the returned promise. The firstthen()(which is whatawaitends up calling) creates aShellInterpreter, moves the parsed script into it and starts it. The parsed script is an arena holding the tokens, strings and AST, so it is at least as large as the script text;finish()resets the arena before resolving the promise, and the interpreter's JS wrapper can outlive that until the next GC. Neither object is reachable from JS.bun testdefaults to 5s per test andbun bd testforwards its arguments unchanged, while the CI runner passes--timeoutof 90s (270s with ASAN) per test. Whetherbun bd testshould mirror the runner is a separate question; this PR only makes this file cheap under either budget.Snapshot timings measured for this PR (debug + ASAN build, 12 CPU container; release numbers from the baked release build)
bun test, debugbun test, debug, with the 1 MiB pipeline pendingStrings are truncated to 1024 characters by both builders, so the 1 MiB strings do not affect the cost; the node count does. Inside
bun testthe count is the same with or without the test file importing harness, sincetest/preload.tsimports it anyway.Fixture phases under debug + ASAN: process start ~0.35s, parse + start + drain of the 1 MiB pipeline ~0.1s, three snapshots ~0.2 to 0.3s each. An earlier version of the fixture used
summarizeByTypefromtest/js/bun/util/heap.ts; that allocates an array per node, which survived into the following snapshots and roughly doubled the fixture's time, so the fixture sums node sizes directly.