Skip to main content

renacer_core/
span_pool.rs

1/// Sprint 36: Memory Pool for Span Data
2///
3/// Provides efficient object pooling for span allocations to reduce
4/// allocator pressure and improve performance.
5///
6/// Zero-copy optimizations: Uses Cow<'static, str> for strings that
7/// are often known at compile time (syscall names, operation types).
8use std::borrow::Cow;
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11/// Configuration for the span pool
12#[derive(Debug, Clone)]
13pub struct SpanPoolConfig {
14    /// Initial capacity of the pool
15    pub capacity: usize,
16    /// Whether pooling is enabled
17    pub enabled: bool,
18}
19
20impl Default for SpanPoolConfig {
21    fn default() -> Self {
22        SpanPoolConfig {
23            capacity: 1024,
24            enabled: true,
25        }
26    }
27}
28
29impl SpanPoolConfig {
30    /// Create a new pool configuration
31    pub fn new(capacity: usize) -> Self {
32        SpanPoolConfig {
33            capacity,
34            enabled: true,
35        }
36    }
37
38    /// Disable pooling (for debugging)
39    pub fn disabled() -> Self {
40        SpanPoolConfig {
41            capacity: 0,
42            enabled: false,
43        }
44    }
45}
46
47/// Pooled span data (Sprint 36: zero-copy with Cow)
48#[derive(Debug, Clone)]
49pub struct PooledSpan {
50    pub trace_id: String,
51    pub span_id: String,
52    /// Operation name - often static (e.g., "syscall:open", "`compute_block:mean`")
53    /// Uses Cow for zero-copy when static strings are used
54    pub name: Cow<'static, str>,
55    /// Attributes with static keys (zero-copy optimization)
56    pub attributes: Vec<(Cow<'static, str>, String)>,
57    pub timestamp_nanos: u64,
58    pub duration_nanos: u64,
59    pub status_code: i32, // 0 = OK, 1 = ERROR
60}
61
62impl PooledSpan {
63    /// Create a new empty span
64    fn new() -> Self {
65        PooledSpan {
66            trace_id: String::new(),
67            span_id: String::new(),
68            name: Cow::Borrowed(""),
69            attributes: Vec::new(),
70            timestamp_nanos: 0,
71            duration_nanos: 0,
72            status_code: 0,
73        }
74    }
75
76    /// Reset span data for reuse
77    fn reset(&mut self) {
78        self.trace_id.clear();
79        self.span_id.clear();
80        self.name = Cow::Borrowed("");
81        self.attributes.clear();
82        self.timestamp_nanos = 0;
83        self.duration_nanos = 0;
84        self.status_code = 0;
85    }
86
87    /// Set span name from static string (zero-copy)
88    pub fn set_name_static(&mut self, name: &'static str) {
89        self.name = Cow::Borrowed(name);
90    }
91
92    /// Set span name from owned string
93    pub fn set_name_owned(&mut self, name: String) {
94        self.name = Cow::Owned(name);
95    }
96
97    /// Add attribute with static key (zero-copy for key)
98    pub fn add_attribute_static(&mut self, key: &'static str, value: String) {
99        self.attributes.push((Cow::Borrowed(key), value));
100    }
101
102    /// Add attribute with owned key
103    pub fn add_attribute_owned(&mut self, key: String, value: String) {
104        self.attributes.push((Cow::Owned(key), value));
105    }
106}
107
108/// Memory pool for span allocations
109pub struct SpanPool {
110    pool: Vec<PooledSpan>,
111    config: SpanPoolConfig,
112    allocated: AtomicUsize,
113    acquired: AtomicUsize,
114}
115
116impl SpanPool {
117    /// Create a new span pool with the given configuration
118    pub fn new(config: SpanPoolConfig) -> Self {
119        let capacity = config.capacity;
120        let mut pool = Vec::with_capacity(capacity);
121
122        if config.enabled {
123            // Pre-allocate pool
124            for _ in 0..capacity {
125                pool.push(PooledSpan::new());
126            }
127        }
128
129        SpanPool {
130            pool,
131            config,
132            allocated: AtomicUsize::new(capacity),
133            acquired: AtomicUsize::new(0),
134        }
135    }
136
137    /// Acquire a span from the pool
138    ///
139    /// If the pool is empty, allocates a new span (automatic growth).
140    /// If pooling is disabled, always allocates a new span.
141    pub fn acquire(&mut self) -> PooledSpan {
142        if !self.config.enabled {
143            // Pooling disabled, always allocate
144            self.allocated.fetch_add(1, Ordering::Relaxed);
145            return PooledSpan::new();
146        }
147
148        self.acquired.fetch_add(1, Ordering::Relaxed);
149
150        if let Some(mut span) = self.pool.pop() {
151            span.reset();
152            span
153        } else {
154            // Pool exhausted, allocate new
155            self.allocated.fetch_add(1, Ordering::Relaxed);
156            PooledSpan::new()
157        }
158    }
159
160    /// Release a span back to the pool
161    ///
162    /// If pooling is disabled, the span is simply dropped.
163    pub fn release(&mut self, span: PooledSpan) {
164        if !self.config.enabled {
165            // Pooling disabled, just drop
166            return;
167        }
168
169        // Only pool if we have capacity
170        if self.pool.len() < self.pool.capacity() {
171            self.pool.push(span);
172        }
173        // Otherwise drop (prevents unbounded growth)
174    }
175
176    /// Get pool statistics
177    pub fn stats(&self) -> PoolStats {
178        PoolStats {
179            capacity: self.config.capacity,
180            available: self.pool.len(),
181            allocated: self.allocated.load(Ordering::Relaxed),
182            acquired: self.acquired.load(Ordering::Relaxed),
183            enabled: self.config.enabled,
184        }
185    }
186
187    /// Check if the pool is enabled
188    pub fn is_enabled(&self) -> bool {
189        self.config.enabled
190    }
191
192    /// Get the current pool size (available spans)
193    pub fn available(&self) -> usize {
194        self.pool.len()
195    }
196
197    /// Get the pool capacity
198    pub fn capacity(&self) -> usize {
199        self.config.capacity
200    }
201}
202
203/// Pool statistics
204#[derive(Debug, Clone)]
205pub struct PoolStats {
206    /// Pool capacity
207    pub capacity: usize,
208    /// Available spans in pool
209    pub available: usize,
210    /// Total spans allocated (including auto-growth)
211    pub allocated: usize,
212    /// Total acquire operations
213    pub acquired: usize,
214    /// Whether pooling is enabled
215    pub enabled: bool,
216}
217
218impl PoolStats {
219    /// Calculate hit rate (percentage of acquires served from pool)
220    pub fn hit_rate(&self) -> f64 {
221        if self.acquired == 0 {
222            return 0.0;
223        }
224        let hits = self.acquired.saturating_sub(self.allocated - self.capacity);
225        (hits as f64 / self.acquired as f64) * 100.0
226    }
227
228    /// Calculate pool utilization (percentage of capacity used)
229    pub fn utilization(&self) -> f64 {
230        if self.capacity == 0 {
231            return 0.0;
232        }
233        let used = self.capacity - self.available;
234        (used as f64 / self.capacity as f64) * 100.0
235    }
236}
237
238// Compile-time thread-safety verification (Sprint 59)
239static_assertions::assert_impl_all!(SpanPoolConfig: Send, Sync);
240static_assertions::assert_impl_all!(PooledSpan: Send, Sync);
241static_assertions::assert_impl_all!(PoolStats: Send, Sync);
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_pool_acquire_release() {
249        let mut pool = SpanPool::new(SpanPoolConfig::new(10));
250
251        // Acquire a span
252        let span = pool.acquire();
253        assert_eq!(pool.available(), 9);
254
255        // Release it back
256        pool.release(span);
257        assert_eq!(pool.available(), 10);
258    }
259
260    #[test]
261    fn test_pool_exhaustion() {
262        let mut pool = SpanPool::new(SpanPoolConfig::new(2));
263
264        // Exhaust the pool
265        let span1 = pool.acquire();
266        let span2 = pool.acquire();
267        assert_eq!(pool.available(), 0);
268
269        // Acquiring more should auto-grow
270        let span3 = pool.acquire();
271        assert_eq!(pool.available(), 0);
272
273        // Release all
274        pool.release(span1);
275        pool.release(span2);
276        pool.release(span3);
277        assert_eq!(pool.available(), 2); // Only capacity worth retained
278    }
279
280    #[test]
281    fn test_pool_reset() {
282        let mut pool = SpanPool::new(SpanPoolConfig::new(10));
283
284        // Acquire and modify a span
285        let mut span = pool.acquire();
286        span.set_name_owned("test".to_string());
287        span.trace_id = "abc123".to_string();
288        span.timestamp_nanos = 12345;
289
290        // Release and re-acquire
291        pool.release(span);
292        let span2 = pool.acquire();
293
294        // Should be reset
295        assert_eq!(span2.name.as_ref(), "");
296        assert_eq!(span2.trace_id, "");
297        assert_eq!(span2.timestamp_nanos, 0);
298    }
299
300    #[test]
301    fn test_zero_copy_static_strings() {
302        let mut pool = SpanPool::new(SpanPoolConfig::new(10));
303
304        // Acquire span and use static string (zero-copy)
305        let mut span = pool.acquire();
306        span.set_name_static("syscall:open");
307        span.add_attribute_static("syscall.name", "open".to_string());
308        span.add_attribute_static("syscall.result", "3".to_string());
309
310        // Verify static borrowing (no allocation for keys)
311        assert_eq!(span.name.as_ref(), "syscall:open");
312        assert!(matches!(span.name, Cow::Borrowed(_)));
313        assert_eq!(span.attributes.len(), 2);
314        assert!(matches!(span.attributes[0].0, Cow::Borrowed(_)));
315        assert!(matches!(span.attributes[1].0, Cow::Borrowed(_)));
316
317        // Compare with owned version
318        let mut span2 = pool.acquire();
319        span2.set_name_owned("syscall:open".to_string());
320        span2.add_attribute_owned("syscall.name".to_string(), "open".to_string());
321
322        assert_eq!(span2.name.as_ref(), "syscall:open");
323        assert!(matches!(span2.name, Cow::Owned(_)));
324        assert!(matches!(span2.attributes[0].0, Cow::Owned(_)));
325
326        pool.release(span);
327        pool.release(span2);
328    }
329
330    #[test]
331    fn test_pool_disabled() {
332        let mut pool = SpanPool::new(SpanPoolConfig::disabled());
333        assert!(!pool.is_enabled());
334        assert_eq!(pool.capacity(), 0);
335
336        // Acquire should always allocate
337        let span1 = pool.acquire();
338        let span2 = pool.acquire();
339
340        // Release should be no-op
341        pool.release(span1);
342        pool.release(span2);
343        assert_eq!(pool.available(), 0);
344    }
345
346    #[test]
347    fn test_pool_stats() {
348        let mut pool = SpanPool::new(SpanPoolConfig::new(10));
349
350        // Initial stats
351        let stats = pool.stats();
352        assert_eq!(stats.capacity, 10);
353        assert_eq!(stats.available, 10);
354        assert_eq!(stats.allocated, 10);
355        assert_eq!(stats.acquired, 0);
356
357        // Acquire some spans
358        let span1 = pool.acquire();
359        let span2 = pool.acquire();
360
361        let stats = pool.stats();
362        assert_eq!(stats.available, 8);
363        assert_eq!(stats.acquired, 2);
364
365        // Release one
366        pool.release(span1);
367        let stats = pool.stats();
368        assert_eq!(stats.available, 9);
369
370        // Keep one alive
371        drop(span2);
372    }
373
374    #[test]
375    fn test_pool_hit_rate() {
376        let mut pool = SpanPool::new(SpanPoolConfig::new(2));
377
378        // All hits (from pool)
379        let span1 = pool.acquire(); // hit
380        let span2 = pool.acquire(); // hit
381        pool.release(span1);
382        pool.release(span2);
383
384        let stats = pool.stats();
385        assert!(stats.hit_rate() >= 99.0); // Should be ~100%
386
387        // Cause a miss (pool exhaustion)
388        let span1 = pool.acquire();
389        let span2 = pool.acquire();
390        let _span3 = pool.acquire(); // miss (allocate)
391
392        let stats = pool.stats();
393        assert!(stats.hit_rate() < 100.0); // Should be ~66%
394
395        drop((span1, span2));
396    }
397
398    #[test]
399    fn test_pool_utilization() {
400        let mut pool = SpanPool::new(SpanPoolConfig::new(10));
401
402        // 0% utilization (all available)
403        let stats = pool.stats();
404        assert_eq!(stats.utilization(), 0.0);
405
406        // Acquire 5 (50% utilization)
407        let mut spans = Vec::new();
408        for _ in 0..5 {
409            spans.push(pool.acquire());
410        }
411
412        let stats = pool.stats();
413        assert_eq!(stats.utilization(), 50.0);
414
415        // Acquire all (100% utilization)
416        for _ in 0..5 {
417            spans.push(pool.acquire());
418        }
419
420        let stats = pool.stats();
421        assert_eq!(stats.utilization(), 100.0);
422
423        // Release all
424        for span in spans {
425            pool.release(span);
426        }
427    }
428
429    #[test]
430    fn test_pool_concurrent_usage() {
431        let mut pool = SpanPool::new(SpanPoolConfig::new(100));
432
433        // Simulate concurrent acquire/release pattern
434        for _ in 0..1000 {
435            let span = pool.acquire();
436            pool.release(span);
437        }
438
439        let stats = pool.stats();
440        assert_eq!(stats.acquired, 1000);
441        assert_eq!(stats.available, 100); // All released
442    }
443
444    #[test]
445    fn test_pool_growth() {
446        let mut pool = SpanPool::new(SpanPoolConfig::new(10));
447
448        // Acquire more than capacity
449        let mut spans = Vec::new();
450        for _ in 0..20 {
451            spans.push(pool.acquire());
452        }
453
454        let stats = pool.stats();
455        assert_eq!(stats.acquired, 20);
456        assert!(stats.allocated >= 20); // Had to allocate more
457
458        // Release all
459        for span in spans {
460            pool.release(span);
461        }
462
463        // Pool should retain only capacity worth
464        assert_eq!(pool.available(), 10);
465    }
466}