perf(cmux-tui): a key to an idle input port is written on its own turn, with no thread hand-off (#35413 A/B)
Summary
With the S1 input lane on, each key the person types went through one extra thread hand-off: the request thread put the key on the terminal's input port and returned, and the port's own writer thread then woke up to write it to the terminal host's socket. Under the L6 typing load that wake-up made the daemon's part of every key slower than with the lane off: +0.025 / +0.17 / +0.53 ms at p50 / p95 / p99 over 1760 keys per arm in the s17fa A/B for #35413 (build 5cd1899b; STATUS 12857, 12866). All of the difference was in one segment, from the key's handoff (daemon.send.written) to the host's PTY write: OFF 0.016-0.019 / 0.14-0.17 / 0.38-0.48 ms, ON 0.038-0.046 / 0.27-0.28 / 0.67-0.77 ms, in every round (STATUS 12890). With the lane off, the request thread writes the socket itself.
Now a person's key (no waiter, at most 1 KB) to an idle input port (nothing queued or in flight, no reconnect wait) is written on the request's own turn, under the port lock, when HostInputRoute::try_send_now can write it at once:
try_lockon the connection's writer. A writer that another thread holds sends the key to the port.poll(POLLOUT)with a zero timeout on the host socket. No room sends the key to the port. This check is needed on macOS: there asendwithMSG_DONTWAITon a blocking stream socket still sleeps until the socket has room (XNU honours only the socket's own non-blocking mode), and this socket must stay blocking because its reader shares it. Without the check, a key to a host that stopped reading waited up to the socket's 2 s send timeout, holding its request, the connection's writer and the port lock. On XNU, POLLOUT means at least the 2048-byte send low-water mark is free; a const assert keepsINLINE_FRAME_MAXplus the frame header under that. The writer guard keeps every other daemon writer off the socket until the send, so the room cannot shrink in between. On Linux, POLLOUT only sends keys to the port earlier (once a quarter of the send buffer is used).sendwithMSG_DONTWAIT(plusMSG_NOSIGNALon Linux and Android). With room for the whole frame, a Unix stream socket takes it whole. If a partial write ever happened, the connection is shut down, as a failed write already does, and the port's writer sends the key again on the reconnected host.
A busy writer, a full socket, an error or a frame ahead leaves the key to the port as before, so S1.6's rule holds: a key never waits for another writer or for a host that stopped reading.
Order: the check and the write are under the port lock and only when the port is idle, so no frame the port took can be passed, and frames taken after the key queue behind it. The key counts as one of the port's frames, taken and finished at once, so the order barrier and the viewer-size queue (PortOrder) see the same state as before.
Waits under the port lock: try_lock and a send with room never wait, and the zero-timeout poll never waits for room or for the host. On Linux it returns at once. On macOS it returns at once when there is room; with no room, XNU's poll sleeps on its timer, which is already due (about 10 us, then one wake), before it reports none, and the key goes to the port. XNU's poll also makes and frees a kqueue on each call, a few us per key. The s17g A/B runs on a Mac (cmux21): it measures the poll on every key, and the no-room sleep only if a host falls behind during the run. If the no-room sleep shows in the slowest keys, a per-connection kqueue checked with kevent64(..., KEVENT_FLAG_IMMEDIATE) removes it.
Testing
Focused command: cmux-ci run --class isolated --script scripts/ci/cmux-tui-rust-check.sh --os linux|macos --arg test --arg FILTER --ref SHA. The head 89075c0eb96 changes only comments after 4cc7e1b9774 (the poll doc above), so the runs are at 4cc7e1b9774 unless named. Red first:
a_key_to_an_idle_port_is_on_the_host_socket_when_its_request_returns(holds the port's writer before it writes anything, sends a key, reads the host socket): fails at dd67a789756 (tests only, on feat 0d30cf03893) on Linux, step 05a6c8fc60135ee8ffa23f55 ("the key was not on the host socket when its request returned"); passes at 4cc7e1b9774.keys_to_a_host_that_stopped_reading_never_block_their_requests(types 1000 keys into a host that reads nothing for 10 s; every request must return, the slowest under 2 s; then the host must get every key once, in order): fails on macOS at 7293b1bd910 (the test, before the room check), step f20ed39d22fb64ecea06cbc3 ("a key's request waited for a host that stopped reading: 1000 keys did not return in 10 s"); passes at 4cc7e1b9774. Without the fix, with the default 8 KB local stream buffer, the inline send sleeps at about the 228th key until the host reads. The first version of this test (b1e8db9aabf) filled the socket withMSG_DONTWAITsends and hung for 120 s on macOS (step 8f40081f419d7d78b217103e), which is how the macOS behaviour came out in review.input_port_inlineat 4cc7e1b9774: 4 of 4 pass on Linux (step a4cd0e1678f9675112ee9794) and on macOS (step 7d282d8ea46f8260b4b4df5c). The other two tests: a key while another writer holds the connection goes to the port and does not wait for it (sent from a helper thread with a 1 s bound); keys keep their order between the inline write and the port's writer (the test waits for an idle port and holds the writer again before the last key).surface::tests::hostedat 4cc7e1b9774: macOS 40 of 40 pass (step 12f6bc3358744ae7c91bb478, with the macOS-only writer QoS test; the idle-port test moved toinput_port_inline). At ef166da6f4d: Linux 40 of 40 (step 619f8f3828391b33024b79ae), macOS 41 of 41 (step 4e1bd96e190692787b21cec2). The port's writer tests that hold a key at a paused writer opt out of the inline write (queue_every_write_for_test), so they still exercise the writer's order, reconnect and QoS.- Input-lane tests on macOS at 4cc7e1b9774: 21 of 21 pass (step 81fa3306e1fa5af9455c67a2:
server::tests::rds_input_lane11,server::tests::input_lane9, one app projection test). - Clippy -D warnings: passed at 4cc7e1b9774 (step 4379817e2e2e72f99fcc684d); at the head 89075c0eb96: passed (step dea1c3415441b741f74021df). Compile-check: passed at 4cc7e1b9774 for CmuxNext, app and cmux-tui (job 96aa752f7247efff50aab98d); at the head 89075c0eb96: passed for CmuxNext, app and cmux-tui (job 43893ef5ae33bd50828876a8).
- Full cmux-tui suite on Linux at 4cc7e1b9774 (step 2b991f960e7e3b848a49ec3c): 17054 tests, 9 failed. 6 failed again alone, all base failures: 3 acpmux tests (
peer_integration,peer_request_deadlines,session_pool; acpmux does not depend on cmux-tui-core) and the 3server::agent_transfer::teststhat fail alone on feat. 3 failed in the full run and passed alone: an acpmux auto-start test, thelink_ensurestop test (a link that exited but was not yet reaped under load; it failed the same way once in a full run of #35765, which has none of this change) and the knownlayout_ops_conserve_tabsload flake. Every input-port and input-lane test passed:surface::tests::hosted39 (its QoS test is macOS-only),input_port_inline4,server::tests::rds_input_lane11,server::tests::input_lane7. At ef166da6f4d (step 47991676362511aa57c988a3): 17051 tests, 9 failed; 6 also fail alone and are base failures (3 acpmux, which does not depend on cmux-tui-core, and the 3server::agent_transfer::teststhat fail alone on feat), 3 failed in the full run and passed alone (the knowna_persons_requests_answer_while_busy_sessions_saturate_the_registryload flake, an agent-session-share room test and an acpmux token test).
Observed / Not run
- Observed: the s17fa per-key segments above (script /work/tmp/r-daemon-state/s17/fa/segments.py on the rounds' flow files), and the red and green runs above.
- Not run yet: the A/B on a build with this fix (set s17g on #35413: HEAD build 2ed758c77964169233240990 at b6b0309d0b7, which has this PR at 4cc7e1b9774 and #35765; it reports the same segment per round). The earlier s17g builds 77f1c11f and 8286c92c had the macOS defect; no round ran on them.
Performance
| metric (s17fa / s17g A/B, L6 load, cmux21) | before (lane on, 5cd1899b) | after (lane on, 2ed758c7) | delta | harness |
|---|---|---|---|---|
| daemon part p50/p95/p99 ms, ON | 0.14/0.76/1.95 (lane off 0.11/0.59/1.42) | pending (s17g) | pending | typing-under-load kit, STATUS 12857 |
| written -> host write p50/p95/p99 ms, ON | 0.038-0.046/0.27-0.28/0.67-0.77 | pending (s17g) | pending | segments.py, STATUS 12890 |
Changelog
none (the S1 input lane is off by default until #35413; this changes only the lane-on path)
Proof
No capture: daemon-only change with no UI. The typing A/B with movies on the fixed build is reported on #35413.