use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;
static STATS: Mutex<Option<HashMap<&'static str, ThunkStats>>> = Mutex::new(None);
#[derive(Default, Clone)]
struct ThunkStats {
count: u64,
total_ns: u128,
gpu_ns: u128,
}
pub fn enabled() -> bool {
rlx_ir::env::flag("RLX_METAL_THUNK_PROFILE")
}
pub fn reset() {
if enabled() {
*STATS.lock().unwrap() = Some(HashMap::new());
}
}
pub fn record(name: &'static str, dt: Duration) {
record_split(name, dt, Duration::ZERO);
}
pub fn record_split(name: &'static str, wall: Duration, gpu: Duration) {
if !enabled() {
return;
}
let mut guard = STATS.lock().unwrap();
let map = guard.get_or_insert_with(HashMap::new);
let e = map.entry(name).or_default();
e.count += 1;
e.total_ns += wall.as_nanos();
e.gpu_ns += gpu.as_nanos();
}
pub fn print_summary() {
if !enabled() {
return;
}
let guard = STATS.lock().unwrap();
let Some(map) = guard.as_ref() else {
return;
};
if map.is_empty() {
eprintln!("[rlx-metal] thunk profile: no samples");
return;
}
let total_wall: u128 = map.values().map(|s| s.total_ns).sum();
let total_gpu: u128 = map.values().map(|s| s.gpu_ns).sum();
let by_gpu = total_gpu > 0;
let denom = if by_gpu { total_gpu } else { total_wall } as f64;
let mut rows: Vec<_> = map
.iter()
.map(|(name, s)| {
let gpu_ms = s.gpu_ns as f64 / 1e6;
let wall_ms = s.total_ns as f64 / 1e6;
let key = if by_gpu { s.gpu_ns } else { s.total_ns } as f64;
let pct = if denom > 0.0 {
100.0 * key / denom
} else {
0.0
};
(*name, s.count, gpu_ms, wall_ms, pct)
})
.collect();
rows.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal));
eprintln!(
"[rlx-metal] thunk profile — GPU-busy {:.2} ms | wall {:.2} ms (sync overhead {:.2} ms), sorted by {}:",
total_gpu as f64 / 1e6,
total_wall as f64 / 1e6,
(total_wall.saturating_sub(total_gpu)) as f64 / 1e6,
if by_gpu { "GPU-busy" } else { "wall" }
);
eprintln!(
"{:<32} {:>6} {:>10} {:>10} {:>7}",
"thunk", "count", "gpu_ms", "wall_ms", "pct"
);
eprintln!("{}", "-".repeat(70));
let mut buckets: HashMap<&'static str, (f64, f64)> = HashMap::new();
for (name, count, gpu_ms, wall_ms, pct) in &rows {
eprintln!("{name:<32} {count:>6} {gpu_ms:>10.2} {wall_ms:>10.2} {pct:>6.1}%");
let bucket = bucket_name(name);
let e = buckets.entry(bucket).or_default();
e.0 += *gpu_ms;
e.1 += *wall_ms;
}
let mut bucket_rows: Vec<_> = buckets.into_iter().collect();
let bkey = |b: &(f64, f64)| if by_gpu { b.0 } else { b.1 };
bucket_rows.sort_by(|a, b| {
bkey(&b.1)
.partial_cmp(&bkey(&a.1))
.unwrap_or(std::cmp::Ordering::Equal)
});
eprintln!("\n[rlx-metal] buckets (gpu_ms / wall_ms):");
for (name, (gpu_ms, wall_ms)) in bucket_rows {
let pct = if denom > 0.0 {
100.0 * (if by_gpu { gpu_ms } else { wall_ms }) / (denom / 1e6)
} else {
0.0
};
eprintln!(" {name:<28} {gpu_ms:>10.2} {wall_ms:>10.2} ({pct:>5.1}%)");
}
}
fn bucket_name(thunk: &str) -> &'static str {
if thunk.contains("conv2d_backward") {
"conv_backward"
} else if thunk.contains("attention") {
"attention"
} else if thunk.contains("sgemm") || thunk.contains("fused_mm") || thunk.contains("matmul") {
"matmul"
} else if thunk.contains("rms_norm") || thunk.contains("layer_norm") {
"norm"
} else if thunk.contains("rope") {
"rope"
} else if thunk.contains("binary") || thunk.contains("activation") {
"elemwise"
} else {
"other"
}
}