1use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11fn now_ms() -> u64 {
14 SystemTime::now()
15 .duration_since(UNIX_EPOCH)
16 .unwrap_or_default()
17 .as_millis() as u64
18}
19
20fn now_us() -> u64 {
21 SystemTime::now()
22 .duration_since(UNIX_EPOCH)
23 .unwrap_or_default()
24 .as_micros() as u64
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum MetricKind {
32 Counter,
34 Gauge,
36 Histogram,
38 Summary,
40}
41
42#[derive(Debug, Clone)]
46pub enum MetricValue {
47 Int(i64),
49 Float(f64),
51 Histogram {
53 buckets: Vec<(f64, u64)>,
54 sum: f64,
55 count: u64,
56 },
57 Summary {
59 p50: f64,
60 p90: f64,
61 p95: f64,
62 p99: f64,
63 count: u64,
64 },
65}
66
67impl Default for MetricValue {
68 fn default() -> Self { MetricValue::Int(0) }
69}
70
71#[derive(Debug, Clone)]
75pub struct Metric {
76 pub name: String,
77 pub kind: MetricKind,
78 pub value: MetricValue,
79 pub labels: HashMap<String, String>,
80 pub last_update: u64,
82}
83
84impl Metric {
85 fn new(name: impl Into<String>, kind: MetricKind, labels: HashMap<String, String>) -> Self {
86 let value = match kind {
87 MetricKind::Counter => MetricValue::Int(0),
88 MetricKind::Gauge => MetricValue::Float(0.0),
89 MetricKind::Histogram => MetricValue::Histogram { buckets: Vec::new(), sum: 0.0, count: 0 },
90 MetricKind::Summary => MetricValue::Summary { p50: 0.0, p90: 0.0, p95: 0.0, p99: 0.0, count: 0 },
91 };
92 Self { name: name.into(), kind, value, labels, last_update: now_ms() }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99struct MetricKey {
100 name: String,
101 sorted_labels: Vec<(String, String)>,
102}
103
104impl MetricKey {
105 fn new(name: &str, labels: &HashMap<String, String>) -> Self {
106 let mut sorted_labels: Vec<(String, String)> = labels
107 .iter()
108 .map(|(k, v)| (k.clone(), v.clone()))
109 .collect();
110 sorted_labels.sort_by(|a, b| a.0.cmp(&b.0));
111 Self { name: name.to_owned(), sorted_labels }
112 }
113}
114
115#[derive(Debug, Clone)]
119pub struct HistogramBuckets {
120 boundaries: Vec<f64>,
122 counts: Vec<u64>,
124 observations: Vec<f64>,
126 sum: f64,
127 count: u64,
128}
129
130impl HistogramBuckets {
131 pub fn new(boundaries: Vec<f64>) -> Self {
133 let n = boundaries.len();
134 Self {
135 boundaries,
136 counts: vec![0; n],
137 observations: Vec::new(),
138 sum: 0.0,
139 count: 0,
140 }
141 }
142
143 pub fn latency_ms() -> Self {
145 Self::new(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 5000.0])
146 }
147
148 pub fn exponential(start: f64, factor: f64, count: usize) -> Self {
150 let mut b = Vec::with_capacity(count);
151 let mut v = start;
152 for _ in 0..count {
153 b.push(v);
154 v *= factor;
155 }
156 Self::new(b)
157 }
158
159 pub fn observe(&mut self, value: f64) {
161 self.sum += value;
162 self.count += 1;
163 self.observations.push(value);
164 for (i, &bound) in self.boundaries.iter().enumerate() {
165 if value <= bound {
166 self.counts[i] += 1;
167 }
168 }
169 }
170
171 pub fn percentile(&self, p: f64) -> f64 {
173 if self.observations.is_empty() { return 0.0; }
174 let mut sorted = self.observations.clone();
175 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
176 let rank = p * (sorted.len() - 1) as f64;
177 let lo = rank.floor() as usize;
178 let hi = rank.ceil() as usize;
179 let frac = rank - lo as f64;
180 if lo == hi { return sorted[lo]; }
181 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
182 }
183
184 pub fn mean(&self) -> f64 {
186 if self.count == 0 { return 0.0; }
187 self.sum / self.count as f64
188 }
189
190 pub fn std_dev(&self) -> f64 {
192 if self.count < 2 { return 0.0; }
193 let mean = self.mean();
194 let var = self.observations.iter()
195 .map(|&x| (x - mean).powi(2))
196 .sum::<f64>() / self.count as f64;
197 var.sqrt()
198 }
199
200 pub fn count(&self) -> u64 { self.count }
202
203 pub fn sum(&self) -> f64 { self.sum }
205
206 pub fn bucket_pairs(&self) -> Vec<(f64, u64)> {
208 self.boundaries.iter().cloned().zip(self.counts.iter().cloned()).collect()
209 }
210
211 pub fn reset(&mut self) {
213 self.counts = vec![0; self.boundaries.len()];
214 self.observations.clear();
215 self.sum = 0.0;
216 self.count = 0;
217 }
218}
219
220#[derive(Debug, Clone)]
224struct InternalHistogram {
225 buckets: HistogramBuckets,
226}
227
228impl InternalHistogram {
229 fn new(boundaries: Vec<f64>) -> Self {
230 Self { buckets: HistogramBuckets::new(boundaries) }
231 }
232
233 fn observe(&mut self, value: f64) {
234 self.buckets.observe(value);
235 }
236
237 fn to_metric_value(&self) -> MetricValue {
238 MetricValue::Histogram {
239 buckets: self.buckets.bucket_pairs(),
240 sum: self.buckets.sum(),
241 count: self.buckets.count(),
242 }
243 }
244
245 fn to_summary_value(&self) -> MetricValue {
246 MetricValue::Summary {
247 p50: self.buckets.percentile(0.50),
248 p90: self.buckets.percentile(0.90),
249 p95: self.buckets.percentile(0.95),
250 p99: self.buckets.percentile(0.99),
251 count: self.buckets.count(),
252 }
253 }
254}
255
256#[derive(Debug, Clone)]
259enum RegistryEntry {
260 Counter(i64),
261 Gauge(f64),
262 Histogram(InternalHistogram),
263}
264
265pub struct MetricsRegistry {
272 inner: Mutex<RegistryInner>,
273}
274
275#[derive(Debug, Default)]
276struct RegistryInner {
277 entries: HashMap<MetricKey, (MetricKind, RegistryEntry, HashMap<String, String>)>,
278 default_buckets: Vec<f64>,
280}
281
282impl RegistryInner {
283 fn new() -> Self {
284 Self {
285 entries: HashMap::new(),
286 default_buckets: vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 5.0, 10.0],
287 }
288 }
289}
290
291impl MetricsRegistry {
292 pub fn new() -> Self {
294 Self { inner: Mutex::new(RegistryInner::new()) }
295 }
296
297 pub fn shared() -> Arc<Self> {
299 Arc::new(Self::new())
300 }
301
302 pub fn counter(&self, name: &str, labels: HashMap<String, String>) -> i64 {
304 self.counter_by(name, labels, 1)
305 }
306
307 pub fn counter_by(&self, name: &str, labels: HashMap<String, String>, delta: i64) -> i64 {
309 let key = MetricKey::new(name, &labels);
310 let mut inner = self.inner.lock().unwrap();
311 let entry = inner.entries.entry(key).or_insert_with(|| {
312 (MetricKind::Counter, RegistryEntry::Counter(0), labels.clone())
313 });
314 if let RegistryEntry::Counter(ref mut v) = entry.1 {
315 *v += delta;
316 *v
317 } else {
318 0
319 }
320 }
321
322 pub fn gauge(&self, name: &str, labels: HashMap<String, String>, value: f64) {
324 let key = MetricKey::new(name, &labels);
325 let mut inner = self.inner.lock().unwrap();
326 let entry = inner.entries.entry(key).or_insert_with(|| {
327 (MetricKind::Gauge, RegistryEntry::Gauge(0.0), labels.clone())
328 });
329 if let RegistryEntry::Gauge(ref mut v) = entry.1 {
330 *v = value;
331 }
332 }
333
334 pub fn gauge_add(&self, name: &str, labels: HashMap<String, String>, delta: f64) {
336 let key = MetricKey::new(name, &labels);
337 let mut inner = self.inner.lock().unwrap();
338 let entry = inner.entries.entry(key).or_insert_with(|| {
339 (MetricKind::Gauge, RegistryEntry::Gauge(0.0), labels.clone())
340 });
341 if let RegistryEntry::Gauge(ref mut v) = entry.1 {
342 *v += delta;
343 }
344 }
345
346 pub fn histogram_observe(&self, name: &str, labels: HashMap<String, String>, value: f64) {
348 let key = MetricKey::new(name, &labels);
349 let mut inner = self.inner.lock().unwrap();
350 let buckets = inner.default_buckets.clone();
351 let entry = inner.entries.entry(key).or_insert_with(|| {
352 (MetricKind::Histogram, RegistryEntry::Histogram(InternalHistogram::new(buckets)), labels.clone())
353 });
354 if let RegistryEntry::Histogram(ref mut h) = entry.1 {
355 h.observe(value);
356 }
357 }
358
359 pub fn set_default_buckets(&self, boundaries: Vec<f64>) {
361 let mut inner = self.inner.lock().unwrap();
362 inner.default_buckets = boundaries;
363 }
364
365 pub fn snapshot(&self) -> Vec<Metric> {
367 let inner = self.inner.lock().unwrap();
368 let ts = now_ms();
369 inner.entries.iter().map(|(key, (kind, entry, labels))| {
370 let value = match entry {
371 RegistryEntry::Counter(v) => MetricValue::Int(*v),
372 RegistryEntry::Gauge(v) => MetricValue::Float(*v),
373 RegistryEntry::Histogram(h) => {
374 match kind {
375 MetricKind::Summary => h.to_summary_value(),
376 _ => h.to_metric_value(),
377 }
378 }
379 };
380 Metric {
381 name: key.name.clone(),
382 kind: kind.clone(),
383 value,
384 labels: labels.clone(),
385 last_update: ts,
386 }
387 }).collect()
388 }
389
390 pub fn get_counter(&self, name: &str, labels: &HashMap<String, String>) -> i64 {
392 let key = MetricKey::new(name, labels);
393 let inner = self.inner.lock().unwrap();
394 if let Some((_, RegistryEntry::Counter(v), _)) = inner.entries.get(&key) {
395 *v
396 } else {
397 0
398 }
399 }
400
401 pub fn get_gauge(&self, name: &str, labels: &HashMap<String, String>) -> f64 {
403 let key = MetricKey::new(name, labels);
404 let inner = self.inner.lock().unwrap();
405 if let Some((_, RegistryEntry::Gauge(v), _)) = inner.entries.get(&key) {
406 *v
407 } else {
408 0.0
409 }
410 }
411
412 pub fn reset(&self) {
414 let mut inner = self.inner.lock().unwrap();
415 inner.entries.clear();
416 }
417}
418
419impl Default for MetricsRegistry {
420 fn default() -> Self { Self::new() }
421}
422
423pub struct RollingCounter {
429 buffer: Vec<(u64, u64)>,
431 head: usize,
432 capacity: usize,
433 window_us: u64,
434 total: u64,
435}
436
437impl RollingCounter {
438 pub fn new(window_secs: f64) -> Self {
440 let capacity = 4096;
441 Self {
442 buffer: vec![(0, 0); capacity],
443 head: 0,
444 capacity,
445 window_us: (window_secs * 1_000_000.0) as u64,
446 total: 0,
447 }
448 }
449
450 pub fn record(&mut self, delta: u64) {
452 let ts = now_us();
453 self.buffer[self.head] = (ts, delta);
454 self.head = (self.head + 1) % self.capacity;
455 self.total += delta;
456 }
457
458 pub fn increment(&mut self) { self.record(1); }
460
461 pub fn rate(&self) -> f64 {
463 let now = now_us();
464 let cutoff = now.saturating_sub(self.window_us);
465 let events_in_window: u64 = self.buffer.iter()
466 .filter(|&&(ts, _)| ts >= cutoff && ts > 0)
467 .map(|&(_, delta)| delta)
468 .sum();
469 let window_secs = self.window_us as f64 / 1_000_000.0;
470 events_in_window as f64 / window_secs
471 }
472
473 pub fn total(&self) -> u64 { self.total }
475
476 pub fn window_count(&self) -> u64 {
478 let now = now_us();
479 let cutoff = now.saturating_sub(self.window_us);
480 self.buffer.iter()
481 .filter(|&&(ts, _)| ts >= cutoff && ts > 0)
482 .map(|&(_, delta)| delta)
483 .sum()
484 }
485
486 pub fn reset(&mut self) {
488 for entry in &mut self.buffer { *entry = (0, 0); }
489 self.head = 0;
490 self.total = 0;
491 }
492}
493
494#[derive(Debug, Clone)]
501pub struct ExponentialMovingAverage {
502 alpha: f64,
503 value: f64,
504 initialized: bool,
505 sample_count: u64,
506}
507
508impl ExponentialMovingAverage {
509 pub fn new(alpha: f64) -> Self {
513 let alpha = alpha.clamp(1e-9, 1.0);
514 Self { alpha, value: 0.0, initialized: false, sample_count: 0 }
515 }
516
517 pub fn with_samples(n: f64) -> Self {
519 Self::new(2.0 / (n + 1.0))
520 }
521
522 pub fn update(&mut self, value: f64) {
524 if !self.initialized {
525 self.value = value;
526 self.initialized = true;
527 } else {
528 self.value = self.alpha * value + (1.0 - self.alpha) * self.value;
529 }
530 self.sample_count += 1;
531 }
532
533 pub fn get(&self) -> f64 { self.value }
535
536 pub fn sample_count(&self) -> u64 { self.sample_count }
538
539 pub fn reset(&mut self) {
541 self.value = 0.0;
542 self.initialized = false;
543 self.sample_count = 0;
544 }
545
546 pub fn alpha(&self) -> f64 { self.alpha }
548}
549
550pub struct MetricsExporter {
561 registry: Arc<MetricsRegistry>,
562}
563
564impl MetricsExporter {
565 pub fn new(registry: Arc<MetricsRegistry>) -> Self {
566 Self { registry }
567 }
568
569 pub fn export(&self) -> String {
571 let metrics = self.registry.snapshot();
572 let mut lines = Vec::new();
573
574 for m in &metrics {
575 let type_str = match m.kind {
576 MetricKind::Counter => "counter",
577 MetricKind::Gauge => "gauge",
578 MetricKind::Histogram => "histogram",
579 MetricKind::Summary => "summary",
580 };
581 lines.push(format!("# HELP {} ", m.name));
582 lines.push(format!("# TYPE {} {}", m.name, type_str));
583
584 let label_str = Self::format_labels(&m.labels);
585
586 match &m.value {
587 MetricValue::Int(v) => {
588 lines.push(format!("{}{} {} {}", m.name, label_str, v, m.last_update));
589 }
590 MetricValue::Float(v) => {
591 lines.push(format!("{}{} {} {}", m.name, label_str, v, m.last_update));
592 }
593 MetricValue::Histogram { buckets, sum, count } => {
594 for (bound, cnt) in buckets {
595 let bucket_label = Self::format_labels_with_extra(&m.labels, "le", &bound.to_string());
596 lines.push(format!("{}_bucket{} {} {}", m.name, bucket_label, cnt, m.last_update));
597 }
598 let inf_label = Self::format_labels_with_extra(&m.labels, "le", "+Inf");
600 lines.push(format!("{}_bucket{} {} {}", m.name, inf_label, count, m.last_update));
601 lines.push(format!("{}_sum{} {} {}", m.name, label_str, sum, m.last_update));
602 lines.push(format!("{}_count{} {} {}", m.name, label_str, count, m.last_update));
603 }
604 MetricValue::Summary { p50, p90, p95, p99, count } => {
605 let q50 = Self::format_labels_with_extra(&m.labels, "quantile", "0.5");
606 let q90 = Self::format_labels_with_extra(&m.labels, "quantile", "0.9");
607 let q95 = Self::format_labels_with_extra(&m.labels, "quantile", "0.95");
608 let q99 = Self::format_labels_with_extra(&m.labels, "quantile", "0.99");
609 lines.push(format!("{}{} {} {}", m.name, q50, p50, m.last_update));
610 lines.push(format!("{}{} {} {}", m.name, q90, p90, m.last_update));
611 lines.push(format!("{}{} {} {}", m.name, q95, p95, m.last_update));
612 lines.push(format!("{}{} {} {}", m.name, q99, p99, m.last_update));
613 lines.push(format!("{}_count{} {} {}", m.name, label_str, count, m.last_update));
614 }
615 }
616 }
617
618 lines.join("\n") + "\n"
619 }
620
621 fn format_labels(labels: &HashMap<String, String>) -> String {
622 if labels.is_empty() { return String::new(); }
623 let mut pairs: Vec<_> = labels.iter().collect();
624 pairs.sort_by_key(|(k, _)| k.as_str());
625 let inner: Vec<String> = pairs.iter().map(|(k, v)| format!("{}=\"{}\"", k, v)).collect();
626 format!("{{{}}}", inner.join(","))
627 }
628
629 fn format_labels_with_extra(labels: &HashMap<String, String>, key: &str, value: &str) -> String {
630 let mut pairs: Vec<_> = labels.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
631 pairs.push((key, value));
632 pairs.sort_by_key(|(k, _)| *k);
633 let inner: Vec<String> = pairs.iter().map(|(k, v)| format!("{}=\"{}\"", k, v)).collect();
634 format!("{{{}}}", inner.join(","))
635 }
636}
637
638#[derive(Debug, Clone, Default)]
642pub struct EngineSnapshot {
643 pub fps: f64,
644 pub frame_time_ms: f64,
645 pub entity_count: usize,
646 pub particle_count: usize,
647 pub glyph_count: usize,
648 pub memory_estimate: usize,
650 pub extras: Vec<(String, String)>,
652}
653
654pub struct PerformanceDashboard {
659 ema_fps: ExponentialMovingAverage,
660 ema_frame_ms: ExponentialMovingAverage,
661 peak_fps: f64,
662 min_fps: f64,
663 peak_frame_ms: f64,
664 last_snapshot: EngineSnapshot,
665 frame_count: u64,
666}
667
668impl PerformanceDashboard {
669 pub fn new() -> Self {
670 Self {
671 ema_fps: ExponentialMovingAverage::new(0.1),
672 ema_frame_ms: ExponentialMovingAverage::new(0.1),
673 peak_fps: 0.0,
674 min_fps: f64::MAX,
675 peak_frame_ms: 0.0,
676 last_snapshot: EngineSnapshot::default(),
677 frame_count: 0,
678 }
679 }
680
681 pub fn update(&mut self, snapshot: EngineSnapshot) {
683 self.ema_fps.update(snapshot.fps);
684 self.ema_frame_ms.update(snapshot.frame_time_ms);
685 if snapshot.fps > self.peak_fps { self.peak_fps = snapshot.fps; }
686 if snapshot.fps < self.min_fps { self.min_fps = snapshot.fps; }
687 if snapshot.frame_time_ms > self.peak_frame_ms { self.peak_frame_ms = snapshot.frame_time_ms; }
688 self.frame_count += 1;
689 self.last_snapshot = snapshot;
690 }
691
692 pub fn format_table(&self) -> String {
694 let s = &self.last_snapshot;
695 let rows: Vec<(&str, String)> = vec![
696 ("FPS (cur)", format!("{:>7.1}", s.fps)),
697 ("FPS (avg)", format!("{:>7.1}", self.ema_fps.get())),
698 ("FPS (peak)", format!("{:>7.1}", self.peak_fps)),
699 ("FPS (min)", format!("{:>7.1}", if self.min_fps == f64::MAX { 0.0 } else { self.min_fps })),
700 ("Frame ms", format!("{:>7.2}", s.frame_time_ms)),
701 ("Frame ms avg", format!("{:>7.2}", self.ema_frame_ms.get())),
702 ("Frame ms pk", format!("{:>7.2}", self.peak_frame_ms)),
703 ("Entities", format!("{:>7}", s.entity_count)),
704 ("Particles", format!("{:>7}", s.particle_count)),
705 ("Glyphs", format!("{:>7}", s.glyph_count)),
706 ("Memory", format!("{:>6.1}K", s.memory_estimate as f64 / 1024.0)),
707 ("Frames", format!("{:>7}", self.frame_count)),
708 ];
709
710 let key_width = rows.iter().map(|(k, _)| k.len()).max().unwrap_or(10);
712 let val_width = rows.iter().map(|(_, v)| v.len()).max().unwrap_or(7);
713 let total_inner = key_width + 3 + val_width; let top = format!("╔{}╗", "═".repeat(total_inner + 2));
716 let title = format!("║ {:<width$} ║", "Performance Dashboard", width = total_inner);
717 let sep = format!("╠{}╣", "═".repeat(total_inner + 2));
718 let bottom = format!("╚{}╝", "═".repeat(total_inner + 2));
719
720 let mut lines = vec![top, title, sep];
721
722 for (key, val) in &rows {
723 lines.push(format!("║ {:<kw$} │ {:<vw$} ║", key, val, kw = key_width, vw = val_width));
724 }
725
726 for (key, val) in &s.extras {
728 lines.push(format!("║ {:<kw$} │ {:<vw$} ║", key, val, kw = key_width, vw = val_width));
729 }
730
731 lines.push(bottom);
732 lines.join("\n")
733 }
734
735 pub fn format_line(&self) -> String {
737 let s = &self.last_snapshot;
738 format!(
739 "FPS:{:.0} dt:{:.1}ms E:{} P:{} G:{} M:{:.0}K",
740 s.fps, s.frame_time_ms,
741 s.entity_count, s.particle_count, s.glyph_count,
742 s.memory_estimate as f64 / 1024.0,
743 )
744 }
745}
746
747impl Default for PerformanceDashboard {
748 fn default() -> Self { Self::new() }
749}
750
751pub struct MemoryTracker {
758 categories: HashMap<String, CategoryStats>,
759}
760
761#[derive(Debug, Clone, Default)]
762struct CategoryStats {
763 current: usize,
764 peak: usize,
765 total_alloc: u64,
766 total_free: u64,
767 alloc_count: u64,
768 free_count: u64,
769}
770
771impl MemoryTracker {
772 pub fn new() -> Self {
773 Self { categories: HashMap::new() }
774 }
775
776 pub fn alloc(&mut self, category: &str, bytes: usize) {
778 let s = self.categories.entry(category.to_owned()).or_default();
779 s.current += bytes;
780 s.total_alloc += bytes as u64;
781 s.alloc_count += 1;
782 if s.current > s.peak { s.peak = s.current; }
783 }
784
785 pub fn free(&mut self, category: &str, bytes: usize) {
787 let s = self.categories.entry(category.to_owned()).or_default();
788 s.current = s.current.saturating_sub(bytes);
789 s.total_free += bytes as u64;
790 s.free_count += 1;
791 }
792
793 pub fn total(&self) -> usize {
795 self.categories.values().map(|s| s.current).sum()
796 }
797
798 pub fn peak_total(&self) -> usize {
800 self.categories.values().map(|s| s.peak).sum()
801 }
802
803 pub fn report_by_category(&self) -> Vec<(String, usize)> {
805 let mut rows: Vec<(String, usize)> = self.categories.iter()
806 .map(|(k, v)| (k.clone(), v.current))
807 .collect();
808 rows.sort_by(|a, b| b.1.cmp(&a.1));
809 rows
810 }
811
812 pub fn detailed_report(&self) -> Vec<CategoryReport> {
814 let mut rows: Vec<CategoryReport> = self.categories.iter().map(|(k, v)| {
815 CategoryReport {
816 category: k.clone(),
817 current: v.current,
818 peak: v.peak,
819 total_alloc: v.total_alloc,
820 total_free: v.total_free,
821 alloc_count: v.alloc_count,
822 free_count: v.free_count,
823 }
824 }).collect();
825 rows.sort_by(|a, b| b.current.cmp(&a.current));
826 rows
827 }
828
829 pub fn format_report(&self) -> String {
831 let mut lines = vec!["=== Memory Tracker ===".to_owned()];
832 lines.push(format!("Total: {} bytes Peak: {} bytes", self.total(), self.peak_total()));
833 for (cat, bytes) in self.report_by_category() {
834 lines.push(format!(" {:24} {:>10} bytes", cat, bytes));
835 }
836 lines.join("\n")
837 }
838
839 pub fn reset(&mut self) {
841 self.categories.clear();
842 }
843
844 pub fn reset_category(&mut self, category: &str) {
846 self.categories.remove(category);
847 }
848}
849
850#[derive(Debug, Clone)]
852pub struct CategoryReport {
853 pub category: String,
854 pub current: usize,
855 pub peak: usize,
856 pub total_alloc: u64,
857 pub total_free: u64,
858 pub alloc_count: u64,
859 pub free_count: u64,
860}
861
862impl Default for MemoryTracker {
863 fn default() -> Self { Self::new() }
864}
865
866#[derive(Debug, Clone)]
870pub struct TimeSeries {
871 samples: Vec<(u64, f64)>,
872 head: usize,
873 capacity: usize,
874 count: usize,
875}
876
877impl TimeSeries {
878 pub fn new(capacity: usize) -> Self {
879 Self {
880 samples: vec![(0, 0.0); capacity.max(1)],
881 head: 0,
882 capacity: capacity.max(1),
883 count: 0,
884 }
885 }
886
887 pub fn push(&mut self, value: f64) {
889 self.samples[self.head] = (now_ms(), value);
890 self.head = (self.head + 1) % self.capacity;
891 self.count = (self.count + 1).min(self.capacity);
892 }
893
894 pub fn push_at(&mut self, ts_ms: u64, value: f64) {
896 self.samples[self.head] = (ts_ms, value);
897 self.head = (self.head + 1) % self.capacity;
898 self.count = (self.count + 1).min(self.capacity);
899 }
900
901 pub fn iter(&self) -> impl Iterator<Item = (u64, f64)> + '_ {
903 let start = if self.count < self.capacity { 0 } else { self.head };
904 (0..self.count).map(move |i| self.samples[(start + i) % self.capacity])
905 }
906
907 pub fn latest(&self) -> f64 {
909 if self.count == 0 { return 0.0; }
910 let idx = if self.head == 0 { self.capacity - 1 } else { self.head - 1 };
911 self.samples[idx].1
912 }
913
914 pub fn len(&self) -> usize { self.count }
915 pub fn is_empty(&self) -> bool { self.count == 0 }
916}
917
918#[derive(Debug, Clone)]
922pub struct AggregateStats {
923 pub min: f64,
924 pub max: f64,
925 pub mean: f64,
926 pub std_dev: f64,
927 pub p50: f64,
928 pub p95: f64,
929 pub p99: f64,
930 pub count: usize,
931}
932
933impl AggregateStats {
934 pub fn compute(values: &[f64]) -> Option<Self> {
935 if values.is_empty() { return None; }
936 let count = values.len();
937 let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
938 let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
939 let sum: f64 = values.iter().sum();
940 let mean = sum / count as f64;
941 let var = values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / count as f64;
942 let std_dev = var.sqrt();
943
944 let mut sorted = values.to_vec();
945 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
946
947 let percentile = |p: f64| -> f64 {
948 let rank = p * (count - 1) as f64;
949 let lo = rank.floor() as usize;
950 let hi = rank.ceil() as usize;
951 let frac = rank - lo as f64;
952 if lo == hi { return sorted[lo]; }
953 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
954 };
955
956 Some(Self { min, max, mean, std_dev, p50: percentile(0.5), p95: percentile(0.95), p99: percentile(0.99), count })
957 }
958}
959
960#[cfg(test)]
963mod tests {
964 use super::*;
965
966 #[test]
967 fn counter_increments() {
968 let reg = MetricsRegistry::new();
969 reg.counter("requests", HashMap::new());
970 reg.counter("requests", HashMap::new());
971 reg.counter("requests", HashMap::new());
972 assert_eq!(reg.get_counter("requests", &HashMap::new()), 3);
973 }
974
975 #[test]
976 fn counter_by_delta() {
977 let reg = MetricsRegistry::new();
978 reg.counter_by("bytes", HashMap::new(), 1024);
979 reg.counter_by("bytes", HashMap::new(), 512);
980 assert_eq!(reg.get_counter("bytes", &HashMap::new()), 1536);
981 }
982
983 #[test]
984 fn gauge_set_and_get() {
985 let reg = MetricsRegistry::new();
986 reg.gauge("temperature", HashMap::new(), 98.6);
987 assert!((reg.get_gauge("temperature", &HashMap::new()) - 98.6).abs() < 1e-9);
988 }
989
990 #[test]
991 fn gauge_add() {
992 let reg = MetricsRegistry::new();
993 reg.gauge("level", HashMap::new(), 10.0);
994 reg.gauge_add("level", HashMap::new(), 5.0);
995 assert!((reg.get_gauge("level", &HashMap::new()) - 15.0).abs() < 1e-9);
996 }
997
998 #[test]
999 fn snapshot_contains_all_metrics() {
1000 let reg = MetricsRegistry::new();
1001 reg.counter("c1", HashMap::new());
1002 reg.gauge("g1", HashMap::new(), 1.0);
1003 reg.histogram_observe("h1", HashMap::new(), 0.5);
1004 let snap = reg.snapshot();
1005 assert!(snap.len() >= 3);
1006 }
1007
1008 #[test]
1009 fn histogram_buckets_percentile() {
1010 let mut h = HistogramBuckets::latency_ms();
1011 for v in [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] {
1012 h.observe(v);
1013 }
1014 let p50 = h.percentile(0.5);
1015 assert!(p50 >= 5.0 && p50 <= 6.0, "p50={}", p50);
1016 let p90 = h.percentile(0.9);
1017 assert!(p90 >= 9.0, "p90={}", p90);
1018 }
1019
1020 #[test]
1021 fn histogram_mean_and_std_dev() {
1022 let mut h = HistogramBuckets::new(vec![10.0, 100.0]);
1023 for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
1024 h.observe(v);
1025 }
1026 let mean = h.mean();
1027 assert!((mean - 5.0).abs() < 0.01, "mean={}", mean);
1028 let sd = h.std_dev();
1029 assert!(sd > 0.0, "std_dev should be positive");
1030 }
1031
1032 #[test]
1033 fn rolling_counter_rate() {
1034 let mut rc = RollingCounter::new(1.0);
1035 for _ in 0..100 { rc.increment(); }
1036 assert_eq!(rc.total(), 100);
1037 assert!(rc.rate() > 0.0);
1039 }
1040
1041 #[test]
1042 fn ema_convergence() {
1043 let mut ema = ExponentialMovingAverage::new(0.5);
1044 for _ in 0..30 { ema.update(10.0); }
1046 assert!((ema.get() - 10.0).abs() < 0.01, "EMA={}", ema.get());
1047 }
1048
1049 #[test]
1050 fn ema_with_samples() {
1051 let mut ema = ExponentialMovingAverage::with_samples(10.0);
1052 for _ in 0..50 { ema.update(5.0); }
1053 assert!((ema.get() - 5.0).abs() < 0.01);
1054 }
1055
1056 #[test]
1057 fn memory_tracker_alloc_free() {
1058 let mut tracker = MemoryTracker::new();
1059 tracker.alloc("textures", 1024);
1060 tracker.alloc("textures", 2048);
1061 tracker.free("textures", 1024);
1062 assert_eq!(tracker.total(), 2048);
1063 let report = tracker.report_by_category();
1064 assert_eq!(report[0].0, "textures");
1065 assert_eq!(report[0].1, 2048);
1066 }
1067
1068 #[test]
1069 fn memory_tracker_peak() {
1070 let mut tracker = MemoryTracker::new();
1071 tracker.alloc("verts", 4096);
1072 tracker.alloc("verts", 4096);
1073 tracker.free("verts", 8192);
1074 assert_eq!(tracker.peak_total(), 8192);
1075 assert_eq!(tracker.total(), 0);
1076 }
1077
1078 #[test]
1079 fn performance_dashboard_update() {
1080 let mut dash = PerformanceDashboard::new();
1081 dash.update(EngineSnapshot {
1082 fps: 60.0,
1083 frame_time_ms: 16.7,
1084 entity_count: 100,
1085 particle_count: 500,
1086 glyph_count: 2000,
1087 memory_estimate: 1024 * 1024,
1088 extras: vec![],
1089 });
1090 let table = dash.format_table();
1091 assert!(table.contains("60"), "table should contain fps=60");
1092 assert!(table.contains("╔"), "table should have box-drawing chars");
1093 assert!(table.contains("╚"), "table should have box-drawing chars");
1094 }
1095
1096 #[test]
1097 fn metrics_exporter_counter() {
1098 let reg = Arc::new(MetricsRegistry::new());
1099 reg.counter("http_requests", HashMap::new());
1100 let exporter = MetricsExporter::new(Arc::clone(®));
1101 let out = exporter.export();
1102 assert!(out.contains("http_requests"), "export should mention metric name");
1103 assert!(out.contains("# TYPE"), "should have type annotation");
1104 }
1105
1106 #[test]
1107 fn aggregate_stats() {
1108 let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1109 let stats = AggregateStats::compute(&vals).unwrap();
1110 assert_eq!(stats.mean, 3.0);
1111 assert_eq!(stats.min, 1.0);
1112 assert_eq!(stats.max, 5.0);
1113 }
1114
1115 #[test]
1116 fn time_series_ring_buffer() {
1117 let mut ts = TimeSeries::new(5);
1118 for i in 0..8u64 { ts.push(i as f64); }
1119 assert_eq!(ts.len(), 5);
1120 assert_eq!(ts.latest(), 7.0);
1121 }
1122
1123 #[test]
1124 fn metrics_with_labels() {
1125 let reg = MetricsRegistry::new();
1126 let mut labels_a = HashMap::new();
1127 labels_a.insert("method".to_owned(), "GET".to_owned());
1128 let mut labels_b = HashMap::new();
1129 labels_b.insert("method".to_owned(), "POST".to_owned());
1130 reg.counter("requests", labels_a.clone());
1131 reg.counter("requests", labels_a.clone());
1132 reg.counter("requests", labels_b.clone());
1133 assert_eq!(reg.get_counter("requests", &labels_a), 2);
1134 assert_eq!(reg.get_counter("requests", &labels_b), 1);
1135 }
1136}