Skip to content

fix(cmux-next pages): a failed subscribe releases its early-event list on every exit - #27500

Merged
lawrencecchen merged 3 commits into
feat-cmux-nextfrom
fix/page-router-release-on-throw
Oct 31, 2026
Merged

lawrencecchen merged 3 commits into
feat-cmux-nextfrom
fix/page-router-release-on-throw

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A page whose provider's subscribe fails (it throws, or the page closes while it runs) leaked memory for the rest of the session when that provider kept its onEvent closure: every event it still sent was appended to a list nobody reads.

PageRouter.subscribe makes a PageEarlyEvents list that the provider's onEvent captures, to hold events sent before the subscribe reply. It released the list only on the success paths. On the throw and closed exits it stayed open, so later events kept growing it. Now one owner frees it on every exit: a defer releases it unless the posted flush has taken it over. After the release, a late event of a failed subscribe goes to deliver, which drops it (the stream is not live).

Found in the review of #27468 (same function; the two merge cleanly). The same leak is fixed on perf/chrome-parity in cp/fix-tip-page-router-release-on-throw.

Changelog

Fixed: A page whose stream failed to start no longer keeps memory for every event its provider still sends.

Testing

Regression test PageRouterSubscribeEndTests (red commit fee857b first, fix b3a4dff, test shape ea37a89). A provider keeps onEvent and its subscribe throws, or closes the page; 10 000 events it sends afterwards must add 0 main-thread allocations over the same empty loop (counted through libmalloc's malloc_logger; a self-check fails the test when the hook sees nothing) and reach no page. The empty loop is subtracted because a Debug for-in allocates once per iteration on the fleet (probe 5529e509: an empty loop and every step of the drop path each counted exactly 10 000).

cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg suite --arg Packages/macOS/CmuxNext --arg PageRouterSubscribeEndTests (through proof-step.py):

  • red (red commit + the final test shape, scratch 0b98ee4, fleet 0bc39d9572897537c00bf513): fails, kept → 13 in both cases (the held list growing).
  • green at ea37a89 (fleet c0b7ef01fdacba5428fe3822): passes, 0.
  • Release test build (CMUX_SWIFT_SUITE_CONFIGURATION=release), on perf (base 96cdd91, where the Release test build compiles):
    • red, without the fix (fleet 195f4e3132e7a4e20488027d, aws-m4pro-8): fails, kept → 13 in both cases. The leak is real in an optimized build.
    • green, with the fix (fleet 57a5b06218559baa01e13334): passes, 0.

Compile: cmux-ci compile-check --app --package CmuxNext --recipe-version 2 at ea37a89 (fleet 6dc98323567433da9601e981): passed.

Observed / Not run

Observed: the fleet runs above; scripts/cmux-next/prepush-gate.py clean.
Not run: a Release run on feat itself (feat's Release test build does not compile yet: UiLayerPreviewTests and two allocation tests; fixed on perf in cp/fix-tip-release-test-compile).

Proof

No capture: memory-only fix in the page router; nothing on screen changes.

Review (independent subagent, correctness first): SHIP at ea37a89

Follow-ups noted in review, not in this PR: a test comment names CrashKeySlotsTests, which is on perf only; after #27468 the nothing-held path releases the list twice (harmless, release() is idempotent); and an older issue: a reset() while a provider's subscribe is suspended registers the old subscription in the new document.

Checklist

  • Behavior changes have added or updated tests (PageRouterSubscribeEndTests)
  • Reviewed with a subagent before merge; verdict above. No user-facing strings change.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes a memory leak in PageRouter.subscribe where a failed subscription kept its early-event list alive for the rest of the session.

When a provider's subscribe threw, or the page closed while it ran, the PageEarlyEvents list captured by onEvent was never released. A provider that kept onEvent would append every later event to a list nobody reads. A defer now releases the list on every exit unless the posted flush has taken ownership, so late events of a failed subscribe are dropped immediately.

Bug Fixes

  • Failed subscriptions now release their early-event list on all exit paths.
  • Added PageRouterSubscribeEndTests to verify a failed subscribe allocates nothing for 10 000 events sent afterwards.

Written for commit ea37a89. Summary will update on new commits.

View guided diff Turn on auto-fix

lawrencecchen added 3 commits October 29, 2026 15:56
…s provider still sends (red)

PageRouterSubscribeEndTests: a provider keeps onEvent and its subscribe
throws, or the page closes while it runs. 10 000 events it sends afterwards
must make the router allocate nothing (main-thread malloc_logger count) and
reach no page. Red on feat-cmux-next 3b3ea3b: the throw and closed exits
of PageRouter.subscribe never release the PageEarlyEvents its onEvent
captured (review of fix/page-router-early-events; found on perf/chrome-parity,
cp/fix-tip-page-router-release-on-throw).
…t on every exit

PageRouter.subscribe released the PageEarlyEvents its onEvent captures only
on the success paths. A subscribe that threw, or ran while the page closed,
left it holding, so a provider that kept onEvent appended every later event
to a list nobody reads, for the rest of the session. A defer now releases it
on every exit unless the posted flush took it over.

Regression test: PageRouterSubscribeEndTests (red in the previous commit).
…ver an empty loop

Probe 5529e509 (fleet, Debug test build) counted each step of the drop path
over 10 000 iterations: the empty loop alone made 10 000 allocations, and every
step, the whole drop path included, made the same 10 000. So the one per event
in the green proofs (3e081c97, de37d97f) is the Debug for-in loop, not the
router: a dropped event allocates nothing. The test now subtracts that empty
loop and still expects 0. The bound is unchanged: the red code still adds 13
(the held list growing, 10013 - 10000 in reds 3847df43 and fb666e88).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 29, 2026
@cursor

cursor Bot commented Oct 29, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review ea37a89: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr27500 -f ref=fix/page-router-release-on-throw -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base 30c653a; does not block

Shipped in:

@blacksmith-sh

blacksmith-sh Bot commented Oct 29, 2026

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp21lu08fz/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpu11dv1fi/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Bead cx-3gwo33 created for this PR by the cmux coordinator.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (codex gpt-6-astra high fast, drain lane): SHIP at ea37a89

No blocking findings.

@lawrencecchen
lawrencecchen merged commit 8a1eb75 into feat-cmux-next Oct 31, 2026
213 of 237 checks passed
@lawrencecchen
lawrencecchen deleted the fix/page-router-release-on-throw branch October 31, 2026 00:34

This branch was previously deployed

1 inactive deployment
ci-routing-agent — ea37a897 Deployed Oct 29, 2026 by lawrencecchen via namespace autofix #6390
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants