Skip to content

test(js): assert retention on reactions and listeners, not a GC heap count - #4142

Merged
kixelated merged 1 commit into
mainfrom
test/js-retention-bookkeeping
Sep 25, 2026
Merged

kixelated merged 1 commit into
mainfrom
test/js-retention-bookkeeping

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

Problem

The retention tests added in #4085 counted heapStats() objects after Bun.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) intercepts Promise.prototype.then and counts reactions left on promises that Bun.peek.status still reports pending. It records calls in the mock implementation because Bun's mock.contexts misses calls the engine makes itself (from Promise.race, Promise.all).
  • pendingListeners(run) spies on Signal.prototype.changed and counts listeners that neither fired nor were disposed. This also covers Once.changed and Signal.race.

Per test:

Test Change Fix reverted Fixed
binary "blocked reads leave nothing behind…" pending reactions over 1000 reads 1000 < 10
json (same) same 1000 < 10
watch/sync.test "wait leaves nothing behind on a stable clock" moved to retention.test.ts next to the helpers, now checks reactions and listeners 1000 reactions 0 / 0
watch/retention player path reactions and listeners over 2000 frames 165 to 1472 reactions 3 reactions, 21 listeners (the same at 20 or 2000 frames, bounded by the track's group window)

I 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 #update promise 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:

  • binary and json: 96 of 96 passed each.
  • watch: 192 of 192 passed at 32 concurrent processes.
  • An earlier revision timed out in 2 of 96 watch runs while just check ran alongside. It played 4000 frames; it now measures both counters in one 2000-frame pass.

Impact

  • Tests only. No public API or wire changes.

Alternatives

  • Set.prototype.add spies (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.
  • Bespoke counting thenables injected per consumer: the leaking promises are private (#pending, the old #update), so reaching them would couple each test to internals.

Follow-ups

(written by Claude Opus 5.5)

🤖 Generated with Claude Code

…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>
@kixelated
kixelated marked this pull request as ready for review September 25, 2026 12:52
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 89a112ef-210b-4750-947a-d220a1d3a19c

📥 Commits

Reviewing files that changed from the base of the PR and between fd31194 and b8405d6.

📒 Files selected for processing (4)
  • js/binary/src/stream/stream.test.ts
  • js/json/src/stream/stream.test.ts
  • js/watch/src/retention.test.ts
  • js/watch/src/sync.test.ts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T12:54:42.026664Z b8405d6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated

Copy link
Copy Markdown
Collaborator Author

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 heapStats noise.

Helpers are appropriately scoped: hand-recording Promise.prototype.then (because Bun's mock.contexts misses engine races) and wrapping Signal.prototype.changed to track undisposed listeners. Moving the stable-clock wait test next to those helpers and tightening thresholds after removing the Sync.#sleep dispose loop makes the regression signal sharper. Duplication of pendingReactions across packages is acceptable without a shared test util.

Tests-only, no API/wire impact; stress numbers look convincing. Prefer this over broader Set.add spies or coupling to private thenables.

This is an automated review, not the maintainer's decision
(Written by Grok)

@kixelated
kixelated merged commit fbbcb25 into main Sep 25, 2026
4 checks passed
@kixelated
kixelated deleted the test/js-retention-bookkeeping branch September 25, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant