Skip to main content

presentar_terminal/perf_trace/
core.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3use std::time::{Duration, Instant};
4
5// =============================================================================
6// GLOBAL ATOMIC STATE (trueno-viz pattern)
7// =============================================================================
8
9/// Global flag to enable/disable tracing (zero cost when disabled - 1 atomic load)
10static TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
11/// Start time in microseconds for relative timestamps
12static START_TIME_US: AtomicU64 = AtomicU64::new(0);
13
14/// Enable global tracing
15pub fn enable_tracing() {
16    START_TIME_US.store(
17        std::time::SystemTime::now()
18            .duration_since(std::time::UNIX_EPOCH)
19            .unwrap_or_default()
20            .as_micros() as u64,
21        Ordering::Relaxed,
22    );
23    TRACE_ENABLED.store(true, Ordering::Release);
24}
25
26/// Disable global tracing
27pub fn disable_tracing() {
28    TRACE_ENABLED.store(false, Ordering::Release);
29}
30
31/// Check if tracing is enabled
32#[inline]
33pub fn is_tracing_enabled() -> bool {
34    TRACE_ENABLED.load(Ordering::Acquire)
35}
36
37// =============================================================================
38// RAII TIMING GUARD (trueno-viz pattern)
39// =============================================================================
40
41/// Zero-cost RAII timing guard (trueno-viz pattern)
42///
43/// Automatically logs entry/exit on Drop. Zero overhead when tracing disabled.
44///
45/// # Example
46/// ```rust,ignore
47/// fn render_panel() {
48///     let _guard = TimingGuard::new("render_panel", 1000); // 1ms budget
49///     // ... rendering code ...
50/// } // Automatically logs duration on drop
51/// ```
52#[derive(Debug)]
53pub struct TimingGuard {
54    name: &'static str,
55    start: Option<Instant>,
56    budget_us: u64,
57}
58
59impl TimingGuard {
60    /// Create a new timing guard with budget in microseconds
61    ///
62    /// If tracing is disabled, returns a no-op guard (zero cost).
63    #[inline]
64    pub fn new(name: &'static str, budget_us: u64) -> Self {
65        if is_tracing_enabled() {
66            Self {
67                name,
68                start: Some(Instant::now()),
69                budget_us,
70            }
71        } else {
72            Self {
73                name,
74                start: None,
75                budget_us,
76            }
77        }
78    }
79
80    /// Create guard with default 1ms budget
81    #[inline]
82    pub fn with_default_budget(name: &'static str) -> Self {
83        Self::new(name, 1000)
84    }
85
86    /// Create guard for render operations (16ms budget for 60fps)
87    #[inline]
88    pub fn render(name: &'static str) -> Self {
89        Self::new(name, 16_000)
90    }
91
92    /// Create guard for collection operations (100ms budget)
93    #[inline]
94    pub fn collect(name: &'static str) -> Self {
95        Self::new(name, 100_000)
96    }
97}
98
99impl Drop for TimingGuard {
100    fn drop(&mut self) {
101        if let Some(start) = self.start {
102            let elapsed = start.elapsed();
103            let elapsed_us = elapsed.as_micros() as u64;
104            let exceeded = elapsed_us > self.budget_us;
105
106            // Log format: [+0042ms] [TRACE] [name] <- done (12.34ms)
107            let relative_ms = (std::time::SystemTime::now()
108                .duration_since(std::time::UNIX_EPOCH)
109                .unwrap_or_default()
110                .as_micros() as u64)
111                .saturating_sub(START_TIME_US.load(Ordering::Relaxed))
112                / 1000;
113
114            let status = if exceeded { "⚠️" } else { "✓" };
115            eprintln!(
116                "[+{:04}ms] [TRACE] {status} {} <- {:.2}ms (budget: {}μs)",
117                relative_ms,
118                self.name,
119                elapsed_us as f64 / 1000.0,
120                self.budget_us
121            );
122        }
123    }
124}
125
126// =============================================================================
127// O(1) SIMD-STYLE STATS (trueno-viz pattern)
128// =============================================================================
129
130/// Cache-aligned running statistics for O(1) access (trueno-viz SimdStats pattern)
131///
132/// All statistics are pre-computed during data collection, never during render.
133/// This guarantees <1ms frame time per SPEC-024.
134#[repr(C, align(64))] // Cache-aligned for multi-threaded access
135#[derive(Debug, Clone, Default)]
136pub struct SimdStats {
137    /// Running count of samples
138    pub count: u64,
139    /// Running sum for mean calculation
140    pub sum: f64,
141    /// Running sum of squares for variance calculation
142    pub sum_sq: f64,
143    /// Running minimum value
144    pub min: f64,
145    /// Running maximum value
146    pub max: f64,
147}
148
149impl SimdStats {
150    /// Create new stats initialized to zero
151    pub fn new() -> Self {
152        Self {
153            count: 0,
154            sum: 0.0,
155            sum_sq: 0.0,
156            min: f64::MAX,
157            max: f64::MIN,
158        }
159    }
160
161    /// Update stats with a new value (O(1))
162    #[inline]
163    pub fn update(&mut self, value: f64) {
164        self.count += 1;
165        self.sum += value;
166        self.sum_sq += value * value;
167        self.min = self.min.min(value);
168        self.max = self.max.max(value);
169    }
170
171    /// Get mean (O(1))
172    #[inline]
173    pub fn mean(&self) -> f64 {
174        if self.count == 0 {
175            0.0
176        } else {
177            self.sum / self.count as f64
178        }
179    }
180
181    /// Get variance (O(1))
182    #[inline]
183    pub fn variance(&self) -> f64 {
184        if self.count < 2 {
185            return 0.0;
186        }
187        let n = self.count as f64;
188        (self.sum_sq - (self.sum * self.sum) / n) / (n - 1.0)
189    }
190
191    /// Get standard deviation (O(1))
192    #[inline]
193    pub fn std_dev(&self) -> f64 {
194        self.variance().sqrt()
195    }
196
197    /// Get coefficient of variation (O(1))
198    #[inline]
199    pub fn cv_percent(&self) -> f64 {
200        let mean = self.mean();
201        if mean.abs() < 1e-9 {
202            0.0
203        } else {
204            (self.std_dev() / mean) * 100.0
205        }
206    }
207
208    /// Reset stats
209    pub fn reset(&mut self) {
210        *self = Self::new();
211    }
212}
213
214// =============================================================================
215// BRICK PROFILER (renacer BrickTracer integration)
216// =============================================================================
217
218/// Brick types for profiling (aligns with renacer brick taxonomy)
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub enum BrickType {
221    /// Data collection brick (I/O bound)
222    Collect,
223    /// Render brick (CPU bound, must be <16ms for 60fps)
224    Render,
225    /// Compute brick (SIMD optimized)
226    Compute,
227    /// Network brick (I/O bound with latency)
228    Network,
229    /// Storage brick (disk I/O)
230    Storage,
231}
232
233impl BrickType {
234    /// Get default budget for this brick type in microseconds
235    #[must_use]
236    pub fn default_budget_us(&self) -> u64 {
237        match self {
238            Self::Collect => 100_000, // 100ms for collection
239            Self::Render => 16_000,   // 16ms for 60fps
240            Self::Compute => 1_000,   // 1ms for compute
241            Self::Network => 500_000, // 500ms for network
242            Self::Storage => 50_000,  // 50ms for storage
243        }
244    }
245
246    /// Get severity threshold (CV%) for escalation
247    #[must_use]
248    pub fn cv_threshold(&self) -> f64 {
249        match self {
250            Self::Render => 10.0,  // Strict for render
251            Self::Compute => 15.0, // Standard
252            Self::Collect => 25.0, // More lenient
253            Self::Network => 50.0, // High variance expected
254            Self::Storage => 30.0, // Moderate variance
255        }
256    }
257}
258
259/// Brick profiler for tracking computational units.
260///
261/// Provides higher-level profiling with brick-specific budgets and thresholds.
262/// Compatible with renacer's BrickTracer for escalation.
263#[derive(Debug)]
264pub struct BrickProfiler {
265    /// Stats per brick name
266    stats: std::collections::HashMap<String, (BrickType, SimdStats)>,
267    /// Whether profiling is enabled
268    enabled: bool,
269}
270
271impl Default for BrickProfiler {
272    fn default() -> Self {
273        Self::new()
274    }
275}
276
277impl BrickProfiler {
278    /// Create a new brick profiler
279    #[must_use]
280    pub fn new() -> Self {
281        Self {
282            stats: std::collections::HashMap::new(),
283            enabled: is_tracing_enabled(),
284        }
285    }
286
287    /// Profile a brick execution
288    pub fn profile<F, R>(&mut self, name: &str, brick_type: BrickType, f: F) -> R
289    where
290        F: FnOnce() -> R,
291    {
292        if !self.enabled {
293            return f();
294        }
295
296        let start = std::time::Instant::now();
297        let result = f();
298        let elapsed_us = start.elapsed().as_micros() as f64;
299
300        // Update stats
301        let (_, stats) = self
302            .stats
303            .entry(name.to_string())
304            .or_insert_with(|| (brick_type, SimdStats::new()));
305        stats.update(elapsed_us);
306
307        result
308    }
309
310    /// Check if a brick should escalate to deep profiling
311    #[must_use]
312    pub fn should_escalate(&self, name: &str) -> bool {
313        if let Some((brick_type, stats)) = self.stats.get(name) {
314            let cv = stats.cv_percent();
315            cv > brick_type.cv_threshold()
316        } else {
317            false
318        }
319    }
320
321    /// Get stats for a brick
322    #[must_use]
323    pub fn get_stats(&self, name: &str) -> Option<&SimdStats> {
324        self.stats.get(name).map(|(_, s)| s)
325    }
326
327    /// Generate a summary report
328    #[must_use]
329    pub fn summary(&self) -> String {
330        let mut lines = vec!["=== Brick Profiler Summary ===".to_string()];
331
332        let mut sorted: Vec<_> = self.stats.iter().collect();
333        sorted.sort_by(|a, b| {
334            let a_total = a.1 .1.sum;
335            let b_total = b.1 .1.sum;
336            b_total
337                .partial_cmp(&a_total)
338                .unwrap_or(std::cmp::Ordering::Equal)
339        });
340
341        for (name, (brick_type, stats)) in sorted {
342            let budget = brick_type.default_budget_us();
343            let avg = stats.mean();
344            let cv = stats.cv_percent();
345            let status = if avg > budget as f64 { "⚠️" } else { "✓" };
346            let escalate = if self.should_escalate(name) {
347                " [ESCALATE]"
348            } else {
349                ""
350            };
351
352            lines.push(format!(
353                "{status} {name} ({brick_type:?}): avg={avg:.0}μs cv={cv:.1}% n={}{escalate}",
354                stats.count
355            ));
356        }
357
358        lines.join("\n")
359    }
360}
361
362/// Performance trace event (compatible with renacer `TraceEvent`)
363#[derive(Debug, Clone)]
364pub struct TraceEvent {
365    /// Event name (e.g., "`collect_metrics`", "`render_cpu_panel`")
366    pub name: String,
367    /// Duration of the event
368    pub duration: Duration,
369    /// Timestamp when event started
370    pub timestamp_us: u64,
371    /// Whether this exceeded the budget
372    pub budget_exceeded: bool,
373    /// Budget in microseconds (if set)
374    pub budget_us: Option<u64>,
375}
376
377/// Aggregated statistics for a traced operation
378#[derive(Debug, Clone, Default)]
379pub struct TraceStats {
380    /// Total number of invocations
381    pub count: u64,
382    /// Total duration across all invocations
383    pub total_duration: Duration,
384    /// Minimum duration
385    pub min_duration: Duration,
386    /// Maximum duration
387    pub max_duration: Duration,
388    /// Number of times budget was exceeded
389    pub budget_violations: u64,
390    /// Budget in microseconds
391    pub budget_us: u64,
392}
393
394impl TraceStats {
395    /// Create new stats with initial values
396    fn new(duration: Duration, budget_us: u64, exceeded: bool) -> Self {
397        Self {
398            count: 1,
399            total_duration: duration,
400            min_duration: duration,
401            max_duration: duration,
402            budget_violations: u64::from(exceeded),
403            budget_us,
404        }
405    }
406
407    /// Update stats with a new measurement
408    fn update(&mut self, duration: Duration, exceeded: bool) {
409        self.count += 1;
410        self.total_duration += duration;
411        self.min_duration = self.min_duration.min(duration);
412        self.max_duration = self.max_duration.max(duration);
413        if exceeded {
414            self.budget_violations += 1;
415        }
416    }
417
418    /// Average duration
419    pub fn avg_duration(&self) -> Duration {
420        if self.count == 0 {
421            Duration::ZERO
422        } else {
423            self.total_duration / self.count as u32
424        }
425    }
426
427    /// Coefficient of variation (CV) percentage
428    /// CV > 15% indicates unstable performance per Curtsinger & Berger (2013)
429    pub fn cv_percent(&self) -> f64 {
430        if self.count < 2 {
431            return 0.0;
432        }
433        let avg = self.avg_duration().as_secs_f64();
434        if avg < 1e-9 {
435            return 0.0;
436        }
437        let range = self
438            .max_duration
439            .checked_sub(self.min_duration)
440            .unwrap_or_default()
441            .as_secs_f64();
442        (range / avg) * 50.0 // Simplified CV approximation
443    }
444
445    /// Budget efficiency percentage
446    /// Efficiency < 25% indicates budget is too tight per Williams et al. (2009)
447    pub fn efficiency_percent(&self) -> f64 {
448        if self.budget_us == 0 {
449            return 100.0;
450        }
451        let avg_us = self.avg_duration().as_micros() as f64;
452        ((self.budget_us as f64) / avg_us * 100.0).min(100.0)
453    }
454}
455
456/// Escalation thresholds for deep tracing (from renacer `BrickEscalationThresholds`)
457#[derive(Debug, Clone, Copy)]
458pub struct EscalationThresholds {
459    /// CV threshold above which to escalate (default: 15.0%)
460    pub cv_percent: f64,
461    /// Efficiency threshold below which to escalate (default: 25.0%)
462    pub efficiency_percent: f64,
463    /// Maximum traces per second (rate limiting)
464    pub max_traces_per_sec: u32,
465}
466
467impl Default for EscalationThresholds {
468    fn default() -> Self {
469        Self {
470            cv_percent: 15.0,
471            efficiency_percent: 25.0,
472            max_traces_per_sec: 100,
473        }
474    }
475}
476
477/// Lightweight performance tracer for ptop
478///
479/// Provides timing measurements and statistics for `ComputeBlocks`.
480/// Can be used standalone or integrated with renacer for deep analysis.
481#[derive(Debug)]
482pub struct PerfTracer {
483    /// Aggregated stats per operation name
484    stats: HashMap<String, TraceStats>,
485    /// Recent events (ring buffer, last N)
486    recent_events: Vec<TraceEvent>,
487    /// Maximum recent events to keep
488    max_recent: usize,
489    /// Start time for relative timestamps
490    start_time: Instant,
491    /// Escalation thresholds
492    thresholds: EscalationThresholds,
493    /// Number of traces since last second
494    traces_this_second: u32,
495    /// Second when trace count was last reset
496    last_second: u64,
497}
498
499impl Default for PerfTracer {
500    fn default() -> Self {
501        Self::new()
502    }
503}
504
505impl PerfTracer {
506    /// Create a new performance tracer
507    #[must_use]
508    pub fn new() -> Self {
509        Self {
510            stats: HashMap::new(),
511            recent_events: Vec::with_capacity(100),
512            max_recent: 100,
513            start_time: Instant::now(),
514            thresholds: EscalationThresholds::default(),
515            traces_this_second: 0,
516            last_second: 0,
517        }
518    }
519
520    /// Create a tracer with custom thresholds
521    #[must_use]
522    pub fn with_thresholds(thresholds: EscalationThresholds) -> Self {
523        Self {
524            thresholds,
525            ..Self::new()
526        }
527    }
528
529    /// Trace a function execution with a name and budget
530    pub fn trace_with_budget<F, R>(&mut self, name: &str, budget_us: u64, f: F) -> R
531    where
532        F: FnOnce() -> R,
533    {
534        let start = Instant::now();
535        let result = f();
536        let duration = start.elapsed();
537
538        self.record_trace(name, duration, budget_us);
539        result
540    }
541
542    /// Trace a function execution with default 1ms budget
543    pub fn trace<F, R>(&mut self, name: &str, f: F) -> R
544    where
545        F: FnOnce() -> R,
546    {
547        self.trace_with_budget(name, 1000, f) // 1ms default budget
548    }
549
550    /// Record a trace event
551    fn record_trace(&mut self, name: &str, duration: Duration, budget_us: u64) {
552        let timestamp_us = self.start_time.elapsed().as_micros() as u64;
553        let budget_exceeded = duration.as_micros() as u64 > budget_us;
554
555        // Rate limiting
556        let current_second = timestamp_us / 1_000_000;
557        if current_second != self.last_second {
558            self.traces_this_second = 0;
559            self.last_second = current_second;
560        }
561        self.traces_this_second += 1;
562
563        // Create event
564        let event = TraceEvent {
565            name: name.to_string(),
566            duration,
567            timestamp_us,
568            budget_exceeded,
569            budget_us: Some(budget_us),
570        };
571
572        // Update stats
573        if let Some(stats) = self.stats.get_mut(name) {
574            stats.update(duration, budget_exceeded);
575        } else {
576            self.stats.insert(
577                name.to_string(),
578                TraceStats::new(duration, budget_us, budget_exceeded),
579            );
580        }
581
582        // Store recent event
583        if self.recent_events.len() >= self.max_recent {
584            self.recent_events.remove(0);
585        }
586        self.recent_events.push(event);
587    }
588
589    /// Check if an operation should escalate to deep tracing
590    #[must_use]
591    pub fn should_escalate(&self, name: &str) -> bool {
592        if let Some(stats) = self.stats.get(name) {
593            let cv = stats.cv_percent();
594            let efficiency = stats.efficiency_percent();
595
596            cv > self.thresholds.cv_percent || efficiency < self.thresholds.efficiency_percent
597        } else {
598            false
599        }
600    }
601
602    /// Get stats for a specific operation
603    #[must_use]
604    pub fn get_stats(&self, name: &str) -> Option<&TraceStats> {
605        self.stats.get(name)
606    }
607
608    /// Get all stats
609    #[must_use]
610    pub fn all_stats(&self) -> &HashMap<String, TraceStats> {
611        &self.stats
612    }
613
614    /// Generate a summary report
615    #[must_use]
616    pub fn summary(&self) -> String {
617        let mut lines = vec![
618            "=== Performance Trace Summary ===".to_string(),
619            String::new(),
620        ];
621
622        let mut sorted: Vec<_> = self.stats.iter().collect();
623        sorted.sort_by(|a, b| b.1.total_duration.cmp(&a.1.total_duration));
624
625        for (name, stats) in sorted {
626            let avg_us = stats.avg_duration().as_micros();
627            let max_us = stats.max_duration.as_micros();
628            let cv = stats.cv_percent();
629            let eff = stats.efficiency_percent();
630            let status = if stats.budget_violations > 0 {
631                "⚠️"
632            } else {
633                "✓"
634            };
635
636            lines.push(format!(
637                "{status} {name}: avg={avg_us}μs max={max_us}μs count={} cv={cv:.1}% eff={eff:.0}%",
638                stats.count
639            ));
640
641            if self.should_escalate(name) {
642                lines.push(format!(
643                    "  └── ESCALATE: CV={cv:.1}% > {}% OR eff={eff:.0}% < {}%",
644                    self.thresholds.cv_percent, self.thresholds.efficiency_percent
645                ));
646            }
647        }
648
649        lines.join("\n")
650    }
651
652    /// Export stats in a format compatible with renacer
653    #[must_use]
654    pub fn export_renacer_format(&self) -> String {
655        let mut lines = vec!["# renacer-compatible trace export".to_string()];
656        lines.push(format!("# timestamp: {:?}", self.start_time.elapsed()));
657
658        for (name, stats) in &self.stats {
659            lines.push(format!(
660                "TRACE {} count={} total_us={} avg_us={} max_us={} cv={:.2} eff={:.2} violations={}",
661                name,
662                stats.count,
663                stats.total_duration.as_micros(),
664                stats.avg_duration().as_micros(),
665                stats.max_duration.as_micros(),
666                stats.cv_percent(),
667                stats.efficiency_percent(),
668                stats.budget_violations
669            ));
670        }
671
672        lines.join("\n")
673    }
674
675    /// Clear all stats
676    pub fn clear(&mut self) {
677        self.stats.clear();
678        self.recent_events.clear();
679    }
680}