ACK Flow Control
ACK Gate is MonoTerm’s solution for preventing IPC flooding when fast producers meet slow consumers.The Problem: IPC Flooding
Without flow control, fast producers can overwhelm slow consumers.Copy
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ THE FLOODING PROBLEM ║
║ ║
║ ║
║ Scenario: Running "cat large_file.txt" (1GB file) ║
║ ║
║ ║
║ ┌──────────────────┐ ┌──────────────────┐ ║
║ │ │ │ │ ║
║ │ Rust Backend │ │ TypeScript UI │ ║
║ │ │ │ │ ║
║ │ Speed: │ │ Speed: │ ║
║ │ 500M chars/sec │ │ 60 FPS render │ ║
║ │ │ │ │ ║
║ │ ════════════▶ │ │ ════▶ │ ║
║ │ VERY FAST │ │ SLOWER │ ║
║ │ │ │ │ ║
║ └──────────────────┘ └──────────────────┘ ║
║ ║
║ ║
║ Without Flow Control: ║
║ ║
║ Rust TypeScript ║
║ │ │ ║
║ │ GridUpdate #1 ═══════════════════▶ │ Processing... ║
║ │ GridUpdate #2 ═══════════════════▶ │ │ ║
║ │ GridUpdate #3 ═══════════════════▶ │ │ Queue ║
║ │ GridUpdate #4 ═══════════════════▶ │ │ growing ║
║ │ GridUpdate #5 ═══════════════════▶ │ │ rapidly ║
║ │ GridUpdate #6 ═══════════════════▶ │ ▼ ║
║ │ ... │ ║
║ │ │ MEMORY ║
║ │ │ EXHAUSTED ║
║ ▼ ▼ ║
║ ║
║ ║
║ Result: ║
║ - Event queue grows unbounded ║
║ - Memory usage spikes ║
║ - UI becomes unresponsive ║
║ - Application may crash ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
The Solution: ACK Handshake
ACK Gate implements a simple handshake: send one, wait for acknowledgment.Copy
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ ACK GATE FLOW CONTROL ║
║ ║
║ ║
║ Principle: ║
║ ║
║ "Do not send the next update until the frontend ║
║ confirms it has processed the previous one." ║
║ ║
║ ║
║ ┌──────────────────┐ ┌──────────────────┐ ║
║ │ │ │ │ ║
║ │ Rust Backend │ │ TypeScript UI │ ║
║ │ │ │ (Injector) │ ║
║ │ │ │ │ ║
║ └────────┬─────────┘ └────────┬─────────┘ ║
║ │ │ ║
║ │ 1. GridUpdate │ ║
║ │ ═══════════════════════════▶ ║
║ │ │ ║
║ │ [WAITING] │ 2. Process update ║
║ │ │ 3. Inject to display ║
║ │ 4. ACK │ 4. Send ACK ║
║ │ ◀═══════════════════════════ ║
║ │ │ ║
║ │ 5. GridUpdate │ ║
║ │ ═══════════════════════════▶ ║
║ │ │ ║
║ │ [WAITING] │ 6. Process... ║
║ │ │ ║
║ │ 7. ACK │ ║
║ │ ◀═══════════════════════════ ║
║ │ │ ║
║ ▼ ▼ ║
║ ║
║ ║
║ Key Insight: ║
║ The producer (Rust) is throttled by the consumer (TS). ║
║ Queue size is always 0 or 1. Never grows unbounded. ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
State Machine
The ACK Gate maintains simple state to control the flow.Copy
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ ACK GATE STATE MACHINE ║
║ ║
║ ║
║ ┌───────────────────────────────────────────┐ ║
║ │ │ ║
║ │ IDLE STATE │ ║
║ │ waiting_ack = false │ ║
║ │ pending_data = false │ ║
║ │ │ ║
║ └─────────────────┬───────────────────────┬─┘ ║
║ │ ║
║ │ PTY data arrives ║
║ ▼ ║
║ ┌───────────────────────────────────────────┐ ║
║ │ │ ║
║ │ Process data with VTE parser │ ║
║ │ Emit GridUpdate to frontend │ ║
║ │ Set waiting_ack = true │ ║
║ │ │ ║
║ └─────────────────┬───────────────────────┬─┘ ║
║ │ ║
║ ▼ ║
║ ┌───────────────────────────────────────────┐ ║
║ │ │ ║
║ │ WAITING STATE │ ║
║ │ waiting_ack = true │ ║
║ │ │ ║
║ └───────────┬─────────────────┬─────────────┘ ║
║ │ │ ║
║ More PTY data │ │ ACK received ║
║ arrives │ │ ║
║ ▼ ▼ ║
║ ┌────────────────────┐ ┌────────────────────┐ ║
║ │ │ │ │ ║
║ │ Process data │ │ pending_data │ ║
║ │ (update state) │ │ = true? │ ║
║ │ │ │ │ ║
║ │ Set pending = true │ └─────────┬──────────┘ ║
║ │ │ │ ║
║ │ DO NOT EMIT │ YES │ NO ║
║ │ (wait for ACK) │ │ ║
║ │ │ ▼ ║
║ └────────────────────┘ ┌────────────────────┐ ║
║ │ │ ║
║ │ Emit fresh update │ ║
║ │ Set waiting = true │ ║
║ │ Set pending = false│ ║
║ │ │ ║
║ └────────────────────┘ ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
Why Full Update on ACK?
When ACK arrives and there’s pending data, we send the complete current state.Copy
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ THE PENDING DATA PROBLEM ║
║ ║
║ ║
║ Scenario: ║
║ While waiting for ACK, 5 more PTY chunks arrived. ║
║ Each chunk modified the grid state. ║
║ ║
║ ║
║ Time Grid State What Happened ║
║ ──── ─────────── ────────────── ║
║ T1 "Hello" Sent GridUpdate, waiting for ACK ║
║ T2 "Hello W" Chunk 1 arrived, processed ║
║ T3 "Hello Wo" Chunk 2 arrived, processed ║
║ T4 "Hello Wor" Chunk 3 arrived, processed ║
║ T5 "Hello Worl" Chunk 4 arrived, processed ║
║ T6 "Hello World" Chunk 5 arrived, processed ║
║ T7 ─── ACK received for T1 ║
║ ║
║ ║
║ Wrong Approach: ║
║ ║
║ Send incremental updates for T2, T3, T4, T5, T6? ║
║ ──▶ 5 more round trips ║
║ ──▶ Complex merging logic ║
║ ──▶ Risk of ordering issues ║
║ ║
║ ║
║ Correct Approach (ACK Gate): ║
║ ║
║ Request FULL update at T7. ║
║ Send current complete state: "Hello World" ║
║ ──▶ One round trip ║
║ ──▶ Simple: just send current state ║
║ ──▶ No partial update complexity ║
║ ║
║ ║
║ ┌─────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ "When in doubt, send the complete state. │ ║
║ │ It's simpler and always correct." │ ║
║ │ │ ║
║ └─────────────────────────────────────────────┘ ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
Timeout Fallback
What if the frontend crashes or ACK is lost?Copy
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ ACK TIMEOUT MECHANISM ║
║ ║
║ ║
║ Problem: ║
║ ║
║ If ACK never arrives, the terminal would freeze forever. ║
║ ║
║ ║
║ Solution: 1-second timeout ║
║ ║
║ ║
║ Rust TypeScript ║
║ │ │ ║
║ │ GridUpdate ══════════════════════▶ │ ║
║ │ │ ║
║ │ Start timer: 1 second │ Crashed ║
║ │ │ │ or ║
║ │ │ │ ACK lost ║
║ │ │ │ ║
║ │ │ 100ms... │ ║
║ │ │ 200ms... │ ║
║ │ │ ... │ ║
║ │ │ 1000ms │ ║
║ │ ▼ │ ║
║ │ TIMEOUT! │ ║
║ │ │ ║
║ │ - Set waiting_ack = false │ ║
║ │ - Resume sending updates │ ║
║ │ - Log warning for debugging │ ║
║ │ │ ║
║ │ GridUpdate ══════════════════════▶ │ ║
║ │ (Terminal recovered) │ ║
║ ▼ ▼ ║
║ ║
║ ║
║ Why 1 Second? ║
║ ║
║ - Long enough: Normal ACK takes < 100ms ║
║ - Short enough: User notices frozen terminal quickly ║
║ - 1 sec allows frontend to recover from temporary issues ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
Two Layers of Flow Control
ACK Gate works alongside BSU/ESU for complementary control.Copy
╔════════════════════════════════════════════════════════════════════════╗
║ ║
║ TWO LAYERS OF FLOW CONTROL ║
║ ║
║ ║
║ Layer 1: ACK Gate (IPC Backpressure) ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ Rust Backend ◀──────▶ TypeScript Frontend │ ║
║ │ │ ║
║ │ - 1 second timeout │ ║
║ │ - Prevents event queue overflow │ ║
║ │ - Process-level backpressure │ ║
║ └─────────────────────────────────────────────────────┘ ║
║ ║
║ Layer 2: BSU/ESU (Frame Synchronization) ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ Application ◀──────▶ Terminal Renderer │ ║
║ │ │ ║
║ │ - 16ms timeout │ ║
║ │ - Prevents screen tearing │ ║
║ │ - Atomic frame rendering │ ║
║ └─────────────────────────────────────────────────────┘ ║
║ ║
║ ║
║ WHY TWO TIMEOUTS? ║
║ ║
║ BSU/ESU (16ms): ║
║ - Application-level frame control ║
║ - Fast timeout for responsive UI ║
║ - Works for well-behaved TUI apps ║
║ ║
║ ACK Gate (1000ms): ║
║ - Process-level IPC control ║
║ - Longer timeout for system delays ║
║ - Prevents memory exhaustion ║
║ ║
╚════════════════════════════════════════════════════════════════════════╝
Comparison with Other Terminals
How other terminals handle (or don’t handle) flow control.Copy
╔═══════════════════════════════════════════════════════════════════════╗
║ ║
║ FLOW CONTROL COMPARISON ║
║ ║
║ ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ MONOTERM (ACK Gate) │ ║
║ │ │ ║
║ │ Producer ──▶ [ACK Handshake] ──▶ Consumer │ ║
║ │ │ ║
║ │ Explicit flow control │ ║
║ │ No memory growth │ ║
║ │ Consumer sets the pace │ ║
║ │ One round-trip latency │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ NATIVE TERMINALS (Alacritty) │ ║
║ │ │ ║
║ │ Producer ──▶ [Mutex Lock] ──▶ Consumer │ ║
║ │ │ ║
║ │ Same process, simple │ ║
║ │ No IPC overhead │ ║
║ │ Lock contention possible │ ║
║ │ Not applicable to IPC scenarios │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────┘ ║
║ ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ │ ║
║ │ WEB TERMINALS (xterm.js default) │ ║
║ │ │ ║
║ │ Producer ──────────────────────▶ Consumer │ ║
║ │ │ ║
║ │ Simplest implementation │ ║
║ │ Lowest latency │ ║
║ │ Queue can grow unbounded │ ║
║ │ UI can freeze under load │ ║
║ │ │ ║
║ └─────────────────────────────────────────────────────┘ ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝
Summary
Copy
╔═══════════════════════════════════════════════════════════════════════╗
║ ║
║ ACK GATE IN ONE DIAGRAM ║
║ ║
║ ║
║ ┌────────────────────────┐ ║
║ │ │ ║
║ │ RUST BACKEND │ ║
║ │ │ ║
║ │ ┌────────────────┐ │ ║
║ │ │ │ │ ║
║ │ │ PTY Data In │ │ ║
║ │ │ │ │ ║
║ │ └───────┬────────┘ │ ║
║ │ │ │ ║
║ │ v │ ║
║ │ ┌────────────────┐ │ ║
║ │ │ │ │ ║
║ │ │ VTE Process │ │ ║
║ │ │ │ │ ║
║ │ └───────┬────────┘ │ ║
║ │ │ │ ║
║ │ v │ ║
║ │ ┌────────────────┐ │ ║
║ │ │ waiting_ack │ YES ║
║ │ │ == true? │───────▶[SKIP EMIT] ║
║ │ │ │ (set pending) ║
║ │ └───────┬────────┘ │ ║
║ │ │ NO │ ║
║ │ v │ ║
║ │ ┌────────────────┐ │ ║
║ │ │ │ │ ║
║ │ │ EMIT UPDATE │─────────────────────────▶ ║
║ │ │ waiting=true │ │ ║
║ │ │ │ │ ║
║ │ └────────────────┘ │ ║
║ │ │ ║
║ └────────────────────────┘ ║
║ │ ║
║ │ GridUpdate ║
║ │ ║
║ v ║
║ ┌────────────────────────┐ ║
║ │ │ ║
║ │ TYPESCRIPT FRONTEND │ ║
║ │ │ ║
║ │ 1. Receive GridUpdate ║
║ │ 2. Inject into xterm.js ║
║ │ 3. Trigger WebGL render ║
║ │ 4. Send ACK ═══════════════════▶ ║
║ │ │ ║
║ └────────────────────────┘ ║
║ ║
╚═══════════════════════════════════════════════════════════════════════╝