1use blit_alacritty::{SearchResult as AlacrittySearchResult, TerminalDriver as AlacrittyDriver};
2use blit_remote::{
3 C2S_ACK, C2S_CLIENT_METRICS, C2S_CLOSE, C2S_COPY_RANGE, C2S_CREATE, C2S_CREATE_AT,
4 C2S_CREATE_N, C2S_CREATE2, C2S_DISPLAY_RATE, C2S_FOCUS, C2S_INPUT, C2S_KILL, C2S_MOUSE,
5 C2S_READ, C2S_RESIZE, C2S_RESTART, C2S_SCROLL, C2S_SEARCH, C2S_SUBSCRIBE, C2S_UNSUBSCRIBE,
6 CREATE2_HAS_COMMAND, CREATE2_HAS_SRC_PTY, FEATURE_COPY_RANGE, FEATURE_CREATE_NONCE,
7 FEATURE_RESIZE_BATCH, FEATURE_RESTART, FrameState, READ_ANSI, READ_TAIL, S2C_CLOSED,
8 S2C_CREATED, S2C_CREATED_N, S2C_LIST, S2C_READY, S2C_SEARCH_RESULTS, S2C_TEXT, S2C_TITLE,
9 build_update_msg, msg_hello,
10};
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
15use tokio::sync::{Mutex, Notify, mpsc};
16
17mod ipc;
18mod pty;
19
20pub use ipc::{IpcListener, default_ipc_path};
21use pty::{PtyHandle, PtyWriteTarget};
22
23type PtyFds = Arc<std::sync::RwLock<HashMap<u16, PtyWriteTarget>>>;
24pub struct Config {
25 pub shell: String,
26 pub shell_flags: String,
27 pub scrollback: usize,
28 pub ipc_path: String,
29 #[cfg(unix)]
30 pub fd_channel: Option<std::os::unix::io::RawFd>,
31 pub verbose: bool,
32}
33
34trait PtyDriver: Send {
35 fn size(&self) -> (u16, u16);
36 fn resize(&mut self, rows: u16, cols: u16);
37 fn process(&mut self, data: &[u8]);
38 fn title(&self) -> &str;
39 fn search_result(&self, query: &str) -> Option<PtySearchResult>;
40 fn take_title_dirty(&mut self) -> bool;
41 fn cursor_position(&self) -> (u16, u16);
42 fn synced_output(&self) -> bool;
43 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
44 fn scrollback_frame(&mut self, offset: usize) -> FrameState;
45 fn reset_modes(&mut self);
46 fn mouse_event(
47 &self,
48 type_: u8,
49 button: u8,
50 col: u16,
51 row: u16,
52 echo: bool,
53 icanon: bool,
54 ) -> Option<Vec<u8>>;
55 fn get_text_range(
56 &self,
57 start_tail: u32,
58 start_col: u16,
59 end_tail: u32,
60 end_col: u16,
61 ) -> String;
62 fn total_lines(&self) -> u32;
63}
64
65struct PtySearchResult {
66 score: u32,
67 primary_source: u8,
68 matched_sources: u8,
69 context: String,
70 scroll_offset: Option<usize>,
71}
72
73impl PtyDriver for AlacrittyDriver {
74 fn size(&self) -> (u16, u16) {
75 AlacrittyDriver::size(self)
76 }
77
78 fn resize(&mut self, rows: u16, cols: u16) {
79 AlacrittyDriver::resize(self, rows, cols);
80 }
81
82 fn process(&mut self, data: &[u8]) {
83 AlacrittyDriver::process(self, data);
84 }
85
86 fn title(&self) -> &str {
87 AlacrittyDriver::title(self)
88 }
89
90 fn search_result(&self, query: &str) -> Option<PtySearchResult> {
91 AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
92 PtySearchResult {
93 score: result.score,
94 primary_source: result.primary_source as u8,
95 matched_sources: result.matched_sources,
96 context: result.context,
97 scroll_offset: result.scroll_offset,
98 }
99 })
100 }
101
102 fn take_title_dirty(&mut self) -> bool {
103 AlacrittyDriver::take_title_dirty(self)
104 }
105
106 fn cursor_position(&self) -> (u16, u16) {
107 AlacrittyDriver::cursor_position(self)
108 }
109
110 fn synced_output(&self) -> bool {
111 AlacrittyDriver::synced_output(self)
112 }
113
114 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
115 AlacrittyDriver::snapshot(self, echo, icanon)
116 }
117
118 fn scrollback_frame(&mut self, offset: usize) -> FrameState {
119 AlacrittyDriver::scrollback_frame(self, offset)
120 }
121
122 fn reset_modes(&mut self) {
123 AlacrittyDriver::reset_modes(self);
124 }
125
126 fn mouse_event(
127 &self,
128 type_: u8,
129 button: u8,
130 col: u16,
131 row: u16,
132 echo: bool,
133 icanon: bool,
134 ) -> Option<Vec<u8>> {
135 AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
136 }
137
138 fn get_text_range(
139 &self,
140 start_tail: u32,
141 start_col: u16,
142 end_tail: u32,
143 end_col: u16,
144 ) -> String {
145 AlacrittyDriver::get_text_range(self, start_tail, start_col, end_tail, end_col)
146 }
147
148 fn total_lines(&self) -> u32 {
149 AlacrittyDriver::total_lines(self)
150 }
151}
152
153const OUTBOX_CAPACITY: usize = 8;
157const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 2;
158const PREVIEW_FRAME_RESERVE: usize = 1;
159const READY_FRAME_QUEUE_CAP: usize = 4;
160const PTY_CHANNEL_CAPACITY: usize = 64;
161const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
162
163enum PtyInput {
166 Data(Vec<u8>),
169 SyncBoundary { before: Vec<u8>, after: Vec<u8> },
172 Eof,
174}
175
176const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
177
178async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
179 let mut len_buf = [0u8; 4];
180 reader.read_exact(&mut len_buf).await.ok()?;
181 let len = u32::from_le_bytes(len_buf) as usize;
182 if len == 0 {
183 return Some(vec![]);
184 }
185 if len > MAX_FRAME_SIZE {
186 return None;
187 }
188 let mut buf = vec![0u8; len];
189 reader.read_exact(&mut buf).await.ok()?;
190 Some(buf)
191}
192
193async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
194 if payload.len() > u32::MAX as usize {
195 return false;
196 }
197 let len = payload.len() as u32;
198 let mut buf = Vec::with_capacity(4 + payload.len());
199 buf.extend_from_slice(&len.to_le_bytes());
200 buf.extend_from_slice(payload);
201 writer.write_all(&buf).await.is_ok()
202}
203
204struct Pty {
205 handle: PtyHandle,
206 driver: Box<dyn PtyDriver>,
207 tag: String,
209 dirty: bool,
210 ready_frames: VecDeque<FrameState>,
211 byte_rx: mpsc::Receiver<PtyInput>,
213 reader_handle: std::thread::JoinHandle<()>,
214 lflag_cache: (bool, bool),
216 lflag_last: Instant,
217 last_title_send: Instant,
219 title_pending: bool,
221 exited: bool,
223 exit_status: i32,
226 command: Option<String>,
228}
229
230impl Pty {
231 fn mark_dirty(&mut self) {
232 self.dirty = true;
233 }
234
235 fn clear_dirty(&mut self) {
236 self.dirty = false;
237 }
238}
239
240struct ClientState {
241 tx: mpsc::Sender<Vec<u8>>,
242 lead: Option<u16>,
243 subscriptions: HashSet<u16>,
244 view_sizes: HashMap<u16, (u16, u16)>,
245 scroll_offsets: HashMap<u16, usize>,
246 scroll_caches: HashMap<u16, FrameState>,
247 last_sent: HashMap<u16, FrameState>,
248 preview_next_send_at: HashMap<u16, Instant>,
249 rtt_ms: f32,
251 min_rtt_ms: f32,
253 display_fps: f32,
255 delivery_bps: f32,
257 goodput_bps: f32,
259 goodput_jitter_bps: f32,
261 max_goodput_jitter_bps: f32,
263 last_goodput_sample_bps: f32,
265 avg_frame_bytes: f32,
267 avg_paced_frame_bytes: f32,
269 avg_preview_frame_bytes: f32,
271 inflight_bytes: usize,
273 inflight_frames: VecDeque<InFlightFrame>,
275 next_send_at: Instant,
277 probe_frames: f32,
280 frames_sent: u32,
282 acks_recv: u32,
283 acked_bytes_since_log: usize,
284 browser_backlog_frames: u16,
285 browser_ack_ahead_frames: u16,
286 browser_apply_ms: f32,
287 last_metrics_update: Instant,
288 last_log: Instant,
289 goodput_window_bytes: usize,
290 goodput_window_start: Instant,
291}
292
293struct InFlightFrame {
294 sent_at: Instant,
295 bytes: usize,
296 paced: bool,
297}
298
299fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
303 let frame_ms = 1_000.0 / display_fps.max(1.0);
304 let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
305 let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
306 base_frames.saturating_add(slack_frames).max(2)
307}
308
309fn path_rtt_ms(client: &ClientState) -> f32 {
310 if client.min_rtt_ms > 0.0 {
311 client.min_rtt_ms
312 } else {
313 client.rtt_ms
314 }
315}
316
317fn display_need_bps(client: &ClientState) -> f32 {
318 client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
319}
320
321fn effective_rtt_ms(client: &ClientState) -> f32 {
322 let path_rtt = path_rtt_ms(client);
323 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
324 let queue_allowance = frame_ms
325 * if throughput_limited(client) {
326 4.0
327 } else {
328 12.0
329 };
330 client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
331}
332
333fn window_rtt_ms(client: &ClientState) -> f32 {
334 let effective = effective_rtt_ms(client);
335 if !throughput_limited(client) {
336 effective
337 } else {
338 client.rtt_ms.clamp(effective, effective * 2.0)
339 }
340}
341
342fn target_frame_window(client: &ClientState) -> usize {
343 let window_fps = if throughput_limited(client) {
344 pacing_fps(client)
345 } else {
346 browser_pacing_fps(client)
347 };
348 frame_window(window_rtt_ms(client), window_fps)
349 .saturating_add(client.probe_frames.round().max(0.0) as usize)
350}
351
352fn base_queue_ms(client: &ClientState) -> f32 {
353 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
354 frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
355}
356
357fn target_queue_ms(client: &ClientState) -> f32 {
358 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
359 let probe_scale = if throughput_limited(client) {
360 0.25
361 } else {
362 1.0
363 };
364 base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
365}
366
367fn browser_ready(client: &ClientState) -> bool {
368 client.browser_ack_ahead_frames <= 1
369 && client.browser_apply_ms <= 1.0
370 && !outbox_backpressured(client)
371}
372
373fn bandwidth_floor_bps(client: &ClientState) -> f32 {
374 let browser_ready = browser_ready(client);
375 let backlog_scale = match client.browser_backlog_frames {
376 0..=2 => 0.9,
377 3..=8 => 0.8,
378 _ => 0.65,
379 };
380 let penalty = client
381 .goodput_jitter_bps
382 .max(client.max_goodput_jitter_bps * 0.5)
383 .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
384 let goodput_floor = (client.goodput_bps - penalty)
385 .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
386 let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
390 let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
391 client.last_goodput_sample_bps * backlog_scale
392 } else {
393 0.0
394 };
395 goodput_floor.max(recent_sample_floor).max(delivery_floor)
396}
397
398fn pacing_fps(client: &ClientState) -> f32 {
399 let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
400 let sustainable = bandwidth_floor_bps(client) / frame_bytes;
401 sustainable.min(browser_pacing_fps(client))
402}
403
404fn throughput_limited(client: &ClientState) -> bool {
405 let floor = bandwidth_floor_bps(client);
406 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
410 let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
411 (lead_bps + preview_bps) > floor * 0.9
412}
413
414fn browser_pacing_fps(client: &ClientState) -> f32 {
415 let mut fps = client.display_fps.max(1.0);
416
417 let backlog = client.browser_backlog_frames as f32;
421 if backlog > 4.0 {
422 fps = fps.min(fps * (4.0 / backlog));
423 }
424
425 if client.browser_ack_ahead_frames > 4 {
426 fps = fps.min(client.display_fps.max(1.0) * 0.5);
427 }
428
429 fps.max(1.0)
430}
431
432fn browser_backlog_blocked(client: &ClientState) -> bool {
433 client.browser_backlog_frames > 8
434}
435
436fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
437 let budget_bps = if throughput_limited(client) {
438 bandwidth_floor_bps(client)
439 } else {
440 client.goodput_bps.max(bandwidth_floor_bps(client))
441 };
442 let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
443 bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
444}
445
446fn target_byte_window(client: &ClientState) -> usize {
447 let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
448 let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
449 let target_frames = target_frame_window(client);
450 let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
451 const PIPELINE_FLOOR_LIMIT: usize = 32_768; let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
458 pipeline_bytes
459 } else {
460 frame_bytes };
462 budget.max(floor)
463}
464
465fn send_interval(client: &ClientState) -> Duration {
466 Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
467}
468
469fn preview_fps(client: &ClientState) -> f32 {
470 let mut fps = client.display_fps.max(1.0);
471 if client.lead.is_some() {
472 let avail = bandwidth_floor_bps(client);
476 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
477 let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
478 let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
479 fps = fps.min(bw_cap.max(1.0));
480 }
481 fps.max(1.0)
482}
483
484fn preview_send_interval(client: &ClientState) -> Duration {
485 Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
486}
487
488fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
489 let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
490 *deadline = if scheduled + interval < now {
491 now + interval
492 } else {
493 scheduled
494 };
495}
496
497fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
498 dirty && needful && !synced_output
499}
500
501fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
502 if queue.len() >= READY_FRAME_QUEUE_CAP {
503 return false;
504 }
505 queue.push_back(frame);
506 true
507}
508
509fn pty_has_visual_update(pty: &Pty) -> bool {
510 pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
511}
512
513fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
517 if bytes.is_empty() {
518 return None;
519 }
520 let needle = SYNC_OUTPUT_END;
521 let nlen = needle.len();
522
523 if !prefix.is_empty() {
525 let tail = if prefix.len() >= nlen - 1 {
526 &prefix[prefix.len() - (nlen - 1)..]
527 } else {
528 prefix
529 };
530 let combined_len = tail.len() + bytes.len().min(nlen);
531 if combined_len >= nlen {
532 let mut buf = [0u8; 32]; let blen = combined_len.min(buf.len());
535 let tlen = tail.len().min(blen);
536 buf[..tlen].copy_from_slice(&tail[..tlen]);
537 let rest = (blen - tlen).min(bytes.len());
538 buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
539 for i in 0..=(blen.saturating_sub(nlen)) {
540 if &buf[i..i + nlen] == needle {
541 let end_in_bytes = (i + nlen).saturating_sub(tail.len());
542 if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
543 return Some(end_in_bytes);
544 }
545 }
546 }
547 }
548 }
549
550 let mut offset = 0;
552 while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
553 let abs = offset + pos;
554 if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
555 return Some(abs + nlen);
556 }
557 offset = abs + 1;
558 }
559 None
560}
561
562fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
563 if bytes.is_empty() {
564 return;
565 }
566 tail.extend_from_slice(bytes);
567 let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
568 if tail.len() > keep {
569 let drop = tail.len() - keep;
570 tail.drain(..drop);
571 }
572}
573
574fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
575 client
576 .preview_next_send_at
577 .get(&pid)
578 .copied()
579 .unwrap_or(now)
580}
581
582fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
583 if client.lead.is_none() {
584 return false;
585 }
586 client.subscriptions.iter().copied().any(|pid| {
587 Some(pid) != client.lead
588 && preview_deadline(client, pid, now) <= now
589 && sess
590 .ptys
591 .get(&pid)
592 .map(pty_has_visual_update)
593 .unwrap_or(false)
594 })
595}
596
597fn outbox_queued_frames(client: &ClientState) -> usize {
598 OUTBOX_CAPACITY.saturating_sub(client.tx.capacity())
599}
600
601fn outbox_backpressured(client: &ClientState) -> bool {
602 outbox_queued_frames(client) >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES
603}
604
605fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
606 window_open(client) && now >= preview_deadline(client, pid, now)
607}
608
609fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
610 let mut deadline = client
611 .preview_next_send_at
612 .get(&pid)
613 .copied()
614 .unwrap_or(now);
615 advance_deadline(&mut deadline, now, preview_send_interval(client));
616 client.preview_next_send_at.insert(pid, deadline);
617}
618
619fn window_open(client: &ClientState) -> bool {
620 !browser_backlog_blocked(client)
621 && !outbox_backpressured(client)
622 && client.inflight_frames.len() < target_frame_window(client)
623 && client.inflight_bytes < target_byte_window(client)
624}
625
626fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
627 if !reserve_preview_slot || client.lead.is_none() {
628 return window_open(client);
629 }
630 if browser_backlog_blocked(client) || outbox_backpressured(client) {
631 return false;
632 }
633 let target_frames = target_frame_window(client);
634 let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
635 let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
636 let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
637 let byte_limit = target_byte_window(client)
638 .saturating_sub(reserve_bytes)
639 .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
640 client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
641}
642
643fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
644 lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
645}
646
647fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
648 client.inflight_bytes += bytes;
649 client.inflight_frames.push_back(InFlightFrame {
650 sent_at: now,
651 bytes,
652 paced,
653 });
654 if paced {
655 let interval = send_interval(client);
656 advance_deadline(&mut client.next_send_at, now, interval);
657 }
658}
659
660fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
661 let alpha = if sample > old { rise_alpha } else { fall_alpha };
662 old * (1.0 - alpha) + sample * alpha
663}
664
665fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
666 let target_frames = target_frame_window(client);
667 let target_bytes = target_byte_window(client);
668 inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
669 || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
670}
671
672fn record_ack(client: &mut ClientState) {
673 if let Some(frame) = client.inflight_frames.pop_front() {
674 let prev_inflight_frames = client.inflight_frames.len() + 1;
675 let prev_inflight_bytes = client.inflight_bytes;
676 client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
677 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
678 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
679 client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
680 if client.min_rtt_ms > 0.0 {
681 client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
684 } else {
685 client.min_rtt_ms = sample_ms;
686 }
687 client.min_rtt_ms = client.min_rtt_ms.max(0.5);
688 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
689 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
690 client.avg_frame_bytes =
691 ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
692 if frame.paced {
693 client.avg_paced_frame_bytes =
694 ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
695 } else {
696 client.avg_preview_frame_bytes = ewma_with_direction(
697 client.avg_preview_frame_bytes,
698 frame.bytes as f32,
699 0.5,
700 0.125,
701 );
702 }
703 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
704 let path_rtt = path_rtt_ms(client);
705 let likely_window_limited =
706 window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
707 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
708 let now = Instant::now();
709 let goodput_elapsed = now
710 .duration_since(client.goodput_window_start)
711 .as_secs_f32();
712 if goodput_elapsed >= 0.02 {
713 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
714 if likely_window_limited || client.browser_backlog_frames > 0 {
715 let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
716 client.last_goodput_sample_bps
717 } else {
718 sample_goodput
719 };
720 let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
721 client.goodput_bps =
722 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
723 let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
729 if client.goodput_window_bytes >= min_reliable {
730 client.goodput_jitter_bps =
731 ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
732 let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
733 0.90
734 } else {
735 0.98
736 };
737 client.max_goodput_jitter_bps =
738 (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
739 client.max_goodput_jitter_bps =
743 client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
744 } else {
745 client.goodput_jitter_bps *= 0.9;
747 client.max_goodput_jitter_bps *= 0.95;
748 }
749 client.last_goodput_sample_bps =
753 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
754 } else {
755 let ratio = client.goodput_bps / sample_goodput.max(1.0);
760 let fall_alpha = if ratio > 10.0 {
761 0.5
762 } else if ratio > 3.0 {
763 0.25
764 } else {
765 0.03
766 };
767 client.goodput_bps =
768 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
769 client.goodput_jitter_bps *= 0.5;
770 client.max_goodput_jitter_bps *= 0.9;
771 client.last_goodput_sample_bps =
772 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
773 }
774 client.goodput_window_bytes = 0;
775 client.goodput_window_start = now;
776 }
777 let queue_baseline_ms = if throughput_limited(client) {
778 window_rtt_ms(client)
779 } else {
780 path_rtt
781 };
782 let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
783 let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
784 let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
785 let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
786 let high_delay_frames = if throughput_limited(client) {
787 4.0
788 } else {
789 12.0
790 };
791 if likely_window_limited
792 && queue_delay_ms <= frame_ms * low_delay_frames
793 && jitter_ratio < 0.25
794 {
795 client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
796 } else if !likely_window_limited
797 && browser_ready(client)
798 && queue_delay_ms <= frame_ms * 2.0
799 && jitter_ratio < 0.25
800 {
801 client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
802 } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
803 client.probe_frames = (client.probe_frames * 0.5).max(1.0);
804 } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
805 client.probe_frames = (client.probe_frames - 0.5).max(0.0);
806 }
807 } else {
808 client.inflight_bytes = 0;
809 }
810}
811
812fn reset_inflight(client: &mut ClientState) {
813 client.inflight_bytes = 0;
814 client.inflight_frames.clear();
815 client.next_send_at = Instant::now();
816 client.browser_backlog_frames = 0;
817 client.browser_ack_ahead_frames = 0;
818}
819
820fn is_unset_view_size(rows: u16, cols: u16) -> bool {
821 rows == 0 && cols == 0
822}
823
824fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
825 if client.subscriptions.insert(pty_id) {
826 client.last_sent.remove(&pty_id);
827 client.preview_next_send_at.remove(&pty_id);
828 }
829}
830
831fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
832 let removed_sub = client.subscriptions.remove(&pty_id);
833 client.last_sent.remove(&pty_id);
834 client.preview_next_send_at.remove(&pty_id);
835 client.scroll_offsets.remove(&pty_id);
836 client.scroll_caches.remove(&pty_id);
837 let removed_view = client.view_sizes.remove(&pty_id).is_some();
838 if client.lead == Some(pty_id) {
839 client.lead = None;
840 }
841 removed_sub || removed_view
842}
843
844fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
845 let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
846 if prev_offset == next_offset {
847 return false;
848 }
849
850 if prev_offset == 0 && next_offset > 0 {
851 client.scroll_caches.insert(
852 pty_id,
853 client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
854 );
855 } else if prev_offset > 0
856 && next_offset == 0
857 && let Some(cache) = client.scroll_caches.remove(&pty_id)
858 {
859 if cache.rows() > 0 && cache.cols() > 0 {
860 client.last_sent.insert(pty_id, cache);
861 } else {
862 client.last_sent.remove(&pty_id);
863 }
864 }
865
866 if next_offset > 0 {
867 client.scroll_offsets.insert(pty_id, next_offset);
868 } else {
869 client.scroll_offsets.remove(&pty_id);
870 }
871 reset_inflight(client);
872 true
873}
874
875struct Session {
876 ptys: HashMap<u16, Pty>,
877 next_client_id: u64,
878 tick_fires: u32,
880 tick_snaps: u32,
882 clients: HashMap<u64, ClientState>,
883}
884
885struct SearchResultRow {
886 pty_id: u16,
887 score: u32,
888 primary_source: u8,
889 matched_sources: u8,
890 context: String,
891 scroll_offset: Option<usize>,
892}
893
894struct TickOutcome {
895 did_work: bool,
896 next_deadline: Option<Instant>,
897}
898
899impl Session {
900 fn new() -> Self {
901 Self {
902 ptys: HashMap::new(),
903 next_client_id: 1,
904 clients: HashMap::new(),
905 tick_fires: 0,
906 tick_snaps: 0,
907 }
908 }
909
910 fn allocate_pty_id(&mut self) -> Option<u16> {
911 (1..=u16::MAX).find(|id| !self.ptys.contains_key(id))
912 }
913
914 fn send_to_all(&self, msg: &[u8]) {
915 for c in self.clients.values() {
916 let _ = c.tx.try_send(msg.to_vec());
917 }
918 }
919
920 fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
921 let mut min_rows: Option<u16> = None;
922 let mut min_cols: Option<u16> = None;
923 for c in self.clients.values() {
924 if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
925 min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
926 min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
927 }
928 }
929 match (min_rows, min_cols) {
930 (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
931 _ => None,
932 }
933 }
934
935 fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
936 let pty = match self.ptys.get_mut(&pty_id) {
937 Some(p) => p,
938 None => return false,
939 };
940 let (cur_rows, cur_cols) = pty.driver.size();
941 if cur_rows == rows && cur_cols == cols {
942 return false;
943 }
944 pty.ready_frames.clear();
945 pty.driver.resize(rows, cols);
946 pty.mark_dirty();
947 for c in self.clients.values_mut() {
948 if c.subscriptions.contains(&pty_id) {
949 c.last_sent.remove(&pty_id);
950 }
951 if c.scroll_caches.remove(&pty_id).is_some() {
952 reset_inflight(c);
953 }
954 }
955 if !pty.exited {
956 pty::resize_pty_os(&pty.handle, rows, cols);
957 }
958 true
959 }
960
961 fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
962 where
963 I: IntoIterator<Item = u16>,
964 {
965 let mut changed = false;
966 let mut seen = HashSet::new();
967 for pty_id in pty_ids {
968 if !seen.insert(pty_id) {
969 continue;
970 }
971 if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
972 changed |= self.resize_pty(pty_id, rows, cols);
973 }
974 }
975 changed
976 }
977
978 fn pty_list_msg(&self) -> Vec<u8> {
979 let mut msg = vec![S2C_LIST];
980 let count = self.ptys.len() as u16;
981 msg.extend_from_slice(&count.to_le_bytes());
982 let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
983 ids.sort();
984 for id in ids {
985 let pty = &self.ptys[&id];
986 let tag = pty.tag.as_bytes();
987 msg.extend_from_slice(&id.to_le_bytes());
988 msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
989 msg.extend_from_slice(tag);
990 let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
991 msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
992 msg.extend_from_slice(cmd);
993 }
994 msg
995 }
996}
997
998type AppState = Arc<(Config, Mutex<Session>, PtyFds, Arc<Notify>)>;
999
1000fn nudge_delivery(state: &AppState) {
1001 state.3.notify_one();
1002}
1003
1004fn parse_terminal_queries(data: &[u8], size: (u16, u16), cursor: (u16, u16)) -> Vec<String> {
1005 const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1006
1007 let mut results = Vec::new();
1008 let mut i = 0;
1009 while i < data.len() {
1010 if data[i] != 0x1b || i + 2 >= data.len() || data[i + 1] != b'[' {
1011 i += 1;
1012 continue;
1013 }
1014 i += 2;
1015 let has_q = i < data.len() && data[i] == b'?';
1016 if has_q {
1017 i += 1;
1018 }
1019 let param_start = i;
1020 while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1021 i += 1;
1022 }
1023 if i >= data.len() {
1024 break;
1025 }
1026 let final_byte = data[i];
1027 let params = &data[param_start..i];
1028 i += 1;
1029 if has_q {
1030 continue;
1031 }
1032 let resp: Option<String> = match final_byte {
1033 b'c' if params.is_empty() || params == b"0" => {
1034 Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1035 }
1036 b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1037 b'n' if params == b"5" => Some("\x1b[0n".into()),
1038 b't' if params == b"18" => {
1039 let (rows, cols) = size;
1040 Some(format!("\x1b[8;{rows};{cols}t"))
1041 }
1042 b't' if params == b"14" => {
1043 let (rows, cols) = size;
1044 Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
1045 }
1046 _ => None,
1047 };
1048 if let Some(r) = resp {
1049 results.push(r);
1050 }
1051 }
1052 results
1053}
1054
1055async fn cleanup_pty_internal(pty_id: u16, state: &AppState) {
1056 state.2.write().unwrap().remove(&pty_id);
1057 let mut sess = state.1.lock().await;
1058 if let Some(pty) = sess.ptys.get_mut(&pty_id) {
1059 if pty.exited {
1060 return;
1061 }
1062 pty.exited = true;
1063 pty::close_pty(&pty.handle);
1064 pty.exit_status = pty::collect_exit_status(&pty.handle);
1065 pty.mark_dirty();
1066 let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
1067 sess.send_to_all(&msg);
1068 }
1069}
1070
1071fn take_snapshot(pty: &mut Pty) -> FrameState {
1072 if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
1073 pty.lflag_cache = pty::pty_lflag(&pty.handle);
1074 pty.lflag_last = Instant::now();
1075 }
1076 let (echo, icanon) = pty.lflag_cache;
1077 pty.driver.snapshot(echo, icanon)
1078}
1079
1080fn build_scrollback_update(
1081 pty: &mut Pty,
1082 id: u16,
1083 offset: usize,
1084 prev_frame: &FrameState,
1085) -> Option<(Vec<u8>, FrameState)> {
1086 let frame = pty.driver.scrollback_frame(offset);
1087 let msg = build_update_msg(id, &frame, prev_frame);
1088 msg.map(|m| (m, frame))
1089}
1090
1091fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
1092 let count = results.len().min(u16::MAX as usize);
1093 let payload_bytes: usize = results[..count]
1094 .iter()
1095 .map(|result| 14 + result.context.len().min(u16::MAX as usize))
1096 .sum();
1097 let mut msg = Vec::with_capacity(5 + payload_bytes);
1098 msg.push(S2C_SEARCH_RESULTS);
1099 msg.extend_from_slice(&request_id.to_le_bytes());
1100 msg.extend_from_slice(&(count as u16).to_le_bytes());
1101 for result in &results[..count] {
1102 msg.extend_from_slice(&result.pty_id.to_le_bytes());
1103 msg.extend_from_slice(&result.score.to_le_bytes());
1104 msg.push(result.primary_source);
1105 msg.push(result.matched_sources);
1106 let scroll_offset = result
1107 .scroll_offset
1108 .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
1109 .unwrap_or(u32::MAX);
1110 msg.extend_from_slice(&scroll_offset.to_le_bytes());
1111 let context = result.context.as_bytes();
1112 let context_len = context.len().min(u16::MAX as usize);
1113 msg.extend_from_slice(&(context_len as u16).to_le_bytes());
1114 msg.extend_from_slice(&context[..context_len]);
1115 }
1116 msg
1117}
1118
1119enum SendOutcome {
1120 NoChange,
1121 Sent,
1122 Backpressured,
1123}
1124
1125fn try_send_update(
1126 client: &mut ClientState,
1127 pid: u16,
1128 current: FrameState,
1129 msg: Option<Vec<u8>>,
1130 now: Instant,
1131 paced: bool,
1132) -> SendOutcome {
1133 let Some(msg) = msg else {
1134 return SendOutcome::NoChange;
1135 };
1136 let bytes = msg.len();
1137 if client.tx.try_send(msg).is_ok() {
1138 client.last_sent.insert(pid, current);
1139 record_send(client, bytes, now, paced);
1140 client.frames_sent = client.frames_sent.wrapping_add(1);
1141 SendOutcome::Sent
1142 } else {
1143 client.last_sent.insert(pid, current);
1149 SendOutcome::Backpressured
1150 }
1151}
1152
1153pub async fn run(config: Config) {
1154 let state: AppState = Arc::new((
1155 config,
1156 Mutex::new(Session::new()),
1157 Arc::new(std::sync::RwLock::new(HashMap::new())),
1158 Arc::new(Notify::new()),
1159 ));
1160
1161 let delivery_state = state.clone();
1162 tokio::spawn(async move {
1163 let mut next_deadline: Option<Instant> = None;
1164 loop {
1165 if let Some(deadline) = next_deadline {
1166 tokio::select! {
1167 _ = delivery_state.3.notified() => {}
1168 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
1169 }
1170 } else {
1171 delivery_state.3.notified().await;
1172 }
1173 loop {
1174 let outcome = tick(&delivery_state).await;
1175 next_deadline = outcome.next_deadline;
1176 if !outcome.did_work {
1177 break;
1178 }
1179 tokio::task::yield_now().await;
1180 }
1181 }
1182 });
1183
1184 tokio::spawn(async {
1185 loop {
1186 tokio::time::sleep(Duration::from_secs(5)).await;
1187 pty::reap_zombies();
1188 }
1189 });
1190
1191 #[cfg(unix)]
1192 if let Some(channel_fd) = state.0.fd_channel {
1193 ipc::run_fd_channel(channel_fd, state).await;
1194 return;
1195 }
1196
1197 #[cfg(unix)]
1198 let listener = {
1199 if let Some(l) = IpcListener::from_systemd_fd(state.0.verbose) {
1200 l
1201 } else {
1202 IpcListener::bind(&state.0.ipc_path, state.0.verbose)
1203 }
1204 };
1205 #[cfg(not(unix))]
1206 let mut listener = IpcListener::bind(&state.0.ipc_path, state.0.verbose);
1207
1208 loop {
1209 let stream = match listener.accept().await {
1210 Ok(s) => s,
1211 Err(e) => {
1212 eprintln!("accept error: {e}");
1213 tokio::time::sleep(Duration::from_millis(100)).await;
1214 continue;
1215 }
1216 };
1217 let state = state.clone();
1218 tokio::spawn(handle_client(stream, state));
1219 }
1220}
1221
1222async fn tick(state: &AppState) -> TickOutcome {
1223 let mut sess = state.1.lock().await;
1224 sess.tick_fires += 1;
1225 let mut did_work = false;
1226 let mut next_deadline: Option<Instant> = None;
1227 let now = Instant::now();
1228
1229 let max_fps = sess
1230 .clients
1231 .values()
1232 .map(browser_pacing_fps)
1233 .fold(1.0_f32, f32::max);
1234 let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
1235 let ids: Vec<u16> = sess.ptys.keys().copied().collect();
1236 for &id in &ids {
1237 let Some(pty) = sess.ptys.get_mut(&id) else {
1238 continue;
1239 };
1240 if pty.driver.take_title_dirty() {
1241 pty.mark_dirty();
1242 pty.title_pending = true;
1243 }
1244 if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
1245 let msg = {
1246 let title_bytes = pty.driver.title().as_bytes();
1247 let mut msg = Vec::with_capacity(3 + title_bytes.len());
1248 msg.push(S2C_TITLE);
1249 msg.extend_from_slice(&id.to_le_bytes());
1250 msg.extend_from_slice(title_bytes);
1251 msg
1252 };
1253 pty.last_title_send = now;
1254 pty.title_pending = false;
1255 sess.send_to_all(&msg);
1256 did_work = true;
1257 }
1258 }
1259
1260 let mut eof_ptys: Vec<u16> = Vec::with_capacity(ids.len());
1263 for &id in &ids {
1264 let Some(pty) = sess.ptys.get_mut(&id) else {
1265 continue;
1266 };
1267 while let Ok(input) = pty.byte_rx.try_recv() {
1268 match input {
1269 PtyInput::Data(data) => {
1270 pty::respond_to_queries(
1271 &pty.handle,
1272 &data,
1273 pty.driver.size(),
1274 pty.driver.cursor_position(),
1275 );
1276 pty.driver.process(&data);
1277 pty.mark_dirty();
1278 did_work = true;
1279 }
1280 PtyInput::SyncBoundary { before, after } => {
1281 if !before.is_empty() {
1282 pty::respond_to_queries(
1283 &pty.handle,
1284 &before,
1285 pty.driver.size(),
1286 pty.driver.cursor_position(),
1287 );
1288 pty.driver.process(&before);
1289 pty.mark_dirty();
1290 }
1291 if !pty.driver.synced_output() {
1292 let frame = take_snapshot(pty);
1293 enqueue_ready_frame(&mut pty.ready_frames, frame);
1294 pty.clear_dirty();
1295 }
1296 if !after.is_empty() {
1297 pty::respond_to_queries(
1298 &pty.handle,
1299 &after,
1300 pty.driver.size(),
1301 pty.driver.cursor_position(),
1302 );
1303 pty.driver.process(&after);
1304 pty.mark_dirty();
1305 }
1306 did_work = true;
1307 }
1308 PtyInput::Eof => {
1309 eof_ptys.push(id);
1310 }
1311 }
1312 }
1313 }
1314 drop(sess);
1316 for id in eof_ptys {
1317 tokio::time::sleep(Duration::from_millis(50)).await;
1318 cleanup_pty_internal(id, state).await;
1319 }
1320 let mut sess = state.1.lock().await;
1321
1322 let needful_ptys: HashSet<u16> = sess
1326 .clients
1327 .values()
1328 .flat_map(|c| {
1329 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1330 c.subscriptions.iter().copied().filter(move |pid| {
1331 let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
1332 if Some(*pid) == c.lead {
1333 !scrolled && can_send_frame(c, now, reserve_preview_slot)
1334 } else {
1335 !scrolled && can_send_preview(c, *pid, now)
1336 }
1337 })
1338 })
1339 .collect();
1340
1341 let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
1342 for &id in &ids {
1343 let Some(pty) = sess.ptys.get_mut(&id) else {
1344 continue;
1345 };
1346 if needful_ptys.contains(&id)
1347 && let Some(frame) = pty.ready_frames.pop_front()
1348 {
1349 snapshots.insert(id, frame);
1350 sess.tick_snaps += 1;
1351 did_work = true;
1352 continue;
1353 }
1354 if !should_snapshot_pty(
1355 pty.dirty,
1356 needful_ptys.contains(&id),
1357 pty.driver.synced_output(),
1358 ) {
1359 continue;
1360 }
1361 snapshots.insert(id, take_snapshot(pty));
1365 pty.clear_dirty();
1366 sess.tick_snaps += 1;
1367 did_work = true;
1368 }
1369
1370 let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
1371 for cid in client_ids {
1372 if let Some(c) = sess.clients.get_mut(&cid) {
1378 if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
1379 c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
1380 }
1381 if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
1384 c.browser_backlog_frames = 0;
1385 c.browser_ack_ahead_frames = 0;
1386 }
1387 }
1388 let (
1389 lead,
1390 subscriptions,
1391 scrolled_ptys,
1392 can_send_lead,
1393 lead_has_window,
1394 any_send_window,
1395 lead_deadline,
1396 ) = {
1397 let Some(c) = sess.clients.get(&cid) else {
1398 continue;
1399 };
1400 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1401 (
1402 c.lead,
1403 c.subscriptions.iter().copied().collect::<Vec<_>>(),
1404 c.scroll_offsets
1405 .iter()
1406 .map(|(&k, &v)| (k, v))
1407 .collect::<Vec<_>>(),
1408 can_send_frame(c, now, reserve_preview_slot),
1409 lead_window_open(c, reserve_preview_slot),
1410 lead_window_open(c, reserve_preview_slot) || window_open(c),
1411 c.next_send_at,
1412 )
1413 };
1414
1415 if subscriptions.is_empty() {
1416 continue;
1417 }
1418
1419 for &(scroll_pid, scroll_offset) in &scrolled_ptys {
1421 if scroll_offset == 0 {
1422 continue;
1423 }
1424 let is_lead = lead == Some(scroll_pid);
1425 let can_send = if is_lead { can_send_lead } else { true };
1426 if can_send {
1427 let prev_frame = {
1428 let Some(c) = sess.clients.get(&cid) else {
1429 continue;
1430 };
1431 c.scroll_caches
1432 .get(&scroll_pid)
1433 .cloned()
1434 .unwrap_or_default()
1435 };
1436 let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1437 if let Some((msg, new_frame)) =
1438 build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
1439 {
1440 let Some(c) = sess.clients.get_mut(&cid) else {
1441 break;
1442 };
1443 let bytes = msg.len();
1444 if c.tx.try_send(msg).is_ok() {
1445 c.scroll_caches.insert(scroll_pid, new_frame);
1446 record_send(c, bytes, now, is_lead);
1447 c.frames_sent += 1;
1448 SendOutcome::Sent
1449 } else {
1450 SendOutcome::Backpressured
1451 }
1452 } else {
1453 SendOutcome::NoChange
1454 }
1455 } else {
1456 SendOutcome::NoChange
1457 };
1458 match outcome {
1459 SendOutcome::Sent => did_work = true,
1460 SendOutcome::Backpressured => {
1461 if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1462 pty.mark_dirty();
1463 }
1464 }
1465 SendOutcome::NoChange => {}
1466 }
1467 } else if is_lead && lead_has_window {
1468 next_deadline = Some(match next_deadline {
1469 Some(existing) => existing.min(lead_deadline),
1470 None => lead_deadline,
1471 });
1472 }
1473 }
1474
1475 let lead_scroll_offset = lead
1476 .and_then(|pid| {
1477 scrolled_ptys
1478 .iter()
1479 .find(|&&(k, _)| k == pid)
1480 .map(|&(_, v)| v)
1481 })
1482 .unwrap_or(0);
1483
1484 if let Some(pid) = lead {
1485 if lead_scroll_offset == 0 && can_send_lead {
1486 if let Some(cur) = snapshots.get(&pid).cloned() {
1487 let previous = sess
1488 .clients
1489 .get(&cid)
1490 .and_then(|c| c.last_sent.get(&pid).cloned())
1491 .unwrap_or_default();
1492 drop(sess);
1493 let msg = build_update_msg(pid, &cur, &previous);
1494 sess = state.1.lock().await;
1495 let Some(c) = sess.clients.get_mut(&cid) else {
1496 continue;
1497 };
1498 match try_send_update(c, pid, cur, msg, now, true) {
1499 SendOutcome::Sent => did_work = true,
1500 SendOutcome::Backpressured => {
1501 if let Some(pty) = sess.ptys.get_mut(&pid) {
1502 pty.mark_dirty();
1503 }
1504 }
1505 SendOutcome::NoChange => {}
1506 }
1507 } else {
1508 let has_pending = sess
1509 .ptys
1510 .get(&pid)
1511 .map(pty_has_visual_update)
1512 .unwrap_or(false);
1513 let _ = has_pending;
1514 }
1515 } else {
1516 let has_pending = sess
1517 .ptys
1518 .get(&pid)
1519 .map(pty_has_visual_update)
1520 .unwrap_or(false);
1521 if has_pending && lead_has_window {
1522 next_deadline = Some(match next_deadline {
1523 Some(existing) => existing.min(lead_deadline),
1524 None => lead_deadline,
1525 });
1526 }
1527 }
1528 }
1529
1530 if !any_send_window {
1531 continue;
1532 }
1533
1534 let mut preview_ids = subscriptions;
1535 preview_ids.retain(|pid| Some(*pid) != lead);
1536 preview_ids.sort_unstable();
1537
1538 for pid in preview_ids {
1539 let (preview_can_send, preview_due_at, preview_has_window) =
1540 match sess.clients.get(&cid) {
1541 Some(c) => (
1542 can_send_preview(c, pid, now),
1543 preview_deadline(c, pid, now),
1544 window_open(c),
1545 ),
1546 None => (false, now, false),
1547 };
1548 if !preview_has_window {
1549 break;
1550 }
1551 if !preview_can_send {
1552 let has_pending = sess
1553 .ptys
1554 .get(&pid)
1555 .map(pty_has_visual_update)
1556 .unwrap_or(false);
1557 if has_pending && preview_due_at > now {
1562 next_deadline = Some(match next_deadline {
1563 Some(existing) => existing.min(preview_due_at),
1564 None => preview_due_at,
1565 });
1566 }
1567 continue;
1568 }
1569 let Some(cur) = snapshots.get(&pid) else {
1570 let has_pending = sess
1571 .ptys
1572 .get(&pid)
1573 .map(pty_has_visual_update)
1574 .unwrap_or(false);
1575 let _ = has_pending;
1576 continue;
1577 };
1578 let cur = cur.clone();
1579 let previous = sess
1580 .clients
1581 .get(&cid)
1582 .and_then(|c| c.last_sent.get(&pid).cloned())
1583 .unwrap_or_default();
1584 drop(sess);
1585 let msg = build_update_msg(pid, &cur, &previous);
1586 sess = state.1.lock().await;
1587 let Some(c) = sess.clients.get_mut(&cid) else {
1588 break;
1589 };
1590 match try_send_update(c, pid, cur, msg, now, false) {
1591 SendOutcome::Sent => {
1592 record_preview_send(c, pid, now);
1593 did_work = true;
1594 }
1595 SendOutcome::Backpressured => {
1596 if let Some(pty) = sess.ptys.get_mut(&pid) {
1597 pty.mark_dirty();
1598 }
1599 break;
1600 }
1601 SendOutcome::NoChange => {}
1602 }
1603 }
1604 }
1605
1606 TickOutcome {
1607 did_work,
1608 next_deadline,
1609 }
1610}
1611
1612async fn handle_client<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
1613 stream: S,
1614 state: AppState,
1615) {
1616 let config = &state.0;
1617 let (mut reader, mut writer) = tokio::io::split(stream);
1618
1619 let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(OUTBOX_CAPACITY);
1620 let sender = tokio::spawn(async move {
1621 while let Some(msg) = out_rx.recv().await {
1622 if !write_frame(&mut writer, &msg).await {
1623 break;
1624 }
1625 }
1626 });
1627 let client_id;
1628
1629 {
1630 let mut sess = state.1.lock().await;
1631 client_id = sess.next_client_id;
1632 sess.next_client_id += 1;
1633 sess.clients.insert(
1634 client_id,
1635 ClientState {
1636 tx: out_tx,
1637 lead: None,
1638 subscriptions: HashSet::new(),
1639 view_sizes: HashMap::new(),
1640 scroll_offsets: HashMap::new(),
1641 scroll_caches: HashMap::new(),
1642 last_sent: HashMap::new(),
1643 preview_next_send_at: HashMap::new(),
1644 rtt_ms: 50.0,
1645 min_rtt_ms: 0.0,
1646 display_fps: 60.0,
1647 delivery_bps: 262_144.0,
1652 goodput_bps: 262_144.0,
1653 goodput_jitter_bps: 0.0,
1654 max_goodput_jitter_bps: 0.0,
1655 last_goodput_sample_bps: 0.0,
1656 avg_frame_bytes: 1_024.0,
1657 avg_paced_frame_bytes: 1_024.0,
1658 avg_preview_frame_bytes: 1_024.0,
1659 inflight_bytes: 0,
1660 inflight_frames: VecDeque::new(),
1661 next_send_at: Instant::now(),
1662 probe_frames: 0.0,
1663 frames_sent: 0,
1664 acks_recv: 0,
1665 acked_bytes_since_log: 0,
1666 browser_backlog_frames: 0,
1667 browser_ack_ahead_frames: 0,
1668 browser_apply_ms: 0.0,
1669 last_metrics_update: Instant::now(),
1670 last_log: Instant::now(),
1671 goodput_window_bytes: 0,
1672 goodput_window_start: Instant::now(),
1673 },
1674 );
1675 if let Some(c) = sess.clients.get(&client_id) {
1676 let _ = c.tx.try_send(msg_hello(
1677 1,
1678 FEATURE_CREATE_NONCE | FEATURE_RESTART | FEATURE_RESIZE_BATCH | FEATURE_COPY_RANGE,
1679 ));
1680 }
1681 let mut initial_msgs = Vec::with_capacity(2 + sess.ptys.len() * 2);
1682 initial_msgs.push(sess.pty_list_msg());
1683 for (&id, pty) in &sess.ptys {
1684 let title = pty.driver.title();
1685 if !title.is_empty() {
1686 let title_bytes = title.as_bytes();
1687 let mut msg = Vec::with_capacity(3 + title_bytes.len());
1688 msg.push(S2C_TITLE);
1689 msg.extend_from_slice(&id.to_le_bytes());
1690 msg.extend_from_slice(title_bytes);
1691 initial_msgs.push(msg);
1692 }
1693 if pty.exited {
1694 initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
1695 }
1696 }
1697 initial_msgs.push(vec![S2C_READY]);
1698 let tx = sess.clients.get(&client_id).map(|c| c.tx.clone());
1699 drop(sess);
1700 if let Some(tx) = tx {
1701 for msg in initial_msgs {
1702 if tx.send(msg).await.is_err() {
1703 break;
1704 }
1705 }
1706 }
1707 }
1708
1709 if state.0.verbose {
1710 eprintln!("client connected");
1711 }
1712
1713 while let Some(data) = read_frame(&mut reader).await {
1714 if data.is_empty() {
1715 continue;
1716 }
1717
1718 if data[0] == C2S_ACK {
1719 let mut sess = state.1.lock().await;
1720 let (
1721 do_log,
1722 frames_sent,
1723 acks_recv,
1724 rtt_ms,
1725 min_rtt_ms,
1726 eff_rtt_ms,
1727 inflight_bytes,
1728 delivery_bps,
1729 goodput_ewma_bps,
1730 goodput_jitter_bps,
1731 max_goodput_jitter_bps,
1732 avg_frame_bytes,
1733 avg_paced_frame_bytes,
1734 avg_preview_frame_bytes,
1735 display_fps,
1736 paced_fps,
1737 display_need_bps,
1738 probe_frames,
1739 goodput_bps,
1740 window_frames,
1741 window_bytes,
1742 outbox_frames,
1743 browser_backlog_frames,
1744 browser_ack_ahead_frames,
1745 browser_apply_ms,
1746 ) = {
1747 let Some(c) = sess.clients.get_mut(&client_id) else {
1748 continue;
1749 };
1750 c.acks_recv += 1;
1751 record_ack(c);
1752 let do_log = c.last_log.elapsed().as_secs_f32() >= 1.0;
1753 let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
1754 let paced_fps = pacing_fps(c);
1755 let display_need_bps = display_need_bps(c);
1756 let out = (
1757 do_log,
1758 c.frames_sent,
1759 c.acks_recv,
1760 c.rtt_ms,
1761 path_rtt_ms(c),
1762 window_rtt_ms(c),
1763 c.inflight_bytes,
1764 c.delivery_bps,
1765 c.goodput_bps,
1766 c.goodput_jitter_bps,
1767 c.max_goodput_jitter_bps,
1768 c.avg_frame_bytes,
1769 c.avg_paced_frame_bytes,
1770 c.avg_preview_frame_bytes,
1771 c.display_fps,
1772 paced_fps,
1773 display_need_bps,
1774 c.probe_frames,
1775 c.acked_bytes_since_log as f32 / log_elapsed,
1776 target_frame_window(c),
1777 target_byte_window(c),
1778 outbox_queued_frames(c),
1779 c.browser_backlog_frames,
1780 c.browser_ack_ahead_frames,
1781 c.browser_apply_ms,
1782 );
1783 if do_log {
1784 c.frames_sent = 0;
1785 c.acks_recv = 0;
1786 c.acked_bytes_since_log = 0;
1787 c.last_log = Instant::now();
1788 }
1789 out
1790 };
1791 if do_log && config.verbose {
1792 eprintln!(
1793 "client {client_id}: sent={frames_sent} acks={acks_recv} rtt={rtt_ms:.0}ms min_rtt={min_rtt_ms:.0}ms eff_rtt={eff_rtt_ms:.0}ms window={window_frames}f/{window_bytes}B probe={probe_frames:.0}f inflight={inflight_bytes}B outbox={outbox_frames}f goodput={goodput_bps:.0}B/s goodput_ewma={goodput_ewma_bps:.0}B/s jitter={goodput_jitter_bps:.0}/{max_goodput_jitter_bps:.0}B/s rate={delivery_bps:.0}B/s avg_frame={avg_frame_bytes:.0}B lead_frame={avg_paced_frame_bytes:.0}B preview_frame={avg_preview_frame_bytes:.0}B need={display_need_bps:.0}B/s display_fps={display_fps:.0} paced_fps={paced_fps:.0} backlog={browser_backlog_frames} ack_ahead={browser_ack_ahead_frames} apply={browser_apply_ms:.1}ms | tick_fires={} tick_snaps={}",
1794 sess.tick_fires, sess.tick_snaps,
1795 );
1796 }
1797 if do_log {
1798 sess.tick_fires = 0;
1799 sess.tick_snaps = 0;
1800 }
1801 nudge_delivery(&state);
1802 continue;
1803 }
1804
1805 if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
1806 let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
1807 if fps > 0.0 {
1808 let mut sess = state.1.lock().await;
1809 if let Some(c) = sess.clients.get_mut(&client_id) {
1810 c.display_fps = fps;
1811 }
1812 }
1813 nudge_delivery(&state);
1814 continue;
1815 }
1816
1817 if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
1818 let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
1819 let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
1820 let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
1821 let mut sess = state.1.lock().await;
1822 if let Some(c) = sess.clients.get_mut(&client_id) {
1823 c.browser_backlog_frames = backlog_frames;
1824 c.browser_ack_ahead_frames = ack_ahead_frames;
1825 c.browser_apply_ms = apply_ms;
1826 c.last_metrics_update = Instant::now();
1827 }
1828 nudge_delivery(&state);
1829 continue;
1830 }
1831
1832 if data[0] == C2S_MOUSE && data.len() >= 9 {
1835 let pid = u16::from_le_bytes([data[1], data[2]]);
1836 let type_ = data[3];
1837 let button = data[4];
1838 let col = u16::from_le_bytes([data[5], data[6]]);
1839 let row = u16::from_le_bytes([data[7], data[8]]);
1840 let sess = state.1.lock().await;
1841 if let Some(pty) = sess.ptys.get(&pid) {
1842 let (echo, icanon) = pty.lflag_cache;
1843 if let Some(seq) = pty
1844 .driver
1845 .mouse_event(type_, button, col, row, echo, icanon)
1846 && let Some(&fd) = state.2.read().unwrap().get(&pid)
1847 {
1848 pty::pty_write_all(fd, &seq);
1849 }
1850 }
1851 continue;
1852 }
1853
1854 if data[0] == C2S_INPUT && data.len() >= 3 {
1855 let pid = u16::from_le_bytes([data[1], data[2]]);
1856 let mut need_nudge = false;
1857 {
1858 let mut sess = state.1.lock().await;
1859 if let Some(c) = sess.clients.get_mut(&client_id)
1860 && update_client_scroll_state(c, pid, 0)
1861 && let Some(pty) = sess.ptys.get_mut(&pid)
1862 {
1863 pty.mark_dirty();
1864 need_nudge = true;
1865 }
1866 }
1867 if need_nudge {
1868 nudge_delivery(&state);
1869 }
1870 if let Some(&fd) = state.2.read().unwrap().get(&pid) {
1871 pty::pty_write_all(fd, &data[3..]);
1872 }
1873 continue;
1874 }
1875
1876 if data[0] == C2S_SEARCH && data.len() >= 3 {
1877 let request_id = u16::from_le_bytes([data[1], data[2]]);
1878 let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
1879 let mut sess = state.1.lock().await;
1880 let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
1881 let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
1882 Vec::new()
1883 } else {
1884 sess.ptys
1885 .iter()
1886 .filter_map(|(&pty_id, pty)| {
1887 pty.driver
1888 .search_result(query)
1889 .map(|result| SearchResultRow {
1890 pty_id,
1891 score: result.score,
1892 primary_source: result.primary_source,
1893 matched_sources: result.matched_sources,
1894 context: result.context,
1895 scroll_offset: result.scroll_offset,
1896 })
1897 })
1898 .collect()
1899 };
1900 ranked.sort_by(|a, b| {
1901 b.score
1902 .cmp(&a.score)
1903 .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
1904 .then_with(|| a.pty_id.cmp(&b.pty_id))
1905 });
1906 if let Some(client) = sess.clients.get_mut(&client_id) {
1907 let _ = client
1908 .tx
1909 .try_send(build_search_results_msg(request_id, &ranked));
1910 }
1911 continue;
1912 }
1913
1914 let mut sess = state.1.lock().await;
1915 let mut need_nudge = false;
1916 match data[0] {
1917 C2S_SCROLL if data.len() >= 7 => {
1918 let pid = u16::from_le_bytes([data[1], data[2]]);
1919 let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
1920 if sess.ptys.contains_key(&pid) {
1921 if let Some(c) = sess.clients.get_mut(&client_id) {
1922 update_client_scroll_state(c, pid, offset);
1923 }
1924 if let Some(pty) = sess.ptys.get_mut(&pid) {
1925 pty.mark_dirty();
1926 need_nudge = true;
1927 }
1928 }
1929 }
1930 C2S_RESIZE if data.len() >= 7 => {
1931 let entries = data[1..].chunks_exact(6);
1932 if !entries.remainder().is_empty() {
1933 continue;
1934 }
1935 let mut touched = Vec::with_capacity((data.len() - 1) / 6);
1936 for entry in entries {
1937 let pid = u16::from_le_bytes([entry[0], entry[1]]);
1938 if !sess.ptys.contains_key(&pid) {
1939 continue;
1940 }
1941 let rows = u16::from_le_bytes([entry[2], entry[3]]);
1942 let cols = u16::from_le_bytes([entry[4], entry[5]]);
1943 if let Some(c) = sess.clients.get_mut(&client_id) {
1944 if is_unset_view_size(rows, cols) {
1945 if c.view_sizes.remove(&pid).is_some() {
1946 touched.push(pid);
1947 }
1948 } else if rows == 0 || cols == 0 {
1949 continue;
1950 } else {
1951 c.view_sizes.insert(pid, (rows, cols));
1952 touched.push(pid);
1953 }
1954 }
1955 }
1956 if sess.resize_ptys_to_mediated_sizes(touched) {
1957 need_nudge = true;
1958 }
1959 }
1960 C2S_CREATE => {
1961 let (rows, cols) = if data.len() >= 5 {
1963 (
1964 u16::from_le_bytes([data[1], data[2]]),
1965 u16::from_le_bytes([data[3], data[4]]),
1966 )
1967 } else {
1968 (24, 80)
1969 };
1970 let tag_len = if data.len() >= 7 {
1971 u16::from_le_bytes([data[5], data[6]]) as usize
1972 } else {
1973 0
1974 };
1975 let tag = if data.len() >= 7 + tag_len {
1976 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
1977 } else {
1978 ""
1979 };
1980 let cmd_start = 7 + tag_len;
1981 let dir: Option<String> = None;
1982 let create_payload = data
1983 .get(cmd_start..)
1984 .and_then(|bytes| std::str::from_utf8(bytes).ok());
1985 let command = create_payload
1986 .filter(|payload| !payload.contains('\0'))
1987 .map(str::trim)
1988 .filter(|payload| !payload.is_empty());
1989 let argv: Option<Vec<&str>> = create_payload
1990 .filter(|payload| payload.contains('\0'))
1991 .map(|payload| {
1992 payload
1993 .split('\0')
1994 .filter(|arg| !arg.is_empty())
1995 .collect::<Vec<_>>()
1996 })
1997 .filter(|args| !args.is_empty());
1998 let Some(id) = sess.allocate_pty_id() else {
1999 continue;
2000 };
2001 if let Some(pty) = pty::spawn_pty(
2002 &config.shell,
2003 &config.shell_flags,
2004 rows,
2005 cols,
2006 id,
2007 tag,
2008 command,
2009 argv.as_deref(),
2010 dir.as_deref(),
2011 config.scrollback,
2012 state.clone(),
2013 ) {
2014 let mut msg = Vec::with_capacity(3 + pty.tag.len());
2015 msg.push(S2C_CREATED);
2016 msg.extend_from_slice(&id.to_le_bytes());
2017 msg.extend_from_slice(pty.tag.as_bytes());
2018 sess.ptys.insert(id, pty);
2019 if let Some(c) = sess.clients.get_mut(&client_id) {
2020 c.lead = Some(id);
2021 c.view_sizes.insert(id, (rows, cols));
2022 subscribe_client_to(c, id);
2023 reset_inflight(c);
2025 }
2026 sess.send_to_all(&msg);
2027 need_nudge = true;
2028 }
2029 }
2030 C2S_CREATE_N => {
2031 let nonce = if data.len() >= 3 {
2033 u16::from_le_bytes([data[1], data[2]])
2034 } else {
2035 0
2036 };
2037 let (rows, cols) = if data.len() >= 7 {
2038 (
2039 u16::from_le_bytes([data[3], data[4]]),
2040 u16::from_le_bytes([data[5], data[6]]),
2041 )
2042 } else {
2043 (24, 80)
2044 };
2045 let tag_len = if data.len() >= 9 {
2046 u16::from_le_bytes([data[7], data[8]]) as usize
2047 } else {
2048 0
2049 };
2050 let tag = if data.len() >= 9 + tag_len {
2051 std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
2052 } else {
2053 ""
2054 };
2055 let cmd_start = 9 + tag_len;
2056 let dir: Option<String> = None;
2057 let create_payload = data
2058 .get(cmd_start..)
2059 .and_then(|bytes| std::str::from_utf8(bytes).ok());
2060 let command = create_payload
2061 .filter(|payload| !payload.contains('\0'))
2062 .map(str::trim)
2063 .filter(|payload| !payload.is_empty());
2064 let argv: Option<Vec<&str>> = create_payload
2065 .filter(|payload| payload.contains('\0'))
2066 .map(|payload| {
2067 payload
2068 .split('\0')
2069 .filter(|arg| !arg.is_empty())
2070 .collect::<Vec<_>>()
2071 })
2072 .filter(|args| !args.is_empty());
2073 let Some(id) = sess.allocate_pty_id() else {
2074 continue;
2075 };
2076 if let Some(pty) = pty::spawn_pty(
2077 &config.shell,
2078 &config.shell_flags,
2079 rows,
2080 cols,
2081 id,
2082 tag,
2083 command,
2084 argv.as_deref(),
2085 dir.as_deref(),
2086 config.scrollback,
2087 state.clone(),
2088 ) {
2089 let tag_bytes = pty.tag.as_bytes();
2090 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2091 nonce_msg.push(S2C_CREATED_N);
2092 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2093 nonce_msg.extend_from_slice(&id.to_le_bytes());
2094 nonce_msg.extend_from_slice(tag_bytes);
2095 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2096 broadcast_msg.push(S2C_CREATED);
2097 broadcast_msg.extend_from_slice(&id.to_le_bytes());
2098 broadcast_msg.extend_from_slice(tag_bytes);
2099 sess.ptys.insert(id, pty);
2100 if let Some(c) = sess.clients.get_mut(&client_id) {
2101 c.lead = Some(id);
2102 c.view_sizes.insert(id, (rows, cols));
2103 subscribe_client_to(c, id);
2104 reset_inflight(c);
2106 let _ = c.tx.try_send(nonce_msg);
2107 }
2108 for (&cid, c) in sess.clients.iter() {
2109 if cid != client_id {
2110 let _ = c.tx.try_send(broadcast_msg.clone());
2111 }
2112 }
2113 need_nudge = true;
2114 }
2115 }
2116 C2S_CREATE_AT => {
2117 let (rows, cols) = if data.len() >= 5 {
2119 (
2120 u16::from_le_bytes([data[1], data[2]]),
2121 u16::from_le_bytes([data[3], data[4]]),
2122 )
2123 } else {
2124 (24, 80)
2125 };
2126 let tag_len = if data.len() >= 7 {
2127 u16::from_le_bytes([data[5], data[6]]) as usize
2128 } else {
2129 0
2130 };
2131 let tag = if data.len() >= 7 + tag_len {
2132 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
2133 } else {
2134 ""
2135 };
2136 let src_start = 7 + tag_len;
2137 let dir = if data.len() >= src_start + 2 {
2138 let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
2139 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
2140 } else {
2141 None
2142 };
2143 let Some(id) = sess.allocate_pty_id() else {
2144 continue;
2145 };
2146 if let Some(pty) = pty::spawn_pty(
2147 &config.shell,
2148 &config.shell_flags,
2149 rows,
2150 cols,
2151 id,
2152 tag,
2153 None,
2154 None,
2155 dir.as_deref(),
2156 config.scrollback,
2157 state.clone(),
2158 ) {
2159 let mut msg = Vec::with_capacity(3 + pty.tag.len());
2160 msg.push(S2C_CREATED);
2161 msg.extend_from_slice(&id.to_le_bytes());
2162 msg.extend_from_slice(pty.tag.as_bytes());
2163 sess.ptys.insert(id, pty);
2164 if let Some(c) = sess.clients.get_mut(&client_id) {
2165 c.lead = Some(id);
2166 c.view_sizes.insert(id, (rows, cols));
2167 subscribe_client_to(c, id);
2168 reset_inflight(c);
2170 }
2171 sess.send_to_all(&msg);
2172 need_nudge = true;
2173 }
2174 }
2175 C2S_CREATE2 => {
2176 if data.len() < 10 {
2178 continue;
2179 }
2180 let nonce = u16::from_le_bytes([data[1], data[2]]);
2181 let rows = u16::from_le_bytes([data[3], data[4]]);
2182 let cols = u16::from_le_bytes([data[5], data[6]]);
2183 let features = data[7];
2184 let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
2185 let tag = if data.len() >= 10 + tag_len {
2186 std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
2187 } else {
2188 ""
2189 };
2190 let mut cursor = 10 + tag_len;
2191 let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
2192 let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
2193 cursor += 2;
2194 sess.ptys.get(&src_id).and_then(|p| pty::pty_cwd(&p.handle))
2195 } else {
2196 None
2197 };
2198 let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
2199 data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
2200 } else {
2201 None
2202 };
2203 let command = create_payload
2204 .filter(|p| !p.contains('\0'))
2205 .map(str::trim)
2206 .filter(|p| !p.is_empty());
2207 let argv: Option<Vec<&str>> = create_payload
2208 .filter(|p| p.contains('\0'))
2209 .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
2210 .filter(|a| !a.is_empty());
2211 let Some(id) = sess.allocate_pty_id() else {
2212 continue;
2213 };
2214 if let Some(pty) = pty::spawn_pty(
2215 &config.shell,
2216 &config.shell_flags,
2217 rows,
2218 cols,
2219 id,
2220 tag,
2221 command,
2222 argv.as_deref(),
2223 dir.as_deref(),
2224 config.scrollback,
2225 state.clone(),
2226 ) {
2227 let tag_bytes = pty.tag.as_bytes();
2228 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2229 nonce_msg.push(S2C_CREATED_N);
2230 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2231 nonce_msg.extend_from_slice(&id.to_le_bytes());
2232 nonce_msg.extend_from_slice(tag_bytes);
2233 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2234 broadcast_msg.push(S2C_CREATED);
2235 broadcast_msg.extend_from_slice(&id.to_le_bytes());
2236 broadcast_msg.extend_from_slice(tag_bytes);
2237 sess.ptys.insert(id, pty);
2238 if let Some(c) = sess.clients.get_mut(&client_id) {
2239 c.lead = Some(id);
2240 c.view_sizes.insert(id, (rows, cols));
2241 subscribe_client_to(c, id);
2242 reset_inflight(c);
2244 let _ = c.tx.try_send(nonce_msg);
2245 }
2246 for (&cid, c) in sess.clients.iter() {
2247 if cid != client_id {
2248 let _ = c.tx.try_send(broadcast_msg.clone());
2249 }
2250 }
2251 need_nudge = true;
2252 }
2253 }
2254 C2S_FOCUS if data.len() >= 3 => {
2255 let pid = u16::from_le_bytes([data[1], data[2]]);
2256 if sess.ptys.contains_key(&pid) {
2257 let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
2258 if let Some(c) = sess.clients.get_mut(&client_id) {
2259 c.lead = Some(pid);
2260 subscribe_client_to(c, pid);
2261 if old_pid == Some(pid) {
2262 update_client_scroll_state(c, pid, 0);
2263 } else {
2264 reset_inflight(c);
2265 }
2266 }
2267 if let Some(pty) = sess.ptys.get_mut(&pid) {
2268 pty.mark_dirty();
2269 need_nudge = true;
2270 }
2271 }
2272 }
2273 C2S_SUBSCRIBE if data.len() >= 3 => {
2274 let pid = u16::from_le_bytes([data[1], data[2]]);
2275 if sess.ptys.contains_key(&pid) {
2276 if let Some(c) = sess.clients.get_mut(&client_id) {
2277 subscribe_client_to(c, pid);
2278 }
2279 if let Some(pty) = sess.ptys.get_mut(&pid) {
2280 pty.mark_dirty();
2281 }
2282 need_nudge = true;
2283 }
2284 }
2285 C2S_UNSUBSCRIBE if data.len() >= 3 => {
2286 let pid = u16::from_le_bytes([data[1], data[2]]);
2287 if sess.ptys.contains_key(&pid) {
2288 let mut touched = Vec::new();
2289 if let Some(c) = sess.clients.get_mut(&client_id) {
2290 if unsubscribe_client_from(c, pid) {
2291 touched.push(pid);
2292 }
2293 reset_inflight(c);
2294 }
2295 if sess.resize_ptys_to_mediated_sizes(touched) {
2296 need_nudge = true;
2297 }
2298 }
2299 }
2300 C2S_RESTART if data.len() >= 3 => {
2301 let pid = u16::from_le_bytes([data[1], data[2]]);
2302 let restart_info = sess
2303 .ptys
2304 .get(&pid)
2305 .filter(|p| p.exited)
2306 .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
2307 if let Some(((rows, cols), command, tag)) = restart_info
2308 && let Some((new_handle, reader, byte_rx)) = pty::respawn_child(
2309 &state.0.shell,
2310 &state.0.shell_flags,
2311 rows,
2312 cols,
2313 pid,
2314 command.as_deref(),
2315 state.clone(),
2316 )
2317 {
2318 let Some(pty) = sess.ptys.get_mut(&pid) else {
2319 break;
2320 };
2321 pty.handle = new_handle;
2322 pty.reader_handle = reader;
2323 pty.byte_rx = byte_rx;
2324 pty.driver.reset_modes();
2325 pty.exited = false;
2326 pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
2327 pty.lflag_cache = pty::pty_lflag(&pty.handle);
2328 pty.lflag_last = Instant::now();
2329 pty.mark_dirty();
2330 if let Some(c) = sess.clients.get_mut(&client_id) {
2331 c.lead = Some(pid);
2332 subscribe_client_to(c, pid);
2333 update_client_scroll_state(c, pid, 0);
2334 reset_inflight(c);
2335 }
2336 let mut msg = Vec::with_capacity(3 + tag.len());
2337 msg.push(S2C_CREATED);
2338 msg.extend_from_slice(&pid.to_le_bytes());
2339 msg.extend_from_slice(tag.as_bytes());
2340 sess.send_to_all(&msg);
2341 need_nudge = true;
2342 }
2343 }
2344 C2S_READ if data.len() >= 13 => {
2345 let nonce = u16::from_le_bytes([data[1], data[2]]);
2346 let pid = u16::from_le_bytes([data[3], data[4]]);
2347 let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
2348 let req_limit =
2349 u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
2350 let flags = data.get(13).copied().unwrap_or(0);
2351 let ansi = flags & READ_ANSI != 0;
2352 let tail = flags & READ_TAIL != 0;
2353
2354 if let Some(pty) = sess.ptys.get_mut(&pid) {
2355 let (rows, _cols) = pty.driver.size();
2356 let viewport = take_snapshot(pty);
2357 let scrollback_lines = viewport.scrollback_lines() as usize;
2358 let total_lines = scrollback_lines + rows as usize;
2359
2360 let extract = |f: &FrameState| -> String {
2361 if ansi {
2362 f.get_ansi_text()
2363 } else {
2364 f.get_all_text()
2365 }
2366 };
2367
2368 let mut all_lines: Vec<String> =
2369 Vec::with_capacity(scrollback_lines + rows as usize);
2370
2371 let mut scroll_offset = scrollback_lines;
2372 while scroll_offset > 0 {
2373 let frame = pty.driver.scrollback_frame(scroll_offset);
2374 let page = extract(&frame);
2375 let page_lines: Vec<&str> = page.lines().collect();
2376 let take = if scroll_offset < rows as usize {
2377 scroll_offset.min(page_lines.len())
2378 } else {
2379 page_lines.len()
2380 };
2381 for line in &page_lines[..take] {
2382 all_lines.push(line.to_string());
2383 }
2384 if scroll_offset <= rows as usize {
2385 break;
2386 }
2387 scroll_offset = scroll_offset.saturating_sub(rows as usize);
2388 }
2389
2390 for line in extract(&viewport).lines() {
2391 all_lines.push(line.to_string());
2392 }
2393
2394 let (start, end) = if tail {
2395 let end = all_lines.len().saturating_sub(req_offset);
2396 let start = if req_limit == 0 {
2397 0
2398 } else {
2399 end.saturating_sub(req_limit)
2400 };
2401 (start, end)
2402 } else {
2403 let start = req_offset.min(all_lines.len());
2404 let end = if req_limit == 0 {
2405 all_lines.len()
2406 } else {
2407 (start + req_limit).min(all_lines.len())
2408 };
2409 (start, end)
2410 };
2411 let text = all_lines[start..end].join("\n");
2412
2413 let mut msg = Vec::with_capacity(13 + text.len());
2414 msg.push(S2C_TEXT);
2415 msg.extend_from_slice(&nonce.to_le_bytes());
2416 msg.extend_from_slice(&pid.to_le_bytes());
2417 msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
2418 msg.extend_from_slice(&(start as u32).to_le_bytes());
2419 msg.extend_from_slice(text.as_bytes());
2420 if let Some(client) = sess.clients.get(&client_id) {
2421 let _ = client.tx.try_send(msg);
2422 }
2423 }
2424 }
2425 C2S_COPY_RANGE if data.len() >= 18 => {
2426 let nonce = u16::from_le_bytes([data[1], data[2]]);
2427 let pid = u16::from_le_bytes([data[3], data[4]]);
2428 let start_tail = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
2429 let start_col = u16::from_le_bytes([data[9], data[10]]);
2430 let end_tail = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
2431 let end_col = u16::from_le_bytes([data[15], data[16]]);
2432
2433 if let Some(pty) = sess.ptys.get(&pid) {
2434 let text = pty
2435 .driver
2436 .get_text_range(start_tail, start_col, end_tail, end_col);
2437 let total_lines = pty.driver.total_lines();
2438
2439 let mut msg = Vec::with_capacity(13 + text.len());
2440 msg.push(S2C_TEXT);
2441 msg.extend_from_slice(&nonce.to_le_bytes());
2442 msg.extend_from_slice(&pid.to_le_bytes());
2443 msg.extend_from_slice(&total_lines.to_le_bytes());
2444 msg.extend_from_slice(&start_tail.to_le_bytes());
2445 msg.extend_from_slice(text.as_bytes());
2446 if let Some(client) = sess.clients.get(&client_id) {
2447 let _ = client.tx.try_send(msg);
2448 }
2449 }
2450 }
2451 C2S_KILL if data.len() >= 7 => {
2452 let pid = u16::from_le_bytes([data[1], data[2]]);
2453 let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
2454 if let Some(pty) = sess.ptys.get(&pid)
2455 && !pty.exited
2456 {
2457 pty::kill_pty(&pty.handle, signal);
2458 }
2459 }
2460 C2S_CLOSE if data.len() >= 3 => {
2461 let pid = u16::from_le_bytes([data[1], data[2]]);
2462 if let Some(pty) = sess.ptys.remove(&pid) {
2463 if !pty.exited {
2464 state.2.write().unwrap().remove(&pid);
2465 drop(pty.reader_handle);
2466 pty::close_pty(&pty.handle);
2467 }
2468 for client in sess.clients.values_mut() {
2469 unsubscribe_client_from(client, pid);
2470 }
2471 let mut msg = vec![S2C_CLOSED];
2472 msg.extend_from_slice(&pid.to_le_bytes());
2473 sess.send_to_all(&msg);
2474 }
2475 }
2476 _ => {}
2477 }
2478 drop(sess);
2479 if need_nudge {
2480 nudge_delivery(&state);
2481 }
2482 }
2483
2484 {
2485 let mut sess = state.1.lock().await;
2486 let mut need_nudge = false;
2487 let affected_ptys = sess
2488 .clients
2489 .remove(&client_id)
2490 .map(|client| client.view_sizes.keys().copied().collect::<Vec<_>>())
2491 .unwrap_or_default();
2492 if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
2493 need_nudge = true;
2494 }
2495 drop(sess);
2496 if need_nudge {
2497 nudge_delivery(&state);
2498 }
2499 }
2500 sender.abort();
2501 if state.0.verbose {
2502 eprintln!("client disconnected");
2503 }
2504}
2505
2506#[cfg(test)]
2507mod tests {
2508 use super::*;
2509
2510 fn test_client_with_capacity(capacity: usize) -> (ClientState, mpsc::Receiver<Vec<u8>>) {
2511 let (tx, rx) = mpsc::channel(capacity);
2512 let client = ClientState {
2513 tx,
2514 lead: None,
2515 subscriptions: HashSet::new(),
2516 view_sizes: HashMap::new(),
2517 scroll_offsets: HashMap::new(),
2518 scroll_caches: HashMap::new(),
2519 last_sent: HashMap::new(),
2520 preview_next_send_at: HashMap::new(),
2521 rtt_ms: 50.0,
2522 min_rtt_ms: 50.0,
2523 display_fps: 60.0,
2524 delivery_bps: 262_144.0,
2525 goodput_bps: 262_144.0,
2526 goodput_jitter_bps: 0.0,
2527 max_goodput_jitter_bps: 0.0,
2528 last_goodput_sample_bps: 0.0,
2529 avg_frame_bytes: 1_024.0,
2530 avg_paced_frame_bytes: 1_024.0,
2531 avg_preview_frame_bytes: 1_024.0,
2532 inflight_bytes: 0,
2533 inflight_frames: VecDeque::new(),
2534 next_send_at: Instant::now(),
2535 probe_frames: 0.0,
2536 frames_sent: 0,
2537 acks_recv: 0,
2538 acked_bytes_since_log: 0,
2539 browser_backlog_frames: 0,
2540 browser_ack_ahead_frames: 0,
2541 browser_apply_ms: 0.0,
2542 last_metrics_update: Instant::now(),
2543 last_log: Instant::now(),
2544 goodput_window_bytes: 0,
2545 goodput_window_start: Instant::now(),
2546 };
2547 (client, rx)
2548 }
2549
2550 fn test_client() -> ClientState {
2551 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
2552 client
2553 }
2554
2555 fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
2556 let now = Instant::now();
2557 client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
2558 client.inflight_frames = (0..frames)
2559 .map(|_| InFlightFrame {
2560 sent_at: now,
2561 bytes: bytes_per_frame,
2562 paced: true,
2563 })
2564 .collect();
2565 }
2566
2567 fn sample_frame(text: &str) -> FrameState {
2568 let mut frame = FrameState::new(2, 8);
2569 frame.write_text(0, 0, text, blit_remote::CellStyle::default());
2570 frame
2571 }
2572
2573 #[test]
2574 fn unset_view_size_accepts_zero_pair_only() {
2575 assert!(is_unset_view_size(0, 0));
2576 assert!(!is_unset_view_size(0, 80));
2577 assert!(!is_unset_view_size(u16::MAX, u16::MAX));
2578 }
2579
2580 #[test]
2581 fn unsubscribe_client_from_clears_view_size() {
2582 let mut client = test_client();
2583 client.subscriptions.insert(7);
2584 client.view_sizes.insert(7, (24, 80));
2585 assert!(unsubscribe_client_from(&mut client, 7));
2586 assert!(!client.subscriptions.contains(&7));
2587 assert!(!client.view_sizes.contains_key(&7));
2588 }
2589
2590 #[test]
2591 fn mediated_size_uses_per_pty_view_sizes_without_lead() {
2592 let mut session = Session::new();
2593 let mut c1 = test_client();
2594 let mut c2 = test_client();
2595 c1.view_sizes.insert(7, (30, 120));
2596 c2.view_sizes.insert(7, (24, 100));
2597 session.clients.insert(1, c1);
2598 session.clients.insert(2, c2);
2599 assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
2600 }
2601
2602 #[test]
2603 fn due_preview_reserves_the_last_lead_slot() {
2604 let mut client = test_client();
2605 client.lead = Some(1);
2606 client.subscriptions.insert(1);
2607 client.subscriptions.insert(2);
2608
2609 let target_frames = target_frame_window(&client);
2610 let lead_limit = target_frames.saturating_sub(1).max(1);
2611 fill_inflight(&mut client, lead_limit, 512);
2612
2613 assert!(window_open(&client));
2614 assert!(lead_window_open(&client, false));
2615 assert!(!lead_window_open(&client, true));
2616 assert!(can_send_preview(&client, 2, Instant::now()));
2617 }
2618
2619 #[test]
2620 fn entering_scrollback_uses_current_visible_frame_as_baseline() {
2621 let mut client = test_client();
2622 let live = sample_frame("live");
2623 client.lead = Some(7);
2624 client.subscriptions.insert(7);
2625 client.last_sent.insert(7, live.clone());
2626
2627 assert!(update_client_scroll_state(&mut client, 7, 12));
2628 assert_eq!(client.scroll_offsets.get(&7), Some(&12));
2629 assert_eq!(client.scroll_caches.get(&7), Some(&live));
2630 }
2631
2632 #[test]
2633 fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
2634 let mut client = test_client();
2635 let history = sample_frame("hist");
2636 client.lead = Some(7);
2637 client.subscriptions.insert(7);
2638 client.scroll_offsets.insert(7, 12);
2639 client.scroll_caches.insert(7, history.clone());
2640
2641 assert!(update_client_scroll_state(&mut client, 7, 0));
2642 assert_eq!(client.scroll_offsets.get(&7), None);
2643 assert_eq!(client.last_sent.get(&7), Some(&history));
2644 assert_eq!(client.scroll_caches.get(&7), None);
2645 }
2646
2647 #[test]
2650 fn frame_window_minimum_is_two() {
2651 assert!(frame_window(0.0, 60.0) >= 2);
2652 }
2653
2654 #[test]
2655 fn frame_window_scales_with_rtt() {
2656 let low = frame_window(10.0, 60.0);
2657 let high = frame_window(200.0, 60.0);
2658 assert!(high > low, "higher RTT should need more frames in flight");
2659 }
2660
2661 #[test]
2662 fn frame_window_scales_with_fps() {
2663 let slow = frame_window(100.0, 10.0);
2664 let fast = frame_window(100.0, 120.0);
2665 assert!(fast > slow, "higher fps should need more frames in flight");
2666 }
2667
2668 #[test]
2669 fn frame_window_zero_rtt() {
2670 assert!(frame_window(0.0, 120.0) >= 2);
2671 }
2672
2673 #[test]
2676 fn path_rtt_ms_uses_min_when_positive() {
2677 let mut client = test_client();
2678 client.rtt_ms = 100.0;
2679 client.min_rtt_ms = 30.0;
2680 assert_eq!(path_rtt_ms(&client), 30.0);
2681 }
2682
2683 #[test]
2684 fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
2685 let mut client = test_client();
2686 client.rtt_ms = 80.0;
2687 client.min_rtt_ms = 0.0;
2688 assert_eq!(path_rtt_ms(&client), 80.0);
2689 }
2690
2691 #[test]
2694 fn ewma_rising_uses_rise_alpha() {
2695 let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
2696 assert!((result - 150.0).abs() < 0.01);
2698 }
2699
2700 #[test]
2701 fn ewma_falling_uses_fall_alpha() {
2702 let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
2703 assert!((result - 190.0).abs() < 0.01);
2705 }
2706
2707 #[test]
2708 fn ewma_same_value_unchanged() {
2709 let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
2710 assert!((result - 50.0).abs() < 0.01);
2711 }
2712
2713 #[test]
2716 fn advance_deadline_steps_forward() {
2717 let now = Instant::now();
2718 let mut deadline = now;
2719 let interval = Duration::from_millis(16);
2720 advance_deadline(&mut deadline, now, interval);
2721 assert!(deadline > now);
2722 assert!(deadline <= now + interval + Duration::from_micros(100));
2723 }
2724
2725 #[test]
2726 fn advance_deadline_resets_when_far_behind() {
2727 let now = Instant::now();
2728 let mut deadline = now - Duration::from_secs(10);
2730 let interval = Duration::from_millis(16);
2731 advance_deadline(&mut deadline, now, interval);
2732 assert!(deadline >= now);
2734 }
2735
2736 #[test]
2737 fn should_snapshot_pty_requires_dirty_and_needful() {
2738 assert!(should_snapshot_pty(true, true, false));
2739 assert!(!should_snapshot_pty(false, true, false));
2740 assert!(!should_snapshot_pty(true, false, false));
2741 }
2742
2743 #[test]
2744 fn should_snapshot_pty_defers_synced_output() {
2745 assert!(!should_snapshot_pty(true, true, true));
2746 assert!(should_snapshot_pty(true, true, false));
2747 }
2748
2749 #[test]
2750 fn enqueue_ready_frame_refuses_new_frames_when_capped() {
2751 let mut queue = VecDeque::new();
2752 for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
2753 assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
2754 }
2755 assert!(!enqueue_ready_frame(
2756 &mut queue,
2757 FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
2758 ));
2759 assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
2760 assert_eq!(queue.front().map(FrameState::cols), Some(1));
2761 assert_eq!(
2762 queue.back().map(FrameState::cols),
2763 Some(READY_FRAME_QUEUE_CAP as u16),
2764 );
2765 }
2766
2767 #[test]
2768 fn find_sync_output_end_returns_end_of_first_close_sequence() {
2769 let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
2770 assert_eq!(find_sync_output_end(&[], bytes), Some(11));
2771 }
2772
2773 #[test]
2774 fn find_sync_output_end_returns_none_without_close_sequence() {
2775 assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
2776 }
2777
2778 #[test]
2779 fn find_sync_output_end_detects_boundary_split_across_reads() {
2780 assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
2781 }
2782
2783 #[test]
2784 fn update_sync_scan_tail_keeps_recent_suffix_only() {
2785 let mut tail = Vec::new();
2786 update_sync_scan_tail(&mut tail, b"123456789");
2787 assert_eq!(tail, b"3456789");
2788 }
2789
2790 #[test]
2793 fn window_saturated_at_90_percent_frames() {
2794 let client = test_client();
2795 let target = target_frame_window(&client);
2796 let frames_90 = (target * 9).div_ceil(10); assert!(window_saturated(&client, frames_90, 0));
2798 }
2799
2800 #[test]
2801 fn window_saturated_not_at_low_usage() {
2802 let client = test_client();
2803 assert!(!window_saturated(&client, 1, 0));
2804 }
2805
2806 #[test]
2807 fn window_saturated_at_90_percent_bytes() {
2808 let client = test_client();
2809 let target_bytes = target_byte_window(&client);
2810 let bytes_90 = (target_bytes * 9).div_ceil(10);
2811 assert!(window_saturated(&client, 0, bytes_90));
2812 }
2813
2814 #[test]
2817 fn outbox_queued_frames_zero_when_empty() {
2818 let client = test_client();
2819 assert_eq!(outbox_queued_frames(&client), 0);
2820 }
2821
2822 #[test]
2823 fn outbox_backpressured_when_queue_full() {
2824 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
2825 for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
2827 let _ = client.tx.try_send(vec![0u8]);
2828 }
2829 assert!(outbox_backpressured(&client));
2830 }
2831
2832 #[test]
2833 fn outbox_not_backpressured_when_empty() {
2834 let client = test_client();
2835 assert!(!outbox_backpressured(&client));
2836 }
2837
2838 #[test]
2841 fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
2842 let mut client = test_client();
2843 client.rtt_ms = 1.0;
2844 client.min_rtt_ms = 1.0;
2845 client.browser_backlog_frames = 0;
2846 client.browser_ack_ahead_frames = 0;
2847 client.browser_apply_ms = 0.0;
2848 client.goodput_bps = 1_000_000.0;
2849 client.delivery_bps = 1_000_000.0;
2850 client.display_fps = 144.0;
2851 assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
2852 }
2853
2854 #[test]
2855 fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
2856 let mut client = test_client();
2857 client.browser_backlog_frames = 20;
2858 let fps = browser_pacing_fps(&client);
2859 assert!(fps >= 1.0);
2860 assert!(fps < client.display_fps);
2861 }
2862
2863 #[test]
2866 fn effective_rtt_ms_equals_path_when_queue_is_empty() {
2867 let mut client = test_client();
2868 client.rtt_ms = 1.0;
2869 client.min_rtt_ms = 1.0;
2870 client.browser_backlog_frames = 0;
2871 client.browser_ack_ahead_frames = 0;
2872 client.browser_apply_ms = 0.0;
2873 client.goodput_bps = 1_000_000.0;
2874 client.delivery_bps = 1_000_000.0;
2875 assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
2876 }
2877
2878 #[test]
2879 fn effective_rtt_ms_at_least_path_rtt() {
2880 let client = test_client();
2881 assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
2882 }
2883
2884 #[test]
2887 fn target_frame_window_at_least_two() {
2888 let client = test_client();
2889 assert!(target_frame_window(&client) >= 2);
2890 }
2891
2892 #[test]
2893 fn target_frame_window_grows_with_probe() {
2894 let mut client = test_client();
2895 let base = target_frame_window(&client);
2896 client.probe_frames = 10.0;
2897 let probed = target_frame_window(&client);
2898 assert!(probed > base, "probe_frames should grow the window");
2899 }
2900
2901 #[test]
2904 fn bandwidth_floor_bps_at_least_16k() {
2905 let mut client = test_client();
2906 client.goodput_bps = 0.0;
2907 client.delivery_bps = 0.0;
2908 assert_eq!(bandwidth_floor_bps(&client), 0.0);
2909 }
2910
2911 #[test]
2912 fn bandwidth_floor_bps_scales_with_goodput() {
2913 let mut client = test_client();
2914 client.goodput_bps = 1_000_000.0;
2915 client.delivery_bps = 1_000_000.0;
2916 let floor = bandwidth_floor_bps(&client);
2917 assert!(floor > 0.0);
2918 }
2919
2920 #[test]
2921 fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
2922 let mut client = test_client();
2923 client.display_fps = 60.0;
2924 client.browser_backlog_frames = 0;
2925 client.browser_ack_ahead_frames = 0;
2926 client.browser_apply_ms = 0.2;
2927 client.goodput_bps = 3_000_000.0;
2928 client.delivery_bps = 9_500_000.0;
2929 client.last_goodput_sample_bps = 3_000_000.0;
2930 client.avg_paced_frame_bytes = 150_000.0;
2931 client.avg_preview_frame_bytes = 1_024.0;
2932 client.avg_frame_bytes = 150_000.0;
2933
2934 assert!(
2935 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
2936 "browser-ready delivery floor should let large frames reach display_fps on a fast path",
2937 );
2938 }
2939
2940 #[test]
2943 fn pacing_fps_zero_when_no_bandwidth() {
2944 let mut client = test_client();
2945 client.goodput_bps = 0.0;
2946 client.delivery_bps = 0.0;
2947 client.last_goodput_sample_bps = 0.0;
2948 assert!(
2949 pacing_fps(&client) == 0.0,
2950 "pacing_fps should be 0 with zero bandwidth"
2951 );
2952 }
2953
2954 #[test]
2955 fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
2956 let mut client = test_client();
2957 client.rtt_ms = 1.0;
2958 client.min_rtt_ms = 1.0;
2959 client.browser_backlog_frames = 0;
2960 client.browser_ack_ahead_frames = 0;
2961 client.browser_apply_ms = 0.0;
2962 client.goodput_bps = 1_000_000.0;
2963 client.delivery_bps = 1_000_000.0;
2964 client.display_fps = 60.0;
2965 assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
2966 }
2967
2968 #[test]
2971 fn throughput_limited_when_low_bandwidth() {
2972 let mut client = test_client();
2973 client.goodput_bps = 1_000.0;
2974 client.delivery_bps = 1_000.0;
2975 client.last_goodput_sample_bps = 0.0;
2976 assert!(throughput_limited(&client));
2977 }
2978
2979 #[test]
2980 fn throughput_not_limited_with_high_bandwidth() {
2981 let mut client = test_client();
2982 client.goodput_bps = 100_000_000.0;
2983 client.delivery_bps = 100_000_000.0;
2984 assert!(!throughput_limited(&client));
2985 }
2986
2987 #[test]
2990 fn browser_pacing_fps_at_least_one() {
2991 let client = test_client();
2992 assert!(browser_pacing_fps(&client) >= 1.0);
2993 }
2994
2995 #[test]
2996 fn browser_pacing_fps_reduced_by_high_backlog() {
2997 let mut client = test_client();
2998 let normal = browser_pacing_fps(&client);
2999 client.browser_backlog_frames = 20;
3000 let backlogged = browser_pacing_fps(&client);
3001 assert!(backlogged < normal, "high backlog should reduce pacing fps");
3002 }
3003
3004 #[test]
3005 fn browser_pacing_fps_reduced_by_high_ack_ahead() {
3006 let mut client = test_client();
3007 let normal = browser_pacing_fps(&client);
3008 client.browser_ack_ahead_frames = 10;
3009 let ahead = browser_pacing_fps(&client);
3010 assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
3011 }
3012
3013 #[test]
3016 fn browser_backlog_blocked_over_threshold() {
3017 let mut client = test_client();
3018 client.browser_backlog_frames = 9;
3019 assert!(browser_backlog_blocked(&client));
3020 }
3021
3022 #[test]
3023 fn browser_backlog_not_blocked_under_threshold() {
3024 let mut client = test_client();
3025 client.browser_backlog_frames = 8;
3026 assert!(!browser_backlog_blocked(&client));
3027 }
3028
3029 #[test]
3032 fn byte_budget_for_at_least_one_frame() {
3033 let client = test_client();
3034 let budget = byte_budget_for(&client, 10.0);
3035 assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
3036 }
3037
3038 #[test]
3039 fn byte_budget_for_grows_with_time() {
3040 let client = test_client();
3041 let short = byte_budget_for(&client, 10.0);
3042 let long = byte_budget_for(&client, 1000.0);
3043 assert!(long >= short);
3044 }
3045
3046 #[test]
3049 fn target_byte_window_positive() {
3050 let client = test_client();
3051 assert!(target_byte_window(&client) > 0);
3052 }
3053
3054 #[test]
3055 fn target_byte_window_covers_frame_window() {
3056 let client = test_client();
3057 let byte_win = target_byte_window(&client);
3058 let frame_win = target_frame_window(&client);
3059 let min_bytes =
3060 (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
3061 assert!(
3062 byte_win >= min_bytes,
3063 "byte window should cover at least frame_window worth of paced frames"
3064 );
3065 }
3066
3067 #[test]
3070 fn send_interval_matches_browser_pacing() {
3071 let client = test_client();
3072 let interval = send_interval(&client);
3073 let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
3074 let diff = interval.abs_diff(expected);
3075 assert!(diff < Duration::from_micros(10));
3076 }
3077
3078 #[test]
3081 fn preview_fps_at_least_one() {
3082 let client = test_client();
3083 assert!(preview_fps(&client) >= 1.0);
3084 }
3085
3086 #[test]
3089 fn window_open_initially() {
3090 let client = test_client();
3091 assert!(window_open(&client));
3092 }
3093
3094 #[test]
3095 fn window_open_false_when_browser_blocked() {
3096 let mut client = test_client();
3097 client.browser_backlog_frames = 20;
3098 assert!(!window_open(&client));
3099 }
3100
3101 #[test]
3102 fn window_open_false_when_inflight_full() {
3103 let mut client = test_client();
3104 let target = target_frame_window(&client);
3105 fill_inflight(&mut client, target + 10, 1024);
3106 assert!(!window_open(&client));
3107 }
3108
3109 #[test]
3112 fn lead_window_open_no_reserve_same_as_window_open() {
3113 let client = test_client();
3114 assert_eq!(lead_window_open(&client, false), window_open(&client));
3115 }
3116
3117 #[test]
3118 fn lead_window_open_reserves_preview_slot() {
3119 let mut client = test_client();
3120 client.lead = Some(1);
3121 client.subscriptions.insert(1);
3122 let target = target_frame_window(&client);
3123 fill_inflight(&mut client, target.saturating_sub(1), 512);
3125 assert!(!lead_window_open(&client, true));
3128 }
3129
3130 #[test]
3133 fn can_send_frame_when_window_open_and_time_due() {
3134 let mut client = test_client();
3135 client.next_send_at = Instant::now() - Duration::from_millis(100);
3136 assert!(can_send_frame(&client, Instant::now(), false));
3137 }
3138
3139 #[test]
3140 fn can_send_frame_false_when_not_due() {
3141 let mut client = test_client();
3142 client.next_send_at = Instant::now() + Duration::from_secs(10);
3143 assert!(!can_send_frame(&client, Instant::now(), false));
3144 }
3145
3146 #[test]
3147 fn can_send_frame_false_when_window_closed() {
3148 let mut client = test_client();
3149 client.browser_backlog_frames = 20; client.next_send_at = Instant::now() - Duration::from_millis(100);
3151 assert!(!can_send_frame(&client, Instant::now(), false));
3152 }
3153
3154 #[test]
3157 fn record_send_increases_inflight() {
3158 let mut client = test_client();
3159 let now = Instant::now();
3160 assert_eq!(client.inflight_bytes, 0);
3161 assert_eq!(client.inflight_frames.len(), 0);
3162
3163 record_send(&mut client, 1000, now, true);
3164 assert_eq!(client.inflight_bytes, 1000);
3165 assert_eq!(client.inflight_frames.len(), 1);
3166
3167 record_send(&mut client, 500, now, false);
3168 assert_eq!(client.inflight_bytes, 1500);
3169 assert_eq!(client.inflight_frames.len(), 2);
3170 }
3171
3172 #[test]
3173 fn record_send_paced_advances_deadline() {
3174 let mut client = test_client();
3175 let now = Instant::now();
3176 client.next_send_at = now;
3177 record_send(&mut client, 1000, now, true);
3178 assert!(client.next_send_at > now);
3179 }
3180
3181 #[test]
3182 fn record_send_unpaced_does_not_advance_deadline() {
3183 let mut client = test_client();
3184 let now = Instant::now();
3185 let before = client.next_send_at;
3186 record_send(&mut client, 1000, now, false);
3187 assert_eq!(client.next_send_at, before);
3188 }
3189
3190 #[test]
3191 fn record_ack_decreases_inflight() {
3192 let mut client = test_client();
3193 let now = Instant::now();
3194 record_send(&mut client, 1000, now, true);
3195 record_send(&mut client, 500, now, true);
3196 assert_eq!(client.inflight_frames.len(), 2);
3197
3198 record_ack(&mut client);
3199 assert_eq!(client.inflight_frames.len(), 1);
3200 assert_eq!(client.inflight_bytes, 500);
3201 }
3202
3203 #[test]
3204 fn record_ack_on_empty_clears_bytes() {
3205 let mut client = test_client();
3206 client.inflight_bytes = 999; record_ack(&mut client);
3208 assert_eq!(client.inflight_bytes, 0);
3209 }
3210
3211 #[test]
3212 fn record_ack_updates_rtt_estimate() {
3213 let mut client = test_client();
3214 let now = Instant::now();
3215 client.inflight_frames.push_back(InFlightFrame {
3216 sent_at: now - Duration::from_millis(20),
3217 bytes: 512,
3218 paced: true,
3219 });
3220 client.inflight_bytes = 512;
3221 let old_rtt = client.rtt_ms;
3222 record_ack(&mut client);
3223 assert!(
3225 (client.rtt_ms - old_rtt).abs() > 0.01,
3226 "rtt_ms should be updated after ack"
3227 );
3228 }
3229
3230 #[test]
3231 fn record_ack_paced_updates_avg_paced_frame_bytes() {
3232 let mut client = test_client();
3233 let now = Instant::now();
3234 client.inflight_frames.push_back(InFlightFrame {
3235 sent_at: now - Duration::from_millis(10),
3236 bytes: 4096,
3237 paced: true,
3238 });
3239 client.inflight_bytes = 4096;
3240 let old_avg = client.avg_paced_frame_bytes;
3241 record_ack(&mut client);
3242 assert!(client.avg_paced_frame_bytes > old_avg);
3244 }
3245
3246 #[test]
3247 fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
3248 let mut client = test_client();
3249 let now = Instant::now();
3250 client.inflight_frames.push_back(InFlightFrame {
3251 sent_at: now - Duration::from_millis(10),
3252 bytes: 8192,
3253 paced: false,
3254 });
3255 client.inflight_bytes = 8192;
3256 let old_avg = client.avg_preview_frame_bytes;
3257 record_ack(&mut client);
3258 assert!(client.avg_preview_frame_bytes > old_avg);
3259 }
3260
3261 #[test]
3264 fn pty_list_msg_empty_session() {
3265 let sess = Session::new();
3266 let msg = sess.pty_list_msg();
3267 assert_eq!(msg[0], S2C_LIST);
3268 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
3269 assert_eq!(msg.len(), 3);
3270 }
3271
3272 #[test]
3273 fn pty_list_msg_includes_tags() {
3274 let _sess = Session::new();
3275 let tag1 = "shell";
3285 let tag2 = "build";
3286
3287 let mut expected = vec![S2C_LIST];
3289 expected.extend_from_slice(&2u16.to_le_bytes());
3290 expected.extend_from_slice(&1u16.to_le_bytes());
3292 expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
3293 expected.extend_from_slice(tag1.as_bytes());
3294 expected.extend_from_slice(&3u16.to_le_bytes());
3296 expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
3297 expected.extend_from_slice(tag2.as_bytes());
3298
3299 assert_eq!(expected[0], S2C_LIST);
3301 assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
3302 let msg_str = String::from_utf8_lossy(&expected);
3304 assert!(msg_str.contains("shell"));
3305 assert!(msg_str.contains("build"));
3306 }
3307
3308 #[test]
3311 fn can_send_preview_true_when_due() {
3312 let mut client = test_client();
3313 let now = Instant::now();
3314 client
3315 .preview_next_send_at
3316 .insert(5, now - Duration::from_millis(100));
3317 assert!(can_send_preview(&client, 5, now));
3318 }
3319
3320 #[test]
3321 fn can_send_preview_false_when_not_due() {
3322 let mut client = test_client();
3323 let now = Instant::now();
3324 client
3325 .preview_next_send_at
3326 .insert(5, now + Duration::from_secs(10));
3327 assert!(!can_send_preview(&client, 5, now));
3328 }
3329
3330 #[test]
3331 fn can_send_preview_false_when_window_closed() {
3332 let mut client = test_client();
3333 client.browser_backlog_frames = 20;
3334 let now = Instant::now();
3335 assert!(!can_send_preview(&client, 5, now));
3336 }
3337
3338 #[test]
3339 fn can_send_preview_true_for_unseen_pid() {
3340 let client = test_client();
3341 let now = Instant::now();
3342 assert!(can_send_preview(&client, 99, now));
3344 }
3345
3346 #[test]
3347 fn record_preview_send_sets_future_deadline() {
3348 let mut client = test_client();
3349 let now = Instant::now();
3350 record_preview_send(&mut client, 5, now);
3351 let deadline = client.preview_next_send_at.get(&5).unwrap();
3352 assert!(*deadline > now);
3353 }
3354
3355 #[test]
3356 fn record_preview_send_successive_calls_advance() {
3357 let mut client = test_client();
3358 let now = Instant::now();
3359 record_preview_send(&mut client, 5, now);
3360 let first = *client.preview_next_send_at.get(&5).unwrap();
3361 record_preview_send(&mut client, 5, first);
3362 let second = *client.preview_next_send_at.get(&5).unwrap();
3363 assert!(second > first, "successive sends should advance deadline");
3364 }
3365
3366 fn browser_ready_high_bandwidth_client() -> ClientState {
3380 let mut c = test_client();
3381 c.display_fps = 120.0;
3382 c.rtt_ms = 1.0;
3383 c.min_rtt_ms = 1.0;
3384 c.goodput_bps = 50_000_000.0;
3385 c.delivery_bps = 50_000_000.0;
3386 c.last_goodput_sample_bps = 50_000_000.0;
3387 c.avg_paced_frame_bytes = 30_000.0;
3388 c.avg_preview_frame_bytes = 1_024.0;
3389 c.avg_frame_bytes = 30_000.0;
3390 c.browser_apply_ms = 0.3;
3391 c
3392 }
3393
3394 fn congested_client() -> ClientState {
3397 let mut c = test_client();
3398 c.display_fps = 120.0;
3399 c.rtt_ms = 500.0;
3400 c.min_rtt_ms = 40.0;
3401 c.goodput_bps = 200_000.0;
3402 c.delivery_bps = 150_000.0;
3403 c.last_goodput_sample_bps = 200_000.0;
3404 c.avg_paced_frame_bytes = 50_000.0;
3405 c.avg_preview_frame_bytes = 1_024.0;
3406 c.avg_frame_bytes = 50_000.0;
3407 c.goodput_jitter_bps = 50_000.0;
3408 c.max_goodput_jitter_bps = 200_000.0;
3409 c.browser_apply_ms = 1.0;
3410 c
3411 }
3412
3413 fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
3417 let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
3418 client.inflight_bytes += bytes;
3419 client.inflight_frames.push_back(InFlightFrame {
3420 sent_at,
3421 bytes,
3422 paced: true,
3423 });
3424 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
3426 record_ack(client);
3427 }
3428
3429 fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
3430 for _ in 0..n {
3431 sim_ack(client, bytes, rtt_ms);
3432 }
3433 }
3434
3435 #[test]
3438 fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
3439 let client = browser_ready_high_bandwidth_client();
3440 assert!(
3441 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
3442 "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
3443 pacing_fps(&client),
3444 client.display_fps,
3445 );
3446 }
3447
3448 #[test]
3449 fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
3450 let client = browser_ready_high_bandwidth_client();
3451 let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
3452 let frame_ms = 1000.0 / client.display_fps;
3453 assert!(
3454 interval_ms <= frame_ms + 0.1,
3455 "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
3456 );
3457 }
3458
3459 #[test]
3462 fn congested_pipe_reduces_pacing_fps_substantially() {
3463 let client = congested_client();
3464 let fps = pacing_fps(&client);
3465 assert!(
3466 fps < client.display_fps * 0.5,
3467 "pacing_fps {fps:.0} should be well below display_fps {} when congested",
3468 client.display_fps,
3469 );
3470 }
3471
3472 #[test]
3473 fn congested_pipe_is_throughput_limited() {
3474 let client = congested_client();
3475 assert!(
3476 throughput_limited(&client),
3477 "congested client must be recognised as throughput-limited"
3478 );
3479 }
3480
3481 #[test]
3487 fn byte_window_bounded_near_bdp_when_congested() {
3488 let client = congested_client();
3489 let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
3491 let window = target_byte_window(&client);
3492 assert!(
3493 window < bdp as usize * 8,
3494 "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
3495 expected ≤ 8× — lead_floor may be dominating",
3496 window as f32 / bdp.max(1.0),
3497 );
3498 }
3499
3500 #[test]
3506 fn min_rtt_not_contaminated_by_congested_rtts() {
3507 let mut client = test_client();
3508 client.display_fps = 120.0;
3509 client.rtt_ms = 40.0;
3510 client.min_rtt_ms = 40.0;
3511 client.goodput_bps = 2_000_000.0;
3512 client.delivery_bps = 2_000_000.0;
3513 client.avg_paced_frame_bytes = 30_000.0;
3514 client.avg_preview_frame_bytes = 1_024.0;
3515 let original_min = client.min_rtt_ms;
3516
3517 sim_acks(&mut client, 200, 30_000, 500.0);
3519
3520 assert!(
3521 client.min_rtt_ms < original_min * 2.0,
3522 "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
3523 client.min_rtt_ms,
3524 );
3525 }
3526
3527 #[test]
3530 fn delivery_bps_rises_quickly_when_congestion_clears() {
3531 let mut client = congested_client();
3532 let before = client.delivery_bps;
3533
3534 sim_acks(&mut client, 10, 30_000, 40.0);
3536
3537 assert!(
3538 client.delivery_bps > before * 2.0,
3539 "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
3540 client.delivery_bps,
3541 );
3542 }
3543
3544 #[test]
3545 fn pacing_fps_recovers_after_congestion_clears() {
3546 let mut client = congested_client();
3547
3548 for _ in 0..40 {
3554 let target = target_frame_window(&client).max(2);
3555 for _ in 0..target {
3556 let sent_at = Instant::now() - Duration::from_millis(40);
3557 client.inflight_bytes += 30_000;
3558 client.inflight_frames.push_back(InFlightFrame {
3559 sent_at,
3560 bytes: 30_000,
3561 paced: true,
3562 });
3563 }
3564 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
3565 for _ in 0..target {
3566 record_ack(&mut client);
3567 }
3568 }
3569
3570 let fps = pacing_fps(&client);
3571 assert!(
3572 fps > client.display_fps * 0.7,
3573 "pacing_fps {fps:.0} didn't recover toward display_fps {} \
3574 after window-saturated rounds at low RTT",
3575 client.display_fps,
3576 );
3577 }
3578
3579 #[test]
3580 fn rtt_estimate_drops_quickly_when_congestion_clears() {
3581 let mut client = test_client();
3582 client.rtt_ms = 500.0;
3583 client.min_rtt_ms = 40.0;
3584 client.goodput_bps = 2_000_000.0;
3585 client.avg_paced_frame_bytes = 30_000.0;
3586 client.avg_preview_frame_bytes = 1_024.0;
3587
3588 sim_acks(&mut client, 10, 30_000, 40.0);
3591
3592 assert!(
3593 client.rtt_ms < 300.0,
3594 "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
3595 client.rtt_ms,
3596 );
3597 }
3598
3599 #[test]
3602 fn probe_collapses_immediately_on_queue_delay() {
3603 let mut client = test_client();
3604 client.display_fps = 120.0;
3605 client.rtt_ms = 40.0;
3606 client.min_rtt_ms = 40.0;
3607 client.goodput_bps = 5_000_000.0;
3608 client.delivery_bps = 5_000_000.0;
3609 client.last_goodput_sample_bps = 5_000_000.0;
3610 client.avg_paced_frame_bytes = 10_000.0;
3611 client.avg_preview_frame_bytes = 1_024.0;
3612 client.probe_frames = 10.0;
3613
3614 sim_acks(&mut client, 5, 10_000, 600.0);
3616
3617 assert!(
3618 client.probe_frames < 5.0,
3619 "probe_frames {:.1} should have collapsed on queue delay signal",
3620 client.probe_frames,
3621 );
3622 }
3623
3624 #[test]
3625 fn probe_grows_when_window_saturated_with_clean_rtt() {
3626 let mut client = test_client();
3627 client.display_fps = 120.0;
3628 client.rtt_ms = 40.0;
3629 client.min_rtt_ms = 40.0;
3630 client.goodput_bps = 5_000_000.0;
3631 client.delivery_bps = 5_000_000.0;
3632 client.last_goodput_sample_bps = 5_000_000.0;
3633 client.avg_paced_frame_bytes = 10_000.0;
3634 client.avg_preview_frame_bytes = 1_024.0;
3635 client.goodput_jitter_bps = 0.0;
3636 client.max_goodput_jitter_bps = 0.0;
3637 client.probe_frames = 0.0;
3638
3639 let target = target_frame_window(&client);
3641 for _ in 0..target {
3642 let sent_at = Instant::now() - Duration::from_millis(40);
3643 client.inflight_bytes += 10_000;
3644 client.inflight_frames.push_back(InFlightFrame {
3645 sent_at,
3646 bytes: 10_000,
3647 paced: true,
3648 });
3649 }
3650
3651 record_ack(&mut client);
3660
3661 assert!(
3662 client.probe_frames > 0.0,
3663 "probe_frames should grow when window-saturated with clean RTT"
3664 );
3665 }
3666
3667 #[test]
3670 fn frame_window_larger_on_high_latency_link() {
3671 let mut lo = test_client();
3672 lo.display_fps = 120.0;
3673 lo.rtt_ms = 10.0;
3674 lo.min_rtt_ms = 10.0;
3675 lo.goodput_bps = 5_000_000.0;
3676 lo.delivery_bps = 5_000_000.0;
3677 lo.avg_paced_frame_bytes = 10_000.0;
3678 lo.avg_preview_frame_bytes = 1_024.0;
3679
3680 let mut hi = test_client();
3681 hi.display_fps = 120.0;
3682 hi.rtt_ms = 200.0;
3683 hi.min_rtt_ms = 200.0;
3684 hi.goodput_bps = 5_000_000.0;
3685 hi.delivery_bps = 5_000_000.0;
3686 hi.avg_paced_frame_bytes = 10_000.0;
3687 hi.avg_preview_frame_bytes = 1_024.0;
3688
3689 let lo_win = target_frame_window(&lo);
3690 let hi_win = target_frame_window(&hi);
3691 assert!(
3692 hi_win > lo_win,
3693 "high-latency link ({hi_win}f) should need more frames in flight \
3694 than low-latency ({lo_win}f)"
3695 );
3696 }
3697
3698 #[test]
3701 fn small_frame_byte_window_enables_pipelining() {
3702 let mut client = test_client();
3707 client.display_fps = 120.0;
3708 client.rtt_ms = 165.0;
3709 client.min_rtt_ms = 8.0;
3710 client.goodput_bps = 11_000.0; client.delivery_bps = 6_800.0;
3712 client.last_goodput_sample_bps = 11_000.0;
3713 client.avg_paced_frame_bytes = 1_120.0;
3714 client.avg_preview_frame_bytes = 1_024.0;
3715 client.goodput_jitter_bps = 4_300.0;
3716 client.max_goodput_jitter_bps = 6_500.0;
3717
3718 let window = target_byte_window(&client);
3719 let frames = target_frame_window(&client);
3720 let pipeline = frames * 1_120;
3721
3722 assert!(
3723 window >= pipeline,
3724 "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
3725 so small frames can pipeline across the RTT"
3726 );
3727 }
3728
3729 #[test]
3730 fn large_frame_byte_window_bounded_by_one_frame_floor() {
3731 let mut client = test_client();
3735 client.display_fps = 120.0;
3736 client.rtt_ms = 165.0;
3737 client.min_rtt_ms = 8.0;
3738 client.goodput_bps = 11_000.0;
3739 client.delivery_bps = 6_800.0;
3740 client.last_goodput_sample_bps = 11_000.0;
3741 client.avg_paced_frame_bytes = 50_000.0; client.avg_preview_frame_bytes = 1_024.0;
3743 client.goodput_jitter_bps = 0.0;
3744 client.max_goodput_jitter_bps = 0.0;
3745
3746 let window = target_byte_window(&client);
3747 let frames = target_frame_window(&client);
3748 let pipeline = frames.saturating_mul(50_000);
3749
3750 assert!(
3751 window < pipeline,
3752 "byte window {window}B should be < full pipeline {pipeline}B \
3753 ({frames}f × 50KB) — large frames must use one-frame floor"
3754 );
3755 assert!(
3756 window >= 50_000,
3757 "byte window {window}B must be at least one frame (50KB)"
3758 );
3759 }
3760
3761 #[test]
3764 fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
3765 let mut client = browser_ready_high_bandwidth_client();
3766 client.lead = Some(1);
3767 client.subscriptions.insert(1);
3768 let target = target_frame_window(&client);
3769 fill_inflight(&mut client, target.saturating_sub(1), 512);
3770 assert!(
3771 !lead_window_open(&client, true),
3772 "preview reservation should apply uniformly for lead clients"
3773 );
3774 }
3775
3776 #[test]
3779 fn probe_recovers_on_healthy_path_after_blip() {
3780 let mut client = browser_ready_high_bandwidth_client();
3781 client.probe_frames = 8.0;
3782
3783 sim_acks(&mut client, 3, 30_000, 200.0);
3785 let post_blip = client.probe_frames;
3786 assert!(
3787 post_blip < 4.0,
3788 "probe_frames {post_blip:.1} should have dropped after blip"
3789 );
3790
3791 client.browser_backlog_frames = 0;
3793 client.browser_ack_ahead_frames = 0;
3794 client.browser_apply_ms = 0.3;
3795
3796 sim_acks(&mut client, 20, 30_000, 1.0);
3798
3799 assert!(
3800 client.probe_frames > post_blip,
3801 "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
3802 client.probe_frames,
3803 );
3804 }
3805
3806 #[test]
3807 fn jitter_decays_fast_on_browser_ready_path() {
3808 let mut client = browser_ready_high_bandwidth_client();
3809
3810 client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
3812 client.goodput_jitter_bps = client.goodput_bps * 0.3;
3813 let initial_jitter = client.max_goodput_jitter_bps;
3814
3815 sim_acks(&mut client, 10, 30_000, 1.0);
3817
3818 assert!(
3819 client.max_goodput_jitter_bps < initial_jitter * 0.5,
3820 "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
3821 (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
3822 client.max_goodput_jitter_bps,
3823 initial_jitter * 0.5,
3824 );
3825 }
3826
3827 #[test]
3828 fn byte_budget_uses_floor_when_goodput_depressed() {
3829 let mut client = browser_ready_high_bandwidth_client();
3830 client.goodput_bps = 100_000.0;
3831
3832 let budget = byte_budget_for(&client, 100.0);
3833 let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
3834
3835 assert!(
3836 budget >= floor_budget,
3837 "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
3838 when goodput_bps is depressed but delivery_bps is high"
3839 );
3840 }
3841
3842 #[test]
3843 fn probe_floor_maintained_under_congestion_signal() {
3844 let mut client = test_client();
3845 client.display_fps = 120.0;
3846 client.rtt_ms = 40.0;
3847 client.min_rtt_ms = 40.0;
3848 client.goodput_bps = 5_000_000.0;
3849 client.delivery_bps = 5_000_000.0;
3850 client.last_goodput_sample_bps = 5_000_000.0;
3851 client.avg_paced_frame_bytes = 10_000.0;
3852 client.avg_preview_frame_bytes = 1_024.0;
3853 client.probe_frames = 10.0;
3854
3855 sim_acks(&mut client, 20, 10_000, 600.0);
3857
3858 assert!(
3859 client.probe_frames >= 1.0,
3860 "probe_frames {:.1} should not drop below the floor of 1.0",
3861 client.probe_frames,
3862 );
3863 }
3864}