1use blit_alacritty::{SearchResult as AlacrittySearchResult, TerminalDriver as AlacrittyDriver};
2use blit_remote::{
3 build_update_msg, msg_hello, FrameState, C2S_ACK, C2S_CLIENT_METRICS, C2S_CLOSE, C2S_CREATE,
4 C2S_CREATE2, C2S_CREATE_AT, C2S_CREATE_N, C2S_DISPLAY_RATE, C2S_FOCUS, C2S_INPUT, C2S_MOUSE,
5 C2S_COPY_RANGE, C2S_KILL, C2S_READ, C2S_RESIZE, C2S_RESTART, C2S_SCROLL, C2S_SEARCH,
6 C2S_SUBSCRIBE, C2S_UNSUBSCRIBE,
7 CREATE2_HAS_COMMAND, CREATE2_HAS_SRC_PTY, FEATURE_COPY_RANGE, FEATURE_CREATE_NONCE,
8 FEATURE_RESIZE_BATCH, FEATURE_RESTART, READ_ANSI, READ_TAIL, S2C_CLOSED, S2C_CREATED,
9 S2C_CREATED_N, S2C_LIST,
10 S2C_READY, S2C_SEARCH_RESULTS, S2C_TEXT, S2C_TITLE,
11};
12use std::collections::{HashMap, HashSet, VecDeque};
13use std::ffi::CString;
14use std::os::unix::fs::PermissionsExt;
15use std::os::unix::io::RawFd;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18use tokio::io::unix::AsyncFd;
19use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
20use tokio::net::UnixListener;
21use tokio::sync::{mpsc, Mutex, Notify};
22
23type PtyFds = Arc<std::sync::RwLock<HashMap<u16, RawFd>>>;
24pub struct Config {
25 pub shell: String,
26 pub shell_flags: String,
27 pub scrollback: usize,
28 pub socket_path: String,
29 pub fd_channel: Option<RawFd>,
30 pub verbose: bool,
31}
32
33fn pty_write_all(fd: libc::c_int, mut data: &[u8]) {
34 while !data.is_empty() {
35 let ret = unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
36 if ret > 0 {
37 data = &data[ret as usize..];
38 } else if ret < 0 {
39 let err = std::io::Error::last_os_error();
40 if err.kind() == std::io::ErrorKind::Interrupted {
41 continue;
42 }
43 break;
44 } else {
45 break;
46 }
47 }
48}
49
50trait PtyDriver: Send {
51 fn size(&self) -> (u16, u16);
52 fn resize(&mut self, rows: u16, cols: u16);
53 fn process(&mut self, data: &[u8]);
54 fn title(&self) -> &str;
55 fn search_result(&self, query: &str) -> Option<PtySearchResult>;
56 fn take_title_dirty(&mut self) -> bool;
57 fn cursor_position(&self) -> (u16, u16);
58 fn synced_output(&self) -> bool;
59 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
60 fn scrollback_frame(&mut self, offset: usize) -> FrameState;
61 fn reset_modes(&mut self);
62 fn mouse_event(
63 &self,
64 type_: u8,
65 button: u8,
66 col: u16,
67 row: u16,
68 echo: bool,
69 icanon: bool,
70 ) -> Option<Vec<u8>>;
71 fn get_text_range(
72 &self,
73 start_tail: u32,
74 start_col: u16,
75 end_tail: u32,
76 end_col: u16,
77 ) -> String;
78 fn total_lines(&self) -> u32;
79}
80
81struct PtySearchResult {
82 score: u32,
83 primary_source: u8,
84 matched_sources: u8,
85 context: String,
86 scroll_offset: Option<usize>,
87}
88
89impl PtyDriver for AlacrittyDriver {
90 fn size(&self) -> (u16, u16) {
91 AlacrittyDriver::size(self)
92 }
93
94 fn resize(&mut self, rows: u16, cols: u16) {
95 AlacrittyDriver::resize(self, rows, cols);
96 }
97
98 fn process(&mut self, data: &[u8]) {
99 AlacrittyDriver::process(self, data);
100 }
101
102 fn title(&self) -> &str {
103 AlacrittyDriver::title(self)
104 }
105
106 fn search_result(&self, query: &str) -> Option<PtySearchResult> {
107 AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
108 PtySearchResult {
109 score: result.score,
110 primary_source: result.primary_source as u8,
111 matched_sources: result.matched_sources,
112 context: result.context,
113 scroll_offset: result.scroll_offset,
114 }
115 })
116 }
117
118 fn take_title_dirty(&mut self) -> bool {
119 AlacrittyDriver::take_title_dirty(self)
120 }
121
122 fn cursor_position(&self) -> (u16, u16) {
123 AlacrittyDriver::cursor_position(self)
124 }
125
126 fn synced_output(&self) -> bool {
127 AlacrittyDriver::synced_output(self)
128 }
129
130 fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
131 AlacrittyDriver::snapshot(self, echo, icanon)
132 }
133
134 fn scrollback_frame(&mut self, offset: usize) -> FrameState {
135 AlacrittyDriver::scrollback_frame(self, offset)
136 }
137
138 fn reset_modes(&mut self) {
139 AlacrittyDriver::reset_modes(self);
140 }
141
142 fn mouse_event(
143 &self,
144 type_: u8,
145 button: u8,
146 col: u16,
147 row: u16,
148 echo: bool,
149 icanon: bool,
150 ) -> Option<Vec<u8>> {
151 AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
152 }
153
154 fn get_text_range(
155 &self,
156 start_tail: u32,
157 start_col: u16,
158 end_tail: u32,
159 end_col: u16,
160 ) -> String {
161 AlacrittyDriver::get_text_range(self, start_tail, start_col, end_tail, end_col)
162 }
163
164 fn total_lines(&self) -> u32 {
165 AlacrittyDriver::total_lines(self)
166 }
167}
168
169const OUTBOX_CAPACITY: usize = 8;
173const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 2;
174const PREVIEW_FRAME_RESERVE: usize = 1;
175const READY_FRAME_QUEUE_CAP: usize = 4;
176const PTY_CHANNEL_CAPACITY: usize = 64;
177const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
178
179enum PtyInput {
182 Data(Vec<u8>),
185 SyncBoundary { before: Vec<u8>, after: Vec<u8> },
188 Eof,
190}
191
192const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
193
194async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
195 let mut len_buf = [0u8; 4];
196 reader.read_exact(&mut len_buf).await.ok()?;
197 let len = u32::from_le_bytes(len_buf) as usize;
198 if len == 0 {
199 return Some(vec![]);
200 }
201 if len > MAX_FRAME_SIZE {
202 return None;
203 }
204 let mut buf = vec![0u8; len];
205 reader.read_exact(&mut buf).await.ok()?;
206 Some(buf)
207}
208
209async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
210 if payload.len() > u32::MAX as usize {
211 return false;
212 }
213 let len = payload.len() as u32;
214 let mut buf = Vec::with_capacity(4 + payload.len());
215 buf.extend_from_slice(&len.to_le_bytes());
216 buf.extend_from_slice(payload);
217 writer.write_all(&buf).await.is_ok()
218}
219
220struct Pty {
221 master_fd: libc::c_int,
222 child_pid: libc::pid_t,
223 driver: Box<dyn PtyDriver>,
224 tag: String,
226 dirty: bool,
227 ready_frames: VecDeque<FrameState>,
228 byte_rx: mpsc::Receiver<PtyInput>,
230 reader_handle: std::thread::JoinHandle<()>,
231 lflag_cache: (bool, bool),
233 lflag_last: Instant,
234 last_title_send: Instant,
236 title_pending: bool,
238 exited: bool,
240 exit_status: i32,
243 command: Option<String>,
245}
246
247impl Pty {
248 fn mark_dirty(&mut self) {
249 self.dirty = true;
250 }
251
252 fn clear_dirty(&mut self) {
253 self.dirty = false;
254 }
255}
256
257struct ClientState {
258 tx: mpsc::Sender<Vec<u8>>,
259 lead: Option<u16>,
260 subscriptions: HashSet<u16>,
261 view_sizes: HashMap<u16, (u16, u16)>,
262 scroll_offsets: HashMap<u16, usize>,
263 scroll_caches: HashMap<u16, FrameState>,
264 last_sent: HashMap<u16, FrameState>,
265 preview_next_send_at: HashMap<u16, Instant>,
266 rtt_ms: f32,
268 min_rtt_ms: f32,
270 display_fps: f32,
272 delivery_bps: f32,
274 goodput_bps: f32,
276 goodput_jitter_bps: f32,
278 max_goodput_jitter_bps: f32,
280 last_goodput_sample_bps: f32,
282 avg_frame_bytes: f32,
284 avg_paced_frame_bytes: f32,
286 avg_preview_frame_bytes: f32,
288 inflight_bytes: usize,
290 inflight_frames: VecDeque<InFlightFrame>,
292 next_send_at: Instant,
294 probe_frames: f32,
297 frames_sent: u32,
299 acks_recv: u32,
300 acked_bytes_since_log: usize,
301 browser_backlog_frames: u16,
302 browser_ack_ahead_frames: u16,
303 browser_apply_ms: f32,
304 last_metrics_update: Instant,
305 last_log: Instant,
306 goodput_window_bytes: usize,
307 goodput_window_start: Instant,
308}
309
310struct InFlightFrame {
311 sent_at: Instant,
312 bytes: usize,
313 paced: bool,
314}
315
316fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
320 let frame_ms = 1_000.0 / display_fps.max(1.0);
321 let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
322 let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
323 base_frames.saturating_add(slack_frames).max(2)
324}
325
326fn path_rtt_ms(client: &ClientState) -> f32 {
327 if client.min_rtt_ms > 0.0 {
328 client.min_rtt_ms
329 } else {
330 client.rtt_ms
331 }
332}
333
334fn display_need_bps(client: &ClientState) -> f32 {
335 client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
336}
337
338fn effective_rtt_ms(client: &ClientState) -> f32 {
339 let path_rtt = path_rtt_ms(client);
340 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
341 let queue_allowance = frame_ms
342 * if throughput_limited(client) {
343 4.0
344 } else {
345 12.0
346 };
347 client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
348}
349
350fn window_rtt_ms(client: &ClientState) -> f32 {
351 let effective = effective_rtt_ms(client);
352 if !throughput_limited(client) {
353 effective
354 } else {
355 client.rtt_ms.clamp(effective, effective * 2.0)
356 }
357}
358
359fn target_frame_window(client: &ClientState) -> usize {
360 let window_fps = if throughput_limited(client) {
361 pacing_fps(client)
362 } else {
363 browser_pacing_fps(client)
364 };
365 frame_window(window_rtt_ms(client), window_fps)
366 .saturating_add(client.probe_frames.round().max(0.0) as usize)
367}
368
369fn base_queue_ms(client: &ClientState) -> f32 {
370 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
371 frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
372}
373
374fn target_queue_ms(client: &ClientState) -> f32 {
375 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
376 let probe_scale = if throughput_limited(client) {
377 0.25
378 } else {
379 1.0
380 };
381 base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
382}
383
384fn browser_ready(client: &ClientState) -> bool {
385 client.browser_ack_ahead_frames <= 1
386 && client.browser_apply_ms <= 1.0
387 && !outbox_backpressured(client)
388}
389
390fn bandwidth_floor_bps(client: &ClientState) -> f32 {
391 let browser_ready = browser_ready(client);
392 let backlog_scale = match client.browser_backlog_frames {
393 0..=2 => 0.9,
394 3..=8 => 0.8,
395 _ => 0.65,
396 };
397 let penalty = client
398 .goodput_jitter_bps
399 .max(client.max_goodput_jitter_bps * 0.5)
400 .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
401 let goodput_floor = (client.goodput_bps - penalty)
402 .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
403 let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
407 let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
408 client.last_goodput_sample_bps * backlog_scale
409 } else {
410 0.0
411 };
412 goodput_floor.max(recent_sample_floor).max(delivery_floor)
413}
414
415fn pacing_fps(client: &ClientState) -> f32 {
416 let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
417 let sustainable = bandwidth_floor_bps(client) / frame_bytes;
418 sustainable.min(browser_pacing_fps(client))
419}
420
421fn throughput_limited(client: &ClientState) -> bool {
422 let floor = bandwidth_floor_bps(client);
423 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
427 let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
428 (lead_bps + preview_bps) > floor * 0.9
429}
430
431fn browser_pacing_fps(client: &ClientState) -> f32 {
432 let mut fps = client.display_fps.max(1.0);
433
434 let backlog = client.browser_backlog_frames as f32;
438 if backlog > 4.0 {
439 fps = fps.min(fps * (4.0 / backlog));
440 }
441
442 if client.browser_ack_ahead_frames > 4 {
443 fps = fps.min(client.display_fps.max(1.0) * 0.5);
444 }
445
446 fps.max(1.0)
447}
448
449fn browser_backlog_blocked(client: &ClientState) -> bool {
450 client.browser_backlog_frames > 8
451}
452
453fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
454 let budget_bps = if throughput_limited(client) {
455 bandwidth_floor_bps(client)
456 } else {
457 client.goodput_bps.max(bandwidth_floor_bps(client))
458 };
459 let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
460 bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
461}
462
463fn target_byte_window(client: &ClientState) -> usize {
464 let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
465 let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
466 let target_frames = target_frame_window(client);
467 let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
468 const PIPELINE_FLOOR_LIMIT: usize = 32_768; let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
475 pipeline_bytes
476 } else {
477 frame_bytes };
479 budget.max(floor)
480}
481
482fn send_interval(client: &ClientState) -> Duration {
483 Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
484}
485
486fn preview_fps(client: &ClientState) -> f32 {
487 let mut fps = client.display_fps.max(1.0);
488 if client.lead.is_some() {
489 let avail = bandwidth_floor_bps(client);
493 let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
494 let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
495 let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
496 fps = fps.min(bw_cap.max(1.0));
497 }
498 fps.max(1.0)
499}
500
501fn preview_send_interval(client: &ClientState) -> Duration {
502 Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
503}
504
505fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
506 let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
507 *deadline = if scheduled + interval < now {
508 now + interval
509 } else {
510 scheduled
511 };
512}
513
514fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
515 dirty && needful && !synced_output
516}
517
518fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
519 if queue.len() >= READY_FRAME_QUEUE_CAP {
520 return false;
521 }
522 queue.push_back(frame);
523 true
524}
525
526fn pty_has_visual_update(pty: &Pty) -> bool {
527 pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
528}
529
530fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
534 if bytes.is_empty() {
535 return None;
536 }
537 let needle = SYNC_OUTPUT_END;
538 let nlen = needle.len();
539
540 if !prefix.is_empty() {
542 let tail = if prefix.len() >= nlen - 1 {
543 &prefix[prefix.len() - (nlen - 1)..]
544 } else {
545 prefix
546 };
547 let combined_len = tail.len() + bytes.len().min(nlen);
548 if combined_len >= nlen {
549 let mut buf = [0u8; 32]; let blen = combined_len.min(buf.len());
552 let tlen = tail.len().min(blen);
553 buf[..tlen].copy_from_slice(&tail[..tlen]);
554 let rest = (blen - tlen).min(bytes.len());
555 buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
556 for i in 0..=(blen.saturating_sub(nlen)) {
557 if &buf[i..i + nlen] == needle {
558 let end_in_bytes = (i + nlen).saturating_sub(tail.len());
559 if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
560 return Some(end_in_bytes);
561 }
562 }
563 }
564 }
565 }
566
567 let mut offset = 0;
569 while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
570 let abs = offset + pos;
571 if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
572 return Some(abs + nlen);
573 }
574 offset = abs + 1;
575 }
576 None
577}
578
579fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
580 if bytes.is_empty() {
581 return;
582 }
583 tail.extend_from_slice(bytes);
584 let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
585 if tail.len() > keep {
586 let drop = tail.len() - keep;
587 tail.drain(..drop);
588 }
589}
590
591fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
592 client
593 .preview_next_send_at
594 .get(&pid)
595 .copied()
596 .unwrap_or(now)
597}
598
599fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
600 if client.lead.is_none() {
601 return false;
602 }
603 client.subscriptions.iter().copied().any(|pid| {
604 Some(pid) != client.lead
605 && preview_deadline(client, pid, now) <= now
606 && sess
607 .ptys
608 .get(&pid)
609 .map(pty_has_visual_update)
610 .unwrap_or(false)
611 })
612}
613
614fn outbox_queued_frames(client: &ClientState) -> usize {
615 OUTBOX_CAPACITY.saturating_sub(client.tx.capacity())
616}
617
618fn outbox_backpressured(client: &ClientState) -> bool {
619 outbox_queued_frames(client) >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES
620}
621
622fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
623 window_open(client) && now >= preview_deadline(client, pid, now)
624}
625
626fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
627 let mut deadline = client
628 .preview_next_send_at
629 .get(&pid)
630 .copied()
631 .unwrap_or(now);
632 advance_deadline(&mut deadline, now, preview_send_interval(client));
633 client.preview_next_send_at.insert(pid, deadline);
634}
635
636fn window_open(client: &ClientState) -> bool {
637 !browser_backlog_blocked(client)
638 && !outbox_backpressured(client)
639 && client.inflight_frames.len() < target_frame_window(client)
640 && client.inflight_bytes < target_byte_window(client)
641}
642
643fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
644 if !reserve_preview_slot || client.lead.is_none() {
645 return window_open(client);
646 }
647 if browser_backlog_blocked(client) || outbox_backpressured(client) {
648 return false;
649 }
650 let target_frames = target_frame_window(client);
651 let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
652 let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
653 let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
654 let byte_limit = target_byte_window(client)
655 .saturating_sub(reserve_bytes)
656 .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
657 client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
658}
659
660fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
661 lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
662}
663
664fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
665 client.inflight_bytes += bytes;
666 client.inflight_frames.push_back(InFlightFrame {
667 sent_at: now,
668 bytes,
669 paced,
670 });
671 if paced {
672 let interval = send_interval(client);
673 advance_deadline(&mut client.next_send_at, now, interval);
674 }
675}
676
677fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
678 let alpha = if sample > old { rise_alpha } else { fall_alpha };
679 old * (1.0 - alpha) + sample * alpha
680}
681
682fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
683 let target_frames = target_frame_window(client);
684 let target_bytes = target_byte_window(client);
685 inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
686 || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
687}
688
689fn record_ack(client: &mut ClientState) {
690 if let Some(frame) = client.inflight_frames.pop_front() {
691 let prev_inflight_frames = client.inflight_frames.len() + 1;
692 let prev_inflight_bytes = client.inflight_bytes;
693 client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
694 client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
695 let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
696 client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
697 if client.min_rtt_ms > 0.0 {
698 client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
701 } else {
702 client.min_rtt_ms = sample_ms;
703 }
704 client.min_rtt_ms = client.min_rtt_ms.max(0.5);
705 let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
706 client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
707 client.avg_frame_bytes =
708 ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
709 if frame.paced {
710 client.avg_paced_frame_bytes =
711 ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
712 } else {
713 client.avg_preview_frame_bytes = ewma_with_direction(
714 client.avg_preview_frame_bytes,
715 frame.bytes as f32,
716 0.5,
717 0.125,
718 );
719 }
720 let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
721 let path_rtt = path_rtt_ms(client);
722 let likely_window_limited =
723 window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
724 client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
725 let now = Instant::now();
726 let goodput_elapsed = now
727 .duration_since(client.goodput_window_start)
728 .as_secs_f32();
729 if goodput_elapsed >= 0.02 {
730 let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
731 if likely_window_limited || client.browser_backlog_frames > 0 {
732 let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
733 client.last_goodput_sample_bps
734 } else {
735 sample_goodput
736 };
737 let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
738 client.goodput_bps =
739 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
740 let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
746 if client.goodput_window_bytes >= min_reliable {
747 client.goodput_jitter_bps =
748 ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
749 let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
750 0.90
751 } else {
752 0.98
753 };
754 client.max_goodput_jitter_bps =
755 (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
756 client.max_goodput_jitter_bps =
760 client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
761 } else {
762 client.goodput_jitter_bps *= 0.9;
764 client.max_goodput_jitter_bps *= 0.95;
765 }
766 client.last_goodput_sample_bps =
770 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
771 } else {
772 let ratio = client.goodput_bps / sample_goodput.max(1.0);
777 let fall_alpha = if ratio > 10.0 {
778 0.5
779 } else if ratio > 3.0 {
780 0.25
781 } else {
782 0.03
783 };
784 client.goodput_bps =
785 ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
786 client.goodput_jitter_bps *= 0.5;
787 client.max_goodput_jitter_bps *= 0.9;
788 client.last_goodput_sample_bps =
789 (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
790 }
791 client.goodput_window_bytes = 0;
792 client.goodput_window_start = now;
793 }
794 let queue_baseline_ms = if throughput_limited(client) {
795 window_rtt_ms(client)
796 } else {
797 path_rtt
798 };
799 let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
800 let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
801 let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
802 let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
803 let high_delay_frames = if throughput_limited(client) {
804 4.0
805 } else {
806 12.0
807 };
808 if likely_window_limited
809 && queue_delay_ms <= frame_ms * low_delay_frames
810 && jitter_ratio < 0.25
811 {
812 client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
813 } else if !likely_window_limited
814 && browser_ready(client)
815 && queue_delay_ms <= frame_ms * 2.0
816 && jitter_ratio < 0.25
817 {
818 client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
819 } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
820 client.probe_frames = (client.probe_frames * 0.5).max(1.0);
821 } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
822 client.probe_frames = (client.probe_frames - 0.5).max(0.0);
823 }
824 } else {
825 client.inflight_bytes = 0;
826 }
827}
828
829fn reset_inflight(client: &mut ClientState) {
830 client.inflight_bytes = 0;
831 client.inflight_frames.clear();
832 client.next_send_at = Instant::now();
833 client.browser_backlog_frames = 0;
834 client.browser_ack_ahead_frames = 0;
835}
836
837fn is_unset_view_size(rows: u16, cols: u16) -> bool {
838 rows == 0 && cols == 0
839}
840
841fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
842 if client.subscriptions.insert(pty_id) {
843 client.last_sent.remove(&pty_id);
844 client.preview_next_send_at.remove(&pty_id);
845 }
846}
847
848fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
849 let removed_sub = client.subscriptions.remove(&pty_id);
850 client.last_sent.remove(&pty_id);
851 client.preview_next_send_at.remove(&pty_id);
852 client.scroll_offsets.remove(&pty_id);
853 client.scroll_caches.remove(&pty_id);
854 let removed_view = client.view_sizes.remove(&pty_id).is_some();
855 if client.lead == Some(pty_id) {
856 client.lead = None;
857 }
858 removed_sub || removed_view
859}
860
861fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
862 let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
863 if prev_offset == next_offset {
864 return false;
865 }
866
867 if prev_offset == 0 && next_offset > 0 {
868 client.scroll_caches.insert(
869 pty_id,
870 client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
871 );
872 } else if prev_offset > 0 && next_offset == 0 {
873 if let Some(cache) = client.scroll_caches.remove(&pty_id) {
874 if cache.rows() > 0 && cache.cols() > 0 {
875 client.last_sent.insert(pty_id, cache);
876 } else {
877 client.last_sent.remove(&pty_id);
878 }
879 }
880 }
881
882 if next_offset > 0 {
883 client.scroll_offsets.insert(pty_id, next_offset);
884 } else {
885 client.scroll_offsets.remove(&pty_id);
886 }
887 reset_inflight(client);
888 true
889}
890
891struct Session {
892 ptys: HashMap<u16, Pty>,
893 next_client_id: u64,
894 tick_fires: u32,
896 tick_snaps: u32,
898 clients: HashMap<u64, ClientState>,
899}
900
901struct SearchResultRow {
902 pty_id: u16,
903 score: u32,
904 primary_source: u8,
905 matched_sources: u8,
906 context: String,
907 scroll_offset: Option<usize>,
908}
909
910struct TickOutcome {
911 did_work: bool,
912 next_deadline: Option<Instant>,
913}
914
915impl Session {
916 fn new() -> Self {
917 Self {
918 ptys: HashMap::new(),
919 next_client_id: 1,
920 clients: HashMap::new(),
921 tick_fires: 0,
922 tick_snaps: 0,
923 }
924 }
925
926 fn allocate_pty_id(&mut self) -> Option<u16> {
927 (1..=u16::MAX).find(|id| !self.ptys.contains_key(id))
928 }
929
930 fn send_to_all(&self, msg: &[u8]) {
931 for c in self.clients.values() {
932 let _ = c.tx.try_send(msg.to_vec());
933 }
934 }
935
936 fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
937 let mut min_rows: Option<u16> = None;
938 let mut min_cols: Option<u16> = None;
939 for c in self.clients.values() {
940 if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
941 min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
942 min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
943 }
944 }
945 match (min_rows, min_cols) {
946 (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
947 _ => None,
948 }
949 }
950
951 fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
952 let pty = match self.ptys.get_mut(&pty_id) {
953 Some(p) => p,
954 None => return false,
955 };
956 let (cur_rows, cur_cols) = pty.driver.size();
957 if cur_rows == rows && cur_cols == cols {
958 return false;
959 }
960 pty.ready_frames.clear();
961 pty.driver.resize(rows, cols);
962 pty.mark_dirty();
963 for c in self.clients.values_mut() {
964 if c.subscriptions.contains(&pty_id) {
965 c.last_sent.remove(&pty_id);
966 }
967 if c.scroll_caches.remove(&pty_id).is_some() {
968 reset_inflight(c);
969 }
970 }
971 if !pty.exited {
972 unsafe {
973 let ws = libc::winsize {
974 ws_row: rows,
975 ws_col: cols,
976 ws_xpixel: 0,
977 ws_ypixel: 0,
978 };
979 libc::ioctl(pty.master_fd, libc::TIOCSWINSZ, &ws);
980 let mut fg_pgid: libc::pid_t = 0;
981 libc::ioctl(pty.master_fd, libc::TIOCGPGRP, &mut fg_pgid);
982 if fg_pgid > 0 {
983 libc::kill(-fg_pgid, libc::SIGWINCH);
984 }
985 libc::kill(-pty.child_pid, libc::SIGWINCH);
986 }
987 }
988 true
989 }
990
991 fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
992 where
993 I: IntoIterator<Item = u16>,
994 {
995 let mut changed = false;
996 let mut seen = HashSet::new();
997 for pty_id in pty_ids {
998 if !seen.insert(pty_id) {
999 continue;
1000 }
1001 if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
1002 changed |= self.resize_pty(pty_id, rows, cols);
1003 }
1004 }
1005 changed
1006 }
1007
1008 fn pty_list_msg(&self) -> Vec<u8> {
1009 let mut msg = vec![S2C_LIST];
1010 let count = self.ptys.len() as u16;
1011 msg.extend_from_slice(&count.to_le_bytes());
1012 let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
1013 ids.sort();
1014 for id in ids {
1015 let pty = &self.ptys[&id];
1016 let tag = pty.tag.as_bytes();
1017 msg.extend_from_slice(&id.to_le_bytes());
1018 msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
1019 msg.extend_from_slice(tag);
1020 let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
1021 msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
1022 msg.extend_from_slice(cmd);
1023 }
1024 msg
1025 }
1026}
1027
1028type AppState = Arc<(Config, Mutex<Session>, PtyFds, Arc<Notify>)>;
1029
1030fn nudge_delivery(state: &AppState) {
1031 state.3.notify_one();
1032}
1033
1034fn pty_cwd(pid: libc::pid_t) -> Option<String> {
1035 #[cfg(target_os = "linux")]
1036 {
1037 std::fs::read_link(format!("/proc/{pid}/cwd"))
1038 .ok()
1039 .and_then(|p| p.into_os_string().into_string().ok())
1040 }
1041 #[cfg(target_os = "macos")]
1042 {
1043 use std::ffi::CStr;
1044 let mut buf = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
1045 let ret = unsafe {
1046 libc::proc_pidinfo(
1047 pid,
1048 libc::PROC_PIDVNODEPATHINFO,
1049 0,
1050 buf.as_mut_ptr() as *mut libc::c_void,
1051 std::mem::size_of::<libc::proc_vnodepathinfo>() as i32,
1052 )
1053 };
1054 if ret <= 0 {
1055 return None;
1056 }
1057 let info = unsafe { &*(buf.as_ptr() as *const libc::proc_vnodepathinfo) };
1058 let cstr =
1059 unsafe { CStr::from_ptr(info.pvi_cdir.vip_path.as_ptr() as *const libc::c_char) };
1060 cstr.to_str().ok().map(|s| s.to_owned())
1061 }
1062 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1063 {
1064 let _ = pid;
1065 None
1066 }
1067}
1068
1069fn set_qos_user_interactive() {
1075 #[cfg(target_os = "macos")]
1076 {
1077 const QOS_CLASS_USER_INTERACTIVE: libc::c_uint = 0x21;
1078 extern "C" {
1079 fn pthread_set_qos_class_self_np(
1080 qos_class: libc::c_uint,
1081 relative_priority: libc::c_int,
1082 ) -> libc::c_int;
1083 }
1084 unsafe {
1085 pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
1086 }
1087 }
1088}
1089
1090#[allow(clippy::too_many_arguments)]
1091fn spawn_pty(
1092 shell: &str,
1093 rows: u16,
1094 cols: u16,
1095 id: u16,
1096 tag: &str,
1097 command: Option<&str>,
1098 argv: Option<&[&str]>,
1099 dir: Option<&str>,
1100 scrollback: usize,
1101 state: AppState,
1102) -> Option<Pty> {
1103 let mut master: libc::c_int = 0;
1104 let mut slave: libc::c_int = 0;
1105 unsafe {
1106 if libc::openpty(
1107 &mut master,
1108 &mut slave,
1109 std::ptr::null_mut(),
1110 std::ptr::null_mut(),
1111 std::ptr::null_mut(),
1112 ) != 0
1113 {
1114 eprintln!("openpty failed for pty {id}");
1115 return None;
1116 }
1117 let ws = libc::winsize {
1118 ws_row: rows,
1119 ws_col: cols,
1120 ws_xpixel: 0,
1121 ws_ypixel: 0,
1122 };
1123 libc::ioctl(master, libc::TIOCSWINSZ, &ws);
1124 }
1125
1126 let pid = unsafe { libc::fork() };
1127 if pid < 0 {
1128 eprintln!("fork failed for pty {id}");
1129 unsafe {
1130 libc::close(master);
1131 libc::close(slave);
1132 }
1133 return None;
1134 }
1135
1136 if pid == 0 {
1137 unsafe {
1138 libc::close(master);
1139 libc::setsid();
1140 libc::ioctl(slave, libc::TIOCSCTTY as _, 0);
1141 libc::dup2(slave, 0);
1142 libc::dup2(slave, 1);
1143 libc::dup2(slave, 2);
1144 if slave > 2 {
1145 libc::close(slave);
1146 }
1147 }
1148 set_qos_user_interactive();
1149 let effective_dir = dir.map(String::from);
1150 if let Some(d) = effective_dir {
1151 if let Ok(dir_c) = CString::new(d) {
1152 unsafe {
1153 libc::chdir(dir_c.as_ptr());
1154 }
1155 }
1156 }
1157 std::env::set_var("TERM", "xterm-256color");
1158 std::env::set_var("COLORTERM", "truecolor");
1159 std::env::remove_var("COLUMNS");
1162 std::env::remove_var("LINES");
1163 for (key, _) in std::env::vars() {
1164 if key.starts_with("BLIT_") && key != "BLIT_HUB" && key != "BLIT_DISPLAY_FPS" {
1165 std::env::remove_var(&key);
1166 }
1167 }
1168 let shell_flags = &state.0.shell_flags;
1169 if let Some(command) = command {
1170 let shell_c = CString::new(shell).unwrap();
1171 let command_c = CString::new(command).unwrap();
1172 let flag = CString::new(if shell_flags.is_empty() {
1173 "-c".to_owned()
1174 } else {
1175 format!("-{}c", shell_flags)
1176 })
1177 .unwrap();
1178 unsafe {
1179 let p = shell_c.as_ptr();
1180 let f = flag.as_ptr();
1181 let c = command_c.as_ptr();
1182 libc::execvp(p, [p, f, c, std::ptr::null()].as_ptr());
1183 libc::_exit(1);
1184 }
1185 }
1186 if let Some(args) = argv {
1187 if !args.is_empty() {
1188 let cargs: Vec<CString> = args.iter().map(|s| CString::new(*s).unwrap()).collect();
1189 let ptrs: Vec<*const libc::c_char> = cargs
1190 .iter()
1191 .map(|c| c.as_ptr())
1192 .chain(std::iter::once(std::ptr::null()))
1193 .collect();
1194 unsafe {
1195 libc::execvp(ptrs[0], ptrs.as_ptr());
1196 libc::_exit(1);
1197 }
1198 }
1199 }
1200 let shell_c = CString::new(shell).unwrap();
1201 unsafe {
1202 if shell_flags.is_empty() {
1203 let p = shell_c.as_ptr();
1204 libc::execvp(p, [p, std::ptr::null()].as_ptr());
1205 } else {
1206 let flag = CString::new(format!("-{}", shell_flags)).unwrap();
1207 let p = shell_c.as_ptr();
1208 let f = flag.as_ptr();
1209 libc::execvp(p, [p, f, std::ptr::null()].as_ptr());
1210 }
1211 libc::_exit(1);
1212 }
1213 }
1214
1215 unsafe {
1216 libc::close(slave);
1217 let flags = libc::fcntl(master, libc::F_GETFL);
1218 libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
1219 }
1220
1221 unsafe {
1222 libc::close(slave);
1223 let flags = libc::fcntl(master, libc::F_GETFL);
1224 libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
1225 }
1226
1227 state.2.write().unwrap().insert(id, master);
1228 let (byte_tx, byte_rx) = mpsc::channel(PTY_CHANNEL_CAPACITY);
1229 let reader_handle = std::thread::spawn({
1230 let notify = state.3.clone();
1231 move || pty_reader(master, byte_tx, notify)
1232 });
1233 let lflag_cache = pty_lflag(master);
1234
1235 Some(Pty {
1236 master_fd: master,
1237 child_pid: pid,
1238 driver: Box::new(AlacrittyDriver::new(rows, cols, scrollback)),
1239 tag: tag.to_owned(),
1240 dirty: true,
1241 ready_frames: VecDeque::new(),
1242 byte_rx,
1243 reader_handle,
1244 lflag_cache,
1245 lflag_last: Instant::now(),
1246 last_title_send: Instant::now(),
1247 title_pending: false,
1248 exited: false,
1249 exit_status: blit_remote::EXIT_STATUS_UNKNOWN,
1250 command: command.map(|s| s.to_owned()),
1251 })
1252}
1253
1254fn respawn_child(
1257 shell: &str,
1258 rows: u16,
1259 cols: u16,
1260 pty_id: u16,
1261 command: Option<&str>,
1262 state: AppState,
1263) -> Option<(libc::c_int, libc::pid_t, std::thread::JoinHandle<()>, mpsc::Receiver<PtyInput>)> {
1264 let mut master: libc::c_int = 0;
1265 let mut slave: libc::c_int = 0;
1266 unsafe {
1267 if libc::openpty(
1268 &mut master,
1269 &mut slave,
1270 std::ptr::null_mut(),
1271 std::ptr::null_mut(),
1272 std::ptr::null_mut(),
1273 ) != 0
1274 {
1275 return None;
1276 }
1277 let ws = libc::winsize {
1278 ws_row: rows,
1279 ws_col: cols,
1280 ws_xpixel: 0,
1281 ws_ypixel: 0,
1282 };
1283 libc::ioctl(master, libc::TIOCSWINSZ, &ws);
1284 }
1285
1286 let pid = unsafe { libc::fork() };
1287 if pid < 0 {
1288 unsafe {
1289 libc::close(master);
1290 libc::close(slave);
1291 }
1292 return None;
1293 }
1294 if pid == 0 {
1295 unsafe {
1296 libc::close(master);
1297 libc::setsid();
1298 libc::ioctl(slave, libc::TIOCSCTTY as _, 0);
1299 libc::dup2(slave, 0);
1300 libc::dup2(slave, 1);
1301 libc::dup2(slave, 2);
1302 if slave > 2 {
1303 libc::close(slave);
1304 }
1305 }
1306 set_qos_user_interactive();
1307 std::env::set_var("TERM", "xterm-256color");
1308 std::env::set_var("COLORTERM", "truecolor");
1309 std::env::remove_var("COLUMNS");
1310 std::env::remove_var("LINES");
1311 for (key, _) in std::env::vars() {
1312 if key.starts_with("BLIT_") && key != "BLIT_HUB" && key != "BLIT_DISPLAY_FPS" {
1313 std::env::remove_var(&key);
1314 }
1315 }
1316 let shell_flags = &state.0.shell_flags;
1317 if let Some(cmd) = command {
1318 let shell_c = CString::new(shell).unwrap();
1319 let flag = CString::new(if shell_flags.is_empty() {
1320 "-c".to_owned()
1321 } else {
1322 format!("-{}c", shell_flags)
1323 })
1324 .unwrap();
1325 let cmd_c = CString::new(cmd).unwrap();
1326 unsafe {
1327 libc::execvp(
1328 shell_c.as_ptr(),
1329 [
1330 shell_c.as_ptr(),
1331 flag.as_ptr(),
1332 cmd_c.as_ptr(),
1333 std::ptr::null(),
1334 ]
1335 .as_ptr(),
1336 );
1337 libc::_exit(1);
1338 }
1339 }
1340 let shell_c = CString::new(shell).unwrap();
1341 unsafe {
1342 if shell_flags.is_empty() {
1343 let p = shell_c.as_ptr();
1344 libc::execvp(p, [p, std::ptr::null()].as_ptr());
1345 } else {
1346 let flag = CString::new(format!("-{}", shell_flags)).unwrap();
1347 let p = shell_c.as_ptr();
1348 let f = flag.as_ptr();
1349 libc::execvp(p, [p, f, std::ptr::null()].as_ptr());
1350 }
1351 libc::_exit(1);
1352 }
1353 }
1354
1355 unsafe {
1356 libc::close(slave);
1357 let flags = libc::fcntl(master, libc::F_GETFL);
1358 libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
1359 }
1360
1361 state.2.write().unwrap().insert(pty_id, master);
1362 let (byte_tx, byte_rx) = mpsc::channel(PTY_CHANNEL_CAPACITY);
1363 let reader_handle = std::thread::spawn({
1364 let notify = state.3.clone();
1365 move || pty_reader(master, byte_tx, notify)
1366 });
1367 Some((master, pid, reader_handle, byte_rx))
1368}
1369
1370fn respond_to_queries(fd: libc::c_int, data: &[u8], size: (u16, u16), cursor: (u16, u16)) {
1371 const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1373
1374 let mut i = 0;
1375 while i < data.len() {
1376 if data[i] != 0x1b || i + 2 >= data.len() || data[i + 1] != b'[' {
1377 i += 1;
1378 continue;
1379 }
1380 i += 2;
1381 let has_q = i < data.len() && data[i] == b'?';
1382 if has_q {
1383 i += 1;
1384 }
1385 let param_start = i;
1386 while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1387 i += 1;
1388 }
1389 if i >= data.len() {
1390 break;
1391 }
1392 let final_byte = data[i];
1393 let params = &data[param_start..i];
1394 i += 1;
1395 if has_q {
1396 continue;
1397 }
1398 let resp: Option<String> = match final_byte {
1399 b'c' if params.is_empty() || params == b"0" => {
1400 Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1401 }
1402 b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1403 b'n' if params == b"5" => Some("\x1b[0n".into()),
1404 b't' if params == b"18" => {
1405 let (rows, cols) = size;
1406 Some(format!("\x1b[8;{rows};{cols}t"))
1407 }
1408 b't' if params == b"14" => {
1409 let (rows, cols) = size;
1410 Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
1411 }
1412 _ => None,
1413 };
1414 if let Some(r) = resp {
1415 pty_write_all(fd, r.as_bytes());
1416 }
1417 }
1418}
1419
1420fn pty_reader(
1421 fd: libc::c_int,
1422 tx: mpsc::Sender<PtyInput>,
1423 notify: Arc<Notify>,
1424) {
1425 unsafe {
1434 let flags = libc::fcntl(fd, libc::F_GETFL);
1435 libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK);
1436 }
1437
1438 let mut buf = vec![0u8; 64 * 1024];
1439 let mut sync_scan_tail = Vec::new();
1440
1441 loop {
1442 let n = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };
1443 if n > 0 {
1444 let data = buf[..n as usize].to_vec();
1445 let mut remaining = data;
1446 loop {
1447 if remaining.is_empty() {
1448 break;
1449 }
1450 if let Some(boundary) = find_sync_output_end(&sync_scan_tail, &remaining) {
1451 let before = remaining[..boundary].to_vec();
1452 let after = remaining[boundary..].to_vec();
1453 update_sync_scan_tail(&mut sync_scan_tail, &before);
1454 if tx.blocking_send(PtyInput::SyncBoundary { before, after: after.clone() }).is_err() {
1455 return;
1456 }
1457 notify.notify_one();
1458 remaining = after;
1459 } else {
1460 update_sync_scan_tail(&mut sync_scan_tail, &remaining);
1461 if tx.blocking_send(PtyInput::Data(remaining)).is_err() {
1462 return;
1463 }
1464 notify.notify_one();
1465 break;
1466 }
1467 }
1468 } else {
1469 let _ = tx.blocking_send(PtyInput::Eof);
1470 notify.notify_one();
1471 return;
1472 }
1473 }
1474}
1475
1476async fn cleanup_pty(pty_id: u16, state: &AppState) {
1478 state.2.write().unwrap().remove(&pty_id);
1480 let mut sess = state.1.lock().await;
1481 if let Some(pty) = sess.ptys.get_mut(&pty_id) {
1482 if pty.exited {
1483 return;
1484 }
1485 pty.exited = true;
1486
1487 unsafe {
1488 libc::kill(pty.child_pid, libc::SIGHUP);
1489 libc::close(pty.master_fd);
1490 let mut wstatus: libc::c_int = 0;
1491 if libc::waitpid(pty.child_pid, &mut wstatus, libc::WNOHANG) > 0 {
1492 if libc::WIFEXITED(wstatus) {
1493 pty.exit_status = libc::WEXITSTATUS(wstatus);
1494 } else if libc::WIFSIGNALED(wstatus) {
1495 pty.exit_status = -(libc::WTERMSIG(wstatus) as i32);
1496 }
1497 }
1498 }
1499 pty.mark_dirty();
1500 let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
1501 sess.send_to_all(&msg);
1502 }
1503}
1504
1505fn pty_lflag(fd: libc::c_int) -> (bool, bool) {
1506 unsafe {
1507 let mut termios: libc::termios = std::mem::zeroed();
1508 if libc::tcgetattr(fd, &mut termios) == 0 {
1509 (
1510 termios.c_lflag & libc::ECHO != 0,
1511 termios.c_lflag & libc::ICANON != 0,
1512 )
1513 } else {
1514 (false, false)
1515 }
1516 }
1517}
1518
1519fn take_snapshot(pty: &mut Pty) -> FrameState {
1520 if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
1521 pty.lflag_cache = pty_lflag(pty.master_fd);
1522 pty.lflag_last = Instant::now();
1523 }
1524 let (echo, icanon) = pty.lflag_cache;
1525 pty.driver.snapshot(echo, icanon)
1526}
1527
1528fn build_scrollback_update(
1529 pty: &mut Pty,
1530 id: u16,
1531 offset: usize,
1532 prev_frame: &FrameState,
1533) -> Option<(Vec<u8>, FrameState)> {
1534 let frame = pty.driver.scrollback_frame(offset);
1535 let msg = build_update_msg(id, &frame, prev_frame);
1536 msg.map(|m| (m, frame))
1537}
1538
1539fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
1540 let count = results.len().min(u16::MAX as usize);
1541 let payload_bytes: usize = results[..count]
1542 .iter()
1543 .map(|result| 14 + result.context.len().min(u16::MAX as usize))
1544 .sum();
1545 let mut msg = Vec::with_capacity(5 + payload_bytes);
1546 msg.push(S2C_SEARCH_RESULTS);
1547 msg.extend_from_slice(&request_id.to_le_bytes());
1548 msg.extend_from_slice(&(count as u16).to_le_bytes());
1549 for result in &results[..count] {
1550 msg.extend_from_slice(&result.pty_id.to_le_bytes());
1551 msg.extend_from_slice(&result.score.to_le_bytes());
1552 msg.push(result.primary_source);
1553 msg.push(result.matched_sources);
1554 let scroll_offset = result
1555 .scroll_offset
1556 .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
1557 .unwrap_or(u32::MAX);
1558 msg.extend_from_slice(&scroll_offset.to_le_bytes());
1559 let context = result.context.as_bytes();
1560 let context_len = context.len().min(u16::MAX as usize);
1561 msg.extend_from_slice(&(context_len as u16).to_le_bytes());
1562 msg.extend_from_slice(&context[..context_len]);
1563 }
1564 msg
1565}
1566
1567enum SendOutcome {
1568 NoChange,
1569 Sent,
1570 Backpressured,
1571}
1572
1573fn try_send_update(
1574 client: &mut ClientState,
1575 pid: u16,
1576 current: FrameState,
1577 msg: Option<Vec<u8>>,
1578 now: Instant,
1579 paced: bool,
1580) -> SendOutcome {
1581 let Some(msg) = msg else {
1582 return SendOutcome::NoChange;
1583 };
1584 let bytes = msg.len();
1585 if client.tx.try_send(msg).is_ok() {
1586 client.last_sent.insert(pid, current);
1587 record_send(client, bytes, now, paced);
1588 client.frames_sent = client.frames_sent.wrapping_add(1);
1589 SendOutcome::Sent
1590 } else {
1591 client.last_sent.insert(pid, current);
1597 SendOutcome::Backpressured
1598 }
1599}
1600
1601pub fn default_socket_path() -> String {
1602 if let Ok(dir) = std::env::var("TMPDIR") {
1603 return format!("{dir}/blit.sock");
1604 }
1605 if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
1606 return format!("{dir}/blit.sock");
1607 }
1608 if let Ok(user) = std::env::var("USER") {
1609 return format!("/tmp/blit-{user}.sock");
1610 }
1611 "/tmp/blit.sock".into()
1612}
1613
1614
1615
1616enum RecvFdResult {
1617 Fd(RawFd),
1618 WouldBlock,
1619 Closed,
1620}
1621
1622fn recv_fd(channel: RawFd) -> RecvFdResult {
1623 unsafe {
1624 let mut buf = [0u8; 1];
1625 let mut iov = libc::iovec {
1626 iov_base: buf.as_mut_ptr() as *mut libc::c_void,
1627 iov_len: buf.len(),
1628 };
1629 let cmsg_space = libc::CMSG_SPACE(std::mem::size_of::<RawFd>() as u32) as usize;
1630 let mut cmsg_buf = vec![0u8; cmsg_space];
1631 let mut msg: libc::msghdr = std::mem::zeroed();
1632 msg.msg_iov = &mut iov;
1633 msg.msg_iovlen = 1;
1634 msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
1635 msg.msg_controllen = cmsg_space as _;
1636 let n = libc::recvmsg(channel, &mut msg, libc::MSG_DONTWAIT);
1637 if n < 0 {
1638 let err = std::io::Error::last_os_error();
1639 if err.kind() == std::io::ErrorKind::WouldBlock {
1640 return RecvFdResult::WouldBlock;
1641 }
1642 if err.raw_os_error() == Some(libc::EINTR) {
1643 return RecvFdResult::WouldBlock;
1644 }
1645 return RecvFdResult::Closed;
1646 }
1647 if n == 0 {
1648 return RecvFdResult::Closed;
1649 }
1650 let cmsg = libc::CMSG_FIRSTHDR(&msg);
1651 if cmsg.is_null() {
1652 return RecvFdResult::Closed;
1653 }
1654 if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS {
1655 let fd_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
1656 RecvFdResult::Fd(std::ptr::read_unaligned(fd_ptr))
1657 } else {
1658 RecvFdResult::Closed
1659 }
1660 }
1661}
1662
1663fn bind_socket(sock_path: &str, verbose: bool) -> UnixListener {
1664 let _ = std::fs::remove_file(sock_path);
1665 let listener = UnixListener::bind(sock_path).unwrap_or_else(|e| {
1666 eprintln!("blit-server: cannot bind to {sock_path}: {e}");
1667 std::process::exit(1);
1668 });
1669 if let Err(e) = std::fs::set_permissions(sock_path, std::fs::Permissions::from_mode(0o700)) {
1670 eprintln!("blit-server: warning: cannot set socket permissions: {e}");
1671 }
1672 if verbose {
1673 eprintln!("listening on {sock_path}");
1674 }
1675 listener
1676}
1677
1678pub async fn run(config: Config) {
1679 let state: AppState = Arc::new((
1680 config,
1681 Mutex::new(Session::new()),
1682 Arc::new(std::sync::RwLock::new(HashMap::new())),
1683 Arc::new(Notify::new()),
1684 ));
1685
1686 let delivery_state = state.clone();
1687 tokio::spawn(async move {
1688 let mut next_deadline: Option<Instant> = None;
1689 loop {
1690 if let Some(deadline) = next_deadline {
1691 tokio::select! {
1692 _ = delivery_state.3.notified() => {}
1693 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
1694 }
1695 } else {
1696 delivery_state.3.notified().await;
1697 }
1698 loop {
1699 let outcome = tick(&delivery_state).await;
1700 next_deadline = outcome.next_deadline;
1701 if !outcome.did_work {
1702 break;
1703 }
1704 tokio::task::yield_now().await;
1705 }
1706 }
1707 });
1708
1709 tokio::spawn(async {
1710 loop {
1711 tokio::time::sleep(Duration::from_secs(5)).await;
1712 unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
1713 }
1714 });
1715
1716 if let Some(channel_fd) = state.0.fd_channel {
1717 use std::os::unix::io::FromRawFd;
1718 if state.0.verbose {
1719 eprintln!("accepting clients via fd-channel (fd {channel_fd})");
1720 }
1721 let channel = unsafe { std::os::unix::net::UnixStream::from_raw_fd(channel_fd) };
1722 channel.set_nonblocking(true).unwrap();
1723 let async_channel = AsyncFd::new(channel).unwrap();
1724 loop {
1725 let mut guard = match async_channel.readable().await {
1726 Ok(g) => g,
1727 Err(e) => {
1728 eprintln!("fd-channel error: {e}");
1729 break;
1730 }
1731 };
1732 match recv_fd(channel_fd) {
1733 RecvFdResult::Fd(client_fd) => {
1734 let std_stream =
1735 unsafe { std::os::unix::net::UnixStream::from_raw_fd(client_fd) };
1736 std_stream.set_nonblocking(true).unwrap();
1737 let stream = tokio::net::UnixStream::from_std(std_stream).unwrap();
1738 let state = state.clone();
1739 tokio::spawn(handle_client(stream, state));
1740 guard.retain_ready();
1741 }
1742 RecvFdResult::WouldBlock => {
1743 guard.clear_ready();
1744 }
1745 RecvFdResult::Closed => {
1746 break;
1747 }
1748 }
1749 }
1750 if state.0.verbose {
1751 eprintln!("fd-channel closed, shutting down");
1752 }
1753 return;
1754 }
1755
1756 let listener = if let Ok(fds) = std::env::var("LISTEN_FDS") {
1760 if fds.trim() == "1" {
1761 use std::os::unix::io::FromRawFd;
1762 let std_listener = unsafe { std::os::unix::net::UnixListener::from_raw_fd(3) };
1763 std_listener.set_nonblocking(true).unwrap();
1764 if state.0.verbose {
1765 eprintln!("using socket activation (fd 3)");
1766 }
1767 UnixListener::from_std(std_listener).unwrap()
1768 } else {
1769 if state.0.verbose {
1770 eprintln!("LISTEN_FDS={fds}, expected 1; falling back to bind");
1771 }
1772 bind_socket(&state.0.socket_path, state.0.verbose)
1773 }
1774 } else {
1775 bind_socket(&state.0.socket_path, state.0.verbose)
1776 };
1777
1778 loop {
1779 let (stream, _) = match listener.accept().await {
1780 Ok(conn) => conn,
1781 Err(e) => {
1782 eprintln!("accept error: {e}");
1783 tokio::time::sleep(Duration::from_millis(100)).await;
1784 continue;
1785 }
1786 };
1787 let state = state.clone();
1788 tokio::spawn(handle_client(stream, state));
1789 }
1790}
1791
1792async fn tick(state: &AppState) -> TickOutcome {
1793 let mut sess = state.1.lock().await;
1794 sess.tick_fires += 1;
1795 let mut did_work = false;
1796 let mut next_deadline: Option<Instant> = None;
1797 let now = Instant::now();
1798
1799 let max_fps = sess
1800 .clients
1801 .values()
1802 .map(browser_pacing_fps)
1803 .fold(1.0_f32, f32::max);
1804 let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
1805 let ids: Vec<u16> = sess.ptys.keys().copied().collect();
1806 for &id in &ids {
1807 let Some(pty) = sess.ptys.get_mut(&id) else {
1808 continue;
1809 };
1810 if pty.driver.take_title_dirty() {
1811 pty.mark_dirty();
1812 pty.title_pending = true;
1813 }
1814 if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
1815 let msg = {
1816 let title_bytes = pty.driver.title().as_bytes();
1817 let mut msg = Vec::with_capacity(3 + title_bytes.len());
1818 msg.push(S2C_TITLE);
1819 msg.extend_from_slice(&id.to_le_bytes());
1820 msg.extend_from_slice(title_bytes);
1821 msg
1822 };
1823 pty.last_title_send = now;
1824 pty.title_pending = false;
1825 sess.send_to_all(&msg);
1826 did_work = true;
1827 }
1828 }
1829
1830 let mut eof_ptys: Vec<u16> = Vec::new();
1833 for &id in &ids {
1834 let Some(pty) = sess.ptys.get_mut(&id) else {
1835 continue;
1836 };
1837 while let Ok(input) = pty.byte_rx.try_recv() {
1838 match input {
1839 PtyInput::Data(data) => {
1840 respond_to_queries(
1841 pty.master_fd,
1842 &data,
1843 pty.driver.size(),
1844 pty.driver.cursor_position(),
1845 );
1846 pty.driver.process(&data);
1847 pty.mark_dirty();
1848 did_work = true;
1849 }
1850 PtyInput::SyncBoundary { before, after } => {
1851 if !before.is_empty() {
1852 respond_to_queries(
1853 pty.master_fd,
1854 &before,
1855 pty.driver.size(),
1856 pty.driver.cursor_position(),
1857 );
1858 pty.driver.process(&before);
1859 pty.mark_dirty();
1860 }
1861 if !pty.driver.synced_output() {
1862 let frame = take_snapshot(pty);
1863 enqueue_ready_frame(&mut pty.ready_frames, frame);
1864 pty.clear_dirty();
1865 }
1866 if !after.is_empty() {
1867 respond_to_queries(
1868 pty.master_fd,
1869 &after,
1870 pty.driver.size(),
1871 pty.driver.cursor_position(),
1872 );
1873 pty.driver.process(&after);
1874 pty.mark_dirty();
1875 }
1876 did_work = true;
1877 }
1878 PtyInput::Eof => {
1879 eof_ptys.push(id);
1880 }
1881 }
1882 }
1883 }
1884 drop(sess);
1886 for id in eof_ptys {
1887 tokio::time::sleep(Duration::from_millis(50)).await;
1888 cleanup_pty(id, state).await;
1889 }
1890 let mut sess = state.1.lock().await;
1891
1892 let needful_ptys: HashSet<u16> = sess
1896 .clients
1897 .values()
1898 .flat_map(|c| {
1899 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1900 c.subscriptions.iter().copied().filter(move |pid| {
1901 let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
1902 if Some(*pid) == c.lead {
1903 !scrolled && can_send_frame(c, now, reserve_preview_slot)
1904 } else {
1905 !scrolled && can_send_preview(c, *pid, now)
1906 }
1907 })
1908 })
1909 .collect();
1910
1911 let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
1912 for &id in &ids {
1913 let Some(pty) = sess.ptys.get_mut(&id) else {
1914 continue;
1915 };
1916 if needful_ptys.contains(&id) {
1917 if let Some(frame) = pty.ready_frames.pop_front() {
1918 snapshots.insert(id, frame);
1919 sess.tick_snaps += 1;
1920 did_work = true;
1921 continue;
1922 }
1923 }
1924 if !should_snapshot_pty(
1925 pty.dirty,
1926 needful_ptys.contains(&id),
1927 pty.driver.synced_output(),
1928 ) {
1929 continue;
1930 }
1931 snapshots.insert(id, take_snapshot(pty));
1935 pty.clear_dirty();
1936 sess.tick_snaps += 1;
1937 did_work = true;
1938 }
1939
1940 let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
1941 for cid in client_ids {
1942 if let Some(c) = sess.clients.get_mut(&cid) {
1948 if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
1949 c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
1950 }
1951 if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
1954 c.browser_backlog_frames = 0;
1955 c.browser_ack_ahead_frames = 0;
1956 }
1957 }
1958 let (
1959 lead,
1960 subscriptions,
1961 scrolled_ptys,
1962 can_send_lead,
1963 lead_has_window,
1964 any_send_window,
1965 lead_deadline,
1966 ) = {
1967 let Some(c) = sess.clients.get(&cid) else {
1968 continue;
1969 };
1970 let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1971 (
1972 c.lead,
1973 c.subscriptions.iter().copied().collect::<Vec<_>>(),
1974 c.scroll_offsets.iter().map(|(&k, &v)| (k, v)).collect::<Vec<_>>(),
1975 can_send_frame(c, now, reserve_preview_slot),
1976 lead_window_open(c, reserve_preview_slot),
1977 lead_window_open(c, reserve_preview_slot) || window_open(c),
1978 c.next_send_at,
1979 )
1980 };
1981
1982 if subscriptions.is_empty() {
1983 continue;
1984 }
1985
1986 for &(scroll_pid, scroll_offset) in &scrolled_ptys {
1988 if scroll_offset == 0 { continue; }
1989 let is_lead = lead == Some(scroll_pid);
1990 let can_send = if is_lead { can_send_lead } else { true };
1991 if can_send {
1992 let prev_frame = {
1993 let Some(c) = sess.clients.get(&cid) else {
1994 continue;
1995 };
1996 c.scroll_caches.get(&scroll_pid).cloned().unwrap_or_default()
1997 };
1998 let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1999 if let Some((msg, new_frame)) =
2000 build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
2001 {
2002 let Some(c) = sess.clients.get_mut(&cid) else {
2003 break;
2004 };
2005 let bytes = msg.len();
2006 if c.tx.try_send(msg).is_ok() {
2007 c.scroll_caches.insert(scroll_pid, new_frame);
2008 record_send(c, bytes, now, is_lead);
2009 c.frames_sent += 1;
2010 SendOutcome::Sent
2011 } else {
2012 SendOutcome::Backpressured
2013 }
2014 } else {
2015 SendOutcome::NoChange
2016 }
2017 } else {
2018 SendOutcome::NoChange
2019 };
2020 match outcome {
2021 SendOutcome::Sent => did_work = true,
2022 SendOutcome::Backpressured => {
2023 if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
2024 pty.mark_dirty();
2025 }
2026 }
2027 SendOutcome::NoChange => {}
2028 }
2029 } else if is_lead && lead_has_window {
2030 next_deadline = Some(match next_deadline {
2031 Some(existing) => existing.min(lead_deadline),
2032 None => lead_deadline,
2033 });
2034 }
2035 }
2036
2037 let lead_scroll_offset = lead.and_then(|pid| scrolled_ptys.iter().find(|&&(k, _)| k == pid).map(|&(_, v)| v)).unwrap_or(0);
2038
2039 if let Some(pid) = lead {
2040 if lead_scroll_offset == 0 && can_send_lead {
2041 if let Some(cur) = snapshots.get(&pid).cloned() {
2042 let previous = sess
2043 .clients
2044 .get(&cid)
2045 .and_then(|c| c.last_sent.get(&pid).cloned())
2046 .unwrap_or_default();
2047 drop(sess);
2048 let msg = build_update_msg(pid, &cur, &previous);
2049 sess = state.1.lock().await;
2050 let Some(c) = sess.clients.get_mut(&cid) else {
2051 continue;
2052 };
2053 match try_send_update(c, pid, cur, msg, now, true) {
2054 SendOutcome::Sent => did_work = true,
2055 SendOutcome::Backpressured => {
2056 if let Some(pty) = sess.ptys.get_mut(&pid) {
2057 pty.mark_dirty();
2058 }
2059 }
2060 SendOutcome::NoChange => {}
2061 }
2062 } else {
2063 let has_pending = sess
2064 .ptys
2065 .get(&pid)
2066 .map(pty_has_visual_update)
2067 .unwrap_or(false);
2068 let _ = has_pending;
2069 }
2070 } else {
2071 let has_pending = sess
2072 .ptys
2073 .get(&pid)
2074 .map(pty_has_visual_update)
2075 .unwrap_or(false);
2076 if has_pending && lead_has_window {
2077 next_deadline = Some(match next_deadline {
2078 Some(existing) => existing.min(lead_deadline),
2079 None => lead_deadline,
2080 });
2081 }
2082 }
2083 }
2084
2085 if !any_send_window {
2086 continue;
2087 }
2088
2089 let mut preview_ids = subscriptions;
2090 preview_ids.retain(|pid| Some(*pid) != lead);
2091 preview_ids.sort_unstable();
2092
2093 for pid in preview_ids {
2094 let (preview_can_send, preview_due_at, preview_has_window) =
2095 match sess.clients.get(&cid) {
2096 Some(c) => (
2097 can_send_preview(c, pid, now),
2098 preview_deadline(c, pid, now),
2099 window_open(c),
2100 ),
2101 None => (false, now, false),
2102 };
2103 if !preview_has_window {
2104 break;
2105 }
2106 if !preview_can_send {
2107 let has_pending = sess
2108 .ptys
2109 .get(&pid)
2110 .map(pty_has_visual_update)
2111 .unwrap_or(false);
2112 if has_pending && preview_due_at > now {
2117 next_deadline = Some(match next_deadline {
2118 Some(existing) => existing.min(preview_due_at),
2119 None => preview_due_at,
2120 });
2121 }
2122 continue;
2123 }
2124 let Some(cur) = snapshots.get(&pid) else {
2125 let has_pending = sess
2126 .ptys
2127 .get(&pid)
2128 .map(pty_has_visual_update)
2129 .unwrap_or(false);
2130 let _ = has_pending;
2131 continue;
2132 };
2133 let cur = cur.clone();
2134 let previous = sess
2135 .clients
2136 .get(&cid)
2137 .and_then(|c| c.last_sent.get(&pid).cloned())
2138 .unwrap_or_default();
2139 drop(sess);
2140 let msg = build_update_msg(pid, &cur, &previous);
2141 sess = state.1.lock().await;
2142 let Some(c) = sess.clients.get_mut(&cid) else {
2143 break;
2144 };
2145 match try_send_update(c, pid, cur, msg, now, false) {
2146 SendOutcome::Sent => {
2147 record_preview_send(c, pid, now);
2148 did_work = true;
2149 }
2150 SendOutcome::Backpressured => {
2151 if let Some(pty) = sess.ptys.get_mut(&pid) {
2152 pty.mark_dirty();
2153 }
2154 break;
2155 }
2156 SendOutcome::NoChange => {}
2157 }
2158 }
2159 }
2160
2161 TickOutcome {
2162 did_work,
2163 next_deadline,
2164 }
2165}
2166
2167async fn handle_client(stream: tokio::net::UnixStream, state: AppState) {
2168 let config = &state.0;
2169 let (mut reader, mut writer) = stream.into_split();
2170
2171 let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(OUTBOX_CAPACITY);
2172 let sender = tokio::spawn(async move {
2173 while let Some(msg) = out_rx.recv().await {
2174 if !write_frame(&mut writer, &msg).await {
2175 break;
2176 }
2177 }
2178 });
2179 let client_id;
2180
2181 {
2182 let mut sess = state.1.lock().await;
2183 client_id = sess.next_client_id;
2184 sess.next_client_id += 1;
2185 sess.clients.insert(
2186 client_id,
2187 ClientState {
2188 tx: out_tx,
2189 lead: None,
2190 subscriptions: HashSet::new(),
2191 view_sizes: HashMap::new(),
2192 scroll_offsets: HashMap::new(),
2193 scroll_caches: HashMap::new(),
2194 last_sent: HashMap::new(),
2195 preview_next_send_at: HashMap::new(),
2196 rtt_ms: 50.0,
2197 min_rtt_ms: 0.0,
2198 display_fps: 60.0,
2199 delivery_bps: 262_144.0,
2204 goodput_bps: 262_144.0,
2205 goodput_jitter_bps: 0.0,
2206 max_goodput_jitter_bps: 0.0,
2207 last_goodput_sample_bps: 0.0,
2208 avg_frame_bytes: 1_024.0,
2209 avg_paced_frame_bytes: 1_024.0,
2210 avg_preview_frame_bytes: 1_024.0,
2211 inflight_bytes: 0,
2212 inflight_frames: VecDeque::new(),
2213 next_send_at: Instant::now(),
2214 probe_frames: 0.0,
2215 frames_sent: 0,
2216 acks_recv: 0,
2217 acked_bytes_since_log: 0,
2218 browser_backlog_frames: 0,
2219 browser_ack_ahead_frames: 0,
2220 browser_apply_ms: 0.0,
2221 last_metrics_update: Instant::now(),
2222 last_log: Instant::now(),
2223 goodput_window_bytes: 0,
2224 goodput_window_start: Instant::now(),
2225 },
2226 );
2227 if let Some(c) = sess.clients.get(&client_id) {
2228 let _ = c.tx.try_send(msg_hello(
2229 1,
2230 FEATURE_CREATE_NONCE | FEATURE_RESTART | FEATURE_RESIZE_BATCH | FEATURE_COPY_RANGE,
2231 ));
2232 }
2233 let mut initial_msgs = Vec::new();
2234 initial_msgs.push(sess.pty_list_msg());
2235 for (&id, pty) in &sess.ptys {
2236 let title = pty.driver.title();
2237 if !title.is_empty() {
2238 let title_bytes = title.as_bytes();
2239 let mut msg = Vec::with_capacity(3 + title_bytes.len());
2240 msg.push(S2C_TITLE);
2241 msg.extend_from_slice(&id.to_le_bytes());
2242 msg.extend_from_slice(title_bytes);
2243 initial_msgs.push(msg);
2244 }
2245 if pty.exited {
2246 initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
2247 }
2248 }
2249 initial_msgs.push(vec![S2C_READY]);
2250 let tx = sess.clients.get(&client_id).map(|c| c.tx.clone());
2251 drop(sess);
2252 if let Some(tx) = tx {
2253 for msg in initial_msgs {
2254 if tx.send(msg).await.is_err() {
2255 break;
2256 }
2257 }
2258 }
2259 }
2260
2261 if state.0.verbose {
2262 eprintln!("client connected");
2263 }
2264
2265 while let Some(data) = read_frame(&mut reader).await {
2266 if data.is_empty() {
2267 continue;
2268 }
2269
2270 if data[0] == C2S_ACK {
2271 let mut sess = state.1.lock().await;
2272 let (
2273 do_log,
2274 frames_sent,
2275 acks_recv,
2276 rtt_ms,
2277 min_rtt_ms,
2278 eff_rtt_ms,
2279 inflight_bytes,
2280 delivery_bps,
2281 goodput_ewma_bps,
2282 goodput_jitter_bps,
2283 max_goodput_jitter_bps,
2284 avg_frame_bytes,
2285 avg_paced_frame_bytes,
2286 avg_preview_frame_bytes,
2287 display_fps,
2288 paced_fps,
2289 display_need_bps,
2290 probe_frames,
2291 goodput_bps,
2292 window_frames,
2293 window_bytes,
2294 outbox_frames,
2295 browser_backlog_frames,
2296 browser_ack_ahead_frames,
2297 browser_apply_ms,
2298 ) = {
2299 let Some(c) = sess.clients.get_mut(&client_id) else {
2300 continue;
2301 };
2302 c.acks_recv += 1;
2303 record_ack(c);
2304 let do_log = c.last_log.elapsed().as_secs_f32() >= 1.0;
2305 let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
2306 let paced_fps = pacing_fps(c);
2307 let display_need_bps = display_need_bps(c);
2308 let out = (
2309 do_log,
2310 c.frames_sent,
2311 c.acks_recv,
2312 c.rtt_ms,
2313 path_rtt_ms(c),
2314 window_rtt_ms(c),
2315 c.inflight_bytes,
2316 c.delivery_bps,
2317 c.goodput_bps,
2318 c.goodput_jitter_bps,
2319 c.max_goodput_jitter_bps,
2320 c.avg_frame_bytes,
2321 c.avg_paced_frame_bytes,
2322 c.avg_preview_frame_bytes,
2323 c.display_fps,
2324 paced_fps,
2325 display_need_bps,
2326 c.probe_frames,
2327 c.acked_bytes_since_log as f32 / log_elapsed,
2328 target_frame_window(c),
2329 target_byte_window(c),
2330 outbox_queued_frames(c),
2331 c.browser_backlog_frames,
2332 c.browser_ack_ahead_frames,
2333 c.browser_apply_ms,
2334 );
2335 if do_log {
2336 c.frames_sent = 0;
2337 c.acks_recv = 0;
2338 c.acked_bytes_since_log = 0;
2339 c.last_log = Instant::now();
2340 }
2341 out
2342 };
2343 if do_log && config.verbose {
2344 eprintln!(
2345 "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={}",
2346 sess.tick_fires, sess.tick_snaps,
2347 );
2348 }
2349 if do_log {
2350 sess.tick_fires = 0;
2351 sess.tick_snaps = 0;
2352 }
2353 nudge_delivery(&state);
2354 continue;
2355 }
2356
2357 if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
2358 let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
2359 if fps > 0.0 {
2360 let mut sess = state.1.lock().await;
2361 if let Some(c) = sess.clients.get_mut(&client_id) {
2362 c.display_fps = fps;
2363 }
2364 }
2365 nudge_delivery(&state);
2366 continue;
2367 }
2368
2369 if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
2370 let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
2371 let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
2372 let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
2373 let mut sess = state.1.lock().await;
2374 if let Some(c) = sess.clients.get_mut(&client_id) {
2375 c.browser_backlog_frames = backlog_frames;
2376 c.browser_ack_ahead_frames = ack_ahead_frames;
2377 c.browser_apply_ms = apply_ms;
2378 c.last_metrics_update = Instant::now();
2379 }
2380 nudge_delivery(&state);
2381 continue;
2382 }
2383
2384 if data[0] == C2S_MOUSE && data.len() >= 9 {
2387 let pid = u16::from_le_bytes([data[1], data[2]]);
2388 let type_ = data[3];
2389 let button = data[4];
2390 let col = u16::from_le_bytes([data[5], data[6]]);
2391 let row = u16::from_le_bytes([data[7], data[8]]);
2392 let sess = state.1.lock().await;
2393 if let Some(pty) = sess.ptys.get(&pid) {
2394 let (echo, icanon) = pty.lflag_cache;
2395 if let Some(seq) = pty
2396 .driver
2397 .mouse_event(type_, button, col, row, echo, icanon)
2398 {
2399 if let Some(&fd) = state.2.read().unwrap().get(&pid) {
2400 pty_write_all(fd, &seq);
2401 }
2402 }
2403 }
2404 continue;
2405 }
2406
2407 if data[0] == C2S_INPUT && data.len() >= 3 {
2408 let pid = u16::from_le_bytes([data[1], data[2]]);
2409 let mut need_nudge = false;
2410 {
2411 let mut sess = state.1.lock().await;
2412 if let Some(c) = sess.clients.get_mut(&client_id) {
2413 if update_client_scroll_state(c, pid, 0) {
2414 if let Some(pty) = sess.ptys.get_mut(&pid) {
2415 pty.mark_dirty();
2416 need_nudge = true;
2417 }
2418 }
2419 }
2420 }
2421 if need_nudge {
2422 nudge_delivery(&state);
2423 }
2424 if let Some(&fd) = state.2.read().unwrap().get(&pid) {
2425 pty_write_all(fd, &data[3..]);
2426 }
2427 continue;
2428 }
2429
2430 if data[0] == C2S_SEARCH && data.len() >= 3 {
2431 let request_id = u16::from_le_bytes([data[1], data[2]]);
2432 let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
2433 let mut sess = state.1.lock().await;
2434 let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
2435 let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
2436 Vec::new()
2437 } else {
2438 sess.ptys
2439 .iter()
2440 .filter_map(|(&pty_id, pty)| {
2441 pty.driver
2442 .search_result(query)
2443 .map(|result| SearchResultRow {
2444 pty_id,
2445 score: result.score,
2446 primary_source: result.primary_source,
2447 matched_sources: result.matched_sources,
2448 context: result.context,
2449 scroll_offset: result.scroll_offset,
2450 })
2451 })
2452 .collect()
2453 };
2454 ranked.sort_by(|a, b| {
2455 b.score
2456 .cmp(&a.score)
2457 .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
2458 .then_with(|| a.pty_id.cmp(&b.pty_id))
2459 });
2460 if let Some(client) = sess.clients.get_mut(&client_id) {
2461 let _ = client
2462 .tx
2463 .try_send(build_search_results_msg(request_id, &ranked));
2464 }
2465 continue;
2466 }
2467
2468 let mut sess = state.1.lock().await;
2469 let mut need_nudge = false;
2470 match data[0] {
2471 C2S_SCROLL if data.len() >= 7 => {
2472 let pid = u16::from_le_bytes([data[1], data[2]]);
2473 let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
2474 if sess.ptys.contains_key(&pid) {
2475 if let Some(c) = sess.clients.get_mut(&client_id) {
2476 update_client_scroll_state(c, pid, offset);
2477 }
2478 if let Some(pty) = sess.ptys.get_mut(&pid) {
2479 pty.mark_dirty();
2480 need_nudge = true;
2481 }
2482 }
2483 }
2484 C2S_RESIZE if data.len() >= 7 => {
2485 let entries = data[1..].chunks_exact(6);
2486 if !entries.remainder().is_empty() {
2487 continue;
2488 }
2489 let mut touched = Vec::new();
2490 for entry in entries {
2491 let pid = u16::from_le_bytes([entry[0], entry[1]]);
2492 if !sess.ptys.contains_key(&pid) {
2493 continue;
2494 }
2495 let rows = u16::from_le_bytes([entry[2], entry[3]]);
2496 let cols = u16::from_le_bytes([entry[4], entry[5]]);
2497 if let Some(c) = sess.clients.get_mut(&client_id) {
2498 if is_unset_view_size(rows, cols) {
2499 if c.view_sizes.remove(&pid).is_some() {
2500 touched.push(pid);
2501 }
2502 } else if rows == 0 || cols == 0 {
2503 continue;
2504 } else {
2505 c.view_sizes.insert(pid, (rows, cols));
2506 touched.push(pid);
2507 }
2508 }
2509 }
2510 if sess.resize_ptys_to_mediated_sizes(touched) {
2511 need_nudge = true;
2512 }
2513 }
2514 C2S_CREATE => {
2515 let (rows, cols) = if data.len() >= 5 {
2517 (
2518 u16::from_le_bytes([data[1], data[2]]),
2519 u16::from_le_bytes([data[3], data[4]]),
2520 )
2521 } else {
2522 (24, 80)
2523 };
2524 let tag_len = if data.len() >= 7 {
2525 u16::from_le_bytes([data[5], data[6]]) as usize
2526 } else {
2527 0
2528 };
2529 let tag = if data.len() >= 7 + tag_len {
2530 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
2531 } else {
2532 ""
2533 };
2534 let cmd_start = 7 + tag_len;
2535 let dir: Option<String> = None;
2536 let create_payload = data
2537 .get(cmd_start..)
2538 .and_then(|bytes| std::str::from_utf8(bytes).ok());
2539 let command = create_payload
2540 .filter(|payload| !payload.contains('\0'))
2541 .map(str::trim)
2542 .filter(|payload| !payload.is_empty());
2543 let argv: Option<Vec<&str>> = create_payload
2544 .filter(|payload| payload.contains('\0'))
2545 .map(|payload| {
2546 payload
2547 .split('\0')
2548 .filter(|arg| !arg.is_empty())
2549 .collect::<Vec<_>>()
2550 })
2551 .filter(|args| !args.is_empty());
2552 let Some(id) = sess.allocate_pty_id() else {
2553 continue;
2554 };
2555 if let Some(pty) = spawn_pty(
2556 &config.shell,
2557 rows,
2558 cols,
2559 id,
2560 tag,
2561 command,
2562 argv.as_deref(),
2563 dir.as_deref(),
2564 config.scrollback,
2565 state.clone(),
2566 ) {
2567 let mut msg = Vec::with_capacity(3 + pty.tag.len());
2568 msg.push(S2C_CREATED);
2569 msg.extend_from_slice(&id.to_le_bytes());
2570 msg.extend_from_slice(pty.tag.as_bytes());
2571 sess.ptys.insert(id, pty);
2572 if let Some(c) = sess.clients.get_mut(&client_id) {
2573 c.lead = Some(id);
2574 c.view_sizes.insert(id, (rows, cols));
2575 subscribe_client_to(c, id);
2576 reset_inflight(c);
2578 }
2579 sess.send_to_all(&msg);
2580 need_nudge = true;
2581 }
2582 }
2583 C2S_CREATE_N => {
2584 let nonce = if data.len() >= 3 {
2586 u16::from_le_bytes([data[1], data[2]])
2587 } else {
2588 0
2589 };
2590 let (rows, cols) = if data.len() >= 7 {
2591 (
2592 u16::from_le_bytes([data[3], data[4]]),
2593 u16::from_le_bytes([data[5], data[6]]),
2594 )
2595 } else {
2596 (24, 80)
2597 };
2598 let tag_len = if data.len() >= 9 {
2599 u16::from_le_bytes([data[7], data[8]]) as usize
2600 } else {
2601 0
2602 };
2603 let tag = if data.len() >= 9 + tag_len {
2604 std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
2605 } else {
2606 ""
2607 };
2608 let cmd_start = 9 + tag_len;
2609 let dir: Option<String> = None;
2610 let create_payload = data
2611 .get(cmd_start..)
2612 .and_then(|bytes| std::str::from_utf8(bytes).ok());
2613 let command = create_payload
2614 .filter(|payload| !payload.contains('\0'))
2615 .map(str::trim)
2616 .filter(|payload| !payload.is_empty());
2617 let argv: Option<Vec<&str>> = create_payload
2618 .filter(|payload| payload.contains('\0'))
2619 .map(|payload| {
2620 payload
2621 .split('\0')
2622 .filter(|arg| !arg.is_empty())
2623 .collect::<Vec<_>>()
2624 })
2625 .filter(|args| !args.is_empty());
2626 let Some(id) = sess.allocate_pty_id() else {
2627 continue;
2628 };
2629 if let Some(pty) = spawn_pty(
2630 &config.shell,
2631 rows,
2632 cols,
2633 id,
2634 tag,
2635 command,
2636 argv.as_deref(),
2637 dir.as_deref(),
2638 config.scrollback,
2639 state.clone(),
2640 ) {
2641 let tag_bytes = pty.tag.as_bytes();
2642 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2643 nonce_msg.push(S2C_CREATED_N);
2644 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2645 nonce_msg.extend_from_slice(&id.to_le_bytes());
2646 nonce_msg.extend_from_slice(tag_bytes);
2647 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2648 broadcast_msg.push(S2C_CREATED);
2649 broadcast_msg.extend_from_slice(&id.to_le_bytes());
2650 broadcast_msg.extend_from_slice(tag_bytes);
2651 sess.ptys.insert(id, pty);
2652 if let Some(c) = sess.clients.get_mut(&client_id) {
2653 c.lead = Some(id);
2654 c.view_sizes.insert(id, (rows, cols));
2655 subscribe_client_to(c, id);
2656 reset_inflight(c);
2658 let _ = c.tx.try_send(nonce_msg);
2659 }
2660 for (&cid, c) in sess.clients.iter() {
2661 if cid != client_id {
2662 let _ = c.tx.try_send(broadcast_msg.clone());
2663 }
2664 }
2665 need_nudge = true;
2666 }
2667 }
2668 C2S_CREATE_AT => {
2669 let (rows, cols) = if data.len() >= 5 {
2671 (
2672 u16::from_le_bytes([data[1], data[2]]),
2673 u16::from_le_bytes([data[3], data[4]]),
2674 )
2675 } else {
2676 (24, 80)
2677 };
2678 let tag_len = if data.len() >= 7 {
2679 u16::from_le_bytes([data[5], data[6]]) as usize
2680 } else {
2681 0
2682 };
2683 let tag = if data.len() >= 7 + tag_len {
2684 std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
2685 } else {
2686 ""
2687 };
2688 let src_start = 7 + tag_len;
2689 let dir = if data.len() >= src_start + 2 {
2690 let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
2691 sess.ptys.get(&src_id).and_then(|p| pty_cwd(p.child_pid))
2692 } else {
2693 None
2694 };
2695 let Some(id) = sess.allocate_pty_id() else {
2696 continue;
2697 };
2698 if let Some(pty) = spawn_pty(
2699 &config.shell,
2700 rows,
2701 cols,
2702 id,
2703 tag,
2704 None,
2705 None,
2706 dir.as_deref(),
2707 config.scrollback,
2708 state.clone(),
2709 ) {
2710 let mut msg = Vec::with_capacity(3 + pty.tag.len());
2711 msg.push(S2C_CREATED);
2712 msg.extend_from_slice(&id.to_le_bytes());
2713 msg.extend_from_slice(pty.tag.as_bytes());
2714 sess.ptys.insert(id, pty);
2715 if let Some(c) = sess.clients.get_mut(&client_id) {
2716 c.lead = Some(id);
2717 c.view_sizes.insert(id, (rows, cols));
2718 subscribe_client_to(c, id);
2719 reset_inflight(c);
2721 }
2722 sess.send_to_all(&msg);
2723 need_nudge = true;
2724 }
2725 }
2726 C2S_CREATE2 => {
2727 if data.len() < 10 {
2729 continue;
2730 }
2731 let nonce = u16::from_le_bytes([data[1], data[2]]);
2732 let rows = u16::from_le_bytes([data[3], data[4]]);
2733 let cols = u16::from_le_bytes([data[5], data[6]]);
2734 let features = data[7];
2735 let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
2736 let tag = if data.len() >= 10 + tag_len {
2737 std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
2738 } else {
2739 ""
2740 };
2741 let mut cursor = 10 + tag_len;
2742 let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
2743 let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
2744 cursor += 2;
2745 sess.ptys.get(&src_id).and_then(|p| pty_cwd(p.child_pid))
2746 } else {
2747 None
2748 };
2749 let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
2750 data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
2751 } else {
2752 None
2753 };
2754 let command = create_payload
2755 .filter(|p| !p.contains('\0'))
2756 .map(str::trim)
2757 .filter(|p| !p.is_empty());
2758 let argv: Option<Vec<&str>> = create_payload
2759 .filter(|p| p.contains('\0'))
2760 .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
2761 .filter(|a| !a.is_empty());
2762 let Some(id) = sess.allocate_pty_id() else {
2763 continue;
2764 };
2765 if let Some(pty) = spawn_pty(
2766 &config.shell,
2767 rows,
2768 cols,
2769 id,
2770 tag,
2771 command,
2772 argv.as_deref(),
2773 dir.as_deref(),
2774 config.scrollback,
2775 state.clone(),
2776 ) {
2777 let tag_bytes = pty.tag.as_bytes();
2778 let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2779 nonce_msg.push(S2C_CREATED_N);
2780 nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2781 nonce_msg.extend_from_slice(&id.to_le_bytes());
2782 nonce_msg.extend_from_slice(tag_bytes);
2783 let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2784 broadcast_msg.push(S2C_CREATED);
2785 broadcast_msg.extend_from_slice(&id.to_le_bytes());
2786 broadcast_msg.extend_from_slice(tag_bytes);
2787 sess.ptys.insert(id, pty);
2788 if let Some(c) = sess.clients.get_mut(&client_id) {
2789 c.lead = Some(id);
2790 c.view_sizes.insert(id, (rows, cols));
2791 subscribe_client_to(c, id);
2792 reset_inflight(c);
2794 let _ = c.tx.try_send(nonce_msg);
2795 }
2796 for (&cid, c) in sess.clients.iter() {
2797 if cid != client_id {
2798 let _ = c.tx.try_send(broadcast_msg.clone());
2799 }
2800 }
2801 need_nudge = true;
2802 }
2803 }
2804 C2S_FOCUS if data.len() >= 3 => {
2805 let pid = u16::from_le_bytes([data[1], data[2]]);
2806 if sess.ptys.contains_key(&pid) {
2807 let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
2808 if let Some(c) = sess.clients.get_mut(&client_id) {
2809 c.lead = Some(pid);
2810 subscribe_client_to(c, pid);
2811 if old_pid == Some(pid) {
2812 update_client_scroll_state(c, pid, 0);
2813 } else {
2814 reset_inflight(c);
2815 }
2816 }
2817 if let Some(pty) = sess.ptys.get_mut(&pid) {
2818 pty.mark_dirty();
2819 need_nudge = true;
2820 }
2821 }
2822 }
2823 C2S_SUBSCRIBE if data.len() >= 3 => {
2824 let pid = u16::from_le_bytes([data[1], data[2]]);
2825 if sess.ptys.contains_key(&pid) {
2826 if let Some(c) = sess.clients.get_mut(&client_id) {
2827 subscribe_client_to(c, pid);
2828 }
2829 if let Some(pty) = sess.ptys.get_mut(&pid) {
2830 pty.mark_dirty();
2831 }
2832 need_nudge = true;
2833 }
2834 }
2835 C2S_UNSUBSCRIBE if data.len() >= 3 => {
2836 let pid = u16::from_le_bytes([data[1], data[2]]);
2837 if sess.ptys.contains_key(&pid) {
2838 let mut touched = Vec::new();
2839 if let Some(c) = sess.clients.get_mut(&client_id) {
2840 if unsubscribe_client_from(c, pid) {
2841 touched.push(pid);
2842 }
2843 reset_inflight(c);
2844 }
2845 if sess.resize_ptys_to_mediated_sizes(touched) {
2846 need_nudge = true;
2847 }
2848 }
2849 }
2850 C2S_RESTART if data.len() >= 3 => {
2851 let pid = u16::from_le_bytes([data[1], data[2]]);
2852 let restart_info = sess
2853 .ptys
2854 .get(&pid)
2855 .filter(|p| p.exited)
2856 .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
2857 if let Some(((rows, cols), command, tag)) = restart_info {
2858 if let Some((master, child, reader, byte_rx)) = respawn_child(
2859 &state.0.shell,
2860 rows,
2861 cols,
2862 pid,
2863 command.as_deref(),
2864 state.clone(),
2865 ) {
2866 let Some(pty) = sess.ptys.get_mut(&pid) else {
2867 break;
2868 };
2869 pty.master_fd = master;
2870 pty.child_pid = child;
2871 pty.reader_handle = reader;
2872 pty.byte_rx = byte_rx;
2873 pty.driver.reset_modes();
2874 pty.exited = false;
2875 pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
2876 pty.lflag_cache = pty_lflag(master);
2877 pty.lflag_last = Instant::now();
2878 pty.mark_dirty();
2879 if let Some(c) = sess.clients.get_mut(&client_id) {
2880 c.lead = Some(pid);
2881 subscribe_client_to(c, pid);
2882 update_client_scroll_state(c, pid, 0);
2883 reset_inflight(c);
2884 }
2885 let mut msg = Vec::with_capacity(3 + tag.len());
2886 msg.push(S2C_CREATED);
2887 msg.extend_from_slice(&pid.to_le_bytes());
2888 msg.extend_from_slice(tag.as_bytes());
2889 sess.send_to_all(&msg);
2890 need_nudge = true;
2891 }
2892 }
2893 }
2894 C2S_READ if data.len() >= 13 => {
2895 let nonce = u16::from_le_bytes([data[1], data[2]]);
2896 let pid = u16::from_le_bytes([data[3], data[4]]);
2897 let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
2898 let req_limit =
2899 u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
2900 let flags = data.get(13).copied().unwrap_or(0);
2901 let ansi = flags & READ_ANSI != 0;
2902 let tail = flags & READ_TAIL != 0;
2903
2904 if let Some(pty) = sess.ptys.get_mut(&pid) {
2905 let (rows, _cols) = pty.driver.size();
2906 let viewport = take_snapshot(pty);
2907 let scrollback_lines = viewport.scrollback_lines() as usize;
2908 let total_lines = scrollback_lines + rows as usize;
2909
2910 let extract = |f: &FrameState| -> String {
2911 if ansi {
2912 f.get_ansi_text()
2913 } else {
2914 f.get_all_text()
2915 }
2916 };
2917
2918 let mut all_lines: Vec<String> = Vec::new();
2919
2920 let mut scroll_offset = scrollback_lines;
2921 while scroll_offset > 0 {
2922 let frame = pty.driver.scrollback_frame(scroll_offset);
2923 let page = extract(&frame);
2924 let page_lines: Vec<&str> = page.lines().collect();
2925 let take = if scroll_offset < rows as usize {
2926 scroll_offset.min(page_lines.len())
2927 } else {
2928 page_lines.len()
2929 };
2930 for line in &page_lines[..take] {
2931 all_lines.push(line.to_string());
2932 }
2933 if scroll_offset <= rows as usize {
2934 break;
2935 }
2936 scroll_offset = scroll_offset.saturating_sub(rows as usize);
2937 }
2938
2939 for line in extract(&viewport).lines() {
2940 all_lines.push(line.to_string());
2941 }
2942
2943 let (start, end) = if tail {
2944 let end = all_lines.len().saturating_sub(req_offset);
2945 let start = if req_limit == 0 {
2946 0
2947 } else {
2948 end.saturating_sub(req_limit)
2949 };
2950 (start, end)
2951 } else {
2952 let start = req_offset.min(all_lines.len());
2953 let end = if req_limit == 0 {
2954 all_lines.len()
2955 } else {
2956 (start + req_limit).min(all_lines.len())
2957 };
2958 (start, end)
2959 };
2960 let text = all_lines[start..end].join("\n");
2961
2962 let mut msg = Vec::with_capacity(13 + text.len());
2963 msg.push(S2C_TEXT);
2964 msg.extend_from_slice(&nonce.to_le_bytes());
2965 msg.extend_from_slice(&pid.to_le_bytes());
2966 msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
2967 msg.extend_from_slice(&(start as u32).to_le_bytes());
2968 msg.extend_from_slice(text.as_bytes());
2969 if let Some(client) = sess.clients.get(&client_id) {
2970 let _ = client.tx.try_send(msg);
2971 }
2972 }
2973 }
2974 C2S_COPY_RANGE if data.len() >= 18 => {
2975 let nonce = u16::from_le_bytes([data[1], data[2]]);
2976 let pid = u16::from_le_bytes([data[3], data[4]]);
2977 let start_tail =
2978 u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
2979 let start_col = u16::from_le_bytes([data[9], data[10]]);
2980 let end_tail =
2981 u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
2982 let end_col = u16::from_le_bytes([data[15], data[16]]);
2983
2984 if let Some(pty) = sess.ptys.get(&pid) {
2985 let text =
2986 pty.driver.get_text_range(start_tail, start_col, end_tail, end_col);
2987 let total_lines = pty.driver.total_lines();
2988
2989 let mut msg = Vec::with_capacity(13 + text.len());
2990 msg.push(S2C_TEXT);
2991 msg.extend_from_slice(&nonce.to_le_bytes());
2992 msg.extend_from_slice(&pid.to_le_bytes());
2993 msg.extend_from_slice(&total_lines.to_le_bytes());
2994 msg.extend_from_slice(&start_tail.to_le_bytes());
2995 msg.extend_from_slice(text.as_bytes());
2996 if let Some(client) = sess.clients.get(&client_id) {
2997 let _ = client.tx.try_send(msg);
2998 }
2999 }
3000 }
3001 C2S_KILL if data.len() >= 7 => {
3002 let pid = u16::from_le_bytes([data[1], data[2]]);
3003 let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
3004 if let Some(pty) = sess.ptys.get(&pid) {
3005 if !pty.exited {
3006 unsafe {
3007 libc::kill(pty.child_pid, signal);
3008 }
3009 }
3010 }
3011 }
3012 C2S_CLOSE if data.len() >= 3 => {
3013 let pid = u16::from_le_bytes([data[1], data[2]]);
3014 if let Some(pty) = sess.ptys.remove(&pid) {
3015 if !pty.exited {
3016 state.2.write().unwrap().remove(&pid);
3017 drop(pty.reader_handle); unsafe {
3019 libc::kill(pty.child_pid, libc::SIGHUP);
3020 libc::close(pty.master_fd);
3021 }
3022 }
3023 for client in sess.clients.values_mut() {
3024 unsubscribe_client_from(client, pid);
3025 }
3026 let mut msg = vec![S2C_CLOSED];
3027 msg.extend_from_slice(&pid.to_le_bytes());
3028 sess.send_to_all(&msg);
3029 }
3030 }
3031 _ => {}
3032 }
3033 drop(sess);
3034 if need_nudge {
3035 nudge_delivery(&state);
3036 }
3037 }
3038
3039 {
3040 let mut sess = state.1.lock().await;
3041 let mut need_nudge = false;
3042 let affected_ptys = sess
3043 .clients
3044 .remove(&client_id)
3045 .map(|client| client.view_sizes.keys().copied().collect::<Vec<_>>())
3046 .unwrap_or_default();
3047 if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
3048 need_nudge = true;
3049 }
3050 drop(sess);
3051 if need_nudge {
3052 nudge_delivery(&state);
3053 }
3054 }
3055 sender.abort();
3056 if state.0.verbose {
3057 eprintln!("client disconnected");
3058 }
3059}
3060
3061#[cfg(test)]
3062mod tests {
3063 use super::*;
3064
3065 fn test_client_with_capacity(capacity: usize) -> (ClientState, mpsc::Receiver<Vec<u8>>) {
3066 let (tx, rx) = mpsc::channel(capacity);
3067 let client = ClientState {
3068 tx,
3069 lead: None,
3070 subscriptions: HashSet::new(),
3071 view_sizes: HashMap::new(),
3072 scroll_offsets: HashMap::new(),
3073 scroll_caches: HashMap::new(),
3074 last_sent: HashMap::new(),
3075 preview_next_send_at: HashMap::new(),
3076 rtt_ms: 50.0,
3077 min_rtt_ms: 50.0,
3078 display_fps: 60.0,
3079 delivery_bps: 262_144.0,
3080 goodput_bps: 262_144.0,
3081 goodput_jitter_bps: 0.0,
3082 max_goodput_jitter_bps: 0.0,
3083 last_goodput_sample_bps: 0.0,
3084 avg_frame_bytes: 1_024.0,
3085 avg_paced_frame_bytes: 1_024.0,
3086 avg_preview_frame_bytes: 1_024.0,
3087 inflight_bytes: 0,
3088 inflight_frames: VecDeque::new(),
3089 next_send_at: Instant::now(),
3090 probe_frames: 0.0,
3091 frames_sent: 0,
3092 acks_recv: 0,
3093 acked_bytes_since_log: 0,
3094 browser_backlog_frames: 0,
3095 browser_ack_ahead_frames: 0,
3096 browser_apply_ms: 0.0,
3097 last_metrics_update: Instant::now(),
3098 last_log: Instant::now(),
3099 goodput_window_bytes: 0,
3100 goodput_window_start: Instant::now(),
3101 };
3102 (client, rx)
3103 }
3104
3105 fn test_client() -> ClientState {
3106 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
3107 client
3108 }
3109
3110 fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
3111 let now = Instant::now();
3112 client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
3113 client.inflight_frames = (0..frames)
3114 .map(|_| InFlightFrame {
3115 sent_at: now,
3116 bytes: bytes_per_frame,
3117 paced: true,
3118 })
3119 .collect();
3120 }
3121
3122 fn sample_frame(text: &str) -> FrameState {
3123 let mut frame = FrameState::new(2, 8);
3124 frame.write_text(0, 0, text, blit_remote::CellStyle::default());
3125 frame
3126 }
3127
3128 #[test]
3129 fn unset_view_size_accepts_zero_pair_only() {
3130 assert!(is_unset_view_size(0, 0));
3131 assert!(!is_unset_view_size(0, 80));
3132 assert!(!is_unset_view_size(u16::MAX, u16::MAX));
3133 }
3134
3135 #[test]
3136 fn unsubscribe_client_from_clears_view_size() {
3137 let mut client = test_client();
3138 client.subscriptions.insert(7);
3139 client.view_sizes.insert(7, (24, 80));
3140 assert!(unsubscribe_client_from(&mut client, 7));
3141 assert!(!client.subscriptions.contains(&7));
3142 assert!(!client.view_sizes.contains_key(&7));
3143 }
3144
3145 #[test]
3146 fn mediated_size_uses_per_pty_view_sizes_without_lead() {
3147 let mut session = Session::new();
3148 let mut c1 = test_client();
3149 let mut c2 = test_client();
3150 c1.view_sizes.insert(7, (30, 120));
3151 c2.view_sizes.insert(7, (24, 100));
3152 session.clients.insert(1, c1);
3153 session.clients.insert(2, c2);
3154 assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
3155 }
3156
3157 #[test]
3158 fn due_preview_reserves_the_last_lead_slot() {
3159 let mut client = test_client();
3160 client.lead = Some(1);
3161 client.subscriptions.insert(1);
3162 client.subscriptions.insert(2);
3163
3164 let target_frames = target_frame_window(&client);
3165 let lead_limit = target_frames.saturating_sub(1).max(1);
3166 fill_inflight(&mut client, lead_limit, 512);
3167
3168 assert!(window_open(&client));
3169 assert!(lead_window_open(&client, false));
3170 assert!(!lead_window_open(&client, true));
3171 assert!(can_send_preview(&client, 2, Instant::now()));
3172 }
3173
3174 #[test]
3175 fn entering_scrollback_uses_current_visible_frame_as_baseline() {
3176 let mut client = test_client();
3177 let live = sample_frame("live");
3178 client.lead = Some(7);
3179 client.subscriptions.insert(7);
3180 client.last_sent.insert(7, live.clone());
3181
3182 assert!(update_client_scroll_state(&mut client, 7, 12));
3183 assert_eq!(client.scroll_offsets.get(&7), Some(&12));
3184 assert_eq!(client.scroll_caches.get(&7), Some(&live));
3185 }
3186
3187 #[test]
3188 fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
3189 let mut client = test_client();
3190 let history = sample_frame("hist");
3191 client.lead = Some(7);
3192 client.subscriptions.insert(7);
3193 client.scroll_offsets.insert(7, 12);
3194 client.scroll_caches.insert(7, history.clone());
3195
3196 assert!(update_client_scroll_state(&mut client, 7, 0));
3197 assert_eq!(client.scroll_offsets.get(&7), None);
3198 assert_eq!(client.last_sent.get(&7), Some(&history));
3199 assert_eq!(client.scroll_caches.get(&7), None);
3200 }
3201
3202 #[test]
3205 fn frame_window_minimum_is_two() {
3206 assert!(frame_window(0.0, 60.0) >= 2);
3207 }
3208
3209 #[test]
3210 fn frame_window_scales_with_rtt() {
3211 let low = frame_window(10.0, 60.0);
3212 let high = frame_window(200.0, 60.0);
3213 assert!(high > low, "higher RTT should need more frames in flight");
3214 }
3215
3216 #[test]
3217 fn frame_window_scales_with_fps() {
3218 let slow = frame_window(100.0, 10.0);
3219 let fast = frame_window(100.0, 120.0);
3220 assert!(fast > slow, "higher fps should need more frames in flight");
3221 }
3222
3223 #[test]
3224 fn frame_window_zero_rtt() {
3225 assert!(frame_window(0.0, 120.0) >= 2);
3226 }
3227
3228 #[test]
3231 fn path_rtt_ms_uses_min_when_positive() {
3232 let mut client = test_client();
3233 client.rtt_ms = 100.0;
3234 client.min_rtt_ms = 30.0;
3235 assert_eq!(path_rtt_ms(&client), 30.0);
3236 }
3237
3238 #[test]
3239 fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
3240 let mut client = test_client();
3241 client.rtt_ms = 80.0;
3242 client.min_rtt_ms = 0.0;
3243 assert_eq!(path_rtt_ms(&client), 80.0);
3244 }
3245
3246 #[test]
3249 fn ewma_rising_uses_rise_alpha() {
3250 let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
3251 assert!((result - 150.0).abs() < 0.01);
3253 }
3254
3255 #[test]
3256 fn ewma_falling_uses_fall_alpha() {
3257 let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
3258 assert!((result - 190.0).abs() < 0.01);
3260 }
3261
3262 #[test]
3263 fn ewma_same_value_unchanged() {
3264 let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
3265 assert!((result - 50.0).abs() < 0.01);
3266 }
3267
3268 #[test]
3271 fn advance_deadline_steps_forward() {
3272 let now = Instant::now();
3273 let mut deadline = now;
3274 let interval = Duration::from_millis(16);
3275 advance_deadline(&mut deadline, now, interval);
3276 assert!(deadline > now);
3277 assert!(deadline <= now + interval + Duration::from_micros(100));
3278 }
3279
3280 #[test]
3281 fn advance_deadline_resets_when_far_behind() {
3282 let now = Instant::now();
3283 let mut deadline = now - Duration::from_secs(10);
3285 let interval = Duration::from_millis(16);
3286 advance_deadline(&mut deadline, now, interval);
3287 assert!(deadline >= now);
3289 }
3290
3291 #[test]
3292 fn should_snapshot_pty_requires_dirty_and_needful() {
3293 assert!(should_snapshot_pty(true, true, false));
3294 assert!(!should_snapshot_pty(false, true, false));
3295 assert!(!should_snapshot_pty(true, false, false));
3296 }
3297
3298 #[test]
3299 fn should_snapshot_pty_defers_synced_output() {
3300 assert!(!should_snapshot_pty(true, true, true));
3301 assert!(should_snapshot_pty(true, true, false));
3302 }
3303
3304 #[test]
3305 fn enqueue_ready_frame_refuses_new_frames_when_capped() {
3306 let mut queue = VecDeque::new();
3307 for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
3308 assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
3309 }
3310 assert!(!enqueue_ready_frame(
3311 &mut queue,
3312 FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
3313 ));
3314 assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
3315 assert_eq!(queue.front().map(FrameState::cols), Some(1));
3316 assert_eq!(
3317 queue.back().map(FrameState::cols),
3318 Some(READY_FRAME_QUEUE_CAP as u16),
3319 );
3320 }
3321
3322 #[test]
3323 fn find_sync_output_end_returns_end_of_first_close_sequence() {
3324 let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
3325 assert_eq!(find_sync_output_end(&[], bytes), Some(11));
3326 }
3327
3328 #[test]
3329 fn find_sync_output_end_returns_none_without_close_sequence() {
3330 assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
3331 }
3332
3333 #[test]
3334 fn find_sync_output_end_detects_boundary_split_across_reads() {
3335 assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
3336 }
3337
3338 #[test]
3339 fn update_sync_scan_tail_keeps_recent_suffix_only() {
3340 let mut tail = Vec::new();
3341 update_sync_scan_tail(&mut tail, b"123456789");
3342 assert_eq!(tail, b"3456789");
3343 }
3344
3345 #[test]
3348 fn window_saturated_at_90_percent_frames() {
3349 let client = test_client();
3350 let target = target_frame_window(&client);
3351 let frames_90 = (target * 9).div_ceil(10); assert!(window_saturated(&client, frames_90, 0));
3353 }
3354
3355 #[test]
3356 fn window_saturated_not_at_low_usage() {
3357 let client = test_client();
3358 assert!(!window_saturated(&client, 1, 0));
3359 }
3360
3361 #[test]
3362 fn window_saturated_at_90_percent_bytes() {
3363 let client = test_client();
3364 let target_bytes = target_byte_window(&client);
3365 let bytes_90 = (target_bytes * 9).div_ceil(10);
3366 assert!(window_saturated(&client, 0, bytes_90));
3367 }
3368
3369 #[test]
3372 fn outbox_queued_frames_zero_when_empty() {
3373 let client = test_client();
3374 assert_eq!(outbox_queued_frames(&client), 0);
3375 }
3376
3377 #[test]
3378 fn outbox_backpressured_when_queue_full() {
3379 let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
3380 for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
3382 let _ = client.tx.try_send(vec![0u8]);
3383 }
3384 assert!(outbox_backpressured(&client));
3385 }
3386
3387 #[test]
3388 fn outbox_not_backpressured_when_empty() {
3389 let client = test_client();
3390 assert!(!outbox_backpressured(&client));
3391 }
3392
3393 #[test]
3396 fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
3397 let mut client = test_client();
3398 client.rtt_ms = 1.0;
3399 client.min_rtt_ms = 1.0;
3400 client.browser_backlog_frames = 0;
3401 client.browser_ack_ahead_frames = 0;
3402 client.browser_apply_ms = 0.0;
3403 client.goodput_bps = 1_000_000.0;
3404 client.delivery_bps = 1_000_000.0;
3405 client.display_fps = 144.0;
3406 assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
3407 }
3408
3409 #[test]
3410 fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
3411 let mut client = test_client();
3412 client.browser_backlog_frames = 20;
3413 let fps = browser_pacing_fps(&client);
3414 assert!(fps >= 1.0);
3415 assert!(fps < client.display_fps);
3416 }
3417
3418 #[test]
3421 fn effective_rtt_ms_equals_path_when_queue_is_empty() {
3422 let mut client = test_client();
3423 client.rtt_ms = 1.0;
3424 client.min_rtt_ms = 1.0;
3425 client.browser_backlog_frames = 0;
3426 client.browser_ack_ahead_frames = 0;
3427 client.browser_apply_ms = 0.0;
3428 client.goodput_bps = 1_000_000.0;
3429 client.delivery_bps = 1_000_000.0;
3430 assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
3431 }
3432
3433 #[test]
3434 fn effective_rtt_ms_at_least_path_rtt() {
3435 let client = test_client();
3436 assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
3437 }
3438
3439 #[test]
3442 fn target_frame_window_at_least_two() {
3443 let client = test_client();
3444 assert!(target_frame_window(&client) >= 2);
3445 }
3446
3447 #[test]
3448 fn target_frame_window_grows_with_probe() {
3449 let mut client = test_client();
3450 let base = target_frame_window(&client);
3451 client.probe_frames = 10.0;
3452 let probed = target_frame_window(&client);
3453 assert!(probed > base, "probe_frames should grow the window");
3454 }
3455
3456 #[test]
3459 fn bandwidth_floor_bps_at_least_16k() {
3460 let mut client = test_client();
3461 client.goodput_bps = 0.0;
3462 client.delivery_bps = 0.0;
3463 assert_eq!(bandwidth_floor_bps(&client), 0.0);
3464 }
3465
3466 #[test]
3467 fn bandwidth_floor_bps_scales_with_goodput() {
3468 let mut client = test_client();
3469 client.goodput_bps = 1_000_000.0;
3470 client.delivery_bps = 1_000_000.0;
3471 let floor = bandwidth_floor_bps(&client);
3472 assert!(floor > 0.0);
3473 }
3474
3475 #[test]
3476 fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
3477 let mut client = test_client();
3478 client.display_fps = 60.0;
3479 client.browser_backlog_frames = 0;
3480 client.browser_ack_ahead_frames = 0;
3481 client.browser_apply_ms = 0.2;
3482 client.goodput_bps = 3_000_000.0;
3483 client.delivery_bps = 9_500_000.0;
3484 client.last_goodput_sample_bps = 3_000_000.0;
3485 client.avg_paced_frame_bytes = 150_000.0;
3486 client.avg_preview_frame_bytes = 1_024.0;
3487 client.avg_frame_bytes = 150_000.0;
3488
3489 assert!(
3490 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
3491 "browser-ready delivery floor should let large frames reach display_fps on a fast path",
3492 );
3493 }
3494
3495 #[test]
3498 fn pacing_fps_zero_when_no_bandwidth() {
3499 let mut client = test_client();
3500 client.goodput_bps = 0.0;
3501 client.delivery_bps = 0.0;
3502 client.last_goodput_sample_bps = 0.0;
3503 assert!(
3504 pacing_fps(&client) == 0.0,
3505 "pacing_fps should be 0 with zero bandwidth"
3506 );
3507 }
3508
3509 #[test]
3510 fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
3511 let mut client = test_client();
3512 client.rtt_ms = 1.0;
3513 client.min_rtt_ms = 1.0;
3514 client.browser_backlog_frames = 0;
3515 client.browser_ack_ahead_frames = 0;
3516 client.browser_apply_ms = 0.0;
3517 client.goodput_bps = 1_000_000.0;
3518 client.delivery_bps = 1_000_000.0;
3519 client.display_fps = 60.0;
3520 assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
3521 }
3522
3523 #[test]
3526 fn throughput_limited_when_low_bandwidth() {
3527 let mut client = test_client();
3528 client.goodput_bps = 1_000.0;
3529 client.delivery_bps = 1_000.0;
3530 client.last_goodput_sample_bps = 0.0;
3531 assert!(throughput_limited(&client));
3532 }
3533
3534 #[test]
3535 fn throughput_not_limited_with_high_bandwidth() {
3536 let mut client = test_client();
3537 client.goodput_bps = 100_000_000.0;
3538 client.delivery_bps = 100_000_000.0;
3539 assert!(!throughput_limited(&client));
3540 }
3541
3542 #[test]
3545 fn browser_pacing_fps_at_least_one() {
3546 let client = test_client();
3547 assert!(browser_pacing_fps(&client) >= 1.0);
3548 }
3549
3550 #[test]
3551 fn browser_pacing_fps_reduced_by_high_backlog() {
3552 let mut client = test_client();
3553 let normal = browser_pacing_fps(&client);
3554 client.browser_backlog_frames = 20;
3555 let backlogged = browser_pacing_fps(&client);
3556 assert!(backlogged < normal, "high backlog should reduce pacing fps");
3557 }
3558
3559 #[test]
3560 fn browser_pacing_fps_reduced_by_high_ack_ahead() {
3561 let mut client = test_client();
3562 let normal = browser_pacing_fps(&client);
3563 client.browser_ack_ahead_frames = 10;
3564 let ahead = browser_pacing_fps(&client);
3565 assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
3566 }
3567
3568 #[test]
3571 fn browser_backlog_blocked_over_threshold() {
3572 let mut client = test_client();
3573 client.browser_backlog_frames = 9;
3574 assert!(browser_backlog_blocked(&client));
3575 }
3576
3577 #[test]
3578 fn browser_backlog_not_blocked_under_threshold() {
3579 let mut client = test_client();
3580 client.browser_backlog_frames = 8;
3581 assert!(!browser_backlog_blocked(&client));
3582 }
3583
3584 #[test]
3587 fn byte_budget_for_at_least_one_frame() {
3588 let client = test_client();
3589 let budget = byte_budget_for(&client, 10.0);
3590 assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
3591 }
3592
3593 #[test]
3594 fn byte_budget_for_grows_with_time() {
3595 let client = test_client();
3596 let short = byte_budget_for(&client, 10.0);
3597 let long = byte_budget_for(&client, 1000.0);
3598 assert!(long >= short);
3599 }
3600
3601 #[test]
3604 fn target_byte_window_positive() {
3605 let client = test_client();
3606 assert!(target_byte_window(&client) > 0);
3607 }
3608
3609 #[test]
3610 fn target_byte_window_covers_frame_window() {
3611 let client = test_client();
3612 let byte_win = target_byte_window(&client);
3613 let frame_win = target_frame_window(&client);
3614 let min_bytes =
3615 (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
3616 assert!(
3617 byte_win >= min_bytes,
3618 "byte window should cover at least frame_window worth of paced frames"
3619 );
3620 }
3621
3622 #[test]
3625 fn send_interval_matches_browser_pacing() {
3626 let client = test_client();
3627 let interval = send_interval(&client);
3628 let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
3629 let diff = interval.abs_diff(expected);
3630 assert!(diff < Duration::from_micros(10));
3631 }
3632
3633 #[test]
3636 fn preview_fps_at_least_one() {
3637 let client = test_client();
3638 assert!(preview_fps(&client) >= 1.0);
3639 }
3640
3641 #[test]
3644 fn window_open_initially() {
3645 let client = test_client();
3646 assert!(window_open(&client));
3647 }
3648
3649 #[test]
3650 fn window_open_false_when_browser_blocked() {
3651 let mut client = test_client();
3652 client.browser_backlog_frames = 20;
3653 assert!(!window_open(&client));
3654 }
3655
3656 #[test]
3657 fn window_open_false_when_inflight_full() {
3658 let mut client = test_client();
3659 let target = target_frame_window(&client);
3660 fill_inflight(&mut client, target + 10, 1024);
3661 assert!(!window_open(&client));
3662 }
3663
3664 #[test]
3667 fn lead_window_open_no_reserve_same_as_window_open() {
3668 let client = test_client();
3669 assert_eq!(lead_window_open(&client, false), window_open(&client));
3670 }
3671
3672 #[test]
3673 fn lead_window_open_reserves_preview_slot() {
3674 let mut client = test_client();
3675 client.lead = Some(1);
3676 client.subscriptions.insert(1);
3677 let target = target_frame_window(&client);
3678 fill_inflight(&mut client, target.saturating_sub(1), 512);
3680 assert!(!lead_window_open(&client, true));
3683 }
3684
3685 #[test]
3688 fn can_send_frame_when_window_open_and_time_due() {
3689 let mut client = test_client();
3690 client.next_send_at = Instant::now() - Duration::from_millis(100);
3691 assert!(can_send_frame(&client, Instant::now(), false));
3692 }
3693
3694 #[test]
3695 fn can_send_frame_false_when_not_due() {
3696 let mut client = test_client();
3697 client.next_send_at = Instant::now() + Duration::from_secs(10);
3698 assert!(!can_send_frame(&client, Instant::now(), false));
3699 }
3700
3701 #[test]
3702 fn can_send_frame_false_when_window_closed() {
3703 let mut client = test_client();
3704 client.browser_backlog_frames = 20; client.next_send_at = Instant::now() - Duration::from_millis(100);
3706 assert!(!can_send_frame(&client, Instant::now(), false));
3707 }
3708
3709 #[test]
3712 fn record_send_increases_inflight() {
3713 let mut client = test_client();
3714 let now = Instant::now();
3715 assert_eq!(client.inflight_bytes, 0);
3716 assert_eq!(client.inflight_frames.len(), 0);
3717
3718 record_send(&mut client, 1000, now, true);
3719 assert_eq!(client.inflight_bytes, 1000);
3720 assert_eq!(client.inflight_frames.len(), 1);
3721
3722 record_send(&mut client, 500, now, false);
3723 assert_eq!(client.inflight_bytes, 1500);
3724 assert_eq!(client.inflight_frames.len(), 2);
3725 }
3726
3727 #[test]
3728 fn record_send_paced_advances_deadline() {
3729 let mut client = test_client();
3730 let now = Instant::now();
3731 client.next_send_at = now;
3732 record_send(&mut client, 1000, now, true);
3733 assert!(client.next_send_at > now);
3734 }
3735
3736 #[test]
3737 fn record_send_unpaced_does_not_advance_deadline() {
3738 let mut client = test_client();
3739 let now = Instant::now();
3740 let before = client.next_send_at;
3741 record_send(&mut client, 1000, now, false);
3742 assert_eq!(client.next_send_at, before);
3743 }
3744
3745 #[test]
3746 fn record_ack_decreases_inflight() {
3747 let mut client = test_client();
3748 let now = Instant::now();
3749 record_send(&mut client, 1000, now, true);
3750 record_send(&mut client, 500, now, true);
3751 assert_eq!(client.inflight_frames.len(), 2);
3752
3753 record_ack(&mut client);
3754 assert_eq!(client.inflight_frames.len(), 1);
3755 assert_eq!(client.inflight_bytes, 500);
3756 }
3757
3758 #[test]
3759 fn record_ack_on_empty_clears_bytes() {
3760 let mut client = test_client();
3761 client.inflight_bytes = 999; record_ack(&mut client);
3763 assert_eq!(client.inflight_bytes, 0);
3764 }
3765
3766 #[test]
3767 fn record_ack_updates_rtt_estimate() {
3768 let mut client = test_client();
3769 let now = Instant::now();
3770 client.inflight_frames.push_back(InFlightFrame {
3771 sent_at: now - Duration::from_millis(20),
3772 bytes: 512,
3773 paced: true,
3774 });
3775 client.inflight_bytes = 512;
3776 let old_rtt = client.rtt_ms;
3777 record_ack(&mut client);
3778 assert!(
3780 (client.rtt_ms - old_rtt).abs() > 0.01,
3781 "rtt_ms should be updated after ack"
3782 );
3783 }
3784
3785 #[test]
3786 fn record_ack_paced_updates_avg_paced_frame_bytes() {
3787 let mut client = test_client();
3788 let now = Instant::now();
3789 client.inflight_frames.push_back(InFlightFrame {
3790 sent_at: now - Duration::from_millis(10),
3791 bytes: 4096,
3792 paced: true,
3793 });
3794 client.inflight_bytes = 4096;
3795 let old_avg = client.avg_paced_frame_bytes;
3796 record_ack(&mut client);
3797 assert!(client.avg_paced_frame_bytes > old_avg);
3799 }
3800
3801 #[test]
3802 fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
3803 let mut client = test_client();
3804 let now = Instant::now();
3805 client.inflight_frames.push_back(InFlightFrame {
3806 sent_at: now - Duration::from_millis(10),
3807 bytes: 8192,
3808 paced: false,
3809 });
3810 client.inflight_bytes = 8192;
3811 let old_avg = client.avg_preview_frame_bytes;
3812 record_ack(&mut client);
3813 assert!(client.avg_preview_frame_bytes > old_avg);
3814 }
3815
3816 #[test]
3819 fn pty_list_msg_empty_session() {
3820 let sess = Session::new();
3821 let msg = sess.pty_list_msg();
3822 assert_eq!(msg[0], S2C_LIST);
3823 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
3824 assert_eq!(msg.len(), 3);
3825 }
3826
3827 #[test]
3828 fn pty_list_msg_includes_tags() {
3829 let _sess = Session::new();
3830 let tag1 = "shell";
3840 let tag2 = "build";
3841
3842 let mut expected = vec![S2C_LIST];
3844 expected.extend_from_slice(&2u16.to_le_bytes());
3845 expected.extend_from_slice(&1u16.to_le_bytes());
3847 expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
3848 expected.extend_from_slice(tag1.as_bytes());
3849 expected.extend_from_slice(&3u16.to_le_bytes());
3851 expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
3852 expected.extend_from_slice(tag2.as_bytes());
3853
3854 assert_eq!(expected[0], S2C_LIST);
3856 assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
3857 let msg_str = String::from_utf8_lossy(&expected);
3859 assert!(msg_str.contains("shell"));
3860 assert!(msg_str.contains("build"));
3861 }
3862
3863 #[test]
3866 fn can_send_preview_true_when_due() {
3867 let mut client = test_client();
3868 let now = Instant::now();
3869 client
3870 .preview_next_send_at
3871 .insert(5, now - Duration::from_millis(100));
3872 assert!(can_send_preview(&client, 5, now));
3873 }
3874
3875 #[test]
3876 fn can_send_preview_false_when_not_due() {
3877 let mut client = test_client();
3878 let now = Instant::now();
3879 client
3880 .preview_next_send_at
3881 .insert(5, now + Duration::from_secs(10));
3882 assert!(!can_send_preview(&client, 5, now));
3883 }
3884
3885 #[test]
3886 fn can_send_preview_false_when_window_closed() {
3887 let mut client = test_client();
3888 client.browser_backlog_frames = 20;
3889 let now = Instant::now();
3890 assert!(!can_send_preview(&client, 5, now));
3891 }
3892
3893 #[test]
3894 fn can_send_preview_true_for_unseen_pid() {
3895 let client = test_client();
3896 let now = Instant::now();
3897 assert!(can_send_preview(&client, 99, now));
3899 }
3900
3901 #[test]
3902 fn record_preview_send_sets_future_deadline() {
3903 let mut client = test_client();
3904 let now = Instant::now();
3905 record_preview_send(&mut client, 5, now);
3906 let deadline = client.preview_next_send_at.get(&5).unwrap();
3907 assert!(*deadline > now);
3908 }
3909
3910 #[test]
3911 fn record_preview_send_successive_calls_advance() {
3912 let mut client = test_client();
3913 let now = Instant::now();
3914 record_preview_send(&mut client, 5, now);
3915 let first = *client.preview_next_send_at.get(&5).unwrap();
3916 record_preview_send(&mut client, 5, first);
3917 let second = *client.preview_next_send_at.get(&5).unwrap();
3918 assert!(second > first, "successive sends should advance deadline");
3919 }
3920
3921 fn browser_ready_high_bandwidth_client() -> ClientState {
3935 let mut c = test_client();
3936 c.display_fps = 120.0;
3937 c.rtt_ms = 1.0;
3938 c.min_rtt_ms = 1.0;
3939 c.goodput_bps = 50_000_000.0;
3940 c.delivery_bps = 50_000_000.0;
3941 c.last_goodput_sample_bps = 50_000_000.0;
3942 c.avg_paced_frame_bytes = 30_000.0;
3943 c.avg_preview_frame_bytes = 1_024.0;
3944 c.avg_frame_bytes = 30_000.0;
3945 c.browser_apply_ms = 0.3;
3946 c
3947 }
3948
3949 fn congested_client() -> ClientState {
3952 let mut c = test_client();
3953 c.display_fps = 120.0;
3954 c.rtt_ms = 500.0;
3955 c.min_rtt_ms = 40.0;
3956 c.goodput_bps = 200_000.0;
3957 c.delivery_bps = 150_000.0;
3958 c.last_goodput_sample_bps = 200_000.0;
3959 c.avg_paced_frame_bytes = 50_000.0;
3960 c.avg_preview_frame_bytes = 1_024.0;
3961 c.avg_frame_bytes = 50_000.0;
3962 c.goodput_jitter_bps = 50_000.0;
3963 c.max_goodput_jitter_bps = 200_000.0;
3964 c.browser_apply_ms = 1.0;
3965 c
3966 }
3967
3968 fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
3972 let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
3973 client.inflight_bytes += bytes;
3974 client.inflight_frames.push_back(InFlightFrame {
3975 sent_at,
3976 bytes,
3977 paced: true,
3978 });
3979 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
3981 record_ack(client);
3982 }
3983
3984 fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
3985 for _ in 0..n {
3986 sim_ack(client, bytes, rtt_ms);
3987 }
3988 }
3989
3990 #[test]
3993 fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
3994 let client = browser_ready_high_bandwidth_client();
3995 assert!(
3996 (pacing_fps(&client) - client.display_fps).abs() < 0.01,
3997 "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
3998 pacing_fps(&client),
3999 client.display_fps,
4000 );
4001 }
4002
4003 #[test]
4004 fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
4005 let client = browser_ready_high_bandwidth_client();
4006 let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
4007 let frame_ms = 1000.0 / client.display_fps;
4008 assert!(
4009 interval_ms <= frame_ms + 0.1,
4010 "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
4011 );
4012 }
4013
4014 #[test]
4017 fn congested_pipe_reduces_pacing_fps_substantially() {
4018 let client = congested_client();
4019 let fps = pacing_fps(&client);
4020 assert!(
4021 fps < client.display_fps * 0.5,
4022 "pacing_fps {fps:.0} should be well below display_fps {} when congested",
4023 client.display_fps,
4024 );
4025 }
4026
4027 #[test]
4028 fn congested_pipe_is_throughput_limited() {
4029 let client = congested_client();
4030 assert!(
4031 throughput_limited(&client),
4032 "congested client must be recognised as throughput-limited"
4033 );
4034 }
4035
4036 #[test]
4042 fn byte_window_bounded_near_bdp_when_congested() {
4043 let client = congested_client();
4044 let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
4046 let window = target_byte_window(&client);
4047 assert!(
4048 window < bdp as usize * 8,
4049 "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
4050 expected ≤ 8× — lead_floor may be dominating",
4051 window as f32 / bdp.max(1.0),
4052 );
4053 }
4054
4055 #[test]
4061 fn min_rtt_not_contaminated_by_congested_rtts() {
4062 let mut client = test_client();
4063 client.display_fps = 120.0;
4064 client.rtt_ms = 40.0;
4065 client.min_rtt_ms = 40.0;
4066 client.goodput_bps = 2_000_000.0;
4067 client.delivery_bps = 2_000_000.0;
4068 client.avg_paced_frame_bytes = 30_000.0;
4069 client.avg_preview_frame_bytes = 1_024.0;
4070 let original_min = client.min_rtt_ms;
4071
4072 sim_acks(&mut client, 200, 30_000, 500.0);
4074
4075 assert!(
4076 client.min_rtt_ms < original_min * 2.0,
4077 "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
4078 client.min_rtt_ms,
4079 );
4080 }
4081
4082 #[test]
4085 fn delivery_bps_rises_quickly_when_congestion_clears() {
4086 let mut client = congested_client();
4087 let before = client.delivery_bps;
4088
4089 sim_acks(&mut client, 10, 30_000, 40.0);
4091
4092 assert!(
4093 client.delivery_bps > before * 2.0,
4094 "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
4095 client.delivery_bps,
4096 );
4097 }
4098
4099 #[test]
4100 fn pacing_fps_recovers_after_congestion_clears() {
4101 let mut client = congested_client();
4102
4103 for _ in 0..40 {
4109 let target = target_frame_window(&client).max(2);
4110 for _ in 0..target {
4111 let sent_at = Instant::now() - Duration::from_millis(40);
4112 client.inflight_bytes += 30_000;
4113 client.inflight_frames.push_back(InFlightFrame {
4114 sent_at,
4115 bytes: 30_000,
4116 paced: true,
4117 });
4118 }
4119 client.goodput_window_start = Instant::now() - Duration::from_millis(25);
4120 for _ in 0..target {
4121 record_ack(&mut client);
4122 }
4123 }
4124
4125 let fps = pacing_fps(&client);
4126 assert!(
4127 fps > client.display_fps * 0.7,
4128 "pacing_fps {fps:.0} didn't recover toward display_fps {} \
4129 after window-saturated rounds at low RTT",
4130 client.display_fps,
4131 );
4132 }
4133
4134 #[test]
4135 fn rtt_estimate_drops_quickly_when_congestion_clears() {
4136 let mut client = test_client();
4137 client.rtt_ms = 500.0;
4138 client.min_rtt_ms = 40.0;
4139 client.goodput_bps = 2_000_000.0;
4140 client.avg_paced_frame_bytes = 30_000.0;
4141 client.avg_preview_frame_bytes = 1_024.0;
4142
4143 sim_acks(&mut client, 10, 30_000, 40.0);
4146
4147 assert!(
4148 client.rtt_ms < 300.0,
4149 "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
4150 client.rtt_ms,
4151 );
4152 }
4153
4154 #[test]
4157 fn probe_collapses_immediately_on_queue_delay() {
4158 let mut client = test_client();
4159 client.display_fps = 120.0;
4160 client.rtt_ms = 40.0;
4161 client.min_rtt_ms = 40.0;
4162 client.goodput_bps = 5_000_000.0;
4163 client.delivery_bps = 5_000_000.0;
4164 client.last_goodput_sample_bps = 5_000_000.0;
4165 client.avg_paced_frame_bytes = 10_000.0;
4166 client.avg_preview_frame_bytes = 1_024.0;
4167 client.probe_frames = 10.0;
4168
4169 sim_acks(&mut client, 5, 10_000, 600.0);
4171
4172 assert!(
4173 client.probe_frames < 5.0,
4174 "probe_frames {:.1} should have collapsed on queue delay signal",
4175 client.probe_frames,
4176 );
4177 }
4178
4179 #[test]
4180 fn probe_grows_when_window_saturated_with_clean_rtt() {
4181 let mut client = test_client();
4182 client.display_fps = 120.0;
4183 client.rtt_ms = 40.0;
4184 client.min_rtt_ms = 40.0;
4185 client.goodput_bps = 5_000_000.0;
4186 client.delivery_bps = 5_000_000.0;
4187 client.last_goodput_sample_bps = 5_000_000.0;
4188 client.avg_paced_frame_bytes = 10_000.0;
4189 client.avg_preview_frame_bytes = 1_024.0;
4190 client.goodput_jitter_bps = 0.0;
4191 client.max_goodput_jitter_bps = 0.0;
4192 client.probe_frames = 0.0;
4193
4194 let target = target_frame_window(&client);
4196 for _ in 0..target {
4197 let sent_at = Instant::now() - Duration::from_millis(40);
4198 client.inflight_bytes += 10_000;
4199 client.inflight_frames.push_back(InFlightFrame {
4200 sent_at,
4201 bytes: 10_000,
4202 paced: true,
4203 });
4204 }
4205
4206 record_ack(&mut client);
4215
4216 assert!(
4217 client.probe_frames > 0.0,
4218 "probe_frames should grow when window-saturated with clean RTT"
4219 );
4220 }
4221
4222 #[test]
4225 fn frame_window_larger_on_high_latency_link() {
4226 let mut lo = test_client();
4227 lo.display_fps = 120.0;
4228 lo.rtt_ms = 10.0;
4229 lo.min_rtt_ms = 10.0;
4230 lo.goodput_bps = 5_000_000.0;
4231 lo.delivery_bps = 5_000_000.0;
4232 lo.avg_paced_frame_bytes = 10_000.0;
4233 lo.avg_preview_frame_bytes = 1_024.0;
4234
4235 let mut hi = test_client();
4236 hi.display_fps = 120.0;
4237 hi.rtt_ms = 200.0;
4238 hi.min_rtt_ms = 200.0;
4239 hi.goodput_bps = 5_000_000.0;
4240 hi.delivery_bps = 5_000_000.0;
4241 hi.avg_paced_frame_bytes = 10_000.0;
4242 hi.avg_preview_frame_bytes = 1_024.0;
4243
4244 let lo_win = target_frame_window(&lo);
4245 let hi_win = target_frame_window(&hi);
4246 assert!(
4247 hi_win > lo_win,
4248 "high-latency link ({hi_win}f) should need more frames in flight \
4249 than low-latency ({lo_win}f)"
4250 );
4251 }
4252
4253 #[test]
4256 fn small_frame_byte_window_enables_pipelining() {
4257 let mut client = test_client();
4262 client.display_fps = 120.0;
4263 client.rtt_ms = 165.0;
4264 client.min_rtt_ms = 8.0;
4265 client.goodput_bps = 11_000.0; client.delivery_bps = 6_800.0;
4267 client.last_goodput_sample_bps = 11_000.0;
4268 client.avg_paced_frame_bytes = 1_120.0;
4269 client.avg_preview_frame_bytes = 1_024.0;
4270 client.goodput_jitter_bps = 4_300.0;
4271 client.max_goodput_jitter_bps = 6_500.0;
4272
4273 let window = target_byte_window(&client);
4274 let frames = target_frame_window(&client);
4275 let pipeline = frames * 1_120;
4276
4277 assert!(
4278 window >= pipeline,
4279 "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
4280 so small frames can pipeline across the RTT"
4281 );
4282 }
4283
4284 #[test]
4285 fn large_frame_byte_window_bounded_by_one_frame_floor() {
4286 let mut client = test_client();
4290 client.display_fps = 120.0;
4291 client.rtt_ms = 165.0;
4292 client.min_rtt_ms = 8.0;
4293 client.goodput_bps = 11_000.0;
4294 client.delivery_bps = 6_800.0;
4295 client.last_goodput_sample_bps = 11_000.0;
4296 client.avg_paced_frame_bytes = 50_000.0; client.avg_preview_frame_bytes = 1_024.0;
4298 client.goodput_jitter_bps = 0.0;
4299 client.max_goodput_jitter_bps = 0.0;
4300
4301 let window = target_byte_window(&client);
4302 let frames = target_frame_window(&client);
4303 let pipeline = frames.saturating_mul(50_000);
4304
4305 assert!(
4306 window < pipeline,
4307 "byte window {window}B should be < full pipeline {pipeline}B \
4308 ({frames}f × 50KB) — large frames must use one-frame floor"
4309 );
4310 assert!(
4311 window >= 50_000,
4312 "byte window {window}B must be at least one frame (50KB)"
4313 );
4314 }
4315
4316 #[test]
4319 fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
4320 let mut client = browser_ready_high_bandwidth_client();
4321 client.lead = Some(1);
4322 client.subscriptions.insert(1);
4323 let target = target_frame_window(&client);
4324 fill_inflight(&mut client, target.saturating_sub(1), 512);
4325 assert!(
4326 !lead_window_open(&client, true),
4327 "preview reservation should apply uniformly for lead clients"
4328 );
4329 }
4330
4331 #[test]
4334 fn probe_recovers_on_healthy_path_after_blip() {
4335 let mut client = browser_ready_high_bandwidth_client();
4336 client.probe_frames = 8.0;
4337
4338 sim_acks(&mut client, 3, 30_000, 200.0);
4340 let post_blip = client.probe_frames;
4341 assert!(
4342 post_blip < 4.0,
4343 "probe_frames {post_blip:.1} should have dropped after blip"
4344 );
4345
4346 client.browser_backlog_frames = 0;
4348 client.browser_ack_ahead_frames = 0;
4349 client.browser_apply_ms = 0.3;
4350
4351 sim_acks(&mut client, 20, 30_000, 1.0);
4353
4354 assert!(
4355 client.probe_frames > post_blip,
4356 "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
4357 client.probe_frames,
4358 );
4359 }
4360
4361 #[test]
4362 fn jitter_decays_fast_on_browser_ready_path() {
4363 let mut client = browser_ready_high_bandwidth_client();
4364
4365 client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
4367 client.goodput_jitter_bps = client.goodput_bps * 0.3;
4368 let initial_jitter = client.max_goodput_jitter_bps;
4369
4370 sim_acks(&mut client, 10, 30_000, 1.0);
4372
4373 assert!(
4374 client.max_goodput_jitter_bps < initial_jitter * 0.5,
4375 "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
4376 (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
4377 client.max_goodput_jitter_bps,
4378 initial_jitter * 0.5,
4379 );
4380 }
4381
4382 #[test]
4383 fn byte_budget_uses_floor_when_goodput_depressed() {
4384 let mut client = browser_ready_high_bandwidth_client();
4385 client.goodput_bps = 100_000.0;
4386
4387 let budget = byte_budget_for(&client, 100.0);
4388 let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
4389
4390 assert!(
4391 budget >= floor_budget,
4392 "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
4393 when goodput_bps is depressed but delivery_bps is high"
4394 );
4395 }
4396
4397 #[test]
4398 fn probe_floor_maintained_under_congestion_signal() {
4399 let mut client = test_client();
4400 client.display_fps = 120.0;
4401 client.rtt_ms = 40.0;
4402 client.min_rtt_ms = 40.0;
4403 client.goodput_bps = 5_000_000.0;
4404 client.delivery_bps = 5_000_000.0;
4405 client.last_goodput_sample_bps = 5_000_000.0;
4406 client.avg_paced_frame_bytes = 10_000.0;
4407 client.avg_preview_frame_bytes = 1_024.0;
4408 client.probe_frames = 10.0;
4409
4410 sim_acks(&mut client, 20, 10_000, 600.0);
4412
4413 assert!(
4414 client.probe_frames >= 1.0,
4415 "probe_frames {:.1} should not drop below the floor of 1.0",
4416 client.probe_frames,
4417 );
4418 }
4419}