blob: 5f57ca5272e591fa4e6d2b27221b68a0cd9a70da [file] [view]
# `ffx-uart-driver` Async Multiplexer & Transport Design
## 1. Overview & Design Goals
`ffx-uart-driver` bridges multiple concurrent host-side `ffx` UNIX domain socket connections (`FDomain` / RCS channels) over a single physical or virtual UART byte stream using the Fuchsia Packet Layer (`uart_fpl`) framing and sliding-window Go-Back-N protocol.
Unlike TCP or USB, a UART link has **no hardware-enforced link-layer flow control (`FlowControl::None`)** and is subject to byte drops, line noise, baud-rate mismatches, and target reboots. At the same time, the daemon must run on a single-threaded asynchronous executor (`fuchsia_async::LocalExecutor`) and satisfy five core requirements:
1. **Lossless Go-Back-N Delivery & Glitch Recovery**: Recover transparently from corrupted frames (Fletcher-16 checksum failures), dropped bytes, and out-of-order arrivals without corrupting or stalling upper-layer `FDomain` streams.
2. **End-to-End Bidirectional Backpressure**: Prevent fast target producers (e.g., `ffx log` or `ffx target snapshot` at 1,000,000 baud) from overwhelming slow host UNIX socket consumers, and prevent fast host producers from overflowing the UART transmit window-without forcefully disconnecting legitimate slow clients.
3. **Multi-Baud-Rate Operation (115,200 bps to 1,000,000+ bps)**: Scale efficiently from low-speed debug serial links (~11.5 KB/s) up to 1 Mbps+ (~100 KB/s) high-speed UART links via adaptive read/write batching and cooperative executor yielding.
4. **Software Rate-Limiting for Virtual & Non-Baud-Paced Links**: Emulate physical baud-rate line pacing (`baud / 10` bytes/sec) when communicating over links where the direct host connection does not naturally impose a physical baud-rate limitation (such as UNIX domain sockets, TCP endpoints, or PTYs). This prevents host writes from overwhelming downstream physical UART buffers or triggering hypervisor VM-exit / interrupt storms in emulated environments, while guaranteeing control ACKs are never starved.
5. **Deadlock & Starvation Freedom**: Guarantee that no combination of saturated channels, slow or disconnecting UNIX sockets, retransmission bursts, or high-rate ingress traffic can cause a cyclic task wait (deadlock) or starve the opposite direction of traffic.
---
## 2. Task Architecture & Data Flow
The driver decomposes each active UART session into four cooperating tasks on the `LocalExecutor`, plus a pair of per-client socket tasks (`client_reader_task` and `client_writer_task`):
```text
Host UNIX Sockets (Client N)
│ ▲
▼ │
client_reader_task client_writer_task
│ ▲
│ client_tx │ state.writer_tx (bounded: 256)
▼ │
┌──────────────────────┐
│ coordinator_task │◄─── serial_rx (bounded: 64) ───┐
└──────────────────────┘ │
│ │
│ sender_tx (bounded, gated by outgoing_queue) │
▼ │
┌──────────────────────┐ │
│ sender_task │◄─── incoming_event_rx (16) ────┤
│ (ResendSender) │ (Cumulative ACKs) │
└──────────────────────┘ │
│ │
│ data_tx (bounded: 16) │
▼ │
┌──────────────────────┐ ┌──────────────────────┐
│ writer_task │◄── AckTracker ────────│ receiver_task │
│ (DeviceWriter) │ (O(1) watch slot) │ (ResendReceiver) │
└──────────────────────┘ └──────────────────────┘
│ ▲
▼ │
UART TX Half UART RX Half
```
---
## 3. How Key Requirements Are Met
### 3.1 Go-Back-N Delivery & Wire Glitch Recovery
* **Checksum Verification & Framing**: `UartReader` feeds raw UART bytes into `FrameParser`, which synchronizes on COBS/frame boundaries and validates each frame's Fletcher-16 checksum. Corrupted frames are discarded immediately and counted in `DaemonMetrics::checksum_errors`.
* **Sequence Gap Detection (`dispatch_ordered_event`)**: When a frame is discarded due to line noise, subsequent frames in the remote sender's window arrive with `seq != expected_seq` (`FrameStatus::OutOfOrder`). `receiver_task` discards these out-of-order frames **without advancing `ResendReceiver`** and immediately updates `AckTracker` (`ack_tracker.set_ack(session_id, ack_seq)`) with the last contiguous sequence number received prior to the gap.
* **Priority ACK Transmission (`writer_task` & `AckTracker`)**: `AckTracker` is a single-slot latest-value register (`Option<(u32, u8)>`) rather than a FIFO queue. Because FPL ACKs are cumulative, overwriting an older unsent ACK with a newer `ack_seq` is always valid and takes $O(1)$ space. `writer_task` checks `ack_tracker.take_ack()` at the top of every loop iteration and prioritizes `ack_tracker.wait_ack()` in `select_biased!` ahead of `data_rx`, ensuring cumulative ACKs (and duplicate ACKs signaling a glitch) jump to the front of the wire ahead of bulk data frames.
* **Sliding-Window Retransmission (`sender_task`)**: `sender_task` maintains a single `active_timer` (`DEFAULT_RETRANSMISSION_TIMEOUT`). When an ACK advances the window (`AckOutcome::Advanced`), RTT estimates are updated and the timer is either cleared (`all_acked`) or reset. On timeout (`handle_sender_timeout`), `ResendSender` yields all unacknowledged frames in `[base, next_seq)` for Go-Back-N retransmission.
### 3.2 End-to-End Bidirectional Backpressure
* **Target-to-Host Backpressure (Lossless Flow Control at 1,000,000 Baud)**:
* At 1,000,000 baud (~100 KB/s), a host client (`ffx log`, `ffx target snapshot`, or a paused terminal) may consume data more slowly than the UART delivers it.
* When a client socket slows down, `client_writer_task` blocks in `writer.write_all(&batch).await`, causing that channel's `state.writer_tx` (`CLIENT_WRITER_CHANNEL_CAPACITY = 256`) to fill up.
* In `coordinator_task`, `handle_uart_event` awaits `state.writer_tx.send(data)` rather than dropping the payload or closing the client channel. Waiting on `state.writer_tx` intentionally pauses `coordinator_task` from draining `serial_rx`.
* Once `serial_rx` fills up, `receiver_task` hits `Err(e) if e.is_full()` on `serial_tx.try_send(event)` in `handle_in_order_event`. Crucially, `receiver_task` **drops the `DATA` frame without advancing `ResendReceiver`** and re-transmits `current_ack_seq()`.
* This withholds window advancement from the target's `ResendSender`, causing the target's 64-frame sliding window to fill and pause transmission until the host client drains its UNIX socket-achieving lossless end-to-end flow control without dropping the client connection.
* **Host-to-Target Backpressure**:
* In `sender_task`, `sender_rx` is only polled when `sender.can_send()` is true (the Go-Back-N window has free sequence slots).
* When the UART TX window is full, `sender_tx` exerts backpressure on `coordinator_task`'s `outgoing_queue`.
* Once `outgoing_queue.len() >= DEFAULT_CLIENT_DATA_CAPACITY`, `coordinator_task` disables polling `client_rx`. This causes `client_reader_task` instances to suspend on `coordinator_tx.send(ClientEvent::ChannelData)`, which stops reading from the client UNIX sockets and propagates standard OS `UnixStream` backpressure to the `ffx` client processes.
### 3.3 Scaling Across Baud Rates & Software Rate-Limiting
* **Batching at High Baud Rates (1,000,000+ baud)**:
* `UartReader::drain_available` non-blockingly drains all immediately available kernel buffer bytes into `FrameParser` before yielding.
* `writer_task` (`collect_data_batch`) coalesces pending frames from `data_rx` up to `MAX_UART_WRITE_BATCH_BYTES` per syscall.
* `client_writer_task` coalesces up to 64 KB of channel chunks from `writer_rx` per UNIX socket write.
* `receiver_task` executes `fuchsia_async::yield_now().await` after each processed frame so that when a single large UART read delivers dozens of frames in memory, `coordinator_task` and `writer_task` get scheduled on the single-threaded runtime before `serial_tx` saturates.
* **Software Rate-Limiting for Non-Baud-Paced Links**:
* When connected directly to a physical UART character device (such as `/dev/ttyUSB0`), the hardware serial transceiver naturally paces byte transmission according to the configured baud rate.
* However, when communicating over non-character devices-such as UNIX domain sockets (e.g., Pontis remote serial tunnels), TCP serial endpoints, or PTYs-the direct transport link imposes no physical baud-rate limitation. The host can transmit into the socket at memory speeds.
* Without pacing, these bursts overwhelm downstream physical UART buffers on the remote bridge, or in emulated environments (such as QEMU serial sockets), trigger hypervisor VM-exit and interrupt storms that consume 100% guest CPU in the ISR, starve userspace, and cause guest lockups.
* To prevent this, the driver automatically determines when the direct connection lacks a physical baud-rate limitation (`!is_char_device(target_path) || is_pty`) and enables software rate limiting capped at `baud / 10` bytes per second (e.g., 100 KB/s at 1 Mbaud, or 11.5 KB/s at 115,200 baud).
* In `writer_task`, `DeviceWriter::write_frame` computes the transmission duration (`frame.len() / rate_limit`) and sleeps until `next_allowed_time` before writing.
* Crucially, because `writer_task` checks `ack_tracker` before `data_rx`, cumulative ACKs are interleaved ahead of pending data batches, and because `sender_task` drains incoming ACKs concurrently while awaiting `data_tx` (see §3.4), rate-limiting pacing on `writer_task` cannot stall ACK processing or deadlock the Go-Back-N window.
### 3.4 Deadlock & Starvation Avoidance
To prevent cyclic wait dependencies across bounded channels on the single-threaded runtime, the tasks enforce four structural invariants:
1. **Wait-Free Frame Dispatch in `receiver_task`**:
* `receiver_task` **never `.await`s** when dispatching received frames. Both `serial_tx.try_send(event)` (`DATA`/`CLOSE`) and `incoming_event_tx.try_send(seq)` (`ACK`) are synchronous and non-blocking.
* Thus, `receiver_task` can never be blocked by downstream congestion in `coordinator_task` or `sender_task`; it always remains free to drain the UART RX buffer and process incoming `FrameType::Ack` and `FrameType::Reset` control frames.
2. **Concurrent ACK Draining in `sender_task` (`send_data_frame`)**:
* When `writer_task` is rate-limited or backpressured, `data_tx` (capacity 16) fills up and `sender_task` blocks inside `send_data_frame` (especially during a Go-Back-N timeout retransmission burst of up to 64 frames).
* `send_data_frame` uses `futures::select_biased!` to continuously poll `incoming_event_rx.next()` alongside `data_tx.send(frame)`. Incoming ACKs are processed immediately even while `data_tx` is full, and `handle_sender_timeout` checks `if sender.is_idle() { break; }` on every iteration so an ACK arriving mid-burst aborts any remaining redundant retransmissions.
3. **Concurrent Egress Draining & Fair Scheduling in `coordinator_task`**:
* `coordinator_task` uses fair `futures::select!` across `serial_rx`, `sender_ready_fut`, and `client_rx_fut` so high-baudrate UART ingress cannot starve host-to-target egress.
* While `handle_uart_event` awaits `state.writer_tx.send(data)` for a congested client socket, it concurrently polls `poll_send_outgoing` in a `select_biased!` loop so messages already in `outgoing_queue` continue flowing to `sender_task`.
4. **Drop-Before-Notify on Client Disconnect (`client_writer_task` / `client_reader_task`)**:
* If a client socket disconnects (`EPIPE` / `EOF`) at the exact moment when both `state.writer_tx` (`coordinator_task` $\rightarrow$ `client_writer_task`) and `coordinator_tx` (`client_writer_task` $\rightarrow$ `coordinator_task`) are 100% full, awaiting `coordinator_tx.send(ClientEvent::ChannelClose)` while still holding `receiver` (`writer_rx`) would deadlock both tasks.
* `client_writer_task` explicitly executes `drop(receiver); drop(writer);` (and `client_reader_task` executes `drop(reader);`) **before** awaiting `coordinator_tx.send(ClientEvent::ChannelClose)`. Dropping `receiver` immediately causes `state.writer_tx.send(data)` in `coordinator_task` to return `Err(disconnected)`, breaking the cycle.