perf(cmux-tui-core): a key to a hosted terminal waits for no other writer (R-DAEMON-STATE S1.6a, input port, default off)

Merged
LA
2d ago
+607-11

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, and PtyTerminalRuntime::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 publishes InputRoute::Failed when it shuts the connection down) or a reader that ended refuses input at once, as the old path's write failed then (cmux send exits non-zero). A replacement's route is accepted as soon as it is installed (before the state leaves Failed, #34544 reviews 2 and 3). While a lost connection is still Reconnecting, 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)).

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.