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