1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use super::cursor::{OutputCursor, OutputCursorItem, OutputGap};
5use crate::TerminalPassthrough;
6
7pub const DEFAULT_OUTPUT_RING_CAPACITY: usize = 2048;
9pub const DEFAULT_RECENT_LIVE_BUFFER_CAPACITY: usize = 256 * 1024;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct OutputEvent {
15 sequence: u64,
16 bytes: Arc<[u8]>,
17 passthroughs: Vec<TerminalPassthrough>,
18}
19
20impl OutputEvent {
21 #[must_use]
23 pub fn from_shared(
24 sequence: u64,
25 bytes: Arc<[u8]>,
26 passthroughs: Vec<TerminalPassthrough>,
27 ) -> Self {
28 Self {
29 sequence,
30 bytes,
31 passthroughs,
32 }
33 }
34
35 #[must_use]
37 pub const fn sequence(&self) -> u64 {
38 self.sequence
39 }
40
41 #[must_use]
43 pub fn bytes(&self) -> &[u8] {
44 &self.bytes
45 }
46
47 #[must_use]
49 pub fn byte_len(&self) -> usize {
50 self.bytes.len()
51 }
52
53 #[must_use]
55 pub fn is_empty(&self) -> bool {
56 self.bytes.is_empty()
57 }
58
59 #[must_use]
61 pub fn passthroughs(&self) -> &[TerminalPassthrough] {
62 &self.passthroughs
63 }
64
65 #[must_use]
67 pub fn into_bytes(self) -> Vec<u8> {
68 self.bytes.to_vec()
69 }
70
71 #[must_use]
73 pub fn into_parts(self) -> (Vec<u8>, Vec<TerminalPassthrough>) {
74 (self.bytes.to_vec(), self.passthroughs)
75 }
76
77 #[must_use]
83 pub fn into_passthroughs(self) -> Vec<TerminalPassthrough> {
84 self.passthroughs
85 }
86
87 #[must_use]
89 pub fn with_passthroughs(mut self, passthroughs: Vec<TerminalPassthrough>) -> Self {
90 self.passthroughs = passthroughs;
91 self
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct RecentOutputSnapshot {
98 bytes: Vec<u8>,
99 oldest_sequence: Option<u64>,
100 newest_sequence: Option<u64>,
101 chunks: Vec<RecentOutputSnapshotChunk>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105struct RecentOutputSnapshotChunk {
106 sequence: u64,
107 start: usize,
108 starts_at_event_start: bool,
109}
110
111impl RecentOutputSnapshot {
112 #[must_use]
114 pub fn bytes(&self) -> &[u8] {
115 &self.bytes
116 }
117
118 #[must_use]
121 pub fn bytes_from_sequence(&self, min_sequence: u64) -> &[u8] {
122 let start = self
123 .chunks
124 .iter()
125 .find(|chunk| chunk.sequence >= min_sequence)
126 .map_or(self.bytes.len(), |chunk| chunk.start);
127 &self.bytes[start..]
128 }
129
130 #[must_use]
132 pub const fn oldest_sequence(&self) -> Option<u64> {
133 self.oldest_sequence
134 }
135
136 #[must_use]
138 pub const fn newest_sequence(&self) -> Option<u64> {
139 self.newest_sequence
140 }
141
142 #[must_use]
145 pub fn oldest_sequence_at_or_after(&self, min_sequence: u64) -> Option<u64> {
146 self.chunks
147 .iter()
148 .find(|chunk| chunk.sequence >= min_sequence)
149 .map(|chunk| chunk.sequence)
150 }
151
152 #[must_use]
155 pub fn starts_at_event_start(&self, sequence: u64) -> bool {
156 self.chunks
157 .iter()
158 .find(|chunk| chunk.sequence == sequence)
159 .is_some_and(|chunk| chunk.starts_at_event_start)
160 }
161
162 #[must_use]
164 pub fn len(&self) -> usize {
165 self.bytes.len()
166 }
167
168 #[must_use]
170 pub fn is_empty(&self) -> bool {
171 self.bytes.is_empty()
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct OutputRing {
178 event_capacity: usize,
179 recent_byte_capacity: usize,
180 retained_event_bytes: usize,
181 next_sequence: u64,
182 events: VecDeque<OutputEvent>,
183 recent: RecentLiveBuffer,
184}
185
186impl OutputRing {
187 #[must_use]
192 pub fn new(event_capacity: usize, recent_byte_capacity: usize) -> Self {
193 assert!(event_capacity > 0, "output ring capacity must be positive");
194 assert!(
195 recent_byte_capacity > 0,
196 "recent live buffer capacity must be positive"
197 );
198 Self {
199 event_capacity,
200 recent_byte_capacity,
201 retained_event_bytes: 0,
202 next_sequence: 0,
203 events: VecDeque::new(),
207 recent: RecentLiveBuffer::new(recent_byte_capacity),
208 }
209 }
210
211 #[must_use]
213 pub fn with_default_capacities() -> Self {
214 Self::new(
215 DEFAULT_OUTPUT_RING_CAPACITY,
216 DEFAULT_RECENT_LIVE_BUFFER_CAPACITY,
217 )
218 }
219
220 pub fn push(&mut self, bytes: Vec<u8>) -> u64 {
222 self.push_shared_with_recent_retention(bytes.into(), true)
223 }
224
225 pub fn push_with_recent_retention(&mut self, bytes: Vec<u8>, retain_recent: bool) -> u64 {
227 self.push_shared_with_recent_retention(bytes.into(), retain_recent)
228 }
229
230 pub fn push_shared(&mut self, bytes: Arc<[u8]>) -> u64 {
232 self.push_shared_with_recent_retention(bytes, true)
233 }
234
235 pub fn push_shared_with_recent_retention(
237 &mut self,
238 bytes: Arc<[u8]>,
239 retain_recent: bool,
240 ) -> u64 {
241 let sequence = self.next_sequence;
242 self.next_sequence = self
243 .next_sequence
244 .checked_add(1)
245 .expect("output ring sequence space exhausted");
246 if retain_recent {
247 self.recent.push(sequence, Arc::clone(&bytes));
248 }
249 self.retained_event_bytes = self.retained_event_bytes.saturating_add(bytes.len());
250 self.events.push_back(OutputEvent {
251 sequence,
252 bytes,
253 passthroughs: Vec::new(),
254 });
255 while self.events.len() > self.event_capacity {
256 self.pop_oldest_event();
257 }
258 while self.retained_event_bytes > self.recent_byte_capacity && self.events.len() > 1 {
259 self.pop_oldest_event();
260 }
261 sequence
262 }
263
264 pub fn clear_retained(&mut self) {
266 self.events.clear();
267 self.retained_event_bytes = 0;
268 self.recent.clear();
269 }
270
271 #[must_use]
273 pub fn cursor_from_oldest(&self) -> OutputCursor {
274 OutputCursor::new(self.oldest_sequence())
275 }
276
277 #[must_use]
279 pub fn cursor_from_now(&self) -> OutputCursor {
280 OutputCursor::new(self.next_sequence)
281 }
282
283 #[must_use]
285 pub const fn cursor_from_sequence(sequence: u64) -> OutputCursor {
286 OutputCursor::new(sequence)
287 }
288
289 pub fn poll_cursor(&self, cursor: &mut OutputCursor) -> Option<OutputCursorItem> {
291 let next = cursor.next_sequence();
292 let oldest = self.oldest_sequence();
293 if next < oldest {
294 let missed = oldest.saturating_sub(next);
295 cursor.record_gap(missed, oldest);
296 return Some(OutputCursorItem::Gap(Box::new(OutputGap::new(
297 next,
298 oldest,
299 missed,
300 self.newest_sequence(),
301 self.recent_snapshot(),
302 ))));
303 }
304
305 if next >= self.next_sequence {
306 return None;
307 }
308
309 let offset = usize::try_from(next.saturating_sub(oldest)).ok()?;
310 let event = self.events.get(offset).cloned()?;
311 cursor.advance_to(next.wrapping_add(1));
312 Some(OutputCursorItem::Event(event))
313 }
314
315 pub fn poll_cursor_batch(
322 &self,
323 cursor: &mut OutputCursor,
324 limit: usize,
325 ) -> Vec<OutputCursorItem> {
326 let mut items = Vec::new();
327 for _ in 0..limit {
328 let Some(item) = self.poll_cursor(cursor) else {
329 break;
330 };
331 let is_gap = matches!(item, OutputCursorItem::Gap(_));
332 items.push(item);
333 if is_gap {
334 break;
335 }
336 }
337 items
338 }
339
340 #[must_use]
342 pub fn oldest_sequence(&self) -> u64 {
343 self.events
344 .front()
345 .map_or(self.next_sequence, OutputEvent::sequence)
346 }
347
348 #[must_use]
350 pub const fn next_sequence(&self) -> u64 {
351 self.next_sequence
352 }
353
354 #[must_use]
356 pub fn newest_sequence(&self) -> u64 {
357 self.next_sequence.saturating_sub(1)
358 }
359
360 #[must_use]
362 pub const fn event_capacity(&self) -> usize {
363 self.event_capacity
364 }
365
366 #[must_use]
368 pub const fn recent_byte_capacity(&self) -> usize {
369 self.recent_byte_capacity
370 }
371
372 #[must_use]
374 pub fn retained_len(&self) -> usize {
375 self.events.len()
376 }
377
378 #[must_use]
380 pub const fn retained_event_bytes(&self) -> usize {
381 self.retained_event_bytes
382 }
383
384 #[must_use]
386 pub fn recent_len(&self) -> usize {
387 self.recent.len()
388 }
389
390 #[must_use]
392 pub fn recent_snapshot(&self) -> RecentOutputSnapshot {
393 self.recent.snapshot()
394 }
395
396 #[must_use]
398 pub fn retained_events(&self) -> Vec<OutputEvent> {
399 self.events.iter().cloned().collect()
400 }
401
402 fn pop_oldest_event(&mut self) {
403 if let Some(event) = self.events.pop_front() {
404 self.retained_event_bytes = self.retained_event_bytes.saturating_sub(event.byte_len());
405 }
406 }
407}
408
409impl Default for OutputRing {
410 fn default() -> Self {
411 Self::with_default_capacities()
412 }
413}
414
415#[derive(Debug, Clone)]
416struct RecentLiveBuffer {
417 capacity: usize,
418 len: usize,
419 chunks: VecDeque<RecentLiveChunk>,
420}
421
422#[derive(Debug, Clone)]
423struct RecentLiveChunk {
424 sequence: u64,
425 bytes: Arc<[u8]>,
426 start: usize,
427 end: usize,
428 starts_at_event_start: bool,
429}
430
431impl RecentLiveBuffer {
432 fn new(capacity: usize) -> Self {
433 Self {
434 capacity,
435 len: 0,
436 chunks: VecDeque::new(),
437 }
438 }
439
440 fn push(&mut self, sequence: u64, bytes: Arc<[u8]>) {
441 if bytes.is_empty() {
442 return;
443 }
444 if bytes.len() >= self.capacity {
445 let starts_at_event_start = bytes.len() == self.capacity;
446 let bytes = if starts_at_event_start {
447 bytes
448 } else {
449 Arc::from(&bytes[bytes.len() - self.capacity..])
450 };
451 self.chunks.clear();
452 self.chunks.push_back(RecentLiveChunk {
453 sequence,
454 start: 0,
455 end: bytes.len(),
456 bytes,
457 starts_at_event_start,
458 });
459 self.len = self.capacity;
460 return;
461 }
462 let byte_len = bytes.len();
463 self.chunks.push_back(RecentLiveChunk {
464 sequence,
465 bytes,
466 start: 0,
467 end: byte_len,
468 starts_at_event_start: true,
469 });
470 self.len = self.len.saturating_add(byte_len);
471 self.trim_front();
472 }
473
474 fn clear(&mut self) {
475 self.chunks.clear();
476 self.len = 0;
477 }
478
479 fn trim_front(&mut self) {
480 while self.len > self.capacity {
481 let overflow = self.len - self.capacity;
482 let Some(front) = self.chunks.front_mut() else {
483 self.len = 0;
484 return;
485 };
486 let front_len = front.end.saturating_sub(front.start);
487 if front_len <= overflow {
488 self.len -= front_len;
489 let _ = self.chunks.pop_front();
490 } else {
491 let start = front.start + overflow;
492 front.bytes = Arc::from(&front.bytes[start..front.end]);
493 front.start = 0;
494 front.end = front.bytes.len();
495 front.starts_at_event_start = false;
496 self.len -= overflow;
497 }
498 }
499 }
500
501 const fn len(&self) -> usize {
502 self.len
503 }
504
505 fn oldest_sequence(&self) -> Option<u64> {
506 self.chunks.front().map(|chunk| chunk.sequence)
507 }
508
509 fn newest_sequence(&self) -> Option<u64> {
510 self.chunks.back().map(|chunk| chunk.sequence)
511 }
512
513 fn snapshot(&self) -> RecentOutputSnapshot {
514 let mut bytes = Vec::with_capacity(self.len);
515 let mut snapshot_chunks = Vec::with_capacity(self.chunks.len());
516 for chunk in &self.chunks {
517 let start = bytes.len();
518 bytes.extend_from_slice(&chunk.bytes[chunk.start..chunk.end]);
519 snapshot_chunks.push(RecentOutputSnapshotChunk {
520 sequence: chunk.sequence,
521 start,
522 starts_at_event_start: chunk.starts_at_event_start,
523 });
524 }
525 RecentOutputSnapshot {
526 bytes,
527 oldest_sequence: self.oldest_sequence(),
528 newest_sequence: self.newest_sequence(),
529 chunks: snapshot_chunks,
530 }
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::{OutputRing, DEFAULT_OUTPUT_RING_CAPACITY, DEFAULT_RECENT_LIVE_BUFFER_CAPACITY};
537 use crate::events::{OutputCursor, OutputCursorItem};
538
539 #[test]
540 fn default_capacities_match_recorded_budget() {
541 let ring = OutputRing::default();
542 assert_eq!(ring.event_capacity(), DEFAULT_OUTPUT_RING_CAPACITY);
543 assert_eq!(
544 ring.recent_byte_capacity(),
545 DEFAULT_RECENT_LIVE_BUFFER_CAPACITY
546 );
547 assert_eq!(DEFAULT_OUTPUT_RING_CAPACITY, 2_048);
548 assert_eq!(DEFAULT_RECENT_LIVE_BUFFER_CAPACITY, 262_144);
549 }
550
551 #[test]
552 fn default_ring_preserves_immediate_sdk_burst_budget() {
553 let mut ring = OutputRing::default();
554 let mut cursor = ring.cursor_from_now();
555
556 for index in 0..1_200 {
557 ring.push(format!("line-{index:04}\n").into_bytes());
558 }
559
560 let Some(OutputCursorItem::Event(first)) = ring.poll_cursor(&mut cursor) else {
561 panic!("default output ring must retain the head of a fresh SDK burst");
562 };
563 assert_eq!(first.sequence(), 0);
564 assert_eq!(first.bytes(), b"line-0000\n");
565 }
566
567 #[test]
568 fn ring_rotation_keeps_only_most_recent_events() {
569 let mut ring = OutputRing::new(2, 64);
570 ring.push(b"zero".to_vec());
571 ring.push(b"one".to_vec());
572 ring.push(b"two".to_vec());
573
574 let sequences = ring
575 .retained_events()
576 .iter()
577 .map(|event| event.sequence())
578 .collect::<Vec<_>>();
579 assert_eq!(sequences, vec![1, 2]);
580 assert_eq!(ring.oldest_sequence(), 1);
581 assert_eq!(ring.next_sequence(), 3);
582 }
583
584 #[test]
585 fn ring_rotation_obeys_recent_byte_budget_for_retained_events() {
586 let mut ring = OutputRing::new(8, 6);
587 ring.push(b"aaaa".to_vec());
588 ring.push(b"bbbb".to_vec());
589 ring.push(b"cc".to_vec());
590
591 let sequences = ring
592 .retained_events()
593 .iter()
594 .map(|event| event.sequence())
595 .collect::<Vec<_>>();
596 assert_eq!(sequences, vec![1, 2]);
597 assert_eq!(ring.retained_event_bytes(), 6);
598 assert_eq!(ring.oldest_sequence(), 1);
599 }
600
601 #[test]
602 fn ring_keeps_newest_event_when_single_event_exceeds_byte_budget() {
603 let mut ring = OutputRing::new(8, 4);
604 ring.push(b"abcdef".to_vec());
605
606 assert_eq!(ring.retained_events().len(), 1);
607 assert_eq!(ring.retained_event_bytes(), 6);
608 assert_eq!(ring.oldest_sequence(), 0);
609 }
610
611 #[test]
612 fn recent_live_buffer_obeys_byte_bound() {
613 let mut ring = OutputRing::new(8, 5);
614 ring.push(b"abc".to_vec());
615 ring.push(b"defg".to_vec());
616 ring.push(b"hi".to_vec());
617
618 let snapshot = ring.recent_snapshot();
619 assert_eq!(snapshot.bytes(), b"efghi");
620 assert_eq!(snapshot.len(), 5);
621 assert_eq!(snapshot.oldest_sequence(), Some(1));
622 assert_eq!(snapshot.newest_sequence(), Some(2));
623 assert_eq!(ring.recent_len(), 5);
624 }
625
626 #[test]
627 fn recent_live_buffer_releases_trimmed_prefix_capacity() {
628 let mut ring = OutputRing::new(8, 4);
629 ring.push(b"abcd".to_vec());
630 ring.push(b"ef".to_vec());
631
632 assert_eq!(ring.recent_snapshot().bytes(), b"cdef");
633 assert_eq!(ring.recent_len(), 4);
634 let retained_capacity = ring
635 .recent
636 .chunks
637 .iter()
638 .map(|chunk| chunk.bytes.len())
639 .sum::<usize>();
640 assert!(
641 retained_capacity <= ring.recent_byte_capacity(),
642 "recent buffer retained capacity {retained_capacity} exceeds configured bound {}",
643 ring.recent_byte_capacity()
644 );
645 }
646
647 #[test]
648 fn recent_live_buffer_trims_oversized_single_event_to_bound() {
649 let mut ring = OutputRing::new(8, 4);
650 ring.push(b"012345".to_vec());
651
652 assert_eq!(ring.recent_snapshot().bytes(), b"2345");
653 assert_eq!(ring.recent_snapshot().oldest_sequence(), Some(0));
654 assert_eq!(ring.recent_snapshot().newest_sequence(), Some(0));
655 assert_eq!(ring.recent_len(), 4);
656 assert_eq!(ring.retained_events()[0].bytes(), b"012345");
657 }
658
659 #[test]
660 fn recent_snapshot_filters_bytes_by_contributing_sequence() {
661 let mut ring = OutputRing::new(8, 64);
662 ring.push(b"stale".to_vec());
663 ring.push(b"future".to_vec());
664 ring.push(b"tail".to_vec());
665
666 let snapshot = ring.recent_snapshot();
667
668 assert_eq!(snapshot.bytes_from_sequence(0), b"stalefuturetail");
669 assert_eq!(snapshot.bytes_from_sequence(1), b"futuretail");
670 assert_eq!(snapshot.bytes_from_sequence(2), b"tail");
671 assert_eq!(snapshot.bytes_from_sequence(3), b"");
672 assert_eq!(snapshot.oldest_sequence_at_or_after(1), Some(1));
673 assert_eq!(snapshot.oldest_sequence_at_or_after(3), None);
674 assert!(snapshot.starts_at_event_start(1));
675 }
676
677 #[test]
678 fn recent_snapshot_records_when_retained_event_prefix_was_trimmed() {
679 let mut ring = OutputRing::new(8, 4);
680 ring.push(b"012345".to_vec());
681
682 let snapshot = ring.recent_snapshot();
683
684 assert_eq!(snapshot.bytes_from_sequence(0), b"2345");
685 assert_eq!(snapshot.oldest_sequence_at_or_after(0), Some(0));
686 assert!(!snapshot.starts_at_event_start(0));
687 }
688
689 #[test]
690 fn cursor_lag_across_full_rotation_reports_all_missed_events() {
691 let mut ring = OutputRing::new(3, 16);
692 let mut cursor = OutputCursor::new(0);
693 for index in 0..6 {
694 ring.push(format!("{index}").into_bytes());
695 }
696
697 let Some(OutputCursorItem::Gap(gap)) = ring.poll_cursor(&mut cursor) else {
698 panic!("cursor should lag after ring rotation");
699 };
700 assert_eq!(gap.expected_sequence(), 0);
701 assert_eq!(gap.resume_sequence(), 3);
702 assert_eq!(gap.missed_events(), 3);
703 assert_eq!(gap.missed_range(), 0..3);
704 assert_eq!(gap.recent_snapshot().bytes(), b"012345");
705 assert_eq!(gap.recent_snapshot().oldest_sequence(), Some(0));
706 assert_eq!(gap.recent_snapshot().newest_sequence(), Some(5));
707 assert_eq!(cursor.missed_events(), 3);
708 }
709
710 #[test]
711 fn cursor_polls_rotated_ring_by_sequence_offset() {
712 let mut ring = OutputRing::new(3, 16);
713 for index in 0..6 {
714 ring.push(format!("{index}").into_bytes());
715 }
716 let mut cursor = OutputCursor::new(4);
717
718 let Some(OutputCursorItem::Event(event)) = ring.poll_cursor(&mut cursor) else {
719 panic!("cursor should read retained event from rotated ring");
720 };
721 assert_eq!(event.sequence(), 4);
722 assert_eq!(event.bytes(), b"4");
723 assert_eq!(cursor.next_sequence(), 5);
724 }
725
726 #[test]
727 fn batch_poll_reports_gap_only_for_lagged_cursor() {
728 let mut ring = OutputRing::new(2, 16);
729 let mut stale = OutputCursor::new(0);
730 let mut aligned = OutputCursor::new(2);
731 for index in 0..4 {
732 ring.push(format!("{index}").into_bytes());
733 }
734
735 let stale_batch = ring.poll_cursor_batch(&mut stale, 8);
736 assert_eq!(stale_batch.len(), 1);
737 let OutputCursorItem::Gap(gap) = &stale_batch[0] else {
738 panic!("stale cursor should report its own output gap");
739 };
740 assert_eq!(gap.expected_sequence(), 0);
741 assert_eq!(gap.resume_sequence(), 2);
742 assert_eq!(gap.missed_events(), 2);
743 assert_eq!(stale.next_sequence(), 2);
744
745 let aligned_batch = ring.poll_cursor_batch(&mut aligned, 8);
746 let sequences = aligned_batch
747 .iter()
748 .map(|item| match item {
749 OutputCursorItem::Event(event) => event.sequence(),
750 OutputCursorItem::Gap(gap) => {
751 panic!("aligned cursor must not inherit stale cursor lag: {gap:?}")
752 }
753 })
754 .collect::<Vec<_>>();
755 assert_eq!(sequences, vec![2, 3]);
756 assert_eq!(aligned.missed_events(), 0);
757 assert_eq!(aligned.next_sequence(), ring.next_sequence());
758 }
759
760 #[test]
761 fn clear_retained_drops_recent_snapshot_range_without_rewinding_sequence() {
762 let mut ring = OutputRing::new(3, 16);
763 let mut cursor = OutputCursor::new(0);
764 ring.push(b"one".to_vec());
765 ring.push(b"two".to_vec());
766
767 ring.clear_retained();
768
769 assert_eq!(ring.next_sequence(), 2);
770 let snapshot = ring.recent_snapshot();
771 assert!(snapshot.is_empty());
772 assert_eq!(snapshot.oldest_sequence(), None);
773 assert_eq!(snapshot.newest_sequence(), None);
774
775 let Some(OutputCursorItem::Gap(gap)) = ring.poll_cursor(&mut cursor) else {
776 panic!("cursor should observe cleared retained output as a gap");
777 };
778 assert_eq!(gap.expected_sequence(), 0);
779 assert_eq!(gap.resume_sequence(), 2);
780 assert_eq!(gap.missed_events(), 2);
781 assert_eq!(gap.missed_range(), 0..2);
782 assert_eq!(gap.newest_sequence(), 1);
783 assert!(gap.recent_snapshot().is_empty());
784 }
785}