perf(cmux-tui-core): a key to a hosted terminal waits for no other writer (R-DAEMON-STATE S1.6a, input port, default off)
Summary
R-DAEMON-STATE stage S1 step 6a (handoff/rewrites/R-DAEMON-STATE/DESIGN.md, section 2.6.1 and Appendix A.2 steps 20-22). Base feat-cmux-next (the S1 stack below it has landed: #34171, #34174, #34261). At fbb7767ad5e the head is the feat tip 191537144e9 plus this PR's 13 files (own-diff patch-id 0e7dec125c62, unchanged since the SHIP at 1f3d5909a26); fleet compile job 22afd1d5f9ee8965e7a5b2f6 passed at the head. Behind the same per-mux switch CMUX_TUI_RDS_INPUT (default off).
A person's key to a hosted terminal was written to the host socket on the request thread, under the terminal's runtime lock and the host connection's writer (surface/input.rs, send_host_frame), bounded only by the socket's send timeout. Another writer of the same terminal held both for its whole write: a large paste or receipted input from another client, a cell-pixel change waiting up to 2 s for its acknowledgement (under the runtime lock), the reconnect install. So the person's key waited for automation's work on the same terminal, and a host that did not read its socket stalled the connection's dispatcher and every later request of that connection.
With the switch on, interactive input to a hosted terminal (Surface::write_bytes, Surface::write_paste: keys, pastes, the creating-input flush, image pastes) goes to the terminal's own input port (surface/input_port.rs):
- One queue and one writer per terminal. The request hands the bytes to the queue and returns; it takes neither the runtime lock nor the host writer. The writer thread (
term-input-port, started by the first input, ended after 30 s with nothing to write, 256 KiB stack) writes the frames in order. A writer that panics drops the frames after the one it lost (logged) and marks itself stopped, so the next input starts a new one. - The route is published, so the writer takes no runtime lock either. The terminal publishes the route of its current host connection (
HostInputRoute, the connection's writer) whenever its runtime changes: the hosted and exited constructors, andPtyTerminalRuntime::replace_runtime, which the reconnect install and the host exit now use (the only two runtime swaps). - The reconnect rule is the same, on the port's thread (cx-6so.49): on a lost connection the writer waits for the reader's next stream change and writes to the new host, until
HOST_INPUT_RECONNECT_WAIT(5 s), reading the revision before each send so no install is missed. - Refusals stay typed and immediate. A port holding 32 MiB refuses with
terminal.input_busy(WouldBlock, retryable); each queued frame counts 64 bytes of bookkeeping too, so many tiny frames cannot grow the queue without bound. A write larger than one host frame is refused as before; the exited final screen drops input as before; a connection the reader shut down (Failed: it keeps retrying; the reader publishesInputRoute::Failedwhen it shuts the connection down) or a reader that ended refuses input at once, as the old path's write failed then (cmux sendexits non-zero). A replacement's route is accepted as soon as it is installed (before the state leavesFailed, #34544 reviews 2 and 3). While a lost connection is stillReconnecting, input is accepted and the writer waits for the reconnect, as the old path waited. - The reply now means "accepted by the terminal's port" (DESIGN 2.6.1). Input the port took and then cannot write (the host is gone and no new one came within the wait) is lost as a failed write was before; the request was already answered, so the port logs it.
With the switch off, every write takes the path it takes on the base; the route is published on both arms (one Arc per runtime change).
Not in this step (S1.6b, required before the default flip, DESIGN section 3, S1.6b bullet, which now also lists the review's deferred items): the connection's other frames (viewer size, cell pixels, receipted input, clear history, Kitty limits, clipboard replies, terminate) still write directly, so while the port is behind (a blocked host or a large write ahead) such a frame can go before a key the port still holds; and a write the port took and then cannot write should give its bytes back to the sending connection (terminal-input-unclaimed, the creating-input rule) instead of only a log line.
Testing
a_key_does_not_wait_for_another_writers_blocked_host_socket (cmux-tui/crates/cmux-tui-core/src/surface/tests/hosted.rs): a hosted terminal on a fake host connection; another thread pastes 8 MiB (far above the socket buffer) while the host reads nothing for 600 ms; then one key. A control first proves the old path waits (the key takes at least half the stall); on the lane the key must return in < 100 ms, and the host must then read the paste before the key, and the key once. The host drains on its own thread, so the old path cannot hang the test.
Focused command: ./scripts/verify-cmux-tui-hosted.sh --filter <filter> (Linux test (linux)).
-
Red at eaef70c313f (
test/rds-s16-red-never-merge), hosted run https://github.com/manaflow-ai/cmux/actions/runs/41744013421: FAILED, "the key waited 452.124607ms for another writer's blocked host socket" (the control passed). -
Green at 8c109c278a9: hosted run https://github.com/manaflow-ai/cmux/actions/runs/41752160637:
test (linux)passed, 18hosted::tests with this one. -
Review fixes (#34544 review, FIX at 8c109c278a9): the order check compared two
Options (None < Some), so a key that passed the paste still passed; it now requires both positions. New:keys_on_the_input_port_reach_the_host_once_each_in_order(26 keys on the lane, each read once, in order). Green at 6d8a5b4ca0d: hosted run https://github.com/manaflow-ai/cmux/actions/runs/41769506863:test (linux)passed, 19hosted::tests (both input port tests among them). Itslint (linux)shows base rows only; the twoarithmetic_side_effectswarnings of the review are gone (cmux-tui-core 1002, as on feat). -
After #34146 merged, the head 4c8d169c573 merges the new S1.5 head; this PR's own diff is unchanged (patch-id f96727d42e85 before and after).
-
Review 2 fix (7c90c2965f6): the lane refused input while a
Failedlink already had its live replacement (the old path wrote to it); it now refuses only when the reader ended. The capacity holds two of the largest frames with their per-frame cost; the 26-key test also checks that no key is written twice; the blocked-paste test no longer claims "once". Hosted run https://github.com/manaflow-ai/cmux/actions/runs/41776605445 at 7c90c2965f6:test (linux)passed, 30 tests with both input port tests. Fleet compile job 10364bb2332ffc2cc9e9784b at 7c90c2965f6: CmuxNext, app and cmux-tui passed. -
Review 3 fix (2dd59d8033c): the reader publishes
InputRoute::Failedunder the runtime lock when it shuts a failed connection down, socmux sendto a failed link fails again (it exited 0 and the port dropped the bytes); the 26-key test's tail loop is awhile let(clippy). Testa_key_to_a_failed_link_is_refused_until_a_live_route_is_published. Hosted run https://github.com/manaflow-ai/cmux/actions/runs/41780203521:test (linux)passed with the three input port tests;lint (linux): no clippy error, the ratchet rows are base rows (cmux-tui-core arithmetic 1007 > 875, as on feat). Fleet compile job 87ab7ccb5a2e12797fe94841 at 2dd59d8033c: passed. -
Review 4 note (5586fb82d24): the failed reconnect completion (terminal_host_reconnected false while the link stays Failed) also publishes
InputRoute::Failedunder the runtime lock. Then the S1.5 head merged in after #34146 and #34171 landed (1f3d5909a26; this PR's own diff unchanged, stable patch-id 0e7dec125c62). Hosted run https://github.com/manaflow-ai/cmux/actions/runs/41785763246 at 1f3d5909a26:test (linux)31 passed with the three input port tests;lint (linux): the only clippy error is the basecmux-gittest target, the ratchet rows are base rows. Fleet compile job b7d1d66bf276617c99bdd688 at 1f3d5909a26: passed.
Compile: fleet compile check job e54f5653cfbc88c469d9e96e at 8c109c278a9: CmuxNext, app and cmux-tui passed. At 6d8a5b4ca0d: fleet compile check job 5d23b5e610e6625abd89a654: CmuxNext, app and cmux-tui passed. The new head 4c8d169c573 compiles in a job running now (the merge changes no file of this PR).
Observed / Not run
- Observed: the hosted runs and the fleet compile above.
- Not run: a test that drives the reader's Failed publication (wait_or_fail past its budget, and the failed completion): the route behavior is covered by
a_key_to_a_failed_link_is_refused_until_a_live_route_is_published, which stores the route by hand (follow-up, DESIGN S1.6b list); the full cmux-tui suite with the lane on at this head (the S1.1-S1.4 rehearsal added no failure; I run it again with S1.5 and S1.6); macOS execution of the focused test (the old base's acpmux red stops the macOS job first); the real-app A/B (its -v3 build does not have this step).
Changelog
none
Proof
No capture: daemon-only change behind a default-off switch, no UI.