use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use wasm_bindgen::prelude::*;
pub const MAX_TIMING_SAMPLES: usize = 1000;
pub const MAX_MEMORY_SAMPLES: usize = 100;
#[derive(Debug, Clone)]
pub struct PerformanceCounter {
name: String,
count: u64,
total_time_ms: f64,
min_time_ms: f64,
max_time_ms: f64,
samples: VecDeque<f64>,
}
impl PerformanceCounter {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
count: 0,
total_time_ms: 0.0,
min_time_ms: f64::MAX,
max_time_ms: f64::MIN,
samples: VecDeque::new(),
}
}
pub fn record(&mut self, duration_ms: f64) {
self.count += 1;
self.total_time_ms += duration_ms;
self.min_time_ms = self.min_time_ms.min(duration_ms);
self.max_time_ms = self.max_time_ms.max(duration_ms);
self.samples.push_back(duration_ms);
if self.samples.len() > MAX_TIMING_SAMPLES {
self.samples.pop_front();
}
}
pub fn average_ms(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.total_time_ms / self.count as f64
}
}
pub fn recent_average_ms(&self) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
let recent: Vec<_> = self.samples.iter().rev().take(100).collect();
let sum: f64 = recent.iter().copied().sum();
sum / recent.len() as f64
}
pub fn percentile(&self, p: f64) -> f64 {
if self.samples.is_empty() {
return 0.0;
}
let mut sorted: Vec<_> = self.samples.iter().copied().collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((p / 100.0) * sorted.len() as f64) as usize;
sorted[idx.min(sorted.len() - 1)]
}
pub fn stats(&self) -> CounterStats {
CounterStats {
name: self.name.clone(),
count: self.count,
total_time_ms: self.total_time_ms,
average_ms: self.average_ms(),
recent_average_ms: self.recent_average_ms(),
min_ms: if self.count > 0 {
self.min_time_ms
} else {
0.0
},
max_ms: if self.count > 0 {
self.max_time_ms
} else {
0.0
},
p50_ms: self.percentile(50.0),
p95_ms: self.percentile(95.0),
p99_ms: self.percentile(99.0),
}
}
pub fn reset(&mut self) {
self.count = 0;
self.total_time_ms = 0.0;
self.min_time_ms = f64::MAX;
self.max_time_ms = f64::MIN;
self.samples.clear();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CounterStats {
pub name: String,
pub count: u64,
pub total_time_ms: f64,
pub average_ms: f64,
pub recent_average_ms: f64,
pub min_ms: f64,
pub max_ms: f64,
pub p50_ms: f64,
pub p95_ms: f64,
pub p99_ms: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct MemorySnapshot {
pub timestamp: f64,
pub heap_used: usize,
pub heap_limit: Option<usize>,
pub external_memory: Option<usize>,
}
impl MemorySnapshot {
pub const fn new(timestamp: f64, heap_used: usize) -> Self {
Self {
timestamp,
heap_used,
heap_limit: None,
external_memory: None,
}
}
pub fn heap_utilization(&self) -> Option<f64> {
self.heap_limit.map(|limit| {
if limit > 0 {
self.heap_used as f64 / limit as f64
} else {
0.0
}
})
}
}
pub struct MemoryMonitor {
snapshots: VecDeque<MemorySnapshot>,
max_snapshots: usize,
}
impl MemoryMonitor {
pub fn new() -> Self {
Self {
snapshots: VecDeque::new(),
max_snapshots: MAX_MEMORY_SAMPLES,
}
}
pub fn record(&mut self, snapshot: MemorySnapshot) {
self.snapshots.push_back(snapshot);
if self.snapshots.len() > self.max_snapshots {
self.snapshots.pop_front();
}
}
pub fn record_current(&mut self, timestamp: f64) {
let snapshot = MemorySnapshot::new(timestamp, 0);
self.record(snapshot);
}
pub fn latest(&self) -> Option<&MemorySnapshot> {
self.snapshots.back()
}
pub fn stats(&self) -> MemoryStats {
if self.snapshots.is_empty() {
return MemoryStats {
current_heap_used: 0,
peak_heap_used: 0,
average_heap_used: 0.0,
sample_count: 0,
};
}
let current = self.snapshots.back().map(|s| s.heap_used).unwrap_or(0);
let peak = self
.snapshots
.iter()
.map(|s| s.heap_used)
.max()
.unwrap_or(0);
let sum: usize = self.snapshots.iter().map(|s| s.heap_used).sum();
let average = sum as f64 / self.snapshots.len() as f64;
MemoryStats {
current_heap_used: current,
peak_heap_used: peak,
average_heap_used: average,
sample_count: self.snapshots.len(),
}
}
pub fn clear(&mut self) {
self.snapshots.clear();
}
}
impl Default for MemoryMonitor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct MemoryStats {
pub current_heap_used: usize,
pub peak_heap_used: usize,
pub average_heap_used: f64,
pub sample_count: usize,
}
pub struct Profiler {
counters: HashMap<String, PerformanceCounter>,
memory: MemoryMonitor,
active_timers: HashMap<String, f64>,
}
impl Profiler {
pub fn new() -> Self {
Self {
counters: HashMap::new(),
memory: MemoryMonitor::new(),
active_timers: HashMap::new(),
}
}
pub fn start_timer(&mut self, name: impl Into<String>, timestamp: f64) {
self.active_timers.insert(name.into(), timestamp);
}
pub fn stop_timer(&mut self, name: impl Into<String>, timestamp: f64) {
let name = name.into();
if let Some(start) = self.active_timers.remove(&name) {
let duration = timestamp - start;
self.record(name, duration);
}
}
pub fn record(&mut self, name: impl Into<String>, duration_ms: f64) {
let name = name.into();
self.counters
.entry(name.clone())
.or_insert_with(|| PerformanceCounter::new(name))
.record(duration_ms);
}
pub fn record_memory(&mut self, timestamp: f64) {
self.memory.record_current(timestamp);
}
pub fn counter_stats(&self, name: &str) -> Option<CounterStats> {
self.counters.get(name).map(|c| c.stats())
}
pub fn all_counter_stats(&self) -> Vec<CounterStats> {
self.counters.values().map(|c| c.stats()).collect()
}
pub fn memory_stats(&self) -> MemoryStats {
self.memory.stats()
}
pub fn summary(&self) -> ProfilerSummary {
ProfilerSummary {
counters: self.all_counter_stats(),
memory: self.memory_stats(),
}
}
pub fn reset(&mut self) {
self.counters.clear();
self.memory.clear();
self.active_timers.clear();
}
pub fn clear_counter(&mut self, name: &str) {
self.counters.remove(name);
}
}
impl Default for Profiler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfilerSummary {
pub counters: Vec<CounterStats>,
pub memory: MemoryStats,
}
#[allow(dead_code)]
pub struct ScopedTimer<'a> {
profiler: &'a mut Profiler,
name: String,
start_time: f64,
}
#[allow(dead_code)]
impl<'a> ScopedTimer<'a> {
pub fn new(profiler: &'a mut Profiler, name: impl Into<String>, start_time: f64) -> Self {
let name = name.into();
profiler.start_timer(name.clone(), start_time);
Self {
profiler,
name,
start_time,
}
}
pub fn elapsed(&self, current_time: f64) -> f64 {
current_time - self.start_time
}
}
impl<'a> Drop for ScopedTimer<'a> {
fn drop(&mut self) {
let current_time = self.start_time; self.profiler.stop_timer(self.name.clone(), current_time);
}
}
pub struct FrameRateTracker {
frame_times: VecDeque<f64>,
max_samples: usize,
target_fps: f64,
}
impl FrameRateTracker {
pub fn new(target_fps: f64) -> Self {
Self {
frame_times: VecDeque::new(),
max_samples: 120,
target_fps,
}
}
pub fn record_frame(&mut self, timestamp: f64) {
self.frame_times.push_back(timestamp);
if self.frame_times.len() > self.max_samples {
self.frame_times.pop_front();
}
}
pub fn current_fps(&self) -> f64 {
if self.frame_times.len() < 2 {
return 0.0;
}
let duration = self.frame_times.back().copied().unwrap_or(0.0)
- self.frame_times.front().copied().unwrap_or(0.0);
if duration > 0.0 {
((self.frame_times.len() - 1) as f64 / duration) * 1000.0
} else {
0.0
}
}
pub fn stats(&self) -> FrameRateStats {
let fps = self.current_fps();
let tolerance = self.target_fps * 0.01;
let is_below_target = fps < (self.target_fps - tolerance);
let mut frame_deltas = Vec::new();
for i in 1..self.frame_times.len() {
let delta = self.frame_times[i] - self.frame_times[i - 1];
frame_deltas.push(delta);
}
let avg_frame_time = if !frame_deltas.is_empty() {
frame_deltas.iter().sum::<f64>() / frame_deltas.len() as f64
} else {
0.0
};
FrameRateStats {
current_fps: fps,
target_fps: self.target_fps,
average_frame_time_ms: avg_frame_time,
is_below_target,
frame_count: self.frame_times.len(),
}
}
pub fn clear(&mut self) {
self.frame_times.clear();
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct FrameRateStats {
pub current_fps: f64,
pub target_fps: f64,
pub average_frame_time_ms: f64,
pub is_below_target: bool,
pub frame_count: usize,
}
pub struct BottleneckDetector {
profiler: Profiler,
slow_threshold_ms: f64,
}
impl BottleneckDetector {
pub fn new(slow_threshold_ms: f64) -> Self {
Self {
profiler: Profiler::new(),
slow_threshold_ms,
}
}
pub fn record(&mut self, name: impl Into<String>, duration_ms: f64) {
self.profiler.record(name, duration_ms);
}
pub fn detect_bottlenecks(&self) -> Vec<Bottleneck> {
let mut bottlenecks = Vec::new();
for stats in self.profiler.all_counter_stats() {
if stats.average_ms > self.slow_threshold_ms {
bottlenecks.push(Bottleneck {
operation: stats.name.clone(),
average_ms: stats.average_ms,
p95_ms: stats.p95_ms,
count: stats.count,
severity: self.calculate_severity(stats.average_ms),
});
}
}
bottlenecks.sort_by(|a, b| {
b.severity
.partial_cmp(&a.severity)
.unwrap_or(std::cmp::Ordering::Equal)
});
bottlenecks
}
fn calculate_severity(&self, average_ms: f64) -> f64 {
average_ms / self.slow_threshold_ms
}
pub fn recommendations(&self) -> Vec<String> {
let bottlenecks = self.detect_bottlenecks();
let mut recommendations = Vec::new();
for bottleneck in bottlenecks {
if bottleneck.severity > 5.0 {
recommendations.push(format!(
"CRITICAL: '{}' is taking {:.2}ms on average ({}x threshold). Consider optimization or caching.",
bottleneck.operation, bottleneck.average_ms, bottleneck.severity as u32
));
} else if bottleneck.severity > 2.0 {
recommendations.push(format!(
"WARNING: '{}' is taking {:.2}ms on average ({}x threshold). May benefit from optimization.",
bottleneck.operation, bottleneck.average_ms, bottleneck.severity as u32
));
}
}
recommendations
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bottleneck {
pub operation: String,
pub average_ms: f64,
pub p95_ms: f64,
pub count: u64,
pub severity: f64,
}
#[wasm_bindgen]
pub struct WasmProfiler {
profiler: Profiler,
}
#[wasm_bindgen]
impl WasmProfiler {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
profiler: Profiler::new(),
}
}
#[wasm_bindgen(js_name = startTimer)]
pub fn start_timer(&mut self, name: &str) {
let timestamp = js_sys::Date::now();
self.profiler.start_timer(name, timestamp);
}
#[wasm_bindgen(js_name = stopTimer)]
pub fn stop_timer(&mut self, name: &str) {
let timestamp = js_sys::Date::now();
self.profiler.stop_timer(name, timestamp);
}
#[wasm_bindgen]
pub fn record(&mut self, name: &str, duration_ms: f64) {
self.profiler.record(name, duration_ms);
}
#[wasm_bindgen(js_name = recordMemory)]
pub fn record_memory(&mut self) {
let timestamp = js_sys::Date::now();
self.profiler.record_memory(timestamp);
}
#[wasm_bindgen(js_name = getCounterStats)]
pub fn get_counter_stats(&self, name: &str) -> Option<String> {
self.profiler
.counter_stats(name)
.and_then(|stats| serde_json::to_string(&stats).ok())
}
#[wasm_bindgen(js_name = getAllStats)]
pub fn get_all_stats(&self) -> String {
let summary = self.profiler.summary();
serde_json::to_string(&summary).unwrap_or_default()
}
#[wasm_bindgen]
pub fn reset(&mut self) {
self.profiler.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_performance_counter() {
let mut counter = PerformanceCounter::new("test");
counter.record(10.0);
counter.record(20.0);
counter.record(30.0);
assert_eq!(counter.count, 3);
assert_eq!(counter.average_ms(), 20.0);
assert_eq!(counter.min_time_ms, 10.0);
assert_eq!(counter.max_time_ms, 30.0);
}
#[test]
fn test_percentile() {
let mut counter = PerformanceCounter::new("test");
for i in 1..=100 {
counter.record(i as f64);
}
let p50 = counter.percentile(50.0);
assert!((49.0..=51.0).contains(&p50));
let p95 = counter.percentile(95.0);
assert!((94.0..=96.0).contains(&p95));
}
#[test]
fn test_profiler() {
let mut profiler = Profiler::new();
profiler.record("test", 10.0);
profiler.record("test", 20.0);
let stats = profiler
.counter_stats("test")
.expect("Counter should exist");
assert_eq!(stats.count, 2);
assert_eq!(stats.average_ms, 15.0);
}
#[test]
fn test_memory_monitor() {
let mut monitor = MemoryMonitor::new();
monitor.record(MemorySnapshot::new(0.0, 1000));
monitor.record(MemorySnapshot::new(1.0, 2000));
monitor.record(MemorySnapshot::new(2.0, 1500));
let stats = monitor.stats();
assert_eq!(stats.current_heap_used, 1500);
assert_eq!(stats.peak_heap_used, 2000);
assert_eq!(stats.average_heap_used, 1500.0);
}
#[test]
fn test_frame_rate_tracker() {
let mut tracker = FrameRateTracker::new(60.0);
for i in 0..120 {
tracker.record_frame((i as f64) * 16.67);
}
let fps = tracker.current_fps();
assert!(fps > 55.0 && fps < 65.0);
}
#[test]
fn test_bottleneck_detector() {
let mut detector = BottleneckDetector::new(10.0);
detector.record("fast_op", 5.0);
detector.record("slow_op", 50.0);
detector.record("slow_op", 60.0);
let bottlenecks = detector.detect_bottlenecks();
assert_eq!(bottlenecks.len(), 1);
assert_eq!(bottlenecks[0].operation, "slow_op");
assert!(bottlenecks[0].severity > 5.0);
}
}