1use std::collections::HashMap;
2use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3use std::time::{Duration, Instant};
4
5static TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
11static START_TIME_US: AtomicU64 = AtomicU64::new(0);
13
14pub 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
26pub fn disable_tracing() {
28 TRACE_ENABLED.store(false, Ordering::Release);
29}
30
31#[inline]
33pub fn is_tracing_enabled() -> bool {
34 TRACE_ENABLED.load(Ordering::Acquire)
35}
36
37#[derive(Debug)]
53pub struct TimingGuard {
54 name: &'static str,
55 start: Option<Instant>,
56 budget_us: u64,
57}
58
59impl TimingGuard {
60 #[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 #[inline]
82 pub fn with_default_budget(name: &'static str) -> Self {
83 Self::new(name, 1000)
84 }
85
86 #[inline]
88 pub fn render(name: &'static str) -> Self {
89 Self::new(name, 16_000)
90 }
91
92 #[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 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#[repr(C, align(64))] #[derive(Debug, Clone, Default)]
136pub struct SimdStats {
137 pub count: u64,
139 pub sum: f64,
141 pub sum_sq: f64,
143 pub min: f64,
145 pub max: f64,
147}
148
149impl SimdStats {
150 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 #[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 #[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 #[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 #[inline]
193 pub fn std_dev(&self) -> f64 {
194 self.variance().sqrt()
195 }
196
197 #[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 pub fn reset(&mut self) {
210 *self = Self::new();
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub enum BrickType {
221 Collect,
223 Render,
225 Compute,
227 Network,
229 Storage,
231}
232
233impl BrickType {
234 #[must_use]
236 pub fn default_budget_us(&self) -> u64 {
237 match self {
238 Self::Collect => 100_000, Self::Render => 16_000, Self::Compute => 1_000, Self::Network => 500_000, Self::Storage => 50_000, }
244 }
245
246 #[must_use]
248 pub fn cv_threshold(&self) -> f64 {
249 match self {
250 Self::Render => 10.0, Self::Compute => 15.0, Self::Collect => 25.0, Self::Network => 50.0, Self::Storage => 30.0, }
256 }
257}
258
259#[derive(Debug)]
264pub struct BrickProfiler {
265 stats: std::collections::HashMap<String, (BrickType, SimdStats)>,
267 enabled: bool,
269}
270
271impl Default for BrickProfiler {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl BrickProfiler {
278 #[must_use]
280 pub fn new() -> Self {
281 Self {
282 stats: std::collections::HashMap::new(),
283 enabled: is_tracing_enabled(),
284 }
285 }
286
287 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 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 #[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 #[must_use]
323 pub fn get_stats(&self, name: &str) -> Option<&SimdStats> {
324 self.stats.get(name).map(|(_, s)| s)
325 }
326
327 #[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#[derive(Debug, Clone)]
364pub struct TraceEvent {
365 pub name: String,
367 pub duration: Duration,
369 pub timestamp_us: u64,
371 pub budget_exceeded: bool,
373 pub budget_us: Option<u64>,
375}
376
377#[derive(Debug, Clone, Default)]
379pub struct TraceStats {
380 pub count: u64,
382 pub total_duration: Duration,
384 pub min_duration: Duration,
386 pub max_duration: Duration,
388 pub budget_violations: u64,
390 pub budget_us: u64,
392}
393
394impl TraceStats {
395 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 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 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 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 }
444
445 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#[derive(Debug, Clone, Copy)]
458pub struct EscalationThresholds {
459 pub cv_percent: f64,
461 pub efficiency_percent: f64,
463 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#[derive(Debug)]
482pub struct PerfTracer {
483 stats: HashMap<String, TraceStats>,
485 recent_events: Vec<TraceEvent>,
487 max_recent: usize,
489 start_time: Instant,
491 thresholds: EscalationThresholds,
493 traces_this_second: u32,
495 last_second: u64,
497}
498
499impl Default for PerfTracer {
500 fn default() -> Self {
501 Self::new()
502 }
503}
504
505impl PerfTracer {
506 #[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 #[must_use]
522 pub fn with_thresholds(thresholds: EscalationThresholds) -> Self {
523 Self {
524 thresholds,
525 ..Self::new()
526 }
527 }
528
529 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 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) }
549
550 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 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 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 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 if self.recent_events.len() >= self.max_recent {
584 self.recent_events.remove(0);
585 }
586 self.recent_events.push(event);
587 }
588
589 #[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 #[must_use]
604 pub fn get_stats(&self, name: &str) -> Option<&TraceStats> {
605 self.stats.get(name)
606 }
607
608 #[must_use]
610 pub fn all_stats(&self) -> &HashMap<String, TraceStats> {
611 &self.stats
612 }
613
614 #[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 #[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 pub fn clear(&mut self) {
677 self.stats.clear();
678 self.recent_events.clear();
679 }
680}