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 || self.approximate_size >= self.config.max_buffer_bytes
66    }
67
68    /// Get buffered lines and clear buffer
69    pub fn flush(&mut self) -> Vec<Vec<InlineSegment>> {
70        self.approximate_size = 0;
71        std::mem::take(&mut self.lines)
72    }
73
74    /// Get current buffer size (number of lines)
75    pub fn len(&self) -> usize {
76        self.lines.len()
77    }
78
79    /// Check if buffer is empty
80    pub fn is_empty(&self) -> bool {
81        self.lines.is_empty()
82    }
83
84    /// Get approximate bytes in buffer
85    pub fn approximate_bytes(&self) -> usize {
86        self.approximate_size
87    }
88
89    /// Force flush regardless of batch size
90    pub fn force_flush(&mut self) -> Vec<Vec<InlineSegment>> {
91        self.flush()
92    }
93
94    /// Clear buffer without returning contents
95    pub fn clear(&mut self) {
96        self.lines.clear();
97        self.approximate_size = 0;
98    }
99}
100
101impl Default for StreamBuffer {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107/// Streaming context for rendering multiple lines with metadata
108#[derive(Debug)]
109pub struct StreamingContext {
110    /// Output message kind for all lines in this stream
111    pub kind: InlineMessageKind,
112    /// Buffer for accumulating output
113    pub buffer: StreamBuffer,
114    /// Total lines rendered so far
115    pub total_lines: usize,
116}
117
118impl StreamingContext {
119    /// Create a new streaming context
120    pub fn new(kind: InlineMessageKind) -> Self {
121        Self { kind, buffer: StreamBuffer::new(), total_lines: 0 }
122    }
123
124    /// Create with custom buffer configuration
125    pub fn with_config(kind: InlineMessageKind, config: StreamConfig) -> Self {
126        Self {
127            kind,
128            buffer: StreamBuffer::with_config(config),
129            total_lines: 0,
130        }
131    }
132
133    /// Add a line and track total
134    pub fn append(&mut self, segments: Vec<InlineSegment>) -> bool {
135        let should_flush = self.buffer.append_line(segments);
136        self.total_lines += 1;
137        should_flush
138    }
139
140    /// Get flushed lines and update tracking
141    pub fn flush(&mut self) -> Vec<Vec<InlineSegment>> {
142        self.buffer.flush()
143    }
144}
145
146/// Predicts memory requirements for rendering markdown
147pub struct AllocationPredictor {
148    /// Estimated bytes per average line
149    bytes_per_line: usize,
150}
151
152impl AllocationPredictor {
153    /// Create predictor with default estimates
154    pub fn new() -> Self {
155        Self {
156            bytes_per_line: 120, // Average terminal line content
157        }
158    }
159
160    /// Estimate total bytes needed for N lines
161    pub fn estimate_total_bytes(&self, line_count: usize) -> usize {
162        line_count * self.bytes_per_line
163    }
164
165    /// Estimate optimal batch size for given document size
166    pub fn optimal_batch_size(&self, _total_bytes: usize) -> usize {
167        // Batches of roughly 8KB (can be tuned)
168        let target_batch_bytes = 8192;
169        let batch_lines = (target_batch_bytes / self.bytes_per_line).max(5);
170        batch_lines.min(50) // Cap at 50 lines per batch
171    }
172
173    /// Predict pre-allocation size for markdown rendering
174    pub fn pre_allocation_capacity(&self, estimated_lines: usize) -> usize {
175        #[allow(
176            clippy::cast_sign_loss,
177            clippy::let_and_return,
178            reason = "Intentional compatibility, platform, or test-only suppression."
179        )]
180        let cap = ((estimated_lines as f64 * 1.2).max(0.0)) as usize; // 20% headroom
181        cap
182    }
183}
184
185impl Default for AllocationPredictor {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_stream_buffer_creation() {
197        let buffer = StreamBuffer::new();
198        assert!(buffer.is_empty());
199        assert_eq!(buffer.len(), 0);
200    }
201
202    #[test]
203    fn test_stream_buffer_append() {
204        let mut buffer = StreamBuffer::new();
205        let segment = InlineSegment {
206            text: "test".to_string(),
207            style: std::sync::Arc::new(Default::default()),
208        };
209        let should_flush = buffer.append_line(vec![segment]);
210        assert!(!should_flush); // Default batch size is 20
211        assert_eq!(buffer.len(), 1);
212    }
213
214    #[test]
215    fn test_stream_buffer_batch_flush() {
216        let mut buffer = StreamBuffer::with_config(StreamConfig { batch_size: 5, max_buffer_bytes: usize::MAX });
217
218        for i in 0..5 {
219            let segment = InlineSegment {
220                text: format!("line {i}"),
221                style: std::sync::Arc::new(Default::default()),
222            };
223            let should_flush = buffer.append_line(vec![segment]);
224            if i < 4 {
225                assert!(!should_flush);
226            } else {
227                assert!(should_flush);
228            }
229        }
230        assert_eq!(buffer.len(), 5);
231    }
232
233    #[test]
234    fn test_stream_buffer_byte_limit_flush() {
235        let mut buffer = StreamBuffer::with_config(StreamConfig { batch_size: 100, max_buffer_bytes: 50 });
236
237        let segment = InlineSegment {
238            text: "x".repeat(60),
239            style: std::sync::Arc::new(Default::default()),
240        };
241        let should_flush = buffer.append_line(vec![segment]);
242        assert!(should_flush);
243    }
244
245    #[test]
246    fn test_stream_buffer_flush_returns_lines() {
247        let mut buffer = StreamBuffer::new();
248        let segment = InlineSegment {
249            text: "test".to_string(),
250            style: std::sync::Arc::new(Default::default()),
251        };
252        buffer.append_line(vec![segment]);
253
254        let flushed = buffer.flush();
255        assert_eq!(flushed.len(), 1);
256        assert!(buffer.is_empty());
257    }
258
259    #[test]
260    fn test_streaming_context() {
261        let mut ctx = StreamingContext::new(InlineMessageKind::Agent);
262        assert_eq!(ctx.total_lines, 0);
263
264        let segment = InlineSegment {
265            text: "test".to_string(),
266            style: std::sync::Arc::new(Default::default()),
267        };
268        ctx.append(vec![segment]);
269        assert_eq!(ctx.total_lines, 1);
270    }
271
272    #[test]
273    fn test_allocation_predictor() {
274        let predictor = AllocationPredictor::new();
275        let estimate = predictor.estimate_total_bytes(100);
276        assert!(estimate > 0);
277
278        let batch = predictor.optimal_batch_size(10000);
279        assert!(batch > 0 && batch <= 50);
280    }
281
282    #[test]
283    fn test_stream_config_defaults() {
284        let config = StreamConfig::default();
285        assert_eq!(config.batch_size, 20);
286        assert_eq!(config.max_buffer_bytes, 65536);
287    }
288
289    #[test]
290    fn test_pre_allocation_capacity() {
291        let predictor = AllocationPredictor::new();
292        let capacity = predictor.pre_allocation_capacity(100);
293        assert!(capacity >= 100); // At least original size
294        assert!(capacity <= 120); // With 20% headroom
295    }
296}