fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the input time is an atomic (R-DAEMON-STATE S1 step 5)
Summary
R-DAEMON-STATE stage S1 step 5 (handoff/rewrites/R-DAEMON-STATE/DESIGN.md, section 3 and Appendix A.2 steps 18-19). Stacked on #34174 (base fix/p0-typing-s1-creating-input); it retargets to feat-cmux-next when the stack below lands. Behind the same per-mux switch CMUX_TUI_RDS_INPUT (default off).
After its PTY write, a person's key notes activity in its terminal's sizing (under the default policy the newest view sets the grid). The note took the daemon-wide sizing lock twice (the note, then the size-state publication), and every attach, detach, resize and size report of every terminal holds that lock, with a PTY resize under it. Then it took the VM activity stream's lock. The S1 census measured a full wait on both (300.2 ms and 300.3 ms, hosted run 41648095321).
With the switch on:
- The settled newest view skips the note. Each terminal's sizing entry publishes, on the terminal's runtime (
PtyTerminalRuntime, shared by every placement of the terminal), the view a key may skip its note for: the sole newest participant, a client's own view (no relay sub-view), and only while the entry is settled (no grid hold; no handheld with a viewport, which caps or holds the grid; and the engine holds the grid, or the grid it applied and the participants it applied for are its current decision). Noting activity for that view changes no decision under any policy (the newest of any set of views that holds it stays it), and the published size state carries no activity values. So the key compares its own(client, placement)with one atomic load and takes no sizing lock. - The publication cannot be skipped.
Mux::client_sizingis aSizingMutexwhose guard republishes, when it drops and under the lock, each entry its holder read or wrote:ClientSizingState.terminal_sizingis aSizingEntriesmap whoseget,get_mut,insert,valuesanditermark the entry (an apply changesCells through a shared borrow, so reads count;keysandcontains_keychange nothing). A removed entry clears its slot; a change that a panic cuts short publishes nothing. - The grid still follows the person. Another view's activity (a focus click, an attach) makes that view the newest and republishes, so the next key from the first view takes the slow path once and takes the grid back.
after_another_views_click_a_key_takes_the_grid_backpins this on both arms. - The input time is an atomic.
ActivityStream::note_user_inputstores the time in an atomic; it takes the stream's lock only while a subscriber exists, once per event the worker sends (awake_pendingflag the worker clears before it reads the time; SeqCst on the time, the subscriber flag and the wake flag, so either the key sees the subscriber or the subscriber's first snapshot sees the time). TerminalSizingEngine::sole_latest(cmux-terminal-sizing, with a unit test);AppliedSeat::ofshares the seat rule between the apply and the settled check.
With the switch off, every key takes the sizing and activity locks as on the base; the marks and the guard's drop check one bool.
The branch also carries the non-blocking notes of the #34146 review (831686b8c79): the surface budget's two mux-wide limit tests run on both arms, a new test checks the same limits on both arms (17th worker, whole byte budget, a byte over, zero bytes, an overflowing sum, released reservations), the lock-test control fails on a lost key, and the admission's mutex state is private.
Testing
Focused command: ./scripts/verify-cmux-tui-hosted.sh --filter rds_input_lane (Linux test (linux)).
-
Red at dfe81bff4d8 merged with the stack (713eac887e7,
test/rds-s15-red-never-merge), hosted run https://github.com/manaflow-ai/cmux/actions/runs/41715923069:a_key_waits_for_neither_the_sizing_nor_the_activity_lockFAILED. Controls (lane off) 300.30 ms (sizing) and 300.22 ms (activity); lane 300.26 ms and 300.24 ms. The other 7 passed,after_another_views_click_a_key_takes_the_grid_backamong them (it holds on the old path too). -
Green at 7fad48f75ff, hosted run https://github.com/manaflow-ai/cmux/actions/runs/41717827681:
rds_input_lane8 of 8 passed. Engine unit test (filtersole_latest), hosted run https://github.com/manaflow-ai/cmux/actions/runs/41717882067: passed. -
The lint job of 41717827681 found my own defect: a test hook put above another hook's
#[cfg(test)]madepin_latest_size_policy_for_testproduction code (dead code, and cmux-tui-coreexpect_used175 > 173). Fixed in cd9a6dc9129; green there: hosted run https://github.com/manaflow-ai/cmux/actions/runs/41722454685,rds_input_lane8 of 8, and itslint (linux)job shows only base findings (acpmux/src/login_env/watch.rs:15, the panic ratchet's acpmux and cmux-host rows). -
Review fix (#34261 review): a respawn under the same runtime id builds a new runtime with a new fast-path slot, and the sizing entry kept the old one, so after a respawn every key took the slow path again. Red first:
after_a_respawn_keys_wait_for_no_sizing_lock_againat 4bf5af5c6ae, hosted run https://github.com/manaflow-ai/cmux/actions/runs/41750633638: FAILED, "after a respawn the keys waited 300.118794ms for the sizing lock held 300ms" (the other 8 passed). Green at 5fe770df4ff (the entry follows its runtime's slot:TerminalSizingEntry::follow_runtime_of; the activity time usesfetch_max, so it never goes back), hosted run https://github.com/manaflow-ai/cmux/actions/runs/41752121159:rds_input_lane9 of 9 passed. The lock tests now report a 100x30 viewport, so the Held branch is no longer the only one they prove. -
The
lint (linux)job of 41752121159 is red on base findings only: acpmuxnotify::Watcherunused import, and the panic ratchet rows of acpmux, cmux-host, cmux-tui-core and others. The cmux-tui-core row (arithmetic_side_effects1002 > baseline 875) is base debt: feat commits show 1005 (run https://github.com/manaflow-ai/cmux/actions/runs/41729153986), and this PR's product diff adds no arithmetic. -
Review 2 fix (#34261 review 2, FIX at 5fe770df4ff): that fix looked the surface up on every sizing-entry access, so with the switch off (the default) every attach, size report, claim and size-state read took the session lock under the sizing lock. Red first:
a_settled_size_state_read_takes_the_session_lock_once_with_the_switch_offat 8ccc6f0f0db (test/rds-s15-red2-never-merge), hosted run https://github.com/manaflow-ai/cmux/actions/runs/41769009857: FAILED, "lane false: one settled size-state read took the session lock 2 times, not 1" (lane on: 0, as expected; the other 9 passed). Fix 8701dbd8433: the surface is looked up only to make the entry; the respawn follow runs in the key's sizing note (which holds the surface), only while the switch is on; an unchanged fast path is not stored again; the respawn test also pins the old slot's clear. Green: hosted run https://github.com/manaflow-ai/cmux/actions/runs/41769012578:rds_input_lane10 of 10 passed. Itslint (linux)shows base rows only (acpmux unused import; cmux-tui-core arithmetic 1002 > 875, as on feat). -
The stack under this PR moved after #34146 merged (squash dfcfac60c35): the head b86e5e88c34 merges the new S1.4 head; this PR's own diff is unchanged (
git diff <base head> <head> | git patch-id --stable= c241cade7bf4 before and after).
Compile: fleet compile check job e6180bc1ade9ad810af3ac87 at 5fe770df4ff: CmuxNext (371 s), app (100 s) and cmux-tui (82 s) passed. At 8701dbd8433: fleet compile check job fe8edfba7fb3bacc67e51fe6: CmuxNext (429 s), app (124 s) and cmux-tui (108 s) passed. The top of the stack (4c8d169c573, which contains this head) compiles in job running now; the merges change no file of this PR.
Known limit (review 2 note, not fixed here): the lock-free activity time uses fetch_max, so after a backward wall-clock step last_user_input_at keeps the later value until the clock passes it again (the Cloud host then sees a future input time). The mutex arm stores each value as it comes, so it follows a backward clock step; only the lane keeps the later time. Before S1.7 (DESIGN section 3, S1.5 bullet): a test that subscribes to the lock-free activity arm, and skip-path tests for the other size policies, handhelds, relayed views, grid holds and detached views.
Observed / Not run
- Observed: the hosted runs and the fleet compile above.
- Not run: the full cmux-tui suite with the lane on, at this head (the S1.1-S1.4 rehearsal added no lane-only failure, STATUS 10281; I run it again with S1.5 and S1.6); macOS execution of the focused tests (the base's acpmux unused-import red stops the macOS job first); the real-app A/B (the A/B owner runs it on the -v3 build).
Changelog
none
Proof
No capture: daemon-only change behind a default-off switch, no UI.
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Under the CMUX_TUI_RDS_INPUT switch (default off), a typed key no longer takes the daemon-wide sizing lock or the activity stream's lock after its PTY write when its view is the terminal's settled newest. Previously each key took the sizing lock twice and the activity stream's lock, measured at a full 300 ms wait on each; with the switch off, keys take both locks as before.
How the skip is published
- Each terminal's sizing entry publishes the view a key may skip its note for: the sole newest participant, a client's own view, only while the entry is settled (no grid hold, no sized handheld, and the applied grid and participants match the current decision). The key compares its own
(client, placement)with one atomic load. - The publication follows every entry read or write: each access marks the entry, and the sizing guard republishes the marked ones when it drops, under the lock. A removed entry clears its slot; a change a panic cuts short publishes nothing; an entry follows its terminal across a respawn (the new runtime's slot is published, the old cleared).
- Another view's activity (a focus click, an attach) becomes the newest and republishes, so the next key from the first view takes the slow path once and takes the grid back; after a respawn, keys skip the sizing lock again. Tests pin both on both arms.
ActivityStream::note_user_inputstores the time in an atomic withfetch_max(a preempted key never moves it back) and takes the stream's lock only while a subscriber exists, once per event the worker sends.- The surface-operation admission's mutex state is private, its limit tests (17th worker, byte-budget boundaries, released reservations) run on both arms, and the old-path lock control fails on a lost key.