ringkernel_core/
analytics_context.rs

1//! Analytics context for grouped buffer lifecycle management.
2//!
3//! This module provides the `AnalyticsContext` type for managing buffer
4//! allocations during analytics operations like DFG mining, BFS traversal,
5//! and pattern detection.
6//!
7//! # Purpose
8//!
9//! Analytics operations often need multiple temporary buffers that are
10//! used together and released together. The `AnalyticsContext` provides:
11//!
12//! - **Grouped lifecycle**: All buffers are released when the context drops
13//! - **Statistics tracking**: Peak memory usage, allocation count
14//! - **Named contexts**: For debugging and profiling
15//!
16//! # Example
17//!
18//! ```
19//! use ringkernel_core::analytics_context::AnalyticsContext;
20//!
21//! // Create context for a BFS operation
22//! let mut ctx = AnalyticsContext::new("bfs_traversal");
23//!
24//! // Allocate buffers for frontier, visited, and distances
25//! let frontier_idx = ctx.allocate(1024);
26//! let visited_idx = ctx.allocate(1024);
27//! let distances_idx = ctx.allocate_typed::<u32>(256);
28//!
29//! // Use the buffers
30//! ctx.get_mut(frontier_idx)[0] = 1;
31//!
32//! // Check stats
33//! let stats = ctx.stats();
34//! println!("Peak memory: {} bytes", stats.peak_bytes);
35//!
36//! // All buffers released when ctx drops
37//! ```
38
39use std::any::TypeId;
40use std::collections::HashMap;
41
42/// Statistics for an analytics context.
43#[derive(Debug, Clone, Default)]
44pub struct ContextStats {
45    /// Total number of allocations made.
46    pub allocations: usize,
47    /// Peak memory usage in bytes.
48    pub peak_bytes: usize,
49    /// Current memory usage in bytes.
50    pub current_bytes: usize,
51    /// Number of typed allocations (via allocate_typed).
52    pub typed_allocations: usize,
53    /// Allocation counts by type (for typed allocations).
54    pub allocations_by_type: HashMap<TypeId, usize>,
55}
56
57impl ContextStats {
58    /// Create new empty stats.
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Get memory efficiency (1.0 = all allocated memory still in use).
64    pub fn memory_efficiency(&self) -> f64 {
65        if self.peak_bytes > 0 {
66            self.current_bytes as f64 / self.peak_bytes as f64
67        } else {
68            1.0
69        }
70    }
71}
72
73/// Handle to an allocation within an AnalyticsContext.
74///
75/// This is an opaque index type for type safety.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub struct AllocationHandle(usize);
78
79impl AllocationHandle {
80    /// Get the raw index (for advanced use).
81    pub fn index(&self) -> usize {
82        self.0
83    }
84}
85
86/// Context for analytics operations with grouped buffer lifecycle.
87///
88/// All buffers allocated through this context are released together
89/// when the context is dropped. This is useful for analytics operations
90/// that need multiple temporary buffers.
91///
92/// # Thread Safety
93///
94/// `AnalyticsContext` is `Send` but not `Sync`. Each context should be
95/// used from a single thread. For parallel analytics, create a separate
96/// context per thread.
97pub struct AnalyticsContext {
98    /// Context name for debugging.
99    name: String,
100    /// Allocated buffers.
101    allocations: Vec<Box<[u8]>>,
102    /// Allocation sizes (for potential future deallocation).
103    sizes: Vec<usize>,
104    /// Statistics.
105    stats: ContextStats,
106}
107
108impl AnalyticsContext {
109    /// Create a new analytics context.
110    ///
111    /// # Arguments
112    ///
113    /// * `name` - Descriptive name for debugging and profiling
114    pub fn new(name: impl Into<String>) -> Self {
115        Self {
116            name: name.into(),
117            allocations: Vec::new(),
118            sizes: Vec::new(),
119            stats: ContextStats::new(),
120        }
121    }
122
123    /// Create a context with pre-allocated capacity.
124    ///
125    /// Reserves space for the expected number of allocations to avoid
126    /// reallocations of the internal vectors.
127    pub fn with_capacity(name: impl Into<String>, expected_allocations: usize) -> Self {
128        Self {
129            name: name.into(),
130            allocations: Vec::with_capacity(expected_allocations),
131            sizes: Vec::with_capacity(expected_allocations),
132            stats: ContextStats::new(),
133        }
134    }
135
136    /// Get the context name.
137    pub fn name(&self) -> &str {
138        &self.name
139    }
140
141    /// Allocate a buffer of the specified size.
142    ///
143    /// Returns a handle that can be used with `get()` and `get_mut()`.
144    ///
145    /// # Arguments
146    ///
147    /// * `size` - Size in bytes
148    ///
149    /// # Returns
150    ///
151    /// Handle to the allocated buffer
152    pub fn allocate(&mut self, size: usize) -> AllocationHandle {
153        let buf = vec![0u8; size].into_boxed_slice();
154        let handle = AllocationHandle(self.allocations.len());
155
156        self.allocations.push(buf);
157        self.sizes.push(size);
158
159        self.stats.allocations += 1;
160        self.stats.current_bytes += size;
161        self.stats.peak_bytes = self.stats.peak_bytes.max(self.stats.current_bytes);
162
163        handle
164    }
165
166    /// Allocate a typed buffer.
167    ///
168    /// Allocates space for `count` elements of type `T`, zero-initialized.
169    ///
170    /// # Arguments
171    ///
172    /// * `count` - Number of elements
173    ///
174    /// # Returns
175    ///
176    /// Handle to the allocated buffer
177    ///
178    /// # Type Parameters
179    ///
180    /// * `T` - Element type (must be Copy and have a meaningful zero value)
181    pub fn allocate_typed<T: Copy + Default + 'static>(
182        &mut self,
183        count: usize,
184    ) -> AllocationHandle {
185        let size = count * std::mem::size_of::<T>();
186        let handle = self.allocate(size);
187
188        // Track typed allocation
189        self.stats.typed_allocations += 1;
190        *self
191            .stats
192            .allocations_by_type
193            .entry(TypeId::of::<T>())
194            .or_insert(0) += 1;
195
196        handle
197    }
198
199    /// Get a reference to an allocated buffer.
200    ///
201    /// # Arguments
202    ///
203    /// * `handle` - Handle returned from `allocate()` or `allocate_typed()`
204    ///
205    /// # Panics
206    ///
207    /// Panics if the handle is invalid.
208    pub fn get(&self, handle: AllocationHandle) -> &[u8] {
209        &self.allocations[handle.0]
210    }
211
212    /// Get a mutable reference to an allocated buffer.
213    ///
214    /// # Arguments
215    ///
216    /// * `handle` - Handle returned from `allocate()` or `allocate_typed()`
217    ///
218    /// # Panics
219    ///
220    /// Panics if the handle is invalid.
221    pub fn get_mut(&mut self, handle: AllocationHandle) -> &mut [u8] {
222        &mut self.allocations[handle.0]
223    }
224
225    /// Get a typed reference to an allocated buffer.
226    ///
227    /// # Safety
228    ///
229    /// The caller must ensure the buffer was allocated with the correct type
230    /// and size using `allocate_typed::<T>()`.
231    ///
232    /// # Panics
233    ///
234    /// Panics if the handle is invalid.
235    pub fn get_typed<T: Copy>(&self, handle: AllocationHandle) -> &[T] {
236        let bytes = &self.allocations[handle.0];
237        let len = bytes.len() / std::mem::size_of::<T>();
238        unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const T, len) }
239    }
240
241    /// Get a mutable typed reference to an allocated buffer.
242    ///
243    /// # Safety
244    ///
245    /// The caller must ensure the buffer was allocated with the correct type
246    /// and size using `allocate_typed::<T>()`.
247    ///
248    /// # Panics
249    ///
250    /// Panics if the handle is invalid.
251    pub fn get_typed_mut<T: Copy>(&mut self, handle: AllocationHandle) -> &mut [T] {
252        let bytes = &mut self.allocations[handle.0];
253        let len = bytes.len() / std::mem::size_of::<T>();
254        unsafe { std::slice::from_raw_parts_mut(bytes.as_mut_ptr() as *mut T, len) }
255    }
256
257    /// Try to get a reference to an allocated buffer.
258    ///
259    /// Returns `None` if the handle is invalid.
260    pub fn try_get(&self, handle: AllocationHandle) -> Option<&[u8]> {
261        self.allocations.get(handle.0).map(|b| b.as_ref())
262    }
263
264    /// Try to get a mutable reference to an allocated buffer.
265    ///
266    /// Returns `None` if the handle is invalid.
267    pub fn try_get_mut(&mut self, handle: AllocationHandle) -> Option<&mut [u8]> {
268        self.allocations.get_mut(handle.0).map(|b| b.as_mut())
269    }
270
271    /// Get the size of an allocation.
272    ///
273    /// # Panics
274    ///
275    /// Panics if the handle is invalid.
276    pub fn allocation_size(&self, handle: AllocationHandle) -> usize {
277        self.sizes[handle.0]
278    }
279
280    /// Get the number of allocations in this context.
281    pub fn allocation_count(&self) -> usize {
282        self.allocations.len()
283    }
284
285    /// Get statistics for this context.
286    pub fn stats(&self) -> &ContextStats {
287        &self.stats
288    }
289
290    /// Release all allocations and reset the context.
291    ///
292    /// After calling this, all handles become invalid.
293    pub fn release_all(&mut self) {
294        self.allocations.clear();
295        self.sizes.clear();
296        self.stats.current_bytes = 0;
297    }
298
299    /// Create a sub-context for a nested operation.
300    ///
301    /// The sub-context shares no allocations with the parent.
302    pub fn sub_context(&self, name: impl Into<String>) -> Self {
303        Self::new(format!("{}::{}", self.name, name.into()))
304    }
305}
306
307impl Drop for AnalyticsContext {
308    fn drop(&mut self) {
309        // All allocations are automatically freed when Vec drops
310        // We could add logging here if needed:
311        // log::trace!("Dropping AnalyticsContext '{}': {} bytes released", self.name, self.stats.current_bytes);
312    }
313}
314
315/// Builder for AnalyticsContext with pre-configuration.
316pub struct AnalyticsContextBuilder {
317    name: String,
318    expected_allocations: Option<usize>,
319    preallocations: Vec<usize>,
320}
321
322impl AnalyticsContextBuilder {
323    /// Create a new builder.
324    pub fn new(name: impl Into<String>) -> Self {
325        Self {
326            name: name.into(),
327            expected_allocations: None,
328            preallocations: Vec::new(),
329        }
330    }
331
332    /// Set expected number of allocations (reserves vector capacity).
333    pub fn with_expected_allocations(mut self, count: usize) -> Self {
334        self.expected_allocations = Some(count);
335        self
336    }
337
338    /// Pre-allocate a buffer of the given size when building.
339    pub fn with_preallocation(mut self, size: usize) -> Self {
340        self.preallocations.push(size);
341        self
342    }
343
344    /// Build the context.
345    pub fn build(self) -> AnalyticsContext {
346        let mut ctx = match self.expected_allocations {
347            Some(cap) => {
348                AnalyticsContext::with_capacity(self.name, cap.max(self.preallocations.len()))
349            }
350            None => AnalyticsContext::new(self.name),
351        };
352
353        for size in self.preallocations {
354            ctx.allocate(size);
355        }
356
357        ctx
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_context_creation() {
367        let ctx = AnalyticsContext::new("test");
368        assert_eq!(ctx.name(), "test");
369        assert_eq!(ctx.allocation_count(), 0);
370    }
371
372    #[test]
373    fn test_context_with_capacity() {
374        let ctx = AnalyticsContext::with_capacity("test", 10);
375        assert_eq!(ctx.name(), "test");
376        assert_eq!(ctx.allocation_count(), 0);
377    }
378
379    #[test]
380    fn test_allocate() {
381        let mut ctx = AnalyticsContext::new("test");
382
383        let h1 = ctx.allocate(100);
384        let h2 = ctx.allocate(200);
385
386        assert_eq!(ctx.allocation_count(), 2);
387        assert_eq!(ctx.allocation_size(h1), 100);
388        assert_eq!(ctx.allocation_size(h2), 200);
389        assert_eq!(ctx.get(h1).len(), 100);
390        assert_eq!(ctx.get(h2).len(), 200);
391    }
392
393    #[test]
394    fn test_allocate_typed() {
395        let mut ctx = AnalyticsContext::new("test");
396
397        let h = ctx.allocate_typed::<u32>(10);
398
399        assert_eq!(ctx.allocation_size(h), 40); // 10 * 4 bytes
400        assert_eq!(ctx.get_typed::<u32>(h).len(), 10);
401
402        let stats = ctx.stats();
403        assert_eq!(stats.typed_allocations, 1);
404    }
405
406    #[test]
407    fn test_get_mut() {
408        let mut ctx = AnalyticsContext::new("test");
409
410        let h = ctx.allocate(10);
411        ctx.get_mut(h)[0] = 42;
412
413        assert_eq!(ctx.get(h)[0], 42);
414    }
415
416    #[test]
417    fn test_get_typed_mut() {
418        let mut ctx = AnalyticsContext::new("test");
419
420        let h = ctx.allocate_typed::<u32>(5);
421        ctx.get_typed_mut::<u32>(h)[2] = 12345;
422
423        assert_eq!(ctx.get_typed::<u32>(h)[2], 12345);
424    }
425
426    #[test]
427    fn test_try_get() {
428        let mut ctx = AnalyticsContext::new("test");
429
430        let h = ctx.allocate(10);
431        let invalid = AllocationHandle(999);
432
433        assert!(ctx.try_get(h).is_some());
434        assert!(ctx.try_get(invalid).is_none());
435        assert!(ctx.try_get_mut(h).is_some());
436        assert!(ctx.try_get_mut(invalid).is_none());
437    }
438
439    #[test]
440    fn test_stats_tracking() {
441        let mut ctx = AnalyticsContext::new("test");
442
443        ctx.allocate(100);
444        ctx.allocate(200);
445        ctx.allocate(50);
446
447        let stats = ctx.stats();
448        assert_eq!(stats.allocations, 3);
449        assert_eq!(stats.current_bytes, 350);
450        assert_eq!(stats.peak_bytes, 350);
451    }
452
453    #[test]
454    fn test_stats_peak_bytes() {
455        let mut ctx = AnalyticsContext::new("test");
456
457        ctx.allocate(100);
458        ctx.allocate(200);
459        let peak = ctx.stats().peak_bytes;
460
461        ctx.release_all();
462        assert_eq!(ctx.stats().current_bytes, 0);
463        assert_eq!(ctx.stats().peak_bytes, peak); // Peak preserved after release
464    }
465
466    #[test]
467    fn test_release_all() {
468        let mut ctx = AnalyticsContext::new("test");
469
470        let h1 = ctx.allocate(100);
471        let h2 = ctx.allocate(200);
472
473        assert_eq!(ctx.allocation_count(), 2);
474
475        ctx.release_all();
476
477        assert_eq!(ctx.allocation_count(), 0);
478        assert_eq!(ctx.stats().current_bytes, 0);
479        // Handles are now invalid - don't use them!
480        assert!(ctx.try_get(h1).is_none());
481        assert!(ctx.try_get(h2).is_none());
482    }
483
484    #[test]
485    fn test_sub_context() {
486        let parent = AnalyticsContext::new("parent");
487        let child = parent.sub_context("child");
488
489        assert_eq!(child.name(), "parent::child");
490    }
491
492    #[test]
493    fn test_builder() {
494        let ctx = AnalyticsContextBuilder::new("builder_test")
495            .with_expected_allocations(10)
496            .with_preallocation(100)
497            .with_preallocation(200)
498            .build();
499
500        assert_eq!(ctx.name(), "builder_test");
501        assert_eq!(ctx.allocation_count(), 2);
502        assert_eq!(ctx.stats().current_bytes, 300);
503    }
504
505    #[test]
506    fn test_context_stats_default() {
507        let stats = ContextStats::default();
508        assert_eq!(stats.allocations, 0);
509        assert_eq!(stats.peak_bytes, 0);
510        assert_eq!(stats.current_bytes, 0);
511        assert_eq!(stats.memory_efficiency(), 1.0);
512    }
513
514    #[test]
515    fn test_memory_efficiency() {
516        let mut ctx = AnalyticsContext::new("test");
517
518        ctx.allocate(100);
519        ctx.allocate(100);
520        // peak = 200, current = 200
521        assert_eq!(ctx.stats().memory_efficiency(), 1.0);
522
523        ctx.release_all();
524        // peak = 200, current = 0
525        assert_eq!(ctx.stats().memory_efficiency(), 0.0);
526    }
527
528    #[test]
529    fn test_handle_index() {
530        let mut ctx = AnalyticsContext::new("test");
531
532        let h0 = ctx.allocate(10);
533        let h1 = ctx.allocate(20);
534        let h2 = ctx.allocate(30);
535
536        assert_eq!(h0.index(), 0);
537        assert_eq!(h1.index(), 1);
538        assert_eq!(h2.index(), 2);
539    }
540
541    #[test]
542    fn test_zero_allocation() {
543        let mut ctx = AnalyticsContext::new("test");
544
545        let h = ctx.allocate(0);
546        assert_eq!(ctx.get(h).len(), 0);
547        assert_eq!(ctx.allocation_size(h), 0);
548    }
549
550    #[test]
551    fn test_large_allocation() {
552        let mut ctx = AnalyticsContext::new("test");
553
554        // 1 MB allocation
555        let h = ctx.allocate(1024 * 1024);
556        assert_eq!(ctx.get(h).len(), 1024 * 1024);
557        assert_eq!(ctx.stats().current_bytes, 1024 * 1024);
558    }
559}