use dashmap::DashMap;
use serde::Serialize;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
static PROFILING_ENABLED: AtomicBool = AtomicBool::new(false);
static TIMING_COLLECTOR: OnceLock<TimingCollector> = OnceLock::new();
pub fn enable_profiling() {
PROFILING_ENABLED.store(true, Ordering::SeqCst);
let _ = get_collector();
}
#[cfg(test)]
pub fn disable_profiling() {
PROFILING_ENABLED.store(false, Ordering::SeqCst);
}
#[inline]
pub fn is_profiling_enabled() -> bool {
PROFILING_ENABLED.load(Ordering::Relaxed)
}
fn get_collector() -> &'static TimingCollector {
TIMING_COLLECTOR.get_or_init(TimingCollector::new)
}
pub fn record_timing(name: &'static str, parent: Option<&'static str>, duration: Duration) {
if is_profiling_enabled() {
get_collector().record(name, parent, duration);
}
}
#[must_use]
pub fn get_timing_report() -> TimingReport {
get_collector().generate_report()
}
pub fn reset_timing_data() {
get_collector().reset();
}
pub struct TimingSpan {
name: &'static str,
start: Instant,
parent: Option<&'static str>,
}
impl TimingSpan {
#[inline]
#[must_use]
pub fn new(name: &'static str) -> Self {
Self {
name,
start: Instant::now(),
parent: None,
}
}
#[inline]
#[must_use]
pub fn with_parent(name: &'static str, parent: &'static str) -> Self {
Self {
name,
start: Instant::now(),
parent: Some(parent),
}
}
}
impl Drop for TimingSpan {
fn drop(&mut self) {
if is_profiling_enabled() {
let duration = self.start.elapsed();
record_timing(self.name, self.parent, duration);
}
}
}
pub struct TimingCollector {
timings: DashMap<&'static str, TimingEntry>,
hierarchy: DashMap<&'static str, &'static str>,
start_time: OnceLock<Instant>,
}
struct TimingEntry {
total_nanos: AtomicU64,
count: AtomicU64,
}
impl TimingEntry {
fn new() -> Self {
Self {
total_nanos: AtomicU64::new(0),
count: AtomicU64::new(0),
}
}
}
impl TimingCollector {
fn new() -> Self {
Self {
timings: DashMap::new(),
hierarchy: DashMap::new(),
start_time: OnceLock::new(),
}
}
pub fn record(&self, name: &'static str, parent: Option<&'static str>, duration: Duration) {
let _ = self.start_time.get_or_init(Instant::now);
let nanos = duration.as_nanos() as u64;
let entry = self.timings.entry(name).or_insert_with(TimingEntry::new);
entry.total_nanos.fetch_add(nanos, Ordering::Relaxed);
entry.count.fetch_add(1, Ordering::Relaxed);
if let Some(p) = parent {
self.hierarchy.insert(name, p);
}
}
pub fn reset(&self) {
self.timings.clear();
self.hierarchy.clear();
}
#[must_use]
pub fn generate_report(&self) -> TimingReport {
let total_duration = self
.start_time
.get()
.map(|start| start.elapsed())
.unwrap_or(Duration::ZERO);
let mut phases: Vec<PhaseTiming> = Vec::new();
for entry in self.timings.iter() {
let name = *entry.key();
let timing = entry.value();
let duration_nanos = timing.total_nanos.load(Ordering::Relaxed);
let count = timing.count.load(Ordering::Relaxed);
let duration = Duration::from_nanos(duration_nanos);
let parent = self.hierarchy.get(name).map(|p| *p);
let percentage = if total_duration.as_nanos() > 0 {
(duration.as_nanos() as f64 / total_duration.as_nanos() as f64) * 100.0
} else {
0.0
};
phases.push(PhaseTiming {
name: name.to_string(),
duration,
percentage,
count,
parent: parent.map(|s| s.to_string()),
children: Vec::new(),
});
}
phases.sort_by(|a, b| b.duration.cmp(&a.duration));
let phases = build_hierarchy(phases);
TimingReport {
total_duration,
phases,
}
}
}
fn build_hierarchy(mut phases: Vec<PhaseTiming>) -> Vec<PhaseTiming> {
let (mut roots, children): (Vec<_>, Vec<_>) =
phases.drain(..).partition(|p| p.parent.is_none());
for child in children {
if let Some(parent_name) = &child.parent {
for root in &mut roots {
if add_child_to_parent(root, parent_name, child.clone()) {
break;
}
}
}
}
for root in &mut roots {
sort_children_recursive(root);
}
roots
}
fn add_child_to_parent(parent: &mut PhaseTiming, target_name: &str, child: PhaseTiming) -> bool {
if parent.name == target_name {
parent.children.push(child);
return true;
}
for existing_child in &mut parent.children {
if add_child_to_parent(existing_child, target_name, child.clone()) {
return true;
}
}
false
}
fn sort_children_recursive(phase: &mut PhaseTiming) {
phase.children.sort_by(|a, b| b.duration.cmp(&a.duration));
for child in &mut phase.children {
sort_children_recursive(child);
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TimingReport {
pub total_duration: Duration,
pub phases: Vec<PhaseTiming>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PhaseTiming {
pub name: String,
pub duration: Duration,
pub percentage: f64,
pub count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<PhaseTiming>,
}
impl TimingReport {
#[must_use]
pub fn to_summary(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"\n=== Profiling Report ===\nTotal analysis time: {:.2?}\n\n",
self.total_duration
));
output.push_str("Phase breakdown:\n");
output.push_str(&format!(
"{:<40} {:>12} {:>8} {:>10}\n",
"Operation", "Duration", "%", "Count"
));
output.push_str(&"-".repeat(72));
output.push('\n');
for phase in &self.phases {
output.push_str(&format_phase(phase, 0));
}
output
}
#[must_use]
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
}
fn format_phase(phase: &PhaseTiming, indent: usize) -> String {
let mut output = String::new();
let indent_str = " ".repeat(indent);
let name = format!("{}{}", indent_str, phase.name);
output.push_str(&format!(
"{:<40} {:>12} {:>7.1}% {:>10}\n",
name,
format_duration(phase.duration),
phase.percentage,
phase.count
));
for child in &phase.children {
output.push_str(&format_phase(child, indent + 1));
}
output
}
fn format_duration(d: Duration) -> String {
if d.as_secs() >= 1 {
format!("{:.2}s", d.as_secs_f64())
} else if d.as_millis() >= 1 {
format!("{:.1}ms", d.as_secs_f64() * 1000.0)
} else {
format!("{:.0}µs", d.as_secs_f64() * 1_000_000.0)
}
}
#[macro_export]
macro_rules! time_span {
($name:expr) => {
let _timing_span = if $crate::observability::profiling::is_profiling_enabled() {
Some($crate::observability::profiling::TimingSpan::new($name))
} else {
None
};
};
($name:expr, parent: $parent:expr) => {
let _timing_span = if $crate::observability::profiling::is_profiling_enabled() {
Some($crate::observability::profiling::TimingSpan::with_parent(
$name, $parent,
))
} else {
None
};
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
fn get_count(name: &str) -> u64 {
get_timing_report()
.phases
.iter()
.find(|p| p.name == name)
.map(|p| p.count)
.unwrap_or(0)
}
#[test]
fn test_timing_span_records_duration() {
enable_profiling();
let before = get_count("test_duration_op");
{
let _span = TimingSpan::new("test_duration_op");
thread::sleep(Duration::from_millis(10));
}
let report = get_timing_report();
let phase = report
.phases
.iter()
.find(|p| p.name == "test_duration_op")
.expect("Should have recorded test_duration_op");
assert_eq!(phase.count, before + 1, "Count should increase by 1");
assert!(
phase.duration >= Duration::from_millis(10),
"Duration should be at least 10ms"
);
}
#[test]
fn test_timing_with_parent() {
enable_profiling();
{
let _parent = TimingSpan::new("test_parent_op");
{
let _child = TimingSpan::with_parent("test_child_op", "test_parent_op");
thread::sleep(Duration::from_millis(5));
}
}
let report = get_timing_report();
let parent = report
.phases
.iter()
.find(|p| p.name == "test_parent_op")
.expect("Parent should exist");
let child = parent.children.iter().find(|c| c.name == "test_child_op");
assert!(child.is_some(), "Child should be nested under parent");
}
#[test]
fn test_timing_count_accumulates() {
enable_profiling();
let before = get_count("test_accumulate_op");
for _ in 0..5 {
let _span = TimingSpan::new("test_accumulate_op");
}
let after = get_count("test_accumulate_op");
assert_eq!(after, before + 5, "Count should increase by 5");
}
#[test]
fn test_timing_conditional_creation() {
enable_profiling();
let before = get_count("test_conditional_op");
for _ in 0..10 {
if is_profiling_enabled() {
let _span = TimingSpan::new("test_conditional_op");
}
}
let after = get_count("test_conditional_op");
assert_eq!(after, before + 10, "Should record 10 spans when enabled");
}
#[test]
fn test_report_to_summary() {
enable_profiling();
{
let _span = TimingSpan::new("test_summary_op");
thread::sleep(Duration::from_millis(1));
}
let report = get_timing_report();
let summary = report.to_summary();
assert!(summary.contains("Profiling Report"));
assert!(summary.contains("test_summary_op"));
}
#[test]
fn test_report_to_json() {
enable_profiling();
{
let _span = TimingSpan::new("test_json_op");
}
let report = get_timing_report();
let json = report.to_json();
assert!(json.contains("\"name\": \"test_json_op\""));
assert!(json.contains("\"total_duration\""));
}
#[test]
fn test_time_span_macro() {
enable_profiling();
let before = get_count("test_macro_op");
fn test_function() {
time_span!("test_macro_op");
thread::sleep(Duration::from_millis(1));
}
test_function();
let after = get_count("test_macro_op");
assert_eq!(after, before + 1, "Macro should record timing");
}
#[test]
fn test_thread_safe_collection() {
enable_profiling();
let before = get_count("test_parallel_op");
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
for _ in 0..10 {
let _span = TimingSpan::new("test_parallel_op");
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let after = get_count("test_parallel_op");
assert_eq!(
after,
before + 40,
"Should record 40 spans (4 threads × 10 each)"
);
}
#[test]
fn test_conditional_timing_not_recorded_when_guard_is_false() {
enable_profiling();
let before_empty = get_count("conditional_operation");
let before_nonempty = get_count("conditional_operation_recorded");
let files: Vec<String> = vec![];
if !files.is_empty() {
time_span!("conditional_operation");
}
let after_empty = get_count("conditional_operation");
assert_eq!(
after_empty, before_empty,
"Timing span inside false conditional should NOT be recorded"
);
let files_with_content = ["test.rs".to_string()];
if !files_with_content.is_empty() {
time_span!("conditional_operation_recorded");
}
let after_nonempty = get_count("conditional_operation_recorded");
assert_eq!(
after_nonempty,
before_nonempty + 1,
"Timing span inside true conditional should be recorded"
);
}
#[test]
fn test_language_specific_timing_not_in_rust_only_codebase() {
enable_profiling();
let before = get_count("typescript_call_graph");
let metrics: Vec<String> = vec![];
let js_ts_files: Vec<String> = metrics
.iter()
.filter(|_m| false) .map(|_| "dummy".to_string())
.collect();
if !js_ts_files.is_empty() {
time_span!("typescript_call_graph");
}
let after = get_count("typescript_call_graph");
assert_eq!(
after, before,
"Language-specific timing should not appear in profiling report for Rust-only codebase"
);
}
}