1use serde::{Deserialize, Serialize};
223use std::collections::{HashMap, VecDeque};
224use wasm_bindgen::prelude::*;
225
226pub const MAX_TIMING_SAMPLES: usize = 1000;
228
229pub const MAX_MEMORY_SAMPLES: usize = 100;
231
232#[derive(Debug, Clone)]
234pub struct PerformanceCounter {
235 name: String,
237 count: u64,
239 total_time_ms: f64,
241 min_time_ms: f64,
243 max_time_ms: f64,
245 samples: VecDeque<f64>,
247}
248
249impl PerformanceCounter {
250 pub fn new(name: impl Into<String>) -> Self {
252 Self {
253 name: name.into(),
254 count: 0,
255 total_time_ms: 0.0,
256 min_time_ms: f64::MAX,
257 max_time_ms: f64::MIN,
258 samples: VecDeque::new(),
259 }
260 }
261
262 pub fn record(&mut self, duration_ms: f64) {
264 self.count += 1;
265 self.total_time_ms += duration_ms;
266 self.min_time_ms = self.min_time_ms.min(duration_ms);
267 self.max_time_ms = self.max_time_ms.max(duration_ms);
268
269 self.samples.push_back(duration_ms);
270 if self.samples.len() > MAX_TIMING_SAMPLES {
271 self.samples.pop_front();
272 }
273 }
274
275 pub fn average_ms(&self) -> f64 {
277 if self.count == 0 {
278 0.0
279 } else {
280 self.total_time_ms / self.count as f64
281 }
282 }
283
284 pub fn recent_average_ms(&self) -> f64 {
286 if self.samples.is_empty() {
287 return 0.0;
288 }
289
290 let recent: Vec<_> = self.samples.iter().rev().take(100).collect();
291 let sum: f64 = recent.iter().copied().sum();
292 sum / recent.len() as f64
293 }
294
295 pub fn percentile(&self, p: f64) -> f64 {
297 if self.samples.is_empty() {
298 return 0.0;
299 }
300
301 let mut sorted: Vec<_> = self.samples.iter().copied().collect();
302 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
303
304 let idx = ((p / 100.0) * sorted.len() as f64) as usize;
305 sorted[idx.min(sorted.len() - 1)]
306 }
307
308 pub fn stats(&self) -> CounterStats {
310 CounterStats {
311 name: self.name.clone(),
312 count: self.count,
313 total_time_ms: self.total_time_ms,
314 average_ms: self.average_ms(),
315 recent_average_ms: self.recent_average_ms(),
316 min_ms: if self.count > 0 {
317 self.min_time_ms
318 } else {
319 0.0
320 },
321 max_ms: if self.count > 0 {
322 self.max_time_ms
323 } else {
324 0.0
325 },
326 p50_ms: self.percentile(50.0),
327 p95_ms: self.percentile(95.0),
328 p99_ms: self.percentile(99.0),
329 }
330 }
331
332 pub fn reset(&mut self) {
334 self.count = 0;
335 self.total_time_ms = 0.0;
336 self.min_time_ms = f64::MAX;
337 self.max_time_ms = f64::MIN;
338 self.samples.clear();
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct CounterStats {
345 pub name: String,
347 pub count: u64,
349 pub total_time_ms: f64,
351 pub average_ms: f64,
353 pub recent_average_ms: f64,
355 pub min_ms: f64,
357 pub max_ms: f64,
359 pub p50_ms: f64,
361 pub p95_ms: f64,
363 pub p99_ms: f64,
365}
366
367#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
369pub struct MemorySnapshot {
370 pub timestamp: f64,
372 pub heap_used: usize,
374 pub heap_limit: Option<usize>,
376 pub external_memory: Option<usize>,
378}
379
380impl MemorySnapshot {
381 pub const fn new(timestamp: f64, heap_used: usize) -> Self {
383 Self {
384 timestamp,
385 heap_used,
386 heap_limit: None,
387 external_memory: None,
388 }
389 }
390
391 pub fn heap_utilization(&self) -> Option<f64> {
393 self.heap_limit.map(|limit| {
394 if limit > 0 {
395 self.heap_used as f64 / limit as f64
396 } else {
397 0.0
398 }
399 })
400 }
401}
402
403pub struct MemoryMonitor {
405 snapshots: VecDeque<MemorySnapshot>,
407 max_snapshots: usize,
409}
410
411impl MemoryMonitor {
412 pub fn new() -> Self {
414 Self {
415 snapshots: VecDeque::new(),
416 max_snapshots: MAX_MEMORY_SAMPLES,
417 }
418 }
419
420 pub fn record(&mut self, snapshot: MemorySnapshot) {
422 self.snapshots.push_back(snapshot);
423 if self.snapshots.len() > self.max_snapshots {
424 self.snapshots.pop_front();
425 }
426 }
427
428 pub fn record_current(&mut self, timestamp: f64) {
430 let snapshot = MemorySnapshot::new(timestamp, 0);
433 self.record(snapshot);
434 }
435
436 pub fn latest(&self) -> Option<&MemorySnapshot> {
438 self.snapshots.back()
439 }
440
441 pub fn stats(&self) -> MemoryStats {
443 if self.snapshots.is_empty() {
444 return MemoryStats {
445 current_heap_used: 0,
446 peak_heap_used: 0,
447 average_heap_used: 0.0,
448 sample_count: 0,
449 };
450 }
451
452 let current = self.snapshots.back().map(|s| s.heap_used).unwrap_or(0);
453 let peak = self
454 .snapshots
455 .iter()
456 .map(|s| s.heap_used)
457 .max()
458 .unwrap_or(0);
459 let sum: usize = self.snapshots.iter().map(|s| s.heap_used).sum();
460 let average = sum as f64 / self.snapshots.len() as f64;
461
462 MemoryStats {
463 current_heap_used: current,
464 peak_heap_used: peak,
465 average_heap_used: average,
466 sample_count: self.snapshots.len(),
467 }
468 }
469
470 pub fn clear(&mut self) {
472 self.snapshots.clear();
473 }
474}
475
476impl Default for MemoryMonitor {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
484pub struct MemoryStats {
485 pub current_heap_used: usize,
487 pub peak_heap_used: usize,
489 pub average_heap_used: f64,
491 pub sample_count: usize,
493}
494
495pub struct Profiler {
497 counters: HashMap<String, PerformanceCounter>,
499 memory: MemoryMonitor,
501 active_timers: HashMap<String, f64>,
503}
504
505impl Profiler {
506 pub fn new() -> Self {
508 Self {
509 counters: HashMap::new(),
510 memory: MemoryMonitor::new(),
511 active_timers: HashMap::new(),
512 }
513 }
514
515 pub fn start_timer(&mut self, name: impl Into<String>, timestamp: f64) {
517 self.active_timers.insert(name.into(), timestamp);
518 }
519
520 pub fn stop_timer(&mut self, name: impl Into<String>, timestamp: f64) {
522 let name = name.into();
523 if let Some(start) = self.active_timers.remove(&name) {
524 let duration = timestamp - start;
525 self.record(name, duration);
526 }
527 }
528
529 pub fn record(&mut self, name: impl Into<String>, duration_ms: f64) {
531 let name = name.into();
532 self.counters
533 .entry(name.clone())
534 .or_insert_with(|| PerformanceCounter::new(name))
535 .record(duration_ms);
536 }
537
538 pub fn record_memory(&mut self, timestamp: f64) {
540 self.memory.record_current(timestamp);
541 }
542
543 pub fn counter_stats(&self, name: &str) -> Option<CounterStats> {
545 self.counters.get(name).map(|c| c.stats())
546 }
547
548 pub fn all_counter_stats(&self) -> Vec<CounterStats> {
550 self.counters.values().map(|c| c.stats()).collect()
551 }
552
553 pub fn memory_stats(&self) -> MemoryStats {
555 self.memory.stats()
556 }
557
558 pub fn summary(&self) -> ProfilerSummary {
560 ProfilerSummary {
561 counters: self.all_counter_stats(),
562 memory: self.memory_stats(),
563 }
564 }
565
566 pub fn reset(&mut self) {
568 self.counters.clear();
569 self.memory.clear();
570 self.active_timers.clear();
571 }
572
573 pub fn clear_counter(&mut self, name: &str) {
575 self.counters.remove(name);
576 }
577}
578
579impl Default for Profiler {
580 fn default() -> Self {
581 Self::new()
582 }
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct ProfilerSummary {
588 pub counters: Vec<CounterStats>,
590 pub memory: MemoryStats,
592}
593
594#[allow(dead_code)]
596pub struct ScopedTimer<'a> {
597 profiler: &'a mut Profiler,
598 name: String,
599 start_time: f64,
600}
601
602#[allow(dead_code)]
603impl<'a> ScopedTimer<'a> {
604 pub fn new(profiler: &'a mut Profiler, name: impl Into<String>, start_time: f64) -> Self {
606 let name = name.into();
607 profiler.start_timer(name.clone(), start_time);
608 Self {
609 profiler,
610 name,
611 start_time,
612 }
613 }
614
615 pub fn elapsed(&self, current_time: f64) -> f64 {
617 current_time - self.start_time
618 }
619}
620
621impl<'a> Drop for ScopedTimer<'a> {
622 fn drop(&mut self) {
623 let current_time = self.start_time; self.profiler.stop_timer(self.name.clone(), current_time);
626 }
627}
628
629pub struct FrameRateTracker {
631 frame_times: VecDeque<f64>,
633 max_samples: usize,
635 target_fps: f64,
637}
638
639impl FrameRateTracker {
640 pub fn new(target_fps: f64) -> Self {
642 Self {
643 frame_times: VecDeque::new(),
644 max_samples: 120,
645 target_fps,
646 }
647 }
648
649 pub fn record_frame(&mut self, timestamp: f64) {
651 self.frame_times.push_back(timestamp);
652 if self.frame_times.len() > self.max_samples {
653 self.frame_times.pop_front();
654 }
655 }
656
657 pub fn current_fps(&self) -> f64 {
659 if self.frame_times.len() < 2 {
660 return 0.0;
661 }
662
663 let duration = self.frame_times.back().copied().unwrap_or(0.0)
664 - self.frame_times.front().copied().unwrap_or(0.0);
665
666 if duration > 0.0 {
667 ((self.frame_times.len() - 1) as f64 / duration) * 1000.0
668 } else {
669 0.0
670 }
671 }
672
673 pub fn stats(&self) -> FrameRateStats {
675 let fps = self.current_fps();
676 let tolerance = self.target_fps * 0.01;
678 let is_below_target = fps < (self.target_fps - tolerance);
679
680 let mut frame_deltas = Vec::new();
682 for i in 1..self.frame_times.len() {
683 let delta = self.frame_times[i] - self.frame_times[i - 1];
684 frame_deltas.push(delta);
685 }
686
687 let avg_frame_time = if !frame_deltas.is_empty() {
688 frame_deltas.iter().sum::<f64>() / frame_deltas.len() as f64
689 } else {
690 0.0
691 };
692
693 FrameRateStats {
694 current_fps: fps,
695 target_fps: self.target_fps,
696 average_frame_time_ms: avg_frame_time,
697 is_below_target,
698 frame_count: self.frame_times.len(),
699 }
700 }
701
702 pub fn clear(&mut self) {
704 self.frame_times.clear();
705 }
706}
707
708#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
710pub struct FrameRateStats {
711 pub current_fps: f64,
713 pub target_fps: f64,
715 pub average_frame_time_ms: f64,
717 pub is_below_target: bool,
719 pub frame_count: usize,
721}
722
723pub struct BottleneckDetector {
725 profiler: Profiler,
727 slow_threshold_ms: f64,
729}
730
731impl BottleneckDetector {
732 pub fn new(slow_threshold_ms: f64) -> Self {
734 Self {
735 profiler: Profiler::new(),
736 slow_threshold_ms,
737 }
738 }
739
740 pub fn record(&mut self, name: impl Into<String>, duration_ms: f64) {
742 self.profiler.record(name, duration_ms);
743 }
744
745 pub fn detect_bottlenecks(&self) -> Vec<Bottleneck> {
747 let mut bottlenecks = Vec::new();
748
749 for stats in self.profiler.all_counter_stats() {
750 if stats.average_ms > self.slow_threshold_ms {
751 bottlenecks.push(Bottleneck {
752 operation: stats.name.clone(),
753 average_ms: stats.average_ms,
754 p95_ms: stats.p95_ms,
755 count: stats.count,
756 severity: self.calculate_severity(stats.average_ms),
757 });
758 }
759 }
760
761 bottlenecks.sort_by(|a, b| {
763 b.severity
764 .partial_cmp(&a.severity)
765 .unwrap_or(std::cmp::Ordering::Equal)
766 });
767
768 bottlenecks
769 }
770
771 fn calculate_severity(&self, average_ms: f64) -> f64 {
773 average_ms / self.slow_threshold_ms
774 }
775
776 pub fn recommendations(&self) -> Vec<String> {
778 let bottlenecks = self.detect_bottlenecks();
779 let mut recommendations = Vec::new();
780
781 for bottleneck in bottlenecks {
782 if bottleneck.severity > 5.0 {
783 recommendations.push(format!(
784 "CRITICAL: '{}' is taking {:.2}ms on average ({}x threshold). Consider optimization or caching.",
785 bottleneck.operation, bottleneck.average_ms, bottleneck.severity as u32
786 ));
787 } else if bottleneck.severity > 2.0 {
788 recommendations.push(format!(
789 "WARNING: '{}' is taking {:.2}ms on average ({}x threshold). May benefit from optimization.",
790 bottleneck.operation, bottleneck.average_ms, bottleneck.severity as u32
791 ));
792 }
793 }
794
795 recommendations
796 }
797}
798
799#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct Bottleneck {
802 pub operation: String,
804 pub average_ms: f64,
806 pub p95_ms: f64,
808 pub count: u64,
810 pub severity: f64,
812}
813
814#[wasm_bindgen]
816pub struct WasmProfiler {
817 profiler: Profiler,
818}
819
820#[wasm_bindgen]
821impl WasmProfiler {
822 #[wasm_bindgen(constructor)]
824 pub fn new() -> Self {
825 Self {
826 profiler: Profiler::new(),
827 }
828 }
829
830 #[wasm_bindgen(js_name = startTimer)]
832 pub fn start_timer(&mut self, name: &str) {
833 let timestamp = js_sys::Date::now();
834 self.profiler.start_timer(name, timestamp);
835 }
836
837 #[wasm_bindgen(js_name = stopTimer)]
839 pub fn stop_timer(&mut self, name: &str) {
840 let timestamp = js_sys::Date::now();
841 self.profiler.stop_timer(name, timestamp);
842 }
843
844 #[wasm_bindgen]
846 pub fn record(&mut self, name: &str, duration_ms: f64) {
847 self.profiler.record(name, duration_ms);
848 }
849
850 #[wasm_bindgen(js_name = recordMemory)]
852 pub fn record_memory(&mut self) {
853 let timestamp = js_sys::Date::now();
854 self.profiler.record_memory(timestamp);
855 }
856
857 #[wasm_bindgen(js_name = getCounterStats)]
859 pub fn get_counter_stats(&self, name: &str) -> Option<String> {
860 self.profiler
861 .counter_stats(name)
862 .and_then(|stats| serde_json::to_string(&stats).ok())
863 }
864
865 #[wasm_bindgen(js_name = getAllStats)]
867 pub fn get_all_stats(&self) -> String {
868 let summary = self.profiler.summary();
869 serde_json::to_string(&summary).unwrap_or_default()
870 }
871
872 #[wasm_bindgen]
874 pub fn reset(&mut self) {
875 self.profiler.reset();
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 #[test]
884 fn test_performance_counter() {
885 let mut counter = PerformanceCounter::new("test");
886 counter.record(10.0);
887 counter.record(20.0);
888 counter.record(30.0);
889
890 assert_eq!(counter.count, 3);
891 assert_eq!(counter.average_ms(), 20.0);
892 assert_eq!(counter.min_time_ms, 10.0);
893 assert_eq!(counter.max_time_ms, 30.0);
894 }
895
896 #[test]
897 fn test_percentile() {
898 let mut counter = PerformanceCounter::new("test");
899 for i in 1..=100 {
900 counter.record(i as f64);
901 }
902
903 let p50 = counter.percentile(50.0);
904 assert!((49.0..=51.0).contains(&p50));
905
906 let p95 = counter.percentile(95.0);
907 assert!((94.0..=96.0).contains(&p95));
908 }
909
910 #[test]
911 fn test_profiler() {
912 let mut profiler = Profiler::new();
913 profiler.record("test", 10.0);
914 profiler.record("test", 20.0);
915
916 let stats = profiler
917 .counter_stats("test")
918 .expect("Counter should exist");
919 assert_eq!(stats.count, 2);
920 assert_eq!(stats.average_ms, 15.0);
921 }
922
923 #[test]
924 fn test_memory_monitor() {
925 let mut monitor = MemoryMonitor::new();
926 monitor.record(MemorySnapshot::new(0.0, 1000));
927 monitor.record(MemorySnapshot::new(1.0, 2000));
928 monitor.record(MemorySnapshot::new(2.0, 1500));
929
930 let stats = monitor.stats();
931 assert_eq!(stats.current_heap_used, 1500);
932 assert_eq!(stats.peak_heap_used, 2000);
933 assert_eq!(stats.average_heap_used, 1500.0);
934 }
935
936 #[test]
937 fn test_frame_rate_tracker() {
938 let mut tracker = FrameRateTracker::new(60.0);
939
940 for i in 0..120 {
942 tracker.record_frame((i as f64) * 16.67);
943 }
944
945 let fps = tracker.current_fps();
946 assert!(fps > 55.0 && fps < 65.0);
947 }
948
949 #[test]
950 fn test_bottleneck_detector() {
951 let mut detector = BottleneckDetector::new(10.0);
952 detector.record("fast_op", 5.0);
953 detector.record("slow_op", 50.0);
954 detector.record("slow_op", 60.0);
955
956 let bottlenecks = detector.detect_bottlenecks();
957 assert_eq!(bottlenecks.len(), 1);
958 assert_eq!(bottlenecks[0].operation, "slow_op");
959 assert!(bottlenecks[0].severity > 5.0);
960 }
961}