Skip to main content

vtcode_core/core/
memory_pool.rs

1//! Memory pool for reducing allocations in hot paths
2
3use parking_lot::Mutex;
4use serde_json::Value;
5use std::collections::VecDeque;
6use std::sync::Arc;
7use vtcode_config::MemoryPoolConfig;
8
9/// Memory pool statistics for auto-tuning
10#[derive(Debug, Clone, Default)]
11pub struct MemoryPoolStats {
12    /// Number of successful string pool retrievals.
13    pub string_hits: usize,
14    /// Number of string pool misses requiring a new allocation.
15    pub string_misses: usize,
16    /// Number of successful value pool retrievals.
17    pub value_hits: usize,
18    /// Number of value pool misses requiring a new allocation.
19    pub value_misses: usize,
20    /// Number of successful `Vec<String>` pool retrievals.
21    pub vec_hits: usize,
22    /// Number of `Vec<String>` pool misses requiring a new allocation.
23    pub vec_misses: usize,
24    /// Total allocations avoided by reusing pooled objects.
25    pub allocations_avoided: usize,
26}
27
28/// Pre-allocated memory pools for common data structures
29pub struct MemoryPool {
30    string_pool: Mutex<VecDeque<String>>,
31    value_pool: Mutex<VecDeque<Value>>,
32    vec_pool: Mutex<VecDeque<Vec<String>>>,
33    stats: Mutex<MemoryPoolStats>,
34}
35
36impl MemoryPool {
37    /// Create a memory pool with default capacities.
38    pub fn new() -> Self {
39        Self {
40            string_pool: Mutex::new(VecDeque::with_capacity(64)),
41            value_pool: Mutex::new(VecDeque::with_capacity(32)),
42            vec_pool: Mutex::new(VecDeque::with_capacity(16)),
43            stats: Mutex::new(MemoryPoolStats::default()),
44        }
45    }
46
47    /// Create a new memory pool with custom sizes
48    pub fn with_capacities(string_capacity: usize, value_capacity: usize, vec_capacity: usize) -> Self {
49        Self {
50            string_pool: Mutex::new(VecDeque::with_capacity(string_capacity)),
51            value_pool: Mutex::new(VecDeque::with_capacity(value_capacity)),
52            vec_pool: Mutex::new(VecDeque::with_capacity(vec_capacity)),
53            stats: Mutex::new(MemoryPoolStats::default()),
54        }
55    }
56
57    /// Create a memory pool from configuration
58    pub fn from_config(config: &MemoryPoolConfig) -> Self {
59        Self {
60            string_pool: Mutex::new(VecDeque::with_capacity(config.max_string_pool_size)),
61            value_pool: Mutex::new(VecDeque::with_capacity(config.max_value_pool_size)),
62            vec_pool: Mutex::new(VecDeque::with_capacity(config.max_vec_pool_size)),
63            stats: Mutex::new(MemoryPoolStats::default()),
64        }
65    }
66
67    /// Get a reusable string, clearing it first
68    pub fn get_string(&self) -> String {
69        let mut stats = self.stats.lock();
70        let result = self.string_pool.lock().pop_front();
71        if let Some(mut s) = result {
72            stats.string_hits += 1;
73            stats.allocations_avoided += 1;
74            s.clear();
75            s
76        } else {
77            stats.string_misses += 1;
78            String::new()
79        }
80    }
81
82    /// Return a string to the pool after clearing it
83    /// Optimization: Only clear if string has content, preserve capacity for reuse
84    pub fn return_string(&self, mut s: String) {
85        // Shrink large strings to avoid memory bloat, reuse existing allocation
86        if s.capacity() > 4096 {
87            s.shrink_to(256);
88        }
89        s.clear();
90        let mut pool = self.string_pool.lock();
91        // Use capacity as the limit to respect configuration
92        if pool.len() < pool.capacity() {
93            pool.push_back(s);
94        }
95    }
96
97    /// Get a reusable Value
98    pub fn get_value(&self) -> Value {
99        let mut stats = self.stats.lock();
100        let result = self.value_pool.lock().pop_front();
101        if let Some(v) = result {
102            stats.value_hits += 1;
103            stats.allocations_avoided += 1;
104            v
105        } else {
106            stats.value_misses += 1;
107            Value::Null
108        }
109    }
110
111    /// Return a Value to the pool
112    /// Optimization: Only pool simple values to avoid memory bloat from large objects
113    pub fn return_value(&self, v: Value) {
114        // Optimization: Only pool null/simple values, skip large objects/arrays
115        let should_pool = match &v {
116            Value::Null | Value::Bool(_) | Value::Number(_) => true,
117            Value::String(s) => s.len() < 1024,
118            Value::Array(arr) => arr.is_empty(),
119            Value::Object(obj) => obj.is_empty(),
120        };
121
122        if should_pool {
123            let mut pool = self.value_pool.lock();
124            if pool.len() < pool.capacity() {
125                pool.push_back(v);
126            }
127        }
128    }
129
130    /// Get a reusable `Vec<String>`
131    pub fn get_vec(&self) -> Vec<String> {
132        let mut stats = self.stats.lock();
133        let result = self.vec_pool.lock().pop_front();
134        if let Some(mut v) = result {
135            stats.vec_hits += 1;
136            stats.allocations_avoided += 1;
137            v.clear();
138            v
139        } else {
140            stats.vec_misses += 1;
141            Vec::new()
142        }
143    }
144
145    /// Return a `Vec<String>` to the pool after clearing it
146    /// Optimization: Preserve capacity for vectors with reasonable size
147    pub fn return_vec(&self, mut v: Vec<String>) {
148        // Optimization: Shrink large vectors to avoid memory bloat
149        if v.capacity() > 128 {
150            v = Vec::with_capacity(32);
151        } else {
152            v.clear();
153        }
154        let mut pool = self.vec_pool.lock();
155        // Use capacity as the limit to respect configuration
156        if pool.len() < pool.capacity() {
157            pool.push_back(v);
158        }
159    }
160
161    /// Get memory pool statistics
162    pub fn get_stats(&self) -> MemoryPoolStats {
163        self.stats.lock().clone()
164    }
165
166    /// Reset statistics
167    pub fn reset_stats(&self) {
168        *self.stats.lock() = MemoryPoolStats::default();
169    }
170
171    /// Auto-tune pool sizes based on usage patterns
172    /// Returns recommendations for configuration adjustments
173    pub fn auto_tune(&self, config: &MemoryPoolConfig) -> MemoryPoolTuningRecommendation {
174        let stats = self.get_stats();
175
176        // Calculate hit rates
177        let string_hit_rate = if stats.string_hits + stats.string_misses > 0 {
178            stats.string_hits as f64 / (stats.string_hits + stats.string_misses) as f64
179        } else {
180            0.0
181        };
182
183        let value_hit_rate = if stats.value_hits + stats.value_misses > 0 {
184            stats.value_hits as f64 / (stats.value_hits + stats.value_misses) as f64
185        } else {
186            0.0
187        };
188
189        let vec_hit_rate = if stats.vec_hits + stats.vec_misses > 0 {
190            stats.vec_hits as f64 / (stats.vec_hits + stats.vec_misses) as f64
191        } else {
192            0.0
193        };
194
195        // Calculate current pool utilization
196        let string_utilization = self.string_pool.lock().len() as f64 / config.max_string_pool_size as f64;
197        let value_utilization = self.value_pool.lock().len() as f64 / config.max_value_pool_size as f64;
198        let vec_utilization = self.vec_pool.lock().len() as f64 / config.max_vec_pool_size as f64;
199
200        // Generate tuning recommendations
201        MemoryPoolTuningRecommendation {
202            string_hit_rate,
203            value_hit_rate,
204            vec_hit_rate,
205            string_utilization,
206            value_utilization,
207            vec_utilization,
208            total_allocations_avoided: stats.allocations_avoided,
209
210            // Recommendations based on usage patterns
211            string_size_recommendation: calculate_size_recommendation(
212                string_hit_rate,
213                string_utilization,
214                config.max_string_pool_size,
215            ),
216            value_size_recommendation: calculate_size_recommendation(
217                value_hit_rate,
218                value_utilization,
219                config.max_value_pool_size,
220            ),
221            vec_size_recommendation: calculate_size_recommendation(
222                vec_hit_rate,
223                vec_utilization,
224                config.max_vec_pool_size,
225            ),
226        }
227    }
228}
229
230/// Calculate size recommendation based on hit rate and utilization
231fn calculate_size_recommendation(hit_rate: f64, utilization: f64, current_size: usize) -> SizeRecommendation {
232    if hit_rate < 0.3 {
233        // Low hit rate - pool might be too small or not used effectively
234        if utilization > 0.8 {
235            SizeRecommendation::Increase(current_size.saturating_mul(2))
236        } else {
237            SizeRecommendation::Maintain
238        }
239    } else if hit_rate > 0.7 {
240        // High hit rate - pool is working well
241        if utilization > 0.9 {
242            SizeRecommendation::Increase(current_size.saturating_add(16))
243        } else if utilization < 0.5 {
244            SizeRecommendation::Decrease(current_size.saturating_sub(8).max(16))
245        } else {
246            SizeRecommendation::Maintain
247        }
248    } else {
249        // Medium hit rate
250        if utilization > 0.85 {
251            SizeRecommendation::Increase(current_size.saturating_add(8))
252        } else {
253            SizeRecommendation::Maintain
254        }
255    }
256}
257
258/// Memory pool tuning recommendation
259#[derive(Debug, Clone)]
260pub struct MemoryPoolTuningRecommendation {
261    /// Hit rate for the string pool (0.0 to 1.0).
262    pub string_hit_rate: f64,
263    /// Hit rate for the value pool (0.0 to 1.0).
264    pub value_hit_rate: f64,
265    /// Hit rate for the vector pool (0.0 to 1.0).
266    pub vec_hit_rate: f64,
267    /// Current utilization of the string pool (0.0 to 1.0).
268    pub string_utilization: f64,
269    /// Current utilization of the value pool (0.0 to 1.0).
270    pub value_utilization: f64,
271    /// Current utilization of the vector pool (0.0 to 1.0).
272    pub vec_utilization: f64,
273    /// Total allocations avoided across all pools.
274    pub total_allocations_avoided: usize,
275    /// Recommended action for the string pool size.
276    pub string_size_recommendation: SizeRecommendation,
277    /// Recommended action for the value pool size.
278    pub value_size_recommendation: SizeRecommendation,
279    /// Recommended action for the vector pool size.
280    pub vec_size_recommendation: SizeRecommendation,
281}
282
283/// Size recommendation enum
284#[derive(Debug, Clone, Copy)]
285pub enum SizeRecommendation {
286    /// Current pool size is appropriate.
287    Maintain,
288    /// Pool size should be increased to the specified capacity.
289    Increase(usize),
290    /// Pool size should be decreased to the specified capacity.
291    Decrease(usize),
292}
293
294impl From<MemoryPoolStats> for crate::telemetry::MemoryPoolTelemetry {
295    fn from(stats: MemoryPoolStats) -> Self {
296        Self {
297            string_hit_rate: if stats.string_hits + stats.string_misses > 0 {
298                stats.string_hits as f64 / (stats.string_hits + stats.string_misses) as f64
299            } else {
300                0.0
301            },
302            value_hit_rate: if stats.value_hits + stats.value_misses > 0 {
303                stats.value_hits as f64 / (stats.value_hits + stats.value_misses) as f64
304            } else {
305                0.0
306            },
307            vec_hit_rate: if stats.vec_hits + stats.vec_misses > 0 {
308                stats.vec_hits as f64 / (stats.vec_hits + stats.vec_misses) as f64
309            } else {
310                0.0
311            },
312            total_allocations_avoided: stats.allocations_avoided,
313        }
314    }
315}
316
317impl Default for MemoryPool {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323impl MemoryPool {
324    /// Pre-warm the pool with initial allocations
325    /// Call this during startup to avoid allocation latency during hot paths
326    pub fn pre_warm(&self, string_count: usize, value_count: usize, vec_count: usize) {
327        {
328            let mut pool = self.string_pool.lock();
329            let to_add = string_count.min(pool.capacity().saturating_sub(pool.len()));
330            for _ in 0..to_add {
331                pool.push_back(String::with_capacity(256));
332            }
333        }
334        {
335            let mut pool = self.value_pool.lock();
336            let to_add = value_count.min(pool.capacity().saturating_sub(pool.len()));
337            for _ in 0..to_add {
338                pool.push_back(Value::Null);
339            }
340        }
341        {
342            let mut pool = self.vec_pool.lock();
343            let to_add = vec_count.min(pool.capacity().saturating_sub(pool.len()));
344            for _ in 0..to_add {
345                pool.push_back(Vec::with_capacity(16));
346            }
347        }
348    }
349
350    /// Get a string with pre-allocated capacity for better performance
351    pub fn get_string_with_capacity(&self, capacity: usize) -> String {
352        let mut stats = self.stats.lock();
353        let result = self.string_pool.lock().pop_front();
354        if let Some(mut s) = result {
355            stats.string_hits += 1;
356            stats.allocations_avoided += 1;
357            s.clear();
358            // Reserve additional capacity if needed
359            if s.capacity() < capacity {
360                s.reserve(capacity - s.capacity());
361            }
362            s
363        } else {
364            stats.string_misses += 1;
365            String::with_capacity(capacity)
366        }
367    }
368}
369
370/// Global memory pool instance
371static MEMORY_POOL: once_cell::sync::Lazy<Arc<MemoryPool>> = once_cell::sync::Lazy::new(|| Arc::new(MemoryPool::new()));
372
373/// Get the global memory pool
374pub fn global_pool() -> Arc<MemoryPool> {
375    Arc::clone(&MEMORY_POOL)
376}