use alloc::{vec::Vec, string::String};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg(not(target_arch = "wasm32"))]
mod native_timing {
use std::time::Instant;
pub fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}
pub fn elapsed_since(start: u64) -> u64 {
now().saturating_sub(start)
}
}
#[cfg(target_arch = "wasm32")]
mod wasm_timing {
#[cfg(feature = "wasm")]
use web_sys::window;
pub fn now() -> u64 {
#[cfg(feature = "wasm")]
{
window()
.and_then(|w| w.performance())
.map(|p| (p.now() * 1_000_000.0) as u64) .unwrap_or(0)
}
#[cfg(not(feature = "wasm"))]
{
0 }
}
pub fn elapsed_since(start: u64) -> u64 {
now().saturating_sub(start)
}
}
#[cfg(not(target_arch = "wasm32"))]
use native_timing::*;
#[cfg(target_arch = "wasm32")]
use wasm_timing::*;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TimingHistogram {
buckets: Vec<u64>,
counts: Vec<u64>,
total_samples: u64,
sum_ns: u64,
sum_squares: f64,
}
impl TimingHistogram {
pub fn new() -> Self {
let mut buckets = Vec::new();
let mut value = 1000; while value <= 1_000_000_000 { buckets.push(value);
value = (value as f64 * 1.5) as u64; }
buckets.push(u64::MAX);
let bucket_count = buckets.len();
Self {
buckets,
counts: vec![0; bucket_count],
total_samples: 0,
sum_ns: 0,
sum_squares: 0.0,
}
}
pub fn with_buckets(mut buckets: Vec<u64>) -> Self {
buckets.sort_unstable();
if buckets.last() != Some(&u64::MAX) {
buckets.push(u64::MAX);
}
let bucket_count = buckets.len();
Self {
buckets,
counts: vec![0; bucket_count],
total_samples: 0,
sum_ns: 0,
sum_squares: 0.0,
}
}
pub fn record(&mut self, duration_ns: u64) {
self.total_samples += 1;
self.sum_ns += duration_ns;
self.sum_squares += (duration_ns as f64).powi(2);
for (i, &bucket_limit) in self.buckets.iter().enumerate() {
if duration_ns <= bucket_limit {
self.counts[i] += 1;
break;
}
}
}
pub fn mean(&self) -> f64 {
if self.total_samples == 0 {
0.0
} else {
self.sum_ns as f64 / self.total_samples as f64
}
}
pub fn std_dev(&self) -> f64 {
if self.total_samples <= 1 {
0.0
} else {
let mean = self.mean();
let variance = (self.sum_squares / self.total_samples as f64) - mean.powi(2);
variance.max(0.0).sqrt()
}
}
pub fn percentile(&self, p: f64) -> u64 {
if self.total_samples == 0 {
return 0;
}
let target_count = (self.total_samples as f64 * p) as u64;
let mut cumulative = 0;
for (i, &count) in self.counts.iter().enumerate() {
cumulative += count;
if cumulative >= target_count {
return self.buckets[i];
}
}
self.buckets[self.buckets.len() - 1]
}
pub fn p50(&self) -> u64 { self.percentile(0.5) }
pub fn p95(&self) -> u64 { self.percentile(0.95) }
pub fn p99(&self) -> u64 { self.percentile(0.99) }
pub fn count(&self) -> u64 { self.total_samples }
pub fn buckets(&self) -> &[u64] { &self.buckets }
pub fn counts(&self) -> &[u64] { &self.counts }
pub fn reset(&mut self) {
self.counts.fill(0);
self.total_samples = 0;
self.sum_ns = 0;
self.sum_squares = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct Timer {
start_time: u64,
label: String,
}
impl Timer {
pub fn start(label: String) -> Self {
Self {
start_time: now(),
label,
}
}
pub fn elapsed_ns(&self) -> u64 {
elapsed_since(self.start_time)
}
pub fn elapsed_us(&self) -> f64 {
self.elapsed_ns() as f64 / 1_000.0
}
pub fn elapsed_ms(&self) -> f64 {
self.elapsed_ns() as f64 / 1_000_000.0
}
pub fn elapsed_s(&self) -> f64 {
self.elapsed_ns() as f64 / 1_000_000_000.0
}
pub fn stop(self) -> TimingInfo {
let elapsed = self.elapsed_ns();
TimingInfo {
label: self.label,
elapsed_ns: elapsed,
}
}
pub fn label(&self) -> &str {
&self.label
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TimingInfo {
pub label: String,
pub elapsed_ns: u64,
}
impl TimingInfo {
pub fn elapsed_us(&self) -> f64 {
self.elapsed_ns as f64 / 1_000.0
}
pub fn elapsed_ms(&self) -> f64 {
self.elapsed_ns as f64 / 1_000_000.0
}
pub fn elapsed_s(&self) -> f64 {
self.elapsed_ns as f64 / 1_000_000_000.0
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TimingReport {
pub measurements: Vec<TimingInfo>,
pub total_ns: u64,
}
impl TimingReport {
pub fn new() -> Self {
Self {
measurements: Vec::new(),
total_ns: 0,
}
}
pub fn add_measurement(&mut self, timing: TimingInfo) {
self.total_ns += timing.elapsed_ns;
self.measurements.push(timing);
}
pub fn total_ms(&self) -> f64 {
self.total_ns as f64 / 1_000_000.0
}
pub fn average_ms(&self) -> f64 {
if self.measurements.is_empty() {
0.0
} else {
self.total_ms() / self.measurements.len() as f64
}
}
pub fn slowest(&self) -> Option<&TimingInfo> {
self.measurements.iter()
.max_by_key(|t| t.elapsed_ns)
}
pub fn fastest(&self) -> Option<&TimingInfo> {
self.measurements.iter()
.min_by_key(|t| t.elapsed_ns)
}
}
impl Default for TimingReport {
fn default() -> Self {
Self::new()
}
}
impl Default for TimingHistogram {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PerformanceProfiler {
histograms: alloc::collections::BTreeMap<String, TimingHistogram>,
recent_samples: alloc::collections::BTreeMap<String, Vec<u64>>,
max_recent_samples: usize,
}
impl PerformanceProfiler {
pub fn new() -> Self {
Self {
histograms: alloc::collections::BTreeMap::new(),
recent_samples: alloc::collections::BTreeMap::new(),
max_recent_samples: 100,
}
}
pub fn with_sample_buffer_size(max_recent_samples: usize) -> Self {
Self {
histograms: alloc::collections::BTreeMap::new(),
recent_samples: alloc::collections::BTreeMap::new(),
max_recent_samples,
}
}
pub fn record(&mut self, operation: &str, duration_ns: u64) {
let histogram = self.histograms
.entry(operation.to_string())
.or_insert_with(TimingHistogram::new);
histogram.record(duration_ns);
let samples = self.recent_samples
.entry(operation.to_string())
.or_insert_with(Vec::new);
samples.push(duration_ns);
if samples.len() > self.max_recent_samples {
samples.remove(0); }
}
pub fn record_timer(&mut self, timer: Timer) {
let timing = timer.stop();
self.record(&timing.label, timing.elapsed_ns);
}
pub fn get_histogram(&self, operation: &str) -> Option<&TimingHistogram> {
self.histograms.get(operation)
}
pub fn get_moving_average(&self, operation: &str) -> Option<f64> {
self.recent_samples.get(operation).map(|samples| {
if samples.is_empty() {
0.0
} else {
let sum: u64 = samples.iter().sum();
sum as f64 / samples.len() as f64
}
})
}
pub fn get_moving_average_ms(&self, operation: &str) -> Option<f64> {
self.get_moving_average(operation)
.map(|avg_ns| avg_ns / 1_000_000.0)
}
pub fn get_operations(&self) -> Vec<String> {
self.histograms.keys().cloned().collect()
}
pub fn get_stats(&self, operation: &str) -> Option<OperationStatistics> {
let histogram = self.histograms.get(operation)?;
let moving_avg = self.get_moving_average(operation)?;
Some(OperationStatistics {
operation: operation.to_string(),
total_samples: histogram.count(),
mean_ns: histogram.mean(),
std_dev_ns: histogram.std_dev(),
moving_avg_ns: moving_avg,
p50_ns: histogram.p50(),
p95_ns: histogram.p95(),
p99_ns: histogram.p99(),
})
}
pub fn get_all_stats(&self) -> Vec<OperationStatistics> {
self.get_operations()
.into_iter()
.filter_map(|op| self.get_stats(&op))
.collect()
}
pub fn reset(&mut self) {
self.histograms.clear();
self.recent_samples.clear();
}
pub fn reset_operation(&mut self, operation: &str) {
if let Some(histogram) = self.histograms.get_mut(operation) {
histogram.reset();
}
if let Some(samples) = self.recent_samples.get_mut(operation) {
samples.clear();
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OperationStatistics {
pub operation: String,
pub total_samples: u64,
pub mean_ns: f64,
pub std_dev_ns: f64,
pub moving_avg_ns: f64,
pub p50_ns: u64,
pub p95_ns: u64,
pub p99_ns: u64,
}
impl OperationStatistics {
pub fn mean_ms(&self) -> f64 { self.mean_ns / 1_000_000.0 }
pub fn std_dev_ms(&self) -> f64 { self.std_dev_ns / 1_000_000.0 }
pub fn moving_avg_ms(&self) -> f64 { self.moving_avg_ns / 1_000_000.0 }
pub fn p50_ms(&self) -> f64 { self.p50_ns as f64 / 1_000_000.0 }
pub fn p95_ms(&self) -> f64 { self.p95_ns as f64 / 1_000_000.0 }
pub fn p99_ms(&self) -> f64 { self.p99_ns as f64 / 1_000_000.0 }
}
impl Default for PerformanceProfiler {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! time_block {
($label:expr, $block:block) => {{
let timer = $crate::timing::Timer::start($label.to_string());
let result = $block;
let timing = timer.stop();
(result, timing)
}};
}