kizzasi_tokenizer/
profiling.rs

1//! Memory usage profiling and performance monitoring utilities.
2//!
3//! This module provides tools for profiling memory usage, tracking allocations,
4//! and analyzing performance characteristics of tokenizers.
5//!
6//! # Features
7//!
8//! - Memory usage tracking per operation
9//! - Peak memory detection
10//! - Allocation statistics
11//! - Memory leak detection
12//! - Performance benchmarking
13//! - Timeline analysis
14//!
15//! # Example
16//!
17//! ```
18//! use kizzasi_tokenizer::profiling::{MemoryProfiler, ProfileScope};
19//! use kizzasi_tokenizer::{LinearQuantizer, Quantizer};
20//! use scirs2_core::ndarray::Array1;
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! let mut profiler = MemoryProfiler::new();
24//!
25//! {
26//!     let _scope = ProfileScope::new(&mut profiler, "quantization");
27//!     let quantizer = LinearQuantizer::new(-1.0, 1.0, 8).expect("Quantizer creation failed");
28//!     let signal = Array1::linspace(-1.0, 1.0, 1000);
29//!     // Quantize individual values
30//!     let _encoded: Vec<i32> = signal.iter().map(|&x| quantizer.quantize(x)).collect();
31//! }
32//!
33//! let report = profiler.report();
34//! println!("{}", report);
35//! # Ok(())
36//! # }
37//! ```
38
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41use std::time::{Duration, Instant};
42
43/// Memory allocation event
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct AllocationEvent {
46    /// Timestamp of the allocation
47    pub timestamp: u64,
48    /// Size in bytes
49    pub size: usize,
50    /// Operation name
51    pub operation: String,
52    /// Allocation or deallocation
53    pub event_type: EventType,
54}
55
56/// Type of memory event
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub enum EventType {
59    /// Memory allocation
60    Allocation,
61    /// Memory deallocation
62    Deallocation,
63}
64
65/// Statistics for a profiling scope
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ScopeStats {
68    /// Operation name
69    pub name: String,
70    /// Number of times executed
71    pub count: usize,
72    /// Total time spent
73    pub total_duration: Duration,
74    /// Minimum duration
75    pub min_duration: Duration,
76    /// Maximum duration
77    pub max_duration: Duration,
78    /// Peak memory used (bytes)
79    pub peak_memory: usize,
80    /// Total memory allocated (bytes)
81    pub total_allocated: usize,
82    /// Total memory deallocated (bytes)
83    pub total_deallocated: usize,
84}
85
86impl ScopeStats {
87    /// Create new scope statistics
88    fn new(name: String) -> Self {
89        Self {
90            name,
91            count: 0,
92            total_duration: Duration::ZERO,
93            min_duration: Duration::MAX,
94            max_duration: Duration::ZERO,
95            peak_memory: 0,
96            total_allocated: 0,
97            total_deallocated: 0,
98        }
99    }
100
101    /// Update with new measurement
102    fn update(&mut self, duration: Duration, allocated: usize, deallocated: usize) {
103        self.count += 1;
104        self.total_duration += duration;
105        self.min_duration = self.min_duration.min(duration);
106        self.max_duration = self.max_duration.max(duration);
107        self.total_allocated += allocated;
108        self.total_deallocated += deallocated;
109
110        let current_usage = self.total_allocated.saturating_sub(self.total_deallocated);
111        self.peak_memory = self.peak_memory.max(current_usage);
112    }
113
114    /// Get average duration
115    pub fn avg_duration(&self) -> Duration {
116        if self.count > 0 {
117            self.total_duration / self.count as u32
118        } else {
119            Duration::ZERO
120        }
121    }
122
123    /// Get net memory usage (allocated - deallocated)
124    pub fn net_memory(&self) -> isize {
125        self.total_allocated as isize - self.total_deallocated as isize
126    }
127}
128
129/// Memory profiler for tracking allocations and performance
130#[derive(Debug)]
131pub struct MemoryProfiler {
132    /// Start time
133    start_time: Instant,
134    /// All allocation events
135    events: Vec<AllocationEvent>,
136    /// Statistics per scope
137    scopes: HashMap<String, ScopeStats>,
138    /// Current memory usage
139    current_memory: usize,
140    /// Peak memory usage
141    peak_memory: usize,
142    /// Active scopes stack
143    active_scopes: Vec<(String, Instant, usize)>,
144}
145
146impl MemoryProfiler {
147    /// Create a new memory profiler
148    pub fn new() -> Self {
149        Self {
150            start_time: Instant::now(),
151            events: Vec::new(),
152            scopes: HashMap::new(),
153            current_memory: 0,
154            peak_memory: 0,
155            active_scopes: Vec::new(),
156        }
157    }
158
159    /// Get elapsed time since profiler creation
160    fn elapsed(&self) -> u64 {
161        self.start_time.elapsed().as_micros() as u64
162    }
163
164    /// Record an allocation
165    pub fn record_allocation(&mut self, operation: &str, size: usize) {
166        let event = AllocationEvent {
167            timestamp: self.elapsed(),
168            size,
169            operation: operation.to_string(),
170            event_type: EventType::Allocation,
171        };
172
173        self.current_memory += size;
174        self.peak_memory = self.peak_memory.max(self.current_memory);
175        self.events.push(event);
176    }
177
178    /// Record a deallocation
179    pub fn record_deallocation(&mut self, operation: &str, size: usize) {
180        let event = AllocationEvent {
181            timestamp: self.elapsed(),
182            size,
183            operation: operation.to_string(),
184            event_type: EventType::Deallocation,
185        };
186
187        self.current_memory = self.current_memory.saturating_sub(size);
188        self.events.push(event);
189    }
190
191    /// Start a profiling scope
192    pub fn start_scope(&mut self, name: &str) {
193        let start_time = Instant::now();
194        let start_memory = self.current_memory;
195        self.active_scopes
196            .push((name.to_string(), start_time, start_memory));
197    }
198
199    /// End a profiling scope
200    pub fn end_scope(&mut self, name: &str) {
201        if let Some(pos) = self.active_scopes.iter().rposition(|(n, _, _)| n == name) {
202            let (scope_name, start_time, start_memory) = self.active_scopes.remove(pos);
203            let duration = start_time.elapsed();
204            let end_memory = self.current_memory;
205
206            let allocated = end_memory.saturating_sub(start_memory);
207            let deallocated = start_memory.saturating_sub(end_memory);
208
209            self.scopes
210                .entry(scope_name.clone())
211                .or_insert_with(|| ScopeStats::new(scope_name))
212                .update(duration, allocated, deallocated);
213        }
214    }
215
216    /// Get current memory usage
217    pub fn current_memory(&self) -> usize {
218        self.current_memory
219    }
220
221    /// Get peak memory usage
222    pub fn peak_memory(&self) -> usize {
223        self.peak_memory
224    }
225
226    /// Get total number of allocation events
227    pub fn total_allocations(&self) -> usize {
228        self.events
229            .iter()
230            .filter(|e| e.event_type == EventType::Allocation)
231            .count()
232    }
233
234    /// Get total number of deallocation events
235    pub fn total_deallocations(&self) -> usize {
236        self.events
237            .iter()
238            .filter(|e| e.event_type == EventType::Deallocation)
239            .count()
240    }
241
242    /// Get total bytes allocated
243    pub fn total_bytes_allocated(&self) -> usize {
244        self.events
245            .iter()
246            .filter(|e| e.event_type == EventType::Allocation)
247            .map(|e| e.size)
248            .sum()
249    }
250
251    /// Get total bytes deallocated
252    pub fn total_bytes_deallocated(&self) -> usize {
253        self.events
254            .iter()
255            .filter(|e| e.event_type == EventType::Deallocation)
256            .map(|e| e.size)
257            .sum()
258    }
259
260    /// Check for potential memory leaks
261    pub fn check_leaks(&self) -> Vec<String> {
262        let mut leaks = Vec::new();
263
264        for (name, stats) in &self.scopes {
265            let net = stats.net_memory();
266            if net > 1024 {
267                // More than 1KB leaked
268                leaks.push(format!("{}: {} bytes potentially leaked", name, net));
269            }
270        }
271
272        if self.current_memory > 1024 && self.total_deallocations() < self.total_allocations() {
273            leaks.push(format!(
274                "Global: {} bytes not deallocated ({} allocs, {} deallocs)",
275                self.current_memory,
276                self.total_allocations(),
277                self.total_deallocations()
278            ));
279        }
280
281        leaks
282    }
283
284    /// Get statistics for a specific scope
285    pub fn scope_stats(&self, name: &str) -> Option<&ScopeStats> {
286        self.scopes.get(name)
287    }
288
289    /// Get all scope statistics
290    pub fn all_scopes(&self) -> &HashMap<String, ScopeStats> {
291        &self.scopes
292    }
293
294    /// Get all events
295    pub fn events(&self) -> &[AllocationEvent] {
296        &self.events
297    }
298
299    /// Generate a profiling report
300    pub fn report(&self) -> String {
301        let mut report = String::new();
302        report.push_str("=== Memory Profiling Report ===\n\n");
303
304        // Overall statistics
305        report.push_str("Overall Statistics:\n");
306        report.push_str(&format!(
307            "  Peak memory: {} bytes ({:.2} MB)\n",
308            self.peak_memory,
309            self.peak_memory as f64 / 1024.0 / 1024.0
310        ));
311        report.push_str(&format!(
312            "  Current memory: {} bytes ({:.2} MB)\n",
313            self.current_memory,
314            self.current_memory as f64 / 1024.0 / 1024.0
315        ));
316        report.push_str(&format!(
317            "  Total allocations: {}\n",
318            self.total_allocations()
319        ));
320        report.push_str(&format!(
321            "  Total deallocations: {}\n",
322            self.total_deallocations()
323        ));
324        report.push_str(&format!(
325            "  Total allocated: {} bytes ({:.2} MB)\n",
326            self.total_bytes_allocated(),
327            self.total_bytes_allocated() as f64 / 1024.0 / 1024.0
328        ));
329        report.push_str(&format!(
330            "  Total deallocated: {} bytes ({:.2} MB)\n\n",
331            self.total_bytes_deallocated(),
332            self.total_bytes_deallocated() as f64 / 1024.0 / 1024.0
333        ));
334
335        // Scope statistics
336        if !self.scopes.is_empty() {
337            report.push_str("Scope Statistics:\n");
338            let mut scopes: Vec<_> = self.scopes.iter().collect();
339            scopes.sort_by_key(|(_, stats)| std::cmp::Reverse(stats.peak_memory));
340
341            for (name, stats) in scopes {
342                report.push_str(&format!("\n  {}:\n", name));
343                report.push_str(&format!("    Count: {}\n", stats.count));
344                report.push_str(&format!("    Avg duration: {:?}\n", stats.avg_duration()));
345                report.push_str(&format!("    Min duration: {:?}\n", stats.min_duration));
346                report.push_str(&format!("    Max duration: {:?}\n", stats.max_duration));
347                report.push_str(&format!("    Peak memory: {} bytes\n", stats.peak_memory));
348                report.push_str(&format!(
349                    "    Total allocated: {} bytes\n",
350                    stats.total_allocated
351                ));
352                report.push_str(&format!(
353                    "    Total deallocated: {} bytes\n",
354                    stats.total_deallocated
355                ));
356                report.push_str(&format!("    Net memory: {} bytes\n", stats.net_memory()));
357            }
358            report.push('\n');
359        }
360
361        // Memory leaks
362        let leaks = self.check_leaks();
363        if !leaks.is_empty() {
364            report.push_str("Potential Memory Leaks:\n");
365            for leak in leaks {
366                report.push_str(&format!("  - {}\n", leak));
367            }
368        } else {
369            report.push_str("No memory leaks detected.\n");
370        }
371
372        report
373    }
374
375    /// Clear all profiling data
376    pub fn clear(&mut self) {
377        self.events.clear();
378        self.scopes.clear();
379        self.current_memory = 0;
380        self.peak_memory = 0;
381        self.active_scopes.clear();
382        self.start_time = Instant::now();
383    }
384}
385
386impl Default for MemoryProfiler {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392/// RAII scope for automatic profiling
393pub struct ProfileScope<'a> {
394    profiler: &'a mut MemoryProfiler,
395    name: String,
396}
397
398impl<'a> ProfileScope<'a> {
399    /// Create a new profile scope
400    pub fn new(profiler: &'a mut MemoryProfiler, name: &str) -> Self {
401        profiler.start_scope(name);
402        Self {
403            profiler,
404            name: name.to_string(),
405        }
406    }
407}
408
409impl<'a> Drop for ProfileScope<'a> {
410    fn drop(&mut self) {
411        self.profiler.end_scope(&self.name);
412    }
413}
414
415/// Memory snapshot for timeline analysis
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct MemorySnapshot {
418    /// Timestamp in microseconds
419    pub timestamp: u64,
420    /// Memory usage in bytes
421    pub memory_used: usize,
422    /// Active operation
423    pub operation: Option<String>,
424}
425
426/// Timeline analyzer for memory usage over time
427#[derive(Debug)]
428pub struct TimelineAnalyzer {
429    /// Memory snapshots
430    snapshots: Vec<MemorySnapshot>,
431    /// Start time
432    start_time: Instant,
433}
434
435impl TimelineAnalyzer {
436    /// Create a new timeline analyzer
437    pub fn new() -> Self {
438        Self {
439            snapshots: Vec::new(),
440            start_time: Instant::now(),
441        }
442    }
443
444    /// Record a snapshot
445    pub fn snapshot(&mut self, memory_used: usize, operation: Option<&str>) {
446        let timestamp = self.start_time.elapsed().as_micros() as u64;
447        self.snapshots.push(MemorySnapshot {
448            timestamp,
449            memory_used,
450            operation: operation.map(String::from),
451        });
452    }
453
454    /// Get all snapshots
455    pub fn snapshots(&self) -> &[MemorySnapshot] {
456        &self.snapshots
457    }
458
459    /// Get peak memory usage
460    pub fn peak_memory(&self) -> usize {
461        self.snapshots
462            .iter()
463            .map(|s| s.memory_used)
464            .max()
465            .unwrap_or(0)
466    }
467
468    /// Get average memory usage
469    pub fn avg_memory(&self) -> usize {
470        if self.snapshots.is_empty() {
471            0
472        } else {
473            self.snapshots.iter().map(|s| s.memory_used).sum::<usize>() / self.snapshots.len()
474        }
475    }
476
477    /// Find operations with highest memory usage
478    pub fn top_operations(&self, n: usize) -> Vec<(String, usize)> {
479        let mut op_memory: HashMap<String, usize> = HashMap::new();
480
481        for snapshot in &self.snapshots {
482            if let Some(ref op) = snapshot.operation {
483                let entry = op_memory.entry(op.clone()).or_insert(0);
484                *entry = (*entry).max(snapshot.memory_used);
485            }
486        }
487
488        let mut sorted: Vec<_> = op_memory.into_iter().collect();
489        sorted.sort_by_key(|(_, mem)| std::cmp::Reverse(*mem));
490        sorted.truncate(n);
491        sorted
492    }
493
494    /// Generate CSV report
495    pub fn to_csv(&self) -> String {
496        let mut csv = String::from("timestamp_us,memory_bytes,operation\n");
497        for snapshot in &self.snapshots {
498            csv.push_str(&format!(
499                "{},{},{}\n",
500                snapshot.timestamp,
501                snapshot.memory_used,
502                snapshot.operation.as_deref().unwrap_or("")
503            ));
504        }
505        csv
506    }
507
508    /// Clear all snapshots
509    pub fn clear(&mut self) {
510        self.snapshots.clear();
511        self.start_time = Instant::now();
512    }
513}
514
515impl Default for TimelineAnalyzer {
516    fn default() -> Self {
517        Self::new()
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524
525    #[test]
526    fn test_memory_profiler_creation() {
527        let profiler = MemoryProfiler::new();
528        assert_eq!(profiler.current_memory(), 0);
529        assert_eq!(profiler.peak_memory(), 0);
530    }
531
532    #[test]
533    fn test_record_allocation() {
534        let mut profiler = MemoryProfiler::new();
535        profiler.record_allocation("test", 1024);
536        assert_eq!(profiler.current_memory(), 1024);
537        assert_eq!(profiler.peak_memory(), 1024);
538        assert_eq!(profiler.total_allocations(), 1);
539    }
540
541    #[test]
542    fn test_record_deallocation() {
543        let mut profiler = MemoryProfiler::new();
544        profiler.record_allocation("test", 2048);
545        profiler.record_deallocation("test", 1024);
546        assert_eq!(profiler.current_memory(), 1024);
547        assert_eq!(profiler.peak_memory(), 2048);
548    }
549
550    #[test]
551    fn test_scope_tracking() {
552        let mut profiler = MemoryProfiler::new();
553
554        profiler.start_scope("operation1");
555        profiler.record_allocation("op1", 512);
556        std::thread::sleep(Duration::from_millis(10));
557        profiler.end_scope("operation1");
558
559        let stats = profiler.scope_stats("operation1").unwrap();
560        assert_eq!(stats.count, 1);
561        assert!(stats.total_duration >= Duration::from_millis(10));
562        assert_eq!(stats.total_allocated, 512);
563    }
564
565    #[test]
566    fn test_profile_scope_raii() {
567        let mut profiler = MemoryProfiler::new();
568
569        {
570            let _scope = ProfileScope::new(&mut profiler, "scoped_op");
571            std::thread::sleep(Duration::from_millis(5));
572        }
573
574        let stats = profiler.scope_stats("scoped_op").unwrap();
575        assert_eq!(stats.count, 1);
576        assert!(stats.total_duration >= Duration::from_millis(5));
577    }
578
579    #[test]
580    fn test_nested_scopes() {
581        let mut profiler = MemoryProfiler::new();
582
583        profiler.start_scope("outer");
584        profiler.start_scope("inner");
585        profiler.end_scope("inner");
586        profiler.end_scope("outer");
587
588        assert!(profiler.scope_stats("outer").is_some());
589        assert!(profiler.scope_stats("inner").is_some());
590    }
591
592    #[test]
593    fn test_leak_detection() {
594        let mut profiler = MemoryProfiler::new();
595
596        profiler.start_scope("leaky_op");
597        profiler.record_allocation("leaky_op", 2048);
598        // No deallocation!
599        profiler.end_scope("leaky_op");
600
601        let leaks = profiler.check_leaks();
602        assert!(!leaks.is_empty());
603    }
604
605    #[test]
606    fn test_report_generation() {
607        let mut profiler = MemoryProfiler::new();
608
609        profiler.start_scope("test_op");
610        profiler.record_allocation("test_op", 1024);
611        profiler.record_deallocation("test_op", 1024);
612        profiler.end_scope("test_op");
613
614        let report = profiler.report();
615        assert!(report.contains("Memory Profiling Report"));
616        assert!(report.contains("test_op"));
617    }
618
619    #[test]
620    fn test_clear() {
621        let mut profiler = MemoryProfiler::new();
622        profiler.record_allocation("test", 1024);
623        profiler.clear();
624
625        assert_eq!(profiler.current_memory(), 0);
626        assert_eq!(profiler.peak_memory(), 0);
627        assert_eq!(profiler.total_allocations(), 0);
628    }
629
630    #[test]
631    fn test_timeline_analyzer() {
632        let mut analyzer = TimelineAnalyzer::new();
633
634        analyzer.snapshot(1024, Some("op1"));
635        analyzer.snapshot(2048, Some("op2"));
636        analyzer.snapshot(512, Some("op1"));
637
638        assert_eq!(analyzer.snapshots().len(), 3);
639        assert_eq!(analyzer.peak_memory(), 2048);
640        assert_eq!(analyzer.avg_memory(), (1024 + 2048 + 512) / 3);
641    }
642
643    #[test]
644    fn test_top_operations() {
645        let mut analyzer = TimelineAnalyzer::new();
646
647        analyzer.snapshot(1024, Some("op1"));
648        analyzer.snapshot(2048, Some("op2"));
649        analyzer.snapshot(1536, Some("op1"));
650        analyzer.snapshot(512, Some("op3"));
651
652        let top = analyzer.top_operations(2);
653        assert_eq!(top.len(), 2);
654        assert_eq!(top[0].0, "op2");
655        assert_eq!(top[0].1, 2048);
656    }
657
658    #[test]
659    fn test_csv_export() {
660        let mut analyzer = TimelineAnalyzer::new();
661        analyzer.snapshot(1024, Some("test"));
662
663        let csv = analyzer.to_csv();
664        assert!(csv.contains("timestamp_us,memory_bytes,operation"));
665        assert!(csv.contains("1024"));
666        assert!(csv.contains("test"));
667    }
668
669    #[test]
670    fn test_scope_stats_avg_duration() {
671        let mut stats = ScopeStats::new("test".to_string());
672        stats.update(Duration::from_millis(10), 0, 0);
673        stats.update(Duration::from_millis(20), 0, 0);
674        stats.update(Duration::from_millis(30), 0, 0);
675
676        let avg = stats.avg_duration();
677        assert_eq!(avg, Duration::from_millis(20));
678    }
679
680    #[test]
681    fn test_scope_stats_peak_memory() {
682        let mut stats = ScopeStats::new("test".to_string());
683        stats.update(Duration::from_millis(1), 1024, 0);
684        stats.update(Duration::from_millis(1), 2048, 512);
685        stats.update(Duration::from_millis(1), 512, 1024);
686
687        // Peak should track maximum net memory
688        assert!(stats.peak_memory > 0);
689    }
690}