use crate::ported::zsh_h::{OPT_ISSET, module, eprog, funcwrap, features, options, MAX_OPS};
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Mutex, OnceLock};
use crate::ported::compat::zgettime_monotonic_if_available;
use crate::ported::mem::ztrdup;
use crate::ported::modules::parameter::FUNCSTACK;
#[derive(Debug, Clone, Default)]
pub struct Pfunc {
pub name: String, pub calls: i64, pub time: f64, pub self_time: f64, pub num: i64, }
#[derive(Debug, Clone, Copy)]
pub struct Sfunc {
pub p: usize, pub beg: f64, }
#[derive(Debug, Clone, Default)]
pub struct Parc {
pub from: usize, pub to: usize, pub calls: i64, pub time: f64, pub self_time: f64, }
pub fn freepfuncs(f: &mut Vec<Pfunc>) {
f.clear(); }
pub fn freeparcs(a: &mut Vec<Parc>) {
a.clear(); }
pub fn findpfunc(name: &str) -> Option<usize> {
let calls = CALLS.lock().unwrap();
calls.iter().position(|f| f.name == name)
}
pub fn findparc(f: usize, t: usize) -> Option<usize> {
let arcs = ARCS.lock().unwrap();
arcs.iter().position(|a| a.from == f && a.to == t)
}
pub fn cmpsfuncs(a: &Pfunc, b: &Pfunc) -> std::cmp::Ordering {
b.self_time
.partial_cmp(&a.self_time)
.unwrap_or(std::cmp::Ordering::Equal)
}
pub fn cmptfuncs(a: &Pfunc, b: &Pfunc) -> std::cmp::Ordering {
b.time
.partial_cmp(&a.time)
.unwrap_or(std::cmp::Ordering::Equal)
}
pub fn cmpparcs(a: &Parc, b: &Parc) -> std::cmp::Ordering {
b.time
.partial_cmp(&a.time)
.unwrap_or(std::cmp::Ordering::Equal)
}
pub fn bin_zprof(
_nam: &str,
_args: &[String], ops: &options,
_func: i32,
) -> i32 {
let opt_c = OPT_ISSET(ops, b'c');
if opt_c {
let mut calls = CALLS.lock().unwrap();
freepfuncs(&mut calls); NCALLS.store(0, Ordering::SeqCst); let mut arcs = ARCS.lock().unwrap();
freeparcs(&mut arcs); NARCS.store(0, Ordering::SeqCst); return 0; }
let calls = CALLS.lock().unwrap();
let arcs = ARCS.lock().unwrap();
let mut fs: Vec<usize> = (0..calls.len()).collect(); let as_arcs: Vec<usize> = (0..arcs.len()).collect(); let mut total: f64 = 0.0; for &i in &fs {
total += calls[i].self_time; }
fs.sort_by(|&a, &b| cmpsfuncs(&calls[a], &calls[b]));
println!("num calls time self name");
println!("-----------------------------------------------------------------------------------");
drop(calls);
{
let mut calls_w = CALLS.lock().unwrap();
for (i, &idx) in fs.iter().enumerate() {
calls_w[idx].num = (i + 1) as i64; }
}
let calls = CALLS.lock().unwrap();
for &idx in &fs {
let f = &calls[idx];
let avg_t = if f.calls > 0 {
f.time / f.calls as f64
} else {
0.0
};
let avg_s = if f.calls > 0 {
f.self_time / f.calls as f64
} else {
0.0
};
let pct_t = if total != 0.0 {
(f.time / total) * 100.0
} else {
0.0
};
let pct_s = if total != 0.0 {
(f.self_time / total) * 100.0
} else {
0.0
};
println!(
"{:2}) {:4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {:6.2}% {}",
f.num,
f.calls, f.time,
avg_t,
pct_t,
f.self_time,
avg_s,
pct_s,
f.name
);
}
let mut fs_t: Vec<usize> = fs.clone();
fs_t.sort_by(|&a, &b| cmptfuncs(&calls[a], &calls[b]));
for &fp_idx in &fs_t {
println!();
println!(
"-----------------------------------------------------------------------------------"
);
println!();
let f = &calls[fp_idx];
for &ap in &as_arcs {
let a = &arcs[ap];
if a.to == fp_idx {
let avg_t = if a.calls > 0 {
a.time / a.calls as f64
} else {
0.0
};
let avg_s = if a.calls > 0 {
a.self_time / a.calls as f64
} else {
0.0
};
let pct_t = if total != 0.0 {
(a.time / total) * 100.0
} else {
0.0
};
let from_name = &calls[a.from].name;
let from_num = calls[a.from].num;
println!(
" {:4}/{:<4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {} [{}]",
a.calls,
f.calls, a.time,
avg_t,
pct_t,
a.self_time,
avg_s,
from_name,
from_num
);
}
}
let avg_t = if f.calls > 0 {
f.time / f.calls as f64
} else {
0.0
};
let avg_s = if f.calls > 0 {
f.self_time / f.calls as f64
} else {
0.0
};
let pct_t = if total != 0.0 {
(f.time / total) * 100.0
} else {
0.0
};
let pct_s = if total != 0.0 {
(f.self_time / total) * 100.0
} else {
0.0
};
println!(
"{:2}) {:4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {:6.2}% {}",
f.num,
f.calls, f.time,
avg_t,
pct_t,
f.self_time,
avg_s,
pct_s,
f.name
);
for &ap in as_arcs.iter().rev() {
let a = &arcs[ap];
if a.from == fp_idx {
let avg_t = if a.calls > 0 {
a.time / a.calls as f64
} else {
0.0
};
let avg_s = if a.calls > 0 {
a.self_time / a.calls as f64
} else {
0.0
};
let pct_t = if total != 0.0 {
(a.time / total) * 100.0
} else {
0.0
};
let to_name = &calls[a.to].name;
let to_num = calls[a.to].num;
let to_calls = calls[a.to].calls;
println!(
" {:4}/{:<4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {} [{}]",
a.calls,
to_calls, a.time,
avg_t,
pct_t,
a.self_time,
avg_s,
to_name,
to_num
);
}
}
}
0 }
pub fn name_for_anonymous_function(name: &str) -> String {
let stack = FUNCSTACK
.lock()
.expect("FUNCSTACK poisoned");
let flineno = stack.first().map(|f| f.flineno).unwrap_or(0); let filename = stack
.first()
.and_then(|f| f.filename.clone())
.unwrap_or_default(); drop(stack);
let lineno_str = format!("{}", flineno); let parts = [name, " [", filename.as_str(), ":", lineno_str.as_str(), "]"];
parts.concat() }
#[allow(non_snake_case)]
pub fn zprof_wrapper(
prog: *const eprog, w: *const funcwrap,
name: &str,
) -> i32 {
let mut active: i32 = 0; let mut sf = Sfunc { p: 0, beg: 0.0 }; let mut f: Option<usize> = None; let mut a: Option<usize> = None; let mut prev: f64 = 0.0;
let name_for_lookups: String = if name == "(anon)" {
name_for_anonymous_function(name) } else {
name.to_string() };
if ZPROF_MODULE.load(Ordering::SeqCst) {
active = 1; f = findpfunc(&name_for_lookups); if f.is_none() {
let new_pfunc = Pfunc {
name: ztrdup(&name_for_lookups), calls: 0, time: 0.0, self_time: 0.0, num: 0,
};
let mut calls = CALLS.lock().unwrap();
f = Some(calls.len()); calls.push(new_pfunc); NCALLS.fetch_add(1, Ordering::SeqCst); }
let stack_top: Option<Sfunc> = {
let st = STACK.lock().unwrap();
st.last().copied()
};
if let Some(top) = stack_top {
a = findparc(top.p, f.unwrap()); if a.is_none() {
let new_parc = Parc {
from: top.p, to: f.unwrap(), calls: 0, self_time: 0.0, time: 0.0, };
let mut arcs = ARCS.lock().unwrap();
a = Some(arcs.len()); arcs.push(new_parc); NARCS.fetch_add(1, Ordering::SeqCst); }
}
sf.p = f.unwrap(); STACK.lock().unwrap().push(sf);
{
let mut calls = CALLS.lock().unwrap();
calls[f.unwrap()].calls += 1; }
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
}; zgettime_monotonic_if_available(&mut ts); sf.beg = (ts.tv_sec as f64) * 1000.0 + (ts.tv_nsec as f64) / 1_000_000.0; prev = sf.beg; let mut st = STACK.lock().unwrap();
if let Some(top) = st.last_mut() {
top.beg = sf.beg;
}
}
let _ = (prog, w);
if active != 0 {
if ZPROF_MODULE.load(Ordering::SeqCst) {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
}; zgettime_monotonic_if_available(&mut ts); let now = (ts.tv_sec as f64) * 1000.0 + (ts.tv_nsec as f64) / 1_000_000.0;
{
let mut calls = CALLS.lock().unwrap();
if let Some(idx) = f {
calls[idx].self_time += now - sf.beg; }
}
let recursion: bool = {
let st = STACK.lock().unwrap();
let cur_f = f.unwrap();
st.iter().rev().skip(1).any(|fr| fr.p == cur_f)
};
if !recursion {
let mut calls = CALLS.lock().unwrap();
if let Some(idx) = f {
calls[idx].time += now - prev; }
}
if let Some(arc_idx) = a {
let mut arcs = ARCS.lock().unwrap();
arcs[arc_idx].calls += 1; arcs[arc_idx].self_time += now - sf.beg; }
{
let mut st = STACK.lock().unwrap();
st.pop(); }
let mut st = STACK.lock().unwrap();
if let Some(top) = st.last_mut() {
top.beg += now - prev; if let Some(arc_idx) = a {
drop(st);
let mut arcs = ARCS.lock().unwrap();
arcs[arc_idx].time += now - prev; }
}
} else {
let mut st = STACK.lock().unwrap();
st.pop();
}
}
0 }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
ZPROF_MODULE.store(true, Ordering::SeqCst); 0 }
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0 }
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables) }
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
let mut calls = CALLS.lock().unwrap();
calls.clear(); NCALLS.store(0, Ordering::SeqCst); let mut arcs = ARCS.lock().unwrap();
arcs.clear(); NARCS.store(0, Ordering::SeqCst); STACK.lock().unwrap().clear(); 0 }
pub fn cleanup_(m: *const module) -> i32 {
let mut calls = CALLS.lock().unwrap();
freepfuncs(&mut calls); let mut arcs = ARCS.lock().unwrap();
freeparcs(&mut arcs); ZPROF_MODULE.store(false, Ordering::SeqCst);
setfeatureenables(m, module_features(), None) }
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0
}
pub static CALLS: Mutex<Vec<Pfunc>> = Mutex::new(Vec::new());
pub static NCALLS: AtomicI32 = AtomicI32::new(0);
pub static ARCS: Mutex<Vec<Parc>> = Mutex::new(Vec::new());
pub static NARCS: AtomicI32 = AtomicI32::new(0);
pub static STACK: Mutex<Vec<Sfunc>> = Mutex::new(Vec::new());
pub static ZPROF_MODULE: AtomicBool = AtomicBool::new(false);
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["b:zprof".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 1]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 0,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use crate::ported::zsh_h::funcstack;
use super::*;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn reset_state() {
let mut c = CALLS.lock().unwrap();
c.clear();
let mut a = ARCS.lock().unwrap();
a.clear();
STACK.lock().unwrap().clear();
NCALLS.store(0, Ordering::SeqCst);
NARCS.store(0, Ordering::SeqCst);
}
#[test]
fn pfunc_default_zeros() {
let _g = crate::test_util::global_state_lock();
let p = Pfunc::default();
assert_eq!(p.name, "");
assert_eq!(p.calls, 0);
assert_eq!(p.time, 0.0);
assert_eq!(p.self_time, 0.0);
assert_eq!(p.num, 0);
}
#[test]
fn freepfuncs_clears() {
let _g = crate::test_util::global_state_lock();
let mut v = vec![Pfunc {
name: "a".into(),
..Default::default()
}];
freepfuncs(&mut v);
assert!(v.is_empty());
}
#[test]
fn findpfunc_matches_by_name() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
CALLS.lock().unwrap().push(Pfunc {
name: "alpha".into(),
..Default::default()
});
CALLS.lock().unwrap().push(Pfunc {
name: "beta".into(),
..Default::default()
});
assert_eq!(findpfunc("alpha"), Some(0));
assert_eq!(findpfunc("beta"), Some(1));
assert_eq!(findpfunc("none"), None);
reset_state();
}
#[test]
fn findparc_matches_pair() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
ARCS.lock().unwrap().push(Parc {
from: 0,
to: 1,
..Default::default()
});
ARCS.lock().unwrap().push(Parc {
from: 0,
to: 2,
..Default::default()
});
assert_eq!(findparc(0, 1), Some(0));
assert_eq!(findparc(0, 2), Some(1));
assert_eq!(findparc(1, 0), None);
reset_state();
}
#[test]
fn cmpsfuncs_descending() {
let _g = crate::test_util::global_state_lock();
let a = Pfunc {
self_time: 5.0,
..Default::default()
};
let b = Pfunc {
self_time: 10.0,
..Default::default()
};
assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Greater);
assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Less);
}
#[test]
fn bin_zprof_clear_resets_tables() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
CALLS.lock().unwrap().push(Pfunc {
name: "x".into(),
..Default::default()
});
ARCS.lock().unwrap().push(Parc {
from: 0,
to: 0,
..Default::default()
});
NCALLS.store(1, Ordering::SeqCst);
NARCS.store(1, Ordering::SeqCst);
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'c' as usize] = 1;
let r = bin_zprof("zprof", &["-c".to_string()], &ops, 0);
assert_eq!(r, 0);
assert!(CALLS.lock().unwrap().is_empty());
assert!(ARCS.lock().unwrap().is_empty());
assert_eq!(NCALLS.load(Ordering::SeqCst), 0);
assert_eq!(NARCS.load(Ordering::SeqCst), 0);
}
#[test]
fn zprof_wrapper_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zprof_wrapper(std::ptr::null(), std::ptr::null(), "foo"), 0,);
}
#[test]
fn name_for_anonymous_function_format() {
let _g = crate::test_util::global_state_lock();
{
let mut stack = FUNCSTACK.lock().unwrap();
stack.clear();
stack.push(funcstack {
filename: Some("/tmp/foo.zsh".to_string()),
flineno: 42,
..Default::default()
});
}
let s = name_for_anonymous_function("anon");
FUNCSTACK
.lock()
.unwrap()
.clear();
assert_eq!(s, "anon [/tmp/foo.zsh:42]");
}
#[test]
fn name_for_anonymous_function_empty_funcstack_defaults() {
let _g = crate::test_util::global_state_lock();
FUNCSTACK
.lock()
.unwrap()
.clear();
let s = std::panic::catch_unwind(|| name_for_anonymous_function("anon"))
.expect("must not panic on empty funcstack");
assert_eq!(
s, "anon [:0]",
"empty funcstack → empty filename + 0 lineno; got {:?}",
s
);
}
#[test]
fn findpfunc_empty_table_returns_none() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
assert!(findpfunc("never-called").is_none());
}
#[test]
fn findpfunc_returns_correct_index_after_insert() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
CALLS.lock().unwrap().push(Pfunc {
name: "alpha".into(),
..Default::default()
});
CALLS.lock().unwrap().push(Pfunc {
name: "beta".into(),
..Default::default()
});
assert_eq!(findpfunc("alpha"), Some(0));
assert_eq!(findpfunc("beta"), Some(1));
assert!(findpfunc("gamma").is_none());
}
#[test]
fn findparc_empty_table_returns_none() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
assert!(findparc(0, 1).is_none());
}
#[test]
fn findparc_distinguishes_to_field() {
let _g = crate::test_util::global_state_lock();
let _g = TEST_LOCK.lock().unwrap();
reset_state();
ARCS.lock().unwrap().push(Parc {
from: 0,
to: 1,
..Default::default()
});
ARCS.lock().unwrap().push(Parc {
from: 0,
to: 2,
..Default::default()
});
assert_eq!(findparc(0, 1), Some(0));
assert_eq!(findparc(0, 2), Some(1));
assert!(findparc(0, 99).is_none());
assert!(findparc(99, 1).is_none());
}
#[test]
fn cmpsfuncs_compares_by_self_time_descending() {
let _g = crate::test_util::global_state_lock();
let high = Pfunc {
name: "_".into(),
self_time: 100.0,
..Default::default()
};
let low = Pfunc {
name: "_".into(),
self_time: 1.0,
..Default::default()
};
assert_eq!(cmpsfuncs(&high, &low), std::cmp::Ordering::Less);
assert_eq!(cmpsfuncs(&low, &high), std::cmp::Ordering::Greater);
assert_eq!(cmpsfuncs(&high, &high), std::cmp::Ordering::Equal);
}
#[test]
fn freepfuncs_empties_input_vec() {
let _g = crate::test_util::global_state_lock();
let mut v = vec![
Pfunc {
name: "x".into(),
..Default::default()
},
Pfunc {
name: "y".into(),
..Default::default()
},
];
freepfuncs(&mut v);
assert!(v.is_empty(), "freepfuncs must clear the input vec");
}
#[test]
fn freeparcs_empties_input_vec() {
let _g = crate::test_util::global_state_lock();
let mut v = vec![
Parc {
from: 0,
to: 1,
..Default::default()
},
Parc {
from: 2,
to: 3,
..Default::default()
},
];
freeparcs(&mut v);
assert!(v.is_empty(), "freeparcs must clear the input vec");
}
#[test]
fn cmpsfuncs_and_cmptfuncs_differ_when_fields_disagree() {
let _g = crate::test_util::global_state_lock();
let alpha = Pfunc {
name: "_".into(),
self_time: 100.0,
time: 1.0,
..Default::default()
};
let beta = Pfunc {
name: "_".into(),
self_time: 1.0,
time: 100.0,
..Default::default()
};
let by_self = cmpsfuncs(&alpha, &beta);
let by_time = cmptfuncs(&alpha, &beta);
assert_ne!(
by_self, by_time,
"cmpsfuncs (self_time) and cmptfuncs (time) must differ when fields disagree"
);
}
#[test]
fn zprof_corpus_cmpsfuncs_higher_self_time_first() {
let _g = crate::test_util::global_state_lock();
let high = Pfunc { name: "high".into(), self_time: 100.0, ..Default::default() };
let low = Pfunc { name: "low".into(), self_time: 1.0, ..Default::default() };
assert_eq!(cmpsfuncs(&high, &low), std::cmp::Ordering::Less,
"higher self_time sorts first");
}
#[test]
fn zprof_corpus_cmptfuncs_higher_time_first() {
let _g = crate::test_util::global_state_lock();
let high = Pfunc { name: "high".into(), time: 100.0, ..Default::default() };
let low = Pfunc { name: "low".into(), time: 1.0, ..Default::default() };
assert_eq!(cmptfuncs(&high, &low), std::cmp::Ordering::Less,
"higher total time sorts first");
}
#[test]
fn zprof_corpus_cmpsfuncs_equal_self_time() {
let _g = crate::test_util::global_state_lock();
let a = Pfunc { name: "a".into(), self_time: 5.0, ..Default::default() };
let b = Pfunc { name: "b".into(), self_time: 5.0, ..Default::default() };
let o = cmpsfuncs(&a, &b);
assert_ne!(o, std::cmp::Ordering::Greater);
}
#[test]
fn zprof_corpus_findpfunc_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(findpfunc("__never_seen_function__").is_none());
}
#[test]
fn zprof_corpus_findparc_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(findparc(usize::MAX, usize::MAX).is_none());
}
#[test]
fn zprof_corpus_name_for_anonymous_function_format() {
let _g = crate::test_util::global_state_lock();
let s = name_for_anonymous_function("(anon)");
assert!(s.starts_with("(anon)"), "starts with name, got {s:?}");
assert!(s.contains('['), "has opening bracket");
assert!(s.contains(']'), "has closing bracket");
assert!(s.contains(':'), "has line-number separator");
}
}