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