ricecoder_tui/
performance.rs

1//! Performance optimization utilities for the TUI
2//!
3//! This module provides utilities for optimizing rendering performance,
4//! including lazy loading of message history and efficient diff rendering.
5
6use std::collections::VecDeque;
7
8/// Configuration for lazy loading
9#[derive(Debug, Clone)]
10pub struct LazyLoadConfig {
11    /// Number of messages to load per chunk
12    pub chunk_size: usize,
13    /// Maximum number of chunks to keep in memory
14    pub max_chunks: usize,
15}
16
17impl Default for LazyLoadConfig {
18    fn default() -> Self {
19        Self {
20            chunk_size: 50,
21            max_chunks: 10,
22        }
23    }
24}
25
26/// Lazy-loaded message history
27#[derive(Debug, Clone)]
28pub struct LazyMessageHistory {
29    /// Currently loaded messages
30    messages: VecDeque<String>,
31    /// Configuration
32    config: LazyLoadConfig,
33    /// Total number of messages (including unloaded)
34    total_count: usize,
35    /// Index of the first loaded message
36    first_loaded_index: usize,
37}
38
39impl LazyMessageHistory {
40    /// Create a new lazy message history
41    pub fn new(config: LazyLoadConfig) -> Self {
42        Self {
43            messages: VecDeque::with_capacity(config.chunk_size * config.max_chunks),
44            config,
45            total_count: 0,
46            first_loaded_index: 0,
47        }
48    }
49
50    /// Add a message to the history
51    pub fn add_message(&mut self, message: String) {
52        self.messages.push_back(message);
53        self.total_count += 1;
54
55        // Evict old messages if we exceed max capacity
56        let max_capacity = self.config.chunk_size * self.config.max_chunks;
57        while self.messages.len() > max_capacity {
58            self.messages.pop_front();
59            self.first_loaded_index += 1;
60        }
61    }
62
63    /// Get visible messages (for rendering)
64    pub fn visible_messages(&self, start_index: usize, count: usize) -> Vec<&String> {
65        let end_index = (start_index + count).min(self.total_count);
66        let relative_start = start_index.saturating_sub(self.first_loaded_index);
67        let relative_end = end_index.saturating_sub(self.first_loaded_index);
68
69        if relative_start >= self.messages.len() || relative_end <= relative_start {
70            return Vec::new();
71        }
72
73        self.messages
74            .iter()
75            .skip(relative_start)
76            .take(relative_end - relative_start)
77            .collect()
78    }
79
80    /// Get total message count
81    pub fn total_count(&self) -> usize {
82        self.total_count
83    }
84
85    /// Get number of loaded messages
86    pub fn loaded_count(&self) -> usize {
87        self.messages.len()
88    }
89
90    /// Clear all messages
91    pub fn clear(&mut self) {
92        self.messages.clear();
93        self.total_count = 0;
94        self.first_loaded_index = 0;
95    }
96}
97
98/// Diff rendering optimizer for large files
99#[derive(Debug, Clone)]
100pub struct DiffRenderOptimizer {
101    /// Maximum lines to render at once
102    pub max_lines_per_render: usize,
103    /// Whether to use syntax highlighting (can be expensive)
104    pub enable_syntax_highlighting: bool,
105}
106
107impl Default for DiffRenderOptimizer {
108    fn default() -> Self {
109        Self {
110            max_lines_per_render: 1000,
111            enable_syntax_highlighting: true,
112        }
113    }
114}
115
116impl DiffRenderOptimizer {
117    /// Create a new diff render optimizer
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Check if a diff is too large for full rendering
123    pub fn is_large_diff(&self, total_lines: usize) -> bool {
124        total_lines > self.max_lines_per_render * 2
125    }
126
127    /// Get the recommended chunk size for rendering
128    pub fn recommended_chunk_size(&self, total_lines: usize) -> usize {
129        if self.is_large_diff(total_lines) {
130            self.max_lines_per_render / 2
131        } else {
132            total_lines
133        }
134    }
135
136    /// Disable syntax highlighting for very large diffs
137    pub fn should_disable_syntax_highlighting(&self, total_lines: usize) -> bool {
138        total_lines > self.max_lines_per_render * 5
139    }
140}
141
142/// Theme switching performance tracker
143#[derive(Debug, Clone, Default)]
144pub struct ThemeSwitchPerformance {
145    /// Last theme switch time in milliseconds
146    pub last_switch_time_ms: u64,
147    /// Average theme switch time in milliseconds
148    pub average_switch_time_ms: u64,
149    /// Number of theme switches
150    pub switch_count: u32,
151}
152
153impl ThemeSwitchPerformance {
154    /// Record a theme switch time
155    pub fn record_switch(&mut self, time_ms: u64) {
156        self.last_switch_time_ms = time_ms;
157        self.average_switch_time_ms = (self.average_switch_time_ms * self.switch_count as u64
158            + time_ms)
159            / (self.switch_count as u64 + 1);
160        self.switch_count += 1;
161    }
162
163    /// Check if theme switching is performant
164    pub fn is_performant(&self) -> bool {
165        self.last_switch_time_ms < 100 && self.average_switch_time_ms < 100
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_lazy_message_history_add_and_retrieve() {
175        let mut history = LazyMessageHistory::new(LazyLoadConfig {
176            chunk_size: 10,
177            max_chunks: 2,
178        });
179
180        for i in 0..15 {
181            history.add_message(format!("Message {}", i));
182        }
183
184        assert_eq!(history.total_count(), 15);
185        assert_eq!(history.loaded_count(), 15);
186    }
187
188    #[test]
189    fn test_lazy_message_history_eviction() {
190        let mut history = LazyMessageHistory::new(LazyLoadConfig {
191            chunk_size: 5,
192            max_chunks: 2,
193        });
194
195        for i in 0..20 {
196            history.add_message(format!("Message {}", i));
197        }
198
199        // Should only keep last 10 messages (2 chunks of 5)
200        assert_eq!(history.loaded_count(), 10);
201        assert_eq!(history.total_count(), 20);
202    }
203
204    #[test]
205    fn test_lazy_message_history_visible_messages() {
206        let mut history = LazyMessageHistory::new(LazyLoadConfig {
207            chunk_size: 10,
208            max_chunks: 2,
209        });
210
211        for i in 0..15 {
212            history.add_message(format!("Message {}", i));
213        }
214
215        let visible = history.visible_messages(5, 5);
216        assert_eq!(visible.len(), 5);
217    }
218
219    #[test]
220    fn test_diff_render_optimizer_large_diff() {
221        let optimizer = DiffRenderOptimizer::new();
222        assert!(!optimizer.is_large_diff(500));
223        assert!(optimizer.is_large_diff(3000));
224    }
225
226    #[test]
227    fn test_theme_switch_performance_tracking() {
228        let mut perf = ThemeSwitchPerformance::default();
229        perf.record_switch(50);
230        perf.record_switch(75);
231
232        assert_eq!(perf.last_switch_time_ms, 75);
233        assert_eq!(perf.switch_count, 2);
234        assert!(perf.is_performant());
235    }
236
237    #[test]
238    fn test_theme_switch_performance_slow() {
239        let mut perf = ThemeSwitchPerformance::default();
240        perf.record_switch(150);
241
242        assert!(!perf.is_performant());
243    }
244}