test(js): assert retention on reactions and listeners, not a GC heap count - #4142
Conversation
…count JSC scans the stack conservatively, so a heap count after Bun.gc can be pinned by a stale pointer and fail under load. Count reactions left on still-pending promises and undisposed signal listeners instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 32 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Verdict: MERGE Good replacement for flaky GC heap counts. JSC's conservative stack scan can pin dead cells under load, so asserting on still-attached promise reactions and signal listeners measures the actual leak surface instead of Helpers are appropriately scoped: hand-recording Tests-only, no API/wire impact; stress numbers look convincing. Prefer this over broader This is an automated review, not the maintainer's decision |
Problem
The retention tests added in #4085 counted
heapStats()objects afterBun.gc(true). JSC scans the stack conservatively, so a stale pointer can pin dead cells and fail the count under load. For example, the watch player-path test reported 7919 objects in roughly 1 of 80 runs across 16 parallel processes. #4128 fixed the two in signals; this PR fixes the other four.Approach
Count what is still attached, not the heap:
pendingReactions(run)interceptsPromise.prototype.thenand counts reactions left on promises thatBun.peek.statusstill reports pending. It records calls in the mock implementation because Bun'smock.contextsmisses calls the engine makes itself (fromPromise.race,Promise.all).pendingListeners(run)spies onSignal.prototype.changedand counts listeners that neither fired nor were disposed. This also coversOnce.changedandSignal.race.Per test:
binary"blocked reads leave nothing behind…"json(same)watch/sync.test"wait leaves nothing behind on a stable clock"retention.test.tsnext to the helpers, now checks reactions and listenerswatch/retentionplayer pathI also removed the dispose loop in
Sync.#sleep. Both watch tests catch that (3000 and ~6000 listeners). The player-path count varies with the fix reverted because the old#updatepromise was swapped whenever the reference re-anchored. That's why the stable-clock test is kept as the precise regression test.Stress runs, 16 parallel
bun test <file>processes per round:just checkran alongside. It played 4000 frames; it now measures both counters in one 2000-frame pass.Impact
Alternatives
Set.prototype.addspies (test(signals): assert retention on bookkeeping, not a GC heap count #4128's approach): too broad for the player path. Effects legitimately re-add their subscribers, and the spy made 2000 frames very slow.#pending, the old#update), so reaching them would couple each test to internals.Follow-ups
heapStatsorBun.gccalls remain injs/.(written by Claude Opus 5.5)
🤖 Generated with Claude Code