Performance Budgets
Budgets are requirements with numbers. The latency
harness measures them continuously;
a regression is a failing test. Numbers marked ~ are initial engineering
targets to be confirmed against M1 measurements (then this doc goes
stable).
Latency (p50 / p95, 1080p30 primary track)
Section titled “Latency (p50 / p95, 1080p30 primary track)”| Path | Glass-to-glass | Input-to-photon |
|---|---|---|
| LAN direct | ~120 / 200 ms | ~150 / 250 ms |
| Internet P2P (STUN) | ~200 / 350 ms | ~250 / 400 ms |
| TURN relay | ~250 / 450 ms | ~300 / 500 ms |
Terminal echo round-trip: < 150 ms LAN. Pointer motion send rate ≤ 60 Hz coalesced; stale motion dropped, never queued.
These are the numbers the nightly run on known hardware is held to. The per-push CI job measures the same paths but fails only above a loose ceiling, because a shared runner cannot hold a p95 honestly and a flaky gate teaches people to re-run it (docs/15).
Measured results {#measured}
Section titled “Measured results {#measured}”What the budgets above have actually measured, on named hardware, with the roadmap entry that recorded each number. This table is what the landing page quotes; a number not here is not on the page. Add a row when a gate measures something new, and replace a row (never average it) when a later run supersedes it.
| What | Measured | Budget | Where, when |
|---|---|---|---|
| Camera glass-to-glass, clean LAN | p50 56 / p95 79 ms | 120 / 200 | nightly runner, 46–52 decoded fps, M1 gate 2026-09-25 (docs/17) |
| Camera glass-to-glass, lossy link | 82 / 100 ms | 200 / 350 | same run |
| Camera glass-to-glass, through a TURN relay | 69 / 102 ms | 250 / 450 | same run |
| Terminal interactive round trip | 11 ms | < 150 ms | M2, 2026-09-27 |
| Desktop input-to-photon, LAN | p50 49 / p95 73 ms (50 presses, 5 runs; slowest 93) | 150 / 250 | mini-PC (Ryzen 7 5700U, GNOME 50, DMA-BUF capture, build c5dba78), measured from the dev machine, both on 5 GHz Wi-Fi, M3 gate review 2026-10-18; replaces 3.L’s p50 52 / p95 59 (2026-10-01, before the local cursor and DMA-BUF) |
| Desktop: monitor plugged in → new track offered | 101 ms, no frames dropped on the others | — | M3 3.4, 2026-10-01 |
| Desktop: killed session → back, unattended | 51 s | — | M3 3.L, 2026-10-01 |
Tunnel: ssh to a shell | ~130 ms | — | M4.5 slice 4.5c, 2026-09-26 |
Tunnel: hash-verified 1 GiB scp via fjarr-connect | 246–253 Mbps | — | M4.5 gate 2026-09-27 (docs/27) |
| Camera beside a saturating tunnel transfer | 31–33 fps, no lost frames | bulk vs interactive | M4.5 slice 4.5c, 2026-09-26 |
Bitrate tiers (per video track)
Section titled “Bitrate tiers (per video track)”| Tier | Resolution/fps | Target bitrate |
|---|---|---|
| Active (operator focused) | up to 1920×1080@30 | 2.5–5 Mbps adaptive |
| Inactive thumbnail | 960×540@5 | ≤ 300 kbps |
| Disabled | — | ~0 (valve dropped; no renegotiation) |
Audio (Opus): 32–64 kbps per track downlink, 24–48 kbps uplink; mouth-to-ear < 250 ms P2P. Stream-class sensor frames (ADR-0018): capability-declared ceiling (e.g. a decimated point cloud ≤ 2 Mbps at 5–10 Hz); frames older than one interval are dropped, never queued.
Adaptive bitrate is a hard requirement (the camera-streamer gap): the encoder target follows congestion feedback (per-peer TWCC into the agent’s own estimator — ADR-0007) between a floor of 250 kbps and the tier target, reacting within ~2 s to loss and recovering within ~10 s. Fixed-CBR-only operation is a spec violation. Because one encoder serves every viewer of a tier, the encoder only follows its viewers within a band (half the tier target and up); a viewer whose link is below the band is moved to the lower tier on its own, so one bad receiver never costs the others more than that band (docs/23).
Re-enabling a track waits on the keyframe rate limit. Requests are throttled to one per second per producer tier and deferred inside that window (docs/23), so a viewer that re-enables a track sooner than that waits the remainder, then the encoder’s own latency, then the hub’s start-at-a-keyframe rule. Measured over 20 rapid enable/disable cycles: worst 757-949 ms on 14 cores with VA-API, and ~2.9 s on a 2-core runner with the software encoder. The throttle is deliberate — it protects the encoder from a viewer toggling in a loop — so this is a floor to design around rather than a regression to chase.
The ~2 s reaction above is the estimator’s, measured from feedback that already
carries a low rate. Demoting a viewer that is behind a bad link from the
moment it connects takes far longer, because nothing can be judged until
transport feedback arrives at all: measured at 11.9–20.4 s after the track
was enabled (bad: 15 % loss, 100 ± 40 ms, 1.5 Mbit), and longer again when
other viewers share the encoder (slice 4.5b). That is not a budget violation —
the 2 s figure governs an established session — but it is the number to design
tests and support expectations around, and it is why a joining viewer on a bad
link streams poorly for a few seconds before it is moved down.
A passthrough track (the camera’s own encoded stream) cannot adapt at the
agent: it adapts by tier only when the source offers a lower stream,
otherwise not at all, and says so (adaptive: false). In exchange it
costs no encoder at all — the robot’s GPU and CPU budgets above are per
transcoded track, so a fleet of passthrough cameras is bounded by the
network, not by the encoder count.
Robot resource budget (Intel NUC class, one active session + one thumbnail)
Section titled “Robot resource budget (Intel NUC class, one active session + one thumbnail)”| Resource | Budget |
|---|---|
| CPU (agent total) | < 25 % of one core with VA-API encode; capture-copy path < 60 % |
| GPU | encode within iGPU capacity for 2 concurrent 1080p30 encodes |
| RAM | agent RSS < 300 MB steady state |
| Store-and-forward disk | bounded ≤ 200 MB (oldest-first eviction + drop counter) |
| Heap allocations per frame on the streaming hot path (FrameHub → appsrc), steady state | one metadata-only GstBuffer header per subscriber per frame (the fan-out’s per-subscriber PTS rebase, docs/23) and GStreamer’s own refs — nothing else: no pixel copy, no per-frame std::function, string or container allocation (heaptrack) |
| Object census after a 200-session soak | identical to baseline; RSS growth < 5 MB |
Multi-viewer scales via FrameHub: +1 viewer ≈ +RTP fan-out cost only (no new encode) — verify ≤ 5 % CPU per additional viewer.
Fleet/relay planning
Section titled “Fleet/relay planning”Browser viewers are relay-realistic: capacity-plan TURN at ~1 stream = 0.5–5 Mbps depending on activity. Ten concurrent operator views ≈ tens of Mbps through the relay — metered per session (docs/10), priced through (docs/03).
Web client budgets {#web-client-budgets}
Section titled “Web client budgets {#web-client-budgets}”Measured by the browser lab profiling scenarios (initial targets, confirmed at slice 5):
| Metric | Budget |
|---|---|
| Main-thread busy while streaming 2 active tracks (60 s) | ≤ 15 % |
| Long tasks (> 50 ms) while streaming | 0 per minute after first frame |
| INP (dashboard interactions while streaming) | ≤ 200 ms |
| JS heap growth per connect/disconnect cycle (100-cycle soak, after GC) | ≤ 50 KB/cycle, no monotonic listener/node growth |
Time to first frame after select-tracks enable (LAN, keyframe requested) | ≤ 300 ms |
| Frames dropped on unchanged tracks during a renegotiation | 0 (frame-stamp counter) |
Connection health thresholds {#connection-health-thresholds}
Section titled “Connection health thresholds {#connection-health-thresholds}”The web client’s health score (docs/21) derives from the selected candidate pair’s RTT and the windowed packet loss across received tracks:
| Level | Condition (any) |
|---|---|
| good | RTT ≤ 300 ms and loss ≤ 5 % and no freeze in the window |
| degraded | RTT > 300 ms, or loss > 5 %, or a freeze ≥ 500 ms in the window |
| poor | RTT > 600 ms, or loss > 15 %, or no frames decoded for a full window while a track is enabled |
A level changes only after three consecutive 1 s samples agree (hysteresis). The numbers are initial targets in the same sense as the latency table above.
Bulk vs interactive isolation
Section titled “Bulk vs interactive isolation”During a saturating file transfer: interactive video g2g p95 may degrade by at most +50 ms and input-to-photon by +30 ms vs baseline. If a shared SCTP association can’t hold that, bulk moves to a separate PeerConnection (measured decision, M4).
Degradation policy (in order)
Section titled “Degradation policy (in order)”- Reduce inactive-tier tracks (fps, then resolution).
- Reduce active-tier bitrate toward the floor.
- Reduce active fps (30→15) before resolution.
- Drop inactive tracks entirely (UI shows “paused — bandwidth”).
- Never: silently stall media, queue stale input, or let heartbeats starve (control DC has priority).
Startup: first frame visible < 2 s after session-accept on P2P, < 3 s on
relay (keyframe-on-connect required). Hot-plug (docs/08 renegotiation):
a newly connected monitor’s first frame < 2 s after the monitors event;
zero dropped frames and no re-attach on unchanged tracks during the
renegotiation.