use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, OnceLock, RwLock};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct Timing {
pub name: String,
pub duration: Duration,
pub start: Instant,
pub parent: Option<String>,
}
impl Timing {
pub fn new(name: impl Into<String>, duration: Duration, start: Instant) -> Self {
Self {
name: name.into(),
duration,
start,
parent: None,
}
}
pub fn with_parent(mut self, parent: impl Into<String>) -> Self {
self.parent = Some(parent.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Stats {
pub count: u64,
pub total: Duration,
pub min: Option<Duration>,
pub max: Option<Duration>,
pub last: Duration,
}
impl Stats {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, duration: Duration) {
self.count += 1;
self.total += duration;
self.last = duration;
match self.min {
Some(m) if duration < m => self.min = Some(duration),
None => self.min = Some(duration),
_ => {}
}
match self.max {
Some(m) if duration > m => self.max = Some(duration),
None => self.max = Some(duration),
_ => {}
}
}
pub fn average(&self) -> Duration {
if self.count == 0 {
Duration::ZERO
} else {
self.total / self.count as u32
}
}
pub fn avg_ms(&self) -> f64 {
self.average().as_secs_f64() * 1000.0
}
pub fn total_ms(&self) -> f64 {
self.total.as_secs_f64() * 1000.0
}
pub fn min_ms(&self) -> f64 {
self.min.map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
}
pub fn max_ms(&self) -> f64 {
self.max.map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0)
}
}
pub struct ProfileGuard {
name: String,
start: Instant,
profiler: Arc<RwLock<ProfilerInner>>,
}
impl ProfileGuard {
fn new(name: impl Into<String>, profiler: Arc<RwLock<ProfilerInner>>) -> Self {
Self {
name: name.into(),
start: Instant::now(),
profiler,
}
}
}
impl Drop for ProfileGuard {
fn drop(&mut self) {
let duration = self.start.elapsed();
if let Ok(mut p) = self.profiler.write() {
p.record(&self.name, duration);
}
}
}
#[derive(Debug, Default)]
struct ProfilerInner {
stats: HashMap<String, Stats>,
recent: Vec<Timing>,
max_recent: usize,
enabled: bool,
stack: Vec<String>,
}
impl ProfilerInner {
fn new() -> Self {
Self {
stats: HashMap::new(),
recent: Vec::new(),
max_recent: 100,
enabled: true,
stack: Vec::new(),
}
}
fn record(&mut self, name: &str, duration: Duration) {
if !self.enabled {
return;
}
let stats = self.stats.entry(name.to_string()).or_default();
stats.record(duration);
let mut timing = Timing::new(name, duration, Instant::now());
if let Some(parent) = self.stack.last() {
timing = timing.with_parent(parent.clone());
}
self.recent.push(timing);
if self.recent.len() > self.max_recent {
self.recent.remove(0);
}
}
fn reset(&mut self) {
self.stats.clear();
self.recent.clear();
}
}
#[derive(Debug, Clone)]
pub struct Profiler {
inner: Arc<RwLock<ProfilerInner>>,
}
impl Default for Profiler {
fn default() -> Self {
Self::new()
}
}
impl Profiler {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(ProfilerInner::new())),
}
}
pub fn global() -> &'static Profiler {
static INSTANCE: OnceLock<Profiler> = OnceLock::new();
INSTANCE.get_or_init(Profiler::new)
}
pub fn enable(&self) {
if let Ok(mut inner) = self.inner.write() {
inner.enabled = true;
}
}
pub fn disable(&self) {
if let Ok(mut inner) = self.inner.write() {
inner.enabled = false;
}
}
pub fn is_enabled(&self) -> bool {
self.inner.read().map(|i| i.enabled).unwrap_or(false)
}
pub fn start(&self, name: impl Into<String>) -> ProfileGuard {
ProfileGuard::new(name, self.inner.clone())
}
pub fn profile<T, F: FnOnce() -> T>(&self, name: &str, f: F) -> T {
let _guard = self.start(name);
f()
}
pub fn record(&self, name: &str, duration: Duration) {
if let Ok(mut inner) = self.inner.write() {
inner.record(name, duration);
}
}
pub fn stats(&self, name: &str) -> Option<Stats> {
self.inner.read().ok()?.stats.get(name).cloned()
}
pub fn all_stats(&self) -> HashMap<String, Stats> {
self.inner
.read()
.map(|i| i.stats.clone())
.unwrap_or_default()
}
pub fn reset(&self) {
if let Ok(mut inner) = self.inner.write() {
inner.reset();
}
}
pub fn report(&self) -> String {
let stats = self.all_stats();
if stats.is_empty() {
return "No profiling data collected.".to_string();
}
let mut output = String::new();
output.push_str("Performance Report\n");
output.push_str("==================\n\n");
let mut entries: Vec<_> = stats.into_iter().collect();
entries.sort_by(|a, b| b.1.total.cmp(&a.1.total));
output.push_str(&format!(
"{:<30} {:>8} {:>10} {:>10} {:>10} {:>10}\n",
"Operation", "Calls", "Total(ms)", "Avg(ms)", "Min(ms)", "Max(ms)"
));
output.push_str(&"-".repeat(80));
output.push('\n');
for (name, stat) in entries {
output.push_str(&format!(
"{:<30} {:>8} {:>10.2} {:>10.3} {:>10.3} {:>10.3}\n",
if name.len() > 30 {
format!("{}...", &name[..27])
} else {
name.clone()
},
stat.count,
stat.total_ms(),
stat.avg_ms(),
stat.min_ms(),
stat.max_ms(),
));
}
output
}
pub fn summary(&self) -> String {
let stats = self.all_stats();
if stats.is_empty() {
return String::new();
}
let total_time: Duration = stats.values().map(|s| s.total).sum();
let total_calls: u64 = stats.values().map(|s| s.count).sum();
format!(
"{} operations, {} calls, {:.2}ms total",
stats.len(),
total_calls,
total_time.as_secs_f64() * 1000.0
)
}
}
pub fn profile<T, F: FnOnce() -> T>(name: &str, f: F) -> T {
Profiler::global().profile(name, f)
}
pub fn start_profile(name: impl Into<String>) -> ProfileGuard {
Profiler::global().start(name)
}
pub fn profiler_report() -> String {
Profiler::global().report()
}
thread_local! {
static THREAD_PROFILER: RefCell<Profiler> = RefCell::new(Profiler::new());
}
pub fn thread_profiler() -> Profiler {
THREAD_PROFILER.with(|p| p.borrow().clone())
}
#[derive(Debug, Clone)]
pub struct FlameNode {
pub name: String,
pub self_time: Duration,
pub total_time: Duration,
pub children: Vec<FlameNode>,
}
impl FlameNode {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
self_time: Duration::ZERO,
total_time: Duration::ZERO,
children: Vec::new(),
}
}
pub fn add_time(&mut self, duration: Duration) {
self.total_time += duration;
self.self_time += duration;
}
pub fn add_child(&mut self, child: FlameNode) {
self.self_time = self.self_time.saturating_sub(child.total_time);
self.children.push(child);
}
pub fn format_text(&self, depth: usize) -> String {
let mut output = String::new();
let indent = " ".repeat(depth);
let percent = if self.total_time.as_nanos() > 0 {
(self.self_time.as_nanos() as f64 / self.total_time.as_nanos() as f64) * 100.0
} else {
100.0
};
output.push_str(&format!(
"{}{} ({:.2}ms / {:.1}%)\n",
indent,
self.name,
self.total_time.as_secs_f64() * 1000.0,
percent,
));
for child in &self.children {
output.push_str(&child.format_text(depth + 1));
}
output
}
}