Skip to main content

oxigdal_wasm/
profiler.rs

1//! Performance profiling and monitoring utilities
2//!
3//! This module provides comprehensive performance tracking, memory monitoring,
4//! timing utilities, frame rate analysis, and bottleneck detection for WASM applications.
5//!
6//! # Overview
7//!
8//! The profiler module enables deep performance analysis for browser-based geospatial applications:
9//!
10//! - **Performance Counters**: High-precision timing with percentile statistics
11//! - **Memory Monitoring**: Heap usage tracking and leak detection
12//! - **Frame Rate Tracking**: FPS monitoring for smooth animations
13//! - **Bottleneck Detection**: Automatic identification of slow operations
14//! - **Scoped Timing**: RAII-style timing for automatic measurement
15//! - **Statistical Analysis**: Min, max, mean, p50, p95, p99 metrics
16//!
17//! # Why Profile WASM Applications?
18//!
19//! WASM performance can vary significantly across:
20//! - Different browsers (Chrome, Firefox, Safari, Edge)
21//! - Different devices (desktop, mobile, tablet)
22//! - Different network conditions (fast, slow, intermittent)
23//! - Different data sizes (small tiles vs large images)
24//!
25//! Profiling helps identify:
26//! 1. Operations that are too slow for 60 FPS rendering
27//! 2. Memory leaks or excessive allocations
28//! 3. Network bottlenecks vs computation bottlenecks
29//! 4. Browser-specific performance issues
30//!
31//! # Performance Counter Usage
32//!
33//! Performance counters track operation timing with statistical aggregation:
34//!
35//! ```rust
36//! use oxigdal_wasm::{Profiler, PerformanceCounter};
37//!
38//! let mut profiler = Profiler::new();
39//!
40//! // Record multiple samples
41//! for i in 0..100 {
42//!     profiler.record("tile_decode", i as f64 * 0.1);
43//! }
44//!
45//! // Analyze statistics
46//! if let Some(stats) = profiler.counter_stats("tile_decode") {
47//!     println!("Average: {:.2}ms", stats.average_ms);
48//!     println!("P95: {:.2}ms", stats.p95_ms);
49//!     println!("P99: {:.2}ms", stats.p99_ms);
50//! }
51//! ```
52//!
53//! # Memory Monitoring
54//!
55//! Track heap usage to detect memory leaks:
56//!
57//! ```ignore
58//! use oxigdal_wasm::profiler::MemoryMonitor;
59//!
60//! let mut monitor = MemoryMonitor::new();
61//!
62//! // Record snapshots periodically
63//! setInterval(|| {
64//!     monitor.record_current(js_sys::Date::now());
65//!
66//!     let stats = monitor.stats();
67//!     if stats.current_heap_used > stats.peak_heap_used * 0.9 {
68//!         console.warn("Memory usage near peak!");
69//!     }
70//! }, 1000);
71//! ```
72//!
73//! # Frame Rate Tracking
74//!
75//! Monitor frame rate for smooth animations:
76//!
77//! ```ignore
78//! use oxigdal_wasm::profiler::FrameRateTracker;
79//!
80//! let mut tracker = FrameRateTracker::new(60.0); // Target 60 FPS
81//!
82//! // In animation loop
83//! requestAnimationFrame(|timestamp| {
84//!     tracker.record_frame(timestamp);
85//!
86//!     let stats = tracker.stats();
87//!     if stats.is_below_target {
88//!         console.warn("FPS dropped to {}", stats.current_fps);
89//!         // Reduce quality or skip frame
90//!     }
91//! });
92//! ```
93//!
94//! # Bottleneck Detection
95//!
96//! Automatically identify slow operations:
97//!
98//! ```rust
99//! use oxigdal_wasm::BottleneckDetector;
100//!
101//! let mut detector = BottleneckDetector::new(10.0); // 10ms threshold
102//!
103//! // Record operations
104//! detector.record("fetch_tile", 5.0);   // Fast - no bottleneck
105//! detector.record("decode_tile", 25.0); // Slow - bottleneck!
106//! detector.record("render_tile", 50.0); // Very slow - critical!
107//!
108//! // Get recommendations
109//! for recommendation in detector.recommendations() {
110//!     println!("{}", recommendation);
111//! }
112//! // Output:
113//! // "CRITICAL: 'render_tile' is taking 50.00ms on average (5x threshold)"
114//! // "WARNING: 'decode_tile' is taking 25.00ms on average (2x threshold)"
115//! ```
116//!
117//! # Percentile Statistics
118//!
119//! Understanding percentiles is crucial for performance analysis:
120//!
121//! - **P50 (Median)**: Half of operations are faster, half are slower
122//! - **P95**: 95% of operations are faster (identifies slow outliers)
123//! - **P99**: 99% of operations are faster (identifies rare worst cases)
124//!
125//! Example interpretation:
126//! ```text
127//! Operation: tile_decode
128//! Average: 10ms    <- Mean time
129//! P50: 8ms         <- Typical case
130//! P95: 20ms        <- Slow but not rare
131//! P99: 50ms        <- Rare worst case
132//! ```
133//!
134//! If P99 is much higher than P95, there are occasional slow outliers.
135//!
136//! # Performance Budgets
137//!
138//! For 60 FPS rendering, each frame has ~16.67ms budget:
139//!
140//! ```text
141//! Operation          Budget    Typical    Status
142//! ─────────────────────────────────────────────
143//! Tile fetch         8ms       5ms        ✓ OK
144//! Tile decode        4ms       3ms        ✓ OK
145//! Tile render        3ms       2ms        ✓ OK
146//! Cache lookup       0.5ms     0.1ms      ✓ OK
147//! Frame overhead     1.5ms     1ms        ✓ OK
148//! ─────────────────────────────────────────────
149//! Total             17ms       11ms       ✓ OK
150//! ```
151//!
152//! If total exceeds 16.67ms, frame rate drops below 60 FPS.
153//!
154//! # Example: Complete Profiling Setup
155//!
156//! ```ignore
157//! use oxigdal_wasm::profiler::{Profiler, FrameRateTracker, BottleneckDetector};
158//!
159//! // Create profiler
160//! let mut profiler = Profiler::new();
161//! let mut fps_tracker = FrameRateTracker::new(60.0);
162//! let mut bottleneck = BottleneckDetector::new(10.0);
163//!
164//! // Profile tile loading
165//! async fn load_and_profile(url: &str) {
166//!     let start = js_sys::Date::now();
167//!
168//!     // Fetch tile
169//!     let fetch_start = start;
170//!     let tile_data = fetch_tile(url).await;
171//!     let fetch_time = js_sys::Date::now() - fetch_start;
172//!     profiler.record("fetch", fetch_time);
173//!
174//!     // Decode tile
175//!     let decode_start = js_sys::Date::now();
176//!     let decoded = decode_tile(&tile_data);
177//!     let decode_time = js_sys::Date::now() - decode_start;
178//!     profiler.record("decode", decode_time);
179//!
180//!     // Render tile
181//!     let render_start = js_sys::Date::now();
182//!     render_to_canvas(&decoded);
183//!     let render_time = js_sys::Date::now() - render_start;
184//!     profiler.record("render", render_time);
185//!
186//!     // Total time
187//!     let total_time = js_sys::Date::now() - start;
188//!     profiler.record("total", total_time);
189//!
190//!     // Check for bottlenecks
191//!     bottleneck.record("fetch", fetch_time);
192//!     bottleneck.record("decode", decode_time);
193//!     bottleneck.record("render", render_time);
194//! }
195//!
196//! // In animation loop
197//! requestAnimationFrame(|timestamp| {
198//!     fps_tracker.record_frame(timestamp);
199//!
200//!     // Check performance periodically
201//!     if frame_count % 60 == 0 {
202//!         let summary = profiler.summary();
203//!         console.log("Performance Report:");
204//!         for counter in summary.counters {
205//!             console.log("  {}: {:.2}ms avg", counter.name, counter.average_ms);
206//!         }
207//!
208//!         let fps = fps_tracker.stats();
209//!         console.log("FPS: {:.1}", fps.current_fps);
210//!
211//!         // Check for bottlenecks
212//!         for bottleneck in bottleneck.detect_bottlenecks() {
213//!             console.warn("Bottleneck: {} ({:.2}ms)",
214//!                 bottleneck.operation,
215//!                 bottleneck.average_ms
216//!             );
217//!         }
218//!     }
219//! });
220//! ```
221
222use serde::{Deserialize, Serialize};
223use std::collections::{HashMap, VecDeque};
224use wasm_bindgen::prelude::*;
225
226/// Maximum number of timing samples to keep
227pub const MAX_TIMING_SAMPLES: usize = 1000;
228
229/// Maximum number of memory samples to keep
230pub const MAX_MEMORY_SAMPLES: usize = 100;
231
232/// Performance counter
233#[derive(Debug, Clone)]
234pub struct PerformanceCounter {
235    /// Counter name
236    name: String,
237    /// Total count
238    count: u64,
239    /// Total time in milliseconds
240    total_time_ms: f64,
241    /// Minimum time
242    min_time_ms: f64,
243    /// Maximum time
244    max_time_ms: f64,
245    /// Recent samples
246    samples: VecDeque<f64>,
247}
248
249impl PerformanceCounter {
250    /// Creates a new performance counter
251    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    /// Records a timing sample
263    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    /// Returns the average time
276    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    /// Returns the recent average (last 100 samples)
285    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    /// Returns the percentile
296    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    /// Returns statistics
309    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    /// Resets the counter
333    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/// Counter statistics
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct CounterStats {
345    /// Counter name
346    pub name: String,
347    /// Total count
348    pub count: u64,
349    /// Total time
350    pub total_time_ms: f64,
351    /// Average time
352    pub average_ms: f64,
353    /// Recent average time
354    pub recent_average_ms: f64,
355    /// Minimum time
356    pub min_ms: f64,
357    /// Maximum time
358    pub max_ms: f64,
359    /// 50th percentile
360    pub p50_ms: f64,
361    /// 95th percentile
362    pub p95_ms: f64,
363    /// 99th percentile
364    pub p99_ms: f64,
365}
366
367/// Memory snapshot
368#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
369pub struct MemorySnapshot {
370    /// Timestamp
371    pub timestamp: f64,
372    /// Heap used in bytes
373    pub heap_used: usize,
374    /// Heap limit in bytes (if available)
375    pub heap_limit: Option<usize>,
376    /// External memory in bytes (if available)
377    pub external_memory: Option<usize>,
378}
379
380impl MemorySnapshot {
381    /// Creates a new memory snapshot
382    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    /// Returns heap utilization as a fraction (0.0 to 1.0)
392    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
403/// Memory monitor
404pub struct MemoryMonitor {
405    /// Memory snapshots
406    snapshots: VecDeque<MemorySnapshot>,
407    /// Maximum snapshots to keep
408    max_snapshots: usize,
409}
410
411impl MemoryMonitor {
412    /// Creates a new memory monitor
413    pub fn new() -> Self {
414        Self {
415            snapshots: VecDeque::new(),
416            max_snapshots: MAX_MEMORY_SAMPLES,
417        }
418    }
419
420    /// Records a memory snapshot
421    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    /// Records current memory usage
429    pub fn record_current(&mut self, timestamp: f64) {
430        // In WASM, we can't easily get memory info without performance.memory API
431        // For now, use a placeholder
432        let snapshot = MemorySnapshot::new(timestamp, 0);
433        self.record(snapshot);
434    }
435
436    /// Returns the latest snapshot
437    pub fn latest(&self) -> Option<&MemorySnapshot> {
438        self.snapshots.back()
439    }
440
441    /// Returns memory statistics
442    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    /// Clears all snapshots
471    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/// Memory statistics
483#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
484pub struct MemoryStats {
485    /// Current heap usage in bytes
486    pub current_heap_used: usize,
487    /// Peak heap usage in bytes
488    pub peak_heap_used: usize,
489    /// Average heap usage in bytes
490    pub average_heap_used: f64,
491    /// Number of samples
492    pub sample_count: usize,
493}
494
495/// Performance profiler
496pub struct Profiler {
497    /// Performance counters
498    counters: HashMap<String, PerformanceCounter>,
499    /// Memory monitor
500    memory: MemoryMonitor,
501    /// Active timers (start times)
502    active_timers: HashMap<String, f64>,
503}
504
505impl Profiler {
506    /// Creates a new profiler
507    pub fn new() -> Self {
508        Self {
509            counters: HashMap::new(),
510            memory: MemoryMonitor::new(),
511            active_timers: HashMap::new(),
512        }
513    }
514
515    /// Starts a timer
516    pub fn start_timer(&mut self, name: impl Into<String>, timestamp: f64) {
517        self.active_timers.insert(name.into(), timestamp);
518    }
519
520    /// Stops a timer and records the duration
521    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    /// Records a timing sample
530    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    /// Records current memory usage
539    pub fn record_memory(&mut self, timestamp: f64) {
540        self.memory.record_current(timestamp);
541    }
542
543    /// Returns counter statistics
544    pub fn counter_stats(&self, name: &str) -> Option<CounterStats> {
545        self.counters.get(name).map(|c| c.stats())
546    }
547
548    /// Returns all counter statistics
549    pub fn all_counter_stats(&self) -> Vec<CounterStats> {
550        self.counters.values().map(|c| c.stats()).collect()
551    }
552
553    /// Returns memory statistics
554    pub fn memory_stats(&self) -> MemoryStats {
555        self.memory.stats()
556    }
557
558    /// Returns a summary report
559    pub fn summary(&self) -> ProfilerSummary {
560        ProfilerSummary {
561            counters: self.all_counter_stats(),
562            memory: self.memory_stats(),
563        }
564    }
565
566    /// Resets all counters
567    pub fn reset(&mut self) {
568        self.counters.clear();
569        self.memory.clear();
570        self.active_timers.clear();
571    }
572
573    /// Clears a specific counter
574    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/// Profiler summary
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct ProfilerSummary {
588    /// Counter statistics
589    pub counters: Vec<CounterStats>,
590    /// Memory statistics
591    pub memory: MemoryStats,
592}
593
594/// Scoped timer for automatic timing
595#[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    /// Creates a new scoped timer
605    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    /// Returns the elapsed time
616    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        // Get current time (in a real WASM environment, use js_sys::Date::now())
624        let current_time = self.start_time; // Placeholder
625        self.profiler.stop_timer(self.name.clone(), current_time);
626    }
627}
628
629/// Frame rate tracker
630pub struct FrameRateTracker {
631    /// Frame timestamps
632    frame_times: VecDeque<f64>,
633    /// Maximum samples
634    max_samples: usize,
635    /// Target FPS
636    target_fps: f64,
637}
638
639impl FrameRateTracker {
640    /// Creates a new frame rate tracker
641    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    /// Records a frame
650    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    /// Returns the current FPS
658    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    /// Returns frame statistics
674    pub fn stats(&self) -> FrameRateStats {
675        let fps = self.current_fps();
676        // Use 1% tolerance to account for floating point precision
677        let tolerance = self.target_fps * 0.01;
678        let is_below_target = fps < (self.target_fps - tolerance);
679
680        // Calculate frame time variance
681        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    /// Clears all frame data
703    pub fn clear(&mut self) {
704        self.frame_times.clear();
705    }
706}
707
708/// Frame rate statistics
709#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
710pub struct FrameRateStats {
711    /// Current FPS
712    pub current_fps: f64,
713    /// Target FPS
714    pub target_fps: f64,
715    /// Average frame time in milliseconds
716    pub average_frame_time_ms: f64,
717    /// Whether below target
718    pub is_below_target: bool,
719    /// Number of frames sampled
720    pub frame_count: usize,
721}
722
723/// Bottleneck detector
724pub struct BottleneckDetector {
725    /// Profiler reference
726    profiler: Profiler,
727    /// Threshold for slow operations (milliseconds)
728    slow_threshold_ms: f64,
729}
730
731impl BottleneckDetector {
732    /// Creates a new bottleneck detector
733    pub fn new(slow_threshold_ms: f64) -> Self {
734        Self {
735            profiler: Profiler::new(),
736            slow_threshold_ms,
737        }
738    }
739
740    /// Records a timing sample
741    pub fn record(&mut self, name: impl Into<String>, duration_ms: f64) {
742        self.profiler.record(name, duration_ms);
743    }
744
745    /// Detects bottlenecks
746    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        // Sort by severity
762        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    /// Calculates bottleneck severity
772    fn calculate_severity(&self, average_ms: f64) -> f64 {
773        average_ms / self.slow_threshold_ms
774    }
775
776    /// Returns recommendations
777    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/// Bottleneck information
800#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct Bottleneck {
802    /// Operation name
803    pub operation: String,
804    /// Average time
805    pub average_ms: f64,
806    /// 95th percentile time
807    pub p95_ms: f64,
808    /// Call count
809    pub count: u64,
810    /// Severity score
811    pub severity: f64,
812}
813
814/// WASM bindings for profiler
815#[wasm_bindgen]
816pub struct WasmProfiler {
817    profiler: Profiler,
818}
819
820#[wasm_bindgen]
821impl WasmProfiler {
822    /// Creates a new profiler
823    #[wasm_bindgen(constructor)]
824    pub fn new() -> Self {
825        Self {
826            profiler: Profiler::new(),
827        }
828    }
829
830    /// Starts a timer
831    #[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    /// Stops a timer
838    #[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    /// Records a timing sample
845    #[wasm_bindgen]
846    pub fn record(&mut self, name: &str, duration_ms: f64) {
847        self.profiler.record(name, duration_ms);
848    }
849
850    /// Records current memory usage
851    #[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    /// Returns counter statistics as JSON
858    #[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    /// Returns all statistics as JSON
866    #[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    /// Resets all counters
873    #[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        // Simulate 60 FPS (16.67ms per frame)
941        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}