Skip to main content

vtcode_core/ui/
stream_buffer.rs

1//! Streaming buffer for batched output rendering
2//!
3//! Optimizes large output rendering by batching inline segments and flushing
4//! in configurable batches rather than line-by-line, reducing overhead.
5
6use crate::ui::tui::{InlineMessageKind, InlineSegment};
7
8/// Configuration for streaming behavior
9#[derive(Clone, Debug)]
10pub struct StreamConfig {
11    /// Number of lines to buffer before automatic flush
12    pub batch_size: usize,
13    /// Maximum buffer size before forced flush (bytes)
14    pub max_buffer_bytes: usize,
15}
16
17impl Default for StreamConfig {
18    fn default() -> Self {
19        Self {
20            batch_size: 20,          // Flush every 20 lines
21            max_buffer_bytes: 65536, // 64KB max buffer
22        }
23    }
24}
25
26/// Streaming buffer that batches output before rendering
27#[derive(Debug)]
28pub struct StreamBuffer {
29    /// Buffered line segments
30    lines: Vec<Vec<InlineSegment>>,
31    /// Configuration for batching behavior
32    config: StreamConfig,
33    /// Approximate size in bytes (for max_buffer_bytes check)
34    approximate_size: usize,
35}
36
37impl StreamBuffer {
38    /// Create a new streaming buffer with default configuration
39    pub fn new() -> Self {
40        Self::with_config(StreamConfig::default())
41    }
42
43    /// Create a streaming buffer with custom configuration
44    pub fn with_config(config: StreamConfig) -> Self {
45        Self {
46            lines: Vec::with_capacity(config.batch_size),
47            config,
48            approximate_size: 0,
49        }
50    }
51
52    /// Add a line of segments to the buffer
53    pub fn append_line(&mut self, segments: Vec<InlineSegment>) -> bool {
54        // Calculate approximate size: sum of all segment text lengths
55        let line_size: usize = segments.iter().map(|s| s.text.len()).sum();
56        self.approximate_size += line_size;
57        self.lines.push(segments);
58
59        // Check if we should flush
60        self.should_flush()
61    }
62
63    /// Check if buffer should be flushed
64    fn should_flush(&self) -> bool {
65        self.lines.len() >= self.config.batch_size
66            || self.approximate_size >= self.config.max_buffer_bytes
67    }
68
69    /// Get buffered lines and clear buffer
70    pub fn flush(&mut self) -> Vec<Vec<InlineSegment>> {
71        self.approximate_size = 0;
72        std::mem::take(&mut self.lines)
73    }
74
75    /// Get current buffer size (number of lines)
76    pub fn len(&self) -> usize {
77        self.lines.len()
78    }
79
80    /// Check if buffer is empty
81    pub fn is_empty(&self) -> bool {
82        self.lines.is_empty()
83    }
84
85    /// Get approximate bytes in buffer
86    pub fn approximate_bytes(&self) -> usize {
87        self.approximate_size
88    }
89
90    /// Force flush regardless of batch size
91    pub fn force_flush(&mut self) -> Vec<Vec<InlineSegment>> {
92        self.flush()
93    }
94
95    /// Clear buffer without returning contents
96    pub fn clear(&mut self) {
97        self.lines.clear();
98        self.approximate_size = 0;
99    }
100}
101
102impl Default for StreamBuffer {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108/// Streaming context for rendering multiple lines with metadata
109#[derive(Debug)]
110pub struct StreamingContext {
111    /// Output message kind for all lines in this stream
112    pub kind: InlineMessageKind,
113    /// Buffer for accumulating output
114    pub buffer: StreamBuffer,
115    /// Total lines rendered so far
116    pub total_lines: usize,
117}
118
119impl StreamingContext {
120    /// Create a new streaming context
121    pub fn new(kind: InlineMessageKind) -> Self {
122        Self {
123            kind,
124            buffer: StreamBuffer::new(),
125            total_lines: 0,
126        }
127    }
128
129    /// Create with custom buffer configuration
130    pub fn with_config(kind: InlineMessageKind, config: StreamConfig) -> Self {
131        Self {
132            kind,
133            buffer: StreamBuffer::with_config(config),
134            total_lines: 0,
135        }
136    }
137
138    /// Add a line and track total
139    pub fn append(&mut self, segments: Vec<InlineSegment>) -> bool {
140        let should_flush = self.buffer.append_line(segments);
141        self.total_lines += 1;
142        should_flush
143    }
144
145    /// Get flushed lines and update tracking
146    pub fn flush(&mut self) -> Vec<Vec<InlineSegment>> {
147        self.buffer.flush()
148    }
149}
150
151/// Predicts memory requirements for rendering markdown
152pub struct AllocationPredictor {
153    /// Estimated bytes per average line
154    bytes_per_line: usize,
155}
156
157impl AllocationPredictor {
158    /// Create predictor with default estimates
159    pub fn new() -> Self {
160        Self {
161            bytes_per_line: 120, // Average terminal line content
162        }
163    }
164
165    /// Estimate total bytes needed for N lines
166    pub fn estimate_total_bytes(&self, line_count: usize) -> usize {
167        line_count * self.bytes_per_line
168    }
169
170    /// Estimate optimal batch size for given document size
171    pub fn optimal_batch_size(&self, _total_bytes: usize) -> usize {
172        // Batches of roughly 8KB (can be tuned)
173        let target_batch_bytes = 8192;
174        let batch_lines = (target_batch_bytes / self.bytes_per_line).max(5);
175        batch_lines.min(50) // Cap at 50 lines per batch
176    }
177
178    /// Predict pre-allocation size for markdown rendering
179    pub fn pre_allocation_capacity(&self, estimated_lines: usize) -> usize {
180        (estimated_lines as f64 * 1.2) as usize // 20% headroom
181    }
182}
183
184impl Default for AllocationPredictor {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_stream_buffer_creation() {
196        let buffer = StreamBuffer::new();
197        assert!(buffer.is_empty());
198        assert_eq!(buffer.len(), 0);
199    }
200
201    #[test]
202    fn test_stream_buffer_append() {
203        let mut buffer = StreamBuffer::new();
204        let segment = InlineSegment {
205            text: "test".to_string(),
206            style: std::sync::Arc::new(Default::default()),
207        };
208        let should_flush = buffer.append_line(vec![segment]);
209        assert!(!should_flush); // Default batch size is 20
210        assert_eq!(buffer.len(), 1);
211    }
212
213    #[test]
214    fn test_stream_buffer_batch_flush() {
215        let mut buffer = StreamBuffer::with_config(StreamConfig {
216            batch_size: 5,
217            max_buffer_bytes: usize::MAX,
218        });
219
220        for i in 0..5 {
221            let segment = InlineSegment {
222                text: format!("line {}", i),
223                style: std::sync::Arc::new(Default::default()),
224            };
225            let should_flush = buffer.append_line(vec![segment]);
226            if i < 4 {
227                assert!(!should_flush);
228            } else {
229                assert!(should_flush);
230            }
231        }
232        assert_eq!(buffer.len(), 5);
233    }
234
235    #[test]
236    fn test_stream_buffer_byte_limit_flush() {
237        let mut buffer = StreamBuffer::with_config(StreamConfig {
238            batch_size: 100,
239            max_buffer_bytes: 50,
240        });
241
242        let segment = InlineSegment {
243            text: "x".repeat(60),
244            style: std::sync::Arc::new(Default::default()),
245        };
246        let should_flush = buffer.append_line(vec![segment]);
247        assert!(should_flush);
248    }
249
250    #[test]
251    fn test_stream_buffer_flush_returns_lines() {
252        let mut buffer = StreamBuffer::new();
253        let segment = InlineSegment {
254            text: "test".to_string(),
255            style: std::sync::Arc::new(Default::default()),
256        };
257        buffer.append_line(vec![segment]);
258
259        let flushed = buffer.flush();
260        assert_eq!(flushed.len(), 1);
261        assert!(buffer.is_empty());
262    }
263
264    #[test]
265    fn test_streaming_context() {
266        let mut ctx = StreamingContext::new(InlineMessageKind::Agent);
267        assert_eq!(ctx.total_lines, 0);
268
269        let segment = InlineSegment {
270            text: "test".to_string(),
271            style: std::sync::Arc::new(Default::default()),
272        };
273        ctx.append(vec![segment]);
274        assert_eq!(ctx.total_lines, 1);
275    }
276
277    #[test]
278    fn test_allocation_predictor() {
279        let predictor = AllocationPredictor::new();
280        let estimate = predictor.estimate_total_bytes(100);
281        assert!(estimate > 0);
282
283        let batch = predictor.optimal_batch_size(10000);
284        assert!(batch > 0 && batch <= 50);
285    }
286
287    #[test]
288    fn test_stream_config_defaults() {
289        let config = StreamConfig::default();
290        assert_eq!(config.batch_size, 20);
291        assert_eq!(config.max_buffer_bytes, 65536);
292    }
293
294    #[test]
295    fn test_pre_allocation_capacity() {
296        let predictor = AllocationPredictor::new();
297        let capacity = predictor.pre_allocation_capacity(100);
298        assert!(capacity >= 100); // At least original size
299        assert!(capacity <= 120); // With 20% headroom
300    }
301}