use std::cell::RefCell;
use std::collections::BTreeMap;
use std::time::Instant;
#[derive(Clone, Debug, Default)]
pub struct ZoneStats {
pub count: u64,
pub total_ns: u128,
pub min_ns: u128,
pub max_ns: u128,
pub sum_sq_ns: u128,
}
impl ZoneStats {
fn update(&mut self, ns: u128) {
if self.count == 0 {
self.min_ns = ns;
self.max_ns = ns;
} else {
if ns < self.min_ns {
self.min_ns = ns;
}
if ns > self.max_ns {
self.max_ns = ns;
}
}
self.count += 1;
self.total_ns = self.total_ns.saturating_add(ns);
let sq = (ns as u128).saturating_mul(ns as u128);
self.sum_sq_ns = self.sum_sq_ns.saturating_add(sq);
}
pub fn mean_ns(&self) -> u128 {
if self.count == 0 {
0
} else {
self.total_ns / (self.count as u128)
}
}
pub fn stddev_ns(&self) -> f64 {
if self.count == 0 {
return 0.0;
}
let mean = self.mean_ns() as f64;
let mean_sq = mean * mean;
let var_raw = (self.sum_sq_ns / (self.count as u128)) as f64 - mean_sq;
if var_raw <= 0.0 {
0.0
} else {
var_raw.sqrt()
}
}
}
pub struct ProfileState {
pub zones: BTreeMap<String, ZoneStats>,
pub active: BTreeMap<i64, (String, Instant)>,
pub next_handle: i64,
}
impl ProfileState {
pub fn new() -> Self {
Self {
zones: BTreeMap::new(),
active: BTreeMap::new(),
next_handle: 0,
}
}
pub fn reset(&mut self) {
self.zones.clear();
self.active.clear();
self.next_handle = 0;
}
}
impl Default for ProfileState {
fn default() -> Self {
Self::new()
}
}
thread_local! {
pub(crate) static PROFILE: RefCell<ProfileState> = RefCell::new(ProfileState::new());
}
pub fn zone_start(name: &str) -> i64 {
PROFILE.with(|cell| {
let mut state = cell.borrow_mut();
let handle = state.next_handle;
state.next_handle = state.next_handle.wrapping_add(1);
state
.active
.insert(handle, (name.to_string(), Instant::now()));
handle
})
}
pub fn zone_stop(handle: i64) -> f64 {
PROFILE.with(|cell| {
let mut state = cell.borrow_mut();
let Some((name, start)) = state.active.remove(&handle) else {
return -1.0;
};
let elapsed = start.elapsed();
let ns = elapsed.as_nanos();
let entry = state.zones.entry(name).or_default();
entry.update(ns);
ns as f64 / 1.0e9
})
}
pub fn dump_to_path(path: &str) -> Result<i64, String> {
let csv = PROFILE.with(|cell| {
let mut state = cell.borrow_mut();
let mut rows: Vec<(String, ZoneStats)> = state
.zones
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
rows.sort_by(|a, b| b.1.total_ns.cmp(&a.1.total_ns));
let mut out = String::new();
out.push_str("zone_name,count,total_ns,min_ns,max_ns,mean_ns,stddev_ns\n");
for (name, stats) in &rows {
let stddev_ns = stats.stddev_ns().round() as u128;
out.push_str(&format!(
"{},{},{},{},{},{},{}\n",
name,
stats.count,
stats.total_ns,
stats.min_ns,
stats.max_ns,
stats.mean_ns(),
stddev_ns,
));
}
let row_count = rows.len() as i64;
state.reset();
(out, row_count)
});
std::fs::write(path, &csv.0).map_err(|e| format!("profile_dump error: {e}"))?;
Ok(csv.1)
}
#[doc(hidden)]
pub fn snapshot_zones() -> BTreeMap<String, ZoneStats> {
PROFILE.with(|cell| cell.borrow().zones.clone())
}
#[doc(hidden)]
pub fn reset_for_test() {
PROFILE.with(|cell| cell.borrow_mut().reset());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_stop_round_trip() {
reset_for_test();
let h = zone_start("zone_a");
assert_eq!(h, 0);
let elapsed = zone_stop(h);
assert!(elapsed >= 0.0);
let snap = snapshot_zones();
assert_eq!(snap.len(), 1);
assert_eq!(snap["zone_a"].count, 1);
}
#[test]
fn handle_is_monotonic() {
reset_for_test();
let a = zone_start("a");
let b = zone_start("b");
let c = zone_start("c");
assert_eq!(a, 0);
assert_eq!(b, 1);
assert_eq!(c, 2);
zone_stop(a);
zone_stop(b);
zone_stop(c);
}
#[test]
fn nested_zones_accumulate_independently() {
reset_for_test();
let outer = zone_start("outer");
let inner = zone_start("inner");
zone_stop(inner);
zone_stop(outer);
let snap = snapshot_zones();
assert_eq!(snap.len(), 2);
assert_eq!(snap["outer"].count, 1);
assert_eq!(snap["inner"].count, 1);
}
#[test]
fn repeated_zone_accumulates_count() {
reset_for_test();
for _ in 0..10 {
let h = zone_start("hot");
zone_stop(h);
}
let snap = snapshot_zones();
assert_eq!(snap["hot"].count, 10);
}
#[test]
fn unknown_handle_returns_negative_one() {
reset_for_test();
let e = zone_stop(9999);
assert!(e < 0.0);
}
#[test]
fn dump_resets_state() {
reset_for_test();
let h = zone_start("zone_x");
zone_stop(h);
let tmp = std::env::temp_dir().join("cjc_profile_dump_resets_state.csv");
let rows = dump_to_path(tmp.to_str().unwrap()).unwrap();
assert_eq!(rows, 1);
assert!(snapshot_zones().is_empty());
let content = std::fs::read_to_string(&tmp).unwrap();
assert!(content.starts_with(
"zone_name,count,total_ns,min_ns,max_ns,mean_ns,stddev_ns\n"
));
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn dump_csv_format_integer_columns() {
reset_for_test();
for _ in 0..3 {
let h = zone_start("z");
zone_stop(h);
}
let tmp = std::env::temp_dir().join("cjc_profile_dump_csv_format.csv");
let rows = dump_to_path(tmp.to_str().unwrap()).unwrap();
assert_eq!(rows, 1);
let content = std::fs::read_to_string(&tmp).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
let fields: Vec<&str> = lines[1].split(',').collect();
assert_eq!(fields.len(), 7);
assert_eq!(fields[0], "z");
for f in &fields[1..] {
assert!(
f.parse::<u128>().is_ok(),
"column {f} is not an integer in v2.3 CSV"
);
}
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn dump_sort_order_hot_first() {
reset_for_test();
let h_cold = zone_start("cold");
zone_stop(h_cold);
PROFILE.with(|cell| {
let mut state = cell.borrow_mut();
let entry = state.zones.entry("hot".to_string()).or_default();
entry.update(10_000_000_000); });
let tmp = std::env::temp_dir().join("cjc_profile_dump_sort_order.csv");
dump_to_path(tmp.to_str().unwrap()).unwrap();
let content = std::fs::read_to_string(&tmp).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3);
assert!(lines[1].starts_with("hot,"), "hot zone should be first");
assert!(lines[2].starts_with("cold,"), "cold zone should be second");
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn empty_dump_writes_header_only() {
reset_for_test();
let tmp = std::env::temp_dir().join("cjc_profile_empty_dump.csv");
let rows = dump_to_path(tmp.to_str().unwrap()).unwrap();
assert_eq!(rows, 0);
let content = std::fs::read_to_string(&tmp).unwrap();
assert_eq!(
content,
"zone_name,count,total_ns,min_ns,max_ns,mean_ns,stddev_ns\n"
);
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn zone_stats_update_tracks_min_max() {
let mut s = ZoneStats::default();
s.update(100);
s.update(50);
s.update(200);
assert_eq!(s.count, 3);
assert_eq!(s.min_ns, 50);
assert_eq!(s.max_ns, 200);
assert_eq!(s.total_ns, 350);
}
#[test]
fn mean_and_stddev_sane() {
let mut s = ZoneStats::default();
for ns in [100u128, 200, 300] {
s.update(ns);
}
assert_eq!(s.mean_ns(), 200);
let sd = s.stddev_ns();
assert!(sd > 70.0 && sd < 100.0, "unexpected stddev: {sd}");
}
}