zsh/ported/modules/zprof.rs
1//! `zsh/zprof` module — port of `Src/Modules/zprof.c`.
2//!
3//! Shell-function profiling: every function call is wrapped via
4//! `zprof_wrapper` to record entry/exit time, build a per-function
5//! `Pfunc` table and a per-arc (caller→callee) `Parc` table, and
6//! emit a sorted report from `bin_zprof`.
7//!
8//! C source: 11 ported total — `freepfuncs`, `freeparcs`, `findpfunc`,
9//! `findparc`, `cmpsfuncs`, `cmptfuncs`, `cmpparcs`, `bin_zprof`,
10//! `name_for_anonymous_function`, `zprof_wrapper`, plus 6 module
11//! loaders. 3 structs: `pfunc` (c:38), `sfunc` (c:49), `parc` (c:57).
12//! 6 file-statics: `calls`, `ncalls`, `arcs`, `narcs`, `stack`,
13//! `zprof_module` (c:66-71).
14//!
15//! Order in this file mirrors C source order verbatim.
16
17use crate::ported::compat::zgettime_monotonic_if_available;
18use crate::ported::mem::ztrdup;
19use crate::ported::modules::parameter::FUNCSTACK;
20use crate::ported::zsh_h::{eprog, features, funcwrap, module, options, MAX_OPS, OPT_ISSET};
21use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
22use std::sync::{Mutex, OnceLock};
23// ---------------------------------------------------------------------------
24// Structs (port of c:36-64).
25// ---------------------------------------------------------------------------
26
27/// Port of `struct pfunc` from `Src/Modules/zprof.c:38`.
28/// Per-function aggregated profiling record.
29///
30/// C definition (c:38-45):
31/// ```c
32/// struct pfunc {
33/// Pfunc next; /* linked list — Vec replaces */
34/// char *name;
35/// long calls;
36/// double time;
37/// double self;
38/// long num;
39/// };
40/// ```
41#[derive(Debug, Clone, Default)]
42pub struct Pfunc {
43 // c:38
44 pub name: String, // c:40
45 pub calls: i64, // c:41
46 pub time: f64, // c:42
47 pub self_time: f64, // c:43 — `self` is a Rust keyword
48 pub num: i64, // c:44
49}
50
51/// Port of `struct sfunc` from `Src/Modules/zprof.c:49`.
52/// Per-active-call stack frame: linked stack the C `zprof_wrapper`
53/// pushes on entry and pops on exit, used to compute self-time and
54/// build the caller→callee arc.
55///
56/// C definition (c:49-53):
57/// ```c
58/// struct sfunc {
59/// Pfunc p; /* index into CALLS — Rust uses usize */
60/// Sfunc prev; /* linked list — Vec replaces */
61/// double beg;
62/// };
63/// ```
64#[derive(Debug, Clone, Copy)]
65pub struct Sfunc {
66 // c:49
67 pub p: usize, // c:50 — index into CALLS
68 pub beg: f64, // c:52
69}
70
71/// Port of `struct parc` from `Src/Modules/zprof.c:57`.
72/// Per-(caller→callee) aggregated arc with timing.
73///
74/// C definition (c:57-64):
75/// ```c
76/// struct parc {
77/// Parc next; /* linked list — Vec replaces */
78/// Pfunc from; /* indices into CALLS */
79/// Pfunc to;
80/// long calls;
81/// double time;
82/// double self;
83/// };
84/// ```
85#[derive(Debug, Clone, Default)]
86pub struct Parc {
87 // c:57
88 pub from: usize, // c:59 — index into CALLS
89 pub to: usize, // c:60 — index into CALLS
90 pub calls: i64, // c:61
91 pub time: f64, // c:62
92 pub self_time: f64, // c:63 — `self` is a Rust keyword
93}
94
95// ---------------------------------------------------------------------------
96// Helpers (port of c:73-136).
97// ---------------------------------------------------------------------------
98
99/// Port of `freepfuncs(Pfunc f)` from `Src/Modules/zprof.c:74`. C iterates
100/// the linked list calling `zsfree(name)` + `zfree(node)` on each
101/// entry. Rust port clears the `Vec`; the contained `String`s and
102/// `Pfunc` slots are dropped at scope-exit.
103///
104/// C signature: `static void freepfuncs(Pfunc f)`.
105pub fn freepfuncs(f: &mut Vec<Pfunc>) {
106 // c:74
107 f.clear(); // c:86-82 zsfree+zfree
108}
109
110/// Port of `freeparcs(Parc a)` from `Src/Modules/zprof.c:86`.
111///
112/// C signature: `static void freeparcs(Parc a)`.
113pub fn freeparcs(a: &mut Vec<Parc>) {
114 // c:86
115 a.clear(); // c:97-93 zfree
116}
117
118/// Port of `findpfunc(char *name)` from `Src/Modules/zprof.c:97`. Linear-scan
119/// lookup in the `calls` list for an entry with matching `name`.
120///
121/// C signature: `static Pfunc findpfunc(char *name)`. Returns NULL on
122/// miss; Rust port returns `None`.
123pub fn findpfunc(name: &str) -> Option<usize> {
124 // c:97
125 // c:109-103 — `for (f = calls; f; f = f->next) if (!strcmp(name, f->name)) return f;`
126 let calls = CALLS.lock().unwrap();
127 calls.iter().position(|f| f.name == name)
128}
129
130/// Port of `findparc(Pfunc f, Pfunc t)` from `Src/Modules/zprof.c:109`. Linear-scan
131/// lookup in the `arcs` list for an arc with matching (from, to)
132/// pair.
133///
134/// C signature: `static Parc findparc(Pfunc f, Pfunc t)`.
135pub fn findparc(f: usize, t: usize) -> Option<usize> {
136 // c:109
137 // c:109-115 — `for (a = arcs; a; a = a->next) if (a->f == f && a->t == t) return a;`
138 let arcs = ARCS.lock().unwrap();
139 arcs.iter().position(|a| a.from == f && a.to == t)
140}
141
142/// Port of `cmpsfuncs(Pfunc *a, Pfunc *b)` from `Src/Modules/zprof.c:121`. The qsort
143/// comparator: descending by `self`. C uses `Pfunc *` pointers
144/// because qsort passes opaque ptrs; Rust takes refs directly.
145///
146/// C body:
147/// ```c
148/// return ((*a)->self > (*b)->self ? -1 :
149/// ((*a)->self != (*b)->self));
150/// ```
151/// (i.e. -1 if a > b, 0 if equal, +1 if a < b — descending order.)
152pub fn cmpsfuncs(a: &Pfunc, b: &Pfunc) -> std::cmp::Ordering {
153 // c:121
154 b.self_time
155 .partial_cmp(&a.self_time)
156 .unwrap_or(std::cmp::Ordering::Equal)
157}
158
159/// Port of `cmptfuncs(Pfunc *a, Pfunc *b)` from `Src/Modules/zprof.c:127`. Comparator
160/// for descending by total `time`.
161pub fn cmptfuncs(a: &Pfunc, b: &Pfunc) -> std::cmp::Ordering {
162 // c:127
163 b.time
164 .partial_cmp(&a.time)
165 .unwrap_or(std::cmp::Ordering::Equal)
166}
167
168/// Port of `cmpparcs(Parc *a, Parc *b)` from `Src/Modules/zprof.c:133`. Comparator
169/// for descending by arc `time`.
170pub fn cmpparcs(a: &Parc, b: &Parc) -> std::cmp::Ordering {
171 // c:133
172 b.time
173 .partial_cmp(&a.time)
174 .unwrap_or(std::cmp::Ordering::Equal)
175}
176
177// ---------------------------------------------------------------------------
178// `bin_zprof` (port of c:139-214).
179// ---------------------------------------------------------------------------
180
181/// Port of `bin_zprof(UNUSED(char *nam), UNUSED(char **args), Options ops, UNUSED(int func))` from `Src/Modules/zprof.c:139`.
182///
183/// C signature: `static int bin_zprof(char *nam, char **args,
184/// Options ops, int func)`.
185/// Builtin spec: `"c"` (c:315) — the `-c` option clears the tables.
186/// No positional args (`0,0` arity at c:315).
187///
188/// `-c` set → free both tables and reset counters. `-c` unset →
189/// sort by self-time, print the c:170 header + per-function row,
190/// re-sort by total-time, print the c:184 per-function caller/callee
191/// blocks.
192/// WARNING: param names don't match C — Rust=(_nam, _args, _func) vs C=(nam, args, ops, func)
193pub fn bin_zprof(
194 _nam: &str,
195 _args: &[String], // c:139
196 ops: &options,
197 _func: i32,
198) -> i32 {
199 // c:140 — `if (OPT_ISSET(ops,'c'))`
200 let opt_c = OPT_ISSET(ops, b'c');
201
202 if opt_c {
203 // c:141-147 — free both tables + reset counters.
204 let mut calls = CALLS.lock().unwrap();
205 freepfuncs(&mut calls); // c:142
206 NCALLS.store(0, Ordering::SeqCst); // c:144
207 let mut arcs = ARCS.lock().unwrap();
208 freeparcs(&mut arcs); // c:145
209 NARCS.store(0, Ordering::SeqCst); // c:147
210 return 0; // c:213
211 }
212
213 // c:149-211 — print path.
214 let calls = CALLS.lock().unwrap();
215 let arcs = ARCS.lock().unwrap();
216
217 // c:149-163 — gather + total. C uses a VARARR Pfunc fs[ncalls+1]
218 // and a VARARR Parc as[narcs+1] with NULL sentinels; Rust uses
219 // index arrays. `total` is the sum of self-times across all funcs.
220 let mut fs: Vec<usize> = (0..calls.len()).collect(); // c:149-159
221 let mut as_arcs: Vec<usize> = (0..arcs.len()).collect(); // c:151-163
222 let mut total: f64 = 0.0; // c:154
223 for &i in &fs {
224 total += calls[i].self_time; // c:158 total += f->self;
225 }
226
227 // c:165-166 — `qsort(fs, ncalls, sizeof(f), cmpsfuncs);`
228 fs.sort_by(|&a, &b| cmpsfuncs(&calls[a], &calls[b]));
229 // c:167-168 — `qsort(as, narcs, sizeof(a), cmpparcs);`
230 // Prior port skipped this sort, so the per-function caller/callee
231 // blocks at c:184-211 listed arcs in chronological insertion order
232 // instead of descending-time order. With many callers, the listing
233 // buried the dominant time consumers under low-cost arcs, defeating
234 // zprof's "find the hot edges" purpose.
235 as_arcs.sort_by(|&a, &b| cmpparcs(&arcs[a], &arcs[b]));
236
237 // c:170 — header.
238 println!("num calls time self name");
239 println!("-----------------------------------------------------------------------------------");
240
241 // c:171-180 — primary listing, also assigns `num` in display order.
242 // Mutating `num` in C requires reborrowing — release the read lock
243 // briefly to take a write lock, then reacquire read order.
244 drop(calls);
245 {
246 let mut calls_w = CALLS.lock().unwrap();
247 for (i, &idx) in fs.iter().enumerate() {
248 // c:171
249 calls_w[idx].num = (i + 1) as i64; // c:173
250 }
251 }
252 let calls = CALLS.lock().unwrap();
253 for &idx in &fs {
254 // c:171 again, after num assignment
255 let f = &calls[idx];
256 let avg_t = if f.calls > 0 {
257 f.time / f.calls as f64
258 } else {
259 0.0
260 };
261 let avg_s = if f.calls > 0 {
262 f.self_time / f.calls as f64
263 } else {
264 0.0
265 };
266 let pct_t = if total != 0.0 {
267 (f.time / total) * 100.0
268 } else {
269 0.0
270 };
271 let pct_s = if total != 0.0 {
272 (f.self_time / total) * 100.0
273 } else {
274 0.0
275 };
276 println!(
277 "{:2}) {:4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {:6.2}% {}",
278 f.num,
279 f.calls, // c:172-179 printf
280 f.time,
281 avg_t,
282 pct_t,
283 f.self_time,
284 avg_s,
285 pct_s,
286 f.name
287 );
288 }
289
290 // c:181-182 — `qsort(fs, ncalls, sizeof(f), cmptfuncs);`
291 let mut fs_t: Vec<usize> = fs.clone();
292 fs_t.sort_by(|&a, &b| cmptfuncs(&calls[a], &calls[b]));
293
294 // c:184-211 — per-function caller/callee blocks.
295 for &fp_idx in &fs_t {
296 // c:184
297 println!();
298 println!(
299 "-----------------------------------------------------------------------------------"
300 );
301 println!();
302 let f = &calls[fp_idx];
303
304 // c:186-194 — callers (arcs where to == fp).
305 for &ap in &as_arcs {
306 // c:186
307 let a = &arcs[ap];
308 if a.to == fp_idx {
309 // c:187
310 let avg_t = if a.calls > 0 {
311 a.time / a.calls as f64
312 } else {
313 0.0
314 };
315 let avg_s = if a.calls > 0 {
316 a.self_time / a.calls as f64
317 } else {
318 0.0
319 };
320 let pct_t = if total != 0.0 {
321 (a.time / total) * 100.0
322 } else {
323 0.0
324 };
325 let from_name = &calls[a.from].name;
326 let from_num = calls[a.from].num;
327 println!(
328 " {:4}/{:<4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {} [{}]",
329 a.calls,
330 f.calls, // c:188-193 printf
331 a.time,
332 avg_t,
333 pct_t,
334 a.self_time,
335 avg_s,
336 from_name,
337 from_num
338 );
339 }
340 }
341
342 // c:195-201 — the function's own row.
343 let avg_t = if f.calls > 0 {
344 f.time / f.calls as f64
345 } else {
346 0.0
347 };
348 let avg_s = if f.calls > 0 {
349 f.self_time / f.calls as f64
350 } else {
351 0.0
352 };
353 let pct_t = if total != 0.0 {
354 (f.time / total) * 100.0
355 } else {
356 0.0
357 };
358 let pct_s = if total != 0.0 {
359 (f.self_time / total) * 100.0
360 } else {
361 0.0
362 };
363 println!(
364 "{:2}) {:4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {:6.2}% {}",
365 f.num,
366 f.calls, // c:195-201 printf
367 f.time,
368 avg_t,
369 pct_t,
370 f.self_time,
371 avg_s,
372 pct_s,
373 f.name
374 );
375
376 // c:202-210 — callees (arcs where from == fp), iterated in
377 // reverse to match C's `for (ap = as + narcs - 1; ap >= as; ap--)`.
378 for &ap in as_arcs.iter().rev() {
379 // c:202
380 let a = &arcs[ap];
381 if a.from == fp_idx {
382 // c:203
383 let avg_t = if a.calls > 0 {
384 a.time / a.calls as f64
385 } else {
386 0.0
387 };
388 let avg_s = if a.calls > 0 {
389 a.self_time / a.calls as f64
390 } else {
391 0.0
392 };
393 let pct_t = if total != 0.0 {
394 (a.time / total) * 100.0
395 } else {
396 0.0
397 };
398 let to_name = &calls[a.to].name;
399 let to_num = calls[a.to].num;
400 let to_calls = calls[a.to].calls;
401 println!(
402 " {:4}/{:<4} {:8.2} {:8.2} {:6.2}% {:8.2} {:8.2} {} [{}]",
403 a.calls,
404 to_calls, // c:204-209 printf
405 a.time,
406 avg_t,
407 pct_t,
408 a.self_time,
409 avg_s,
410 to_name,
411 to_num
412 );
413 }
414 }
415 }
416
417 0 // c:217
418}
419
420/// Port of `name_for_anonymous_function(char *name)` from `Src/Modules/zprof.c:217`.
421/// Anonymous functions don't have a real name; the profiler synthesises
422/// `name [filename:lineno]` using the current `funcstack[0]` frame.
423///
424/// C signature: `static char *name_for_anonymous_function(char *name)`.
425pub fn name_for_anonymous_function(name: &str) -> String {
426 // c:217
427 // c:219 — char lineno[DIGBUFSIZE];
428 // c:220 — char *parts[7];
429 // c:222 — convbase(lineno, funcstack[0].flineno, 10);
430 let stack = FUNCSTACK.lock().expect("FUNCSTACK poisoned");
431 let flineno = stack.first().map(|f| f.flineno).unwrap_or(0); // c:222
432 let filename = stack
433 .first()
434 .and_then(|f| f.filename.clone())
435 .unwrap_or_default(); // c:226
436 drop(stack);
437 let lineno_str = format!("{}", flineno); // c:222 convbase base=10
438 // c:224-230 — parts[] = { name, " [", filename, ":", lineno, "]", NULL };
439 // c:232 — return sepjoin(parts, "", 1);
440 let parts = [name, " [", filename.as_str(), ":", lineno_str.as_str(), "]"];
441 parts.concat() // c:232
442}
443
444/// Port of `zprof_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Modules/zprof.c:236`. The
445/// per-function-call wrapper hook: records call entry, measures wall
446/// time, runs the wrapped function via `runshfunc`, then accumulates
447/// self/total time on the function's `pfunc` entry and on the
448/// (caller→callee) `parc` arc.
449///
450/// C signature: `static int zprof_wrapper(Eprog prog, FuncWrap w, char *name)`.
451///
452/// C body (c:238-311):
453/// 1. Resolve `name_for_lookups` via `name_for_anonymous_function` for
454/// anonymous funcs (c:246-250).
455/// 2. If `zprof_module` is loaded (c:252), find-or-create the Pfunc
456/// (c:254-262), find-or-create the caller→callee Parc (c:263-274),
457/// push the Sfunc frame and record start time (c:275-283).
458/// 3. `runshfunc(prog, w, name)` (c:285) — runs the wrapped function.
459/// 4. On return, recompute elapsed time, update Pfunc.self_time
460/// (c:293), Pfunc.time when non-recursive (c:294-296), Parc.calls/
461/// self/time (c:297-307), pop the stack frame (c:301).
462///
463/// zshrs's call-execution path doesn't have an `addwrapper`-installable
464/// runshfunc callback, so the live integration is the executor's
465/// funcstack push/pop hooks (in `crate::ported::exec`). This entry is
466/// the static-link stub that mirrors C's `return 0;` exit path; the
467/// actual timing accumulation happens via direct CALLS/ARCS/STACK
468/// updates from the executor when `ZPROF_MODULE` is true.
469/// Port of `static int zprof_wrapper(Eprog prog, FuncWrap w, char *name)`
470/// from `Src/Modules/zprof.c:236`.
471///
472/// ```c
473/// static int
474/// zprof_wrapper(Eprog prog, FuncWrap w, char *name)
475/// {
476/// int active = 0;
477/// struct sfunc sf, *sp;
478/// Pfunc f = NULL;
479/// Parc a = NULL;
480/// struct timespec ts;
481/// double prev = 0, now;
482/// char *name_for_lookups;
483/// if (is_anonymous_function_name(name))
484/// name_for_lookups = name_for_anonymous_function(name);
485/// else
486/// name_for_lookups = name;
487/// if (zprof_module && !(zprof_module->node.flags & MOD_UNLOAD)) {
488/// active = 1;
489/// if (!(f = findpfunc(name_for_lookups))) { ... append calls ... }
490/// if (stack) {
491/// if (!(a = findparc(stack->p, f))) { ... append arcs ... }
492/// }
493/// sf.prev = stack; sf.p = f; stack = &sf;
494/// f->calls++;
495/// zgettime_monotonic_if_available(&ts);
496/// sf.beg = prev = ms_now(ts);
497/// }
498/// runshfunc(prog, w, name);
499/// if (active) {
500/// if (zprof_module && !(zprof_module->node.flags & MOD_UNLOAD)) {
501/// zgettime_monotonic_if_available(&ts);
502/// now = ms_now(ts);
503/// f->self += now - sf.beg;
504/// for (sp = sf.prev; sp && sp->p != f; sp = sp->prev);
505/// if (!sp) f->time += now - prev;
506/// if (a) { a->calls++; a->self += now - sf.beg; }
507/// stack = sf.prev;
508/// if (stack) { stack->beg += now - prev;
509/// if (a) a->time += now - prev; }
510/// } else stack = sf.prev;
511/// }
512/// return 0;
513/// }
514/// ```
515#[allow(non_snake_case)]
516pub fn zprof_wrapper(
517 prog: *const eprog, // c:236
518 w: *const funcwrap,
519 name: &str,
520 runshfunc: impl FnOnce(),
521) -> i32 {
522 let mut active: i32 = 0; // c:238
523 let mut sf = Sfunc { p: 0, beg: 0.0 }; // c:239 struct sfunc sf
524 let mut f: Option<usize> = None; // c:240 Pfunc f = NULL
525 let mut a: Option<usize> = None; // c:241 Parc a = NULL
526 let mut prev: f64 = 0.0; // c:243 double prev = 0
527
528 // c:246-250 — resolve display name for anonymous functions.
529 // `is_anonymous_function_name(name)` is `!strcmp(name, "(anon)")`
530 // per Src/exec.c:5303-5306. ANONYMOUS_FUNCTION_NAME = "(anon)".
531 let name_for_lookups: String = if name == "(anon)" {
532 // c:246
533 // `name_for_anonymous_function(name)` reads funcstack[0]
534 // internally (S1 rule — signature matches C).
535 name_for_anonymous_function(name) // c:247
536 } else {
537 // c:248
538 name.to_string() // c:249
539 };
540
541 if ZPROF_MODULE.load(Ordering::SeqCst) {
542 // c:252
543 active = 1; // c:253
544 f = findpfunc(&name_for_lookups); // c:254
545 if f.is_none() {
546 // c:254
547 // c:255-261 — `f = zalloc(...); f->name = ztrdup(...); f->next = calls; calls = f; ncalls++;`
548 let new_pfunc = Pfunc {
549 // c:255
550 name: ztrdup(&name_for_lookups), // c:256
551 calls: 0, // c:257
552 time: 0.0, // c:258 self/time = 0
553 self_time: 0.0, // c:258
554 num: 0,
555 };
556 let mut calls = CALLS.lock().unwrap();
557 f = Some(calls.len()); // c:260 head-insert in C; Rust appends
558 calls.push(new_pfunc); // c:260
559 NCALLS.fetch_add(1, Ordering::SeqCst); // c:261
560 }
561 // c:263 — `if (stack)` — top-of-stack frame exists.
562 let stack_top: Option<Sfunc> = {
563 // c:263
564 let st = STACK.lock().unwrap();
565 st.last().copied()
566 };
567 if let Some(top) = stack_top {
568 // c:263
569 a = findparc(top.p, f.unwrap()); // c:264
570 if a.is_none() {
571 // c:264
572 let new_parc = Parc {
573 // c:265
574 from: top.p, // c:266
575 to: f.unwrap(), // c:267
576 calls: 0, // c:268
577 self_time: 0.0, // c:269
578 time: 0.0, // c:269
579 };
580 let mut arcs = ARCS.lock().unwrap();
581 a = Some(arcs.len()); // c:271
582 arcs.push(new_parc); // c:271
583 NARCS.fetch_add(1, Ordering::SeqCst); // c:272
584 }
585 }
586 // c:275-277 — `sf.prev = stack; sf.p = f; stack = &sf;`
587 sf.p = f.unwrap(); // c:276
588 STACK.lock().unwrap().push(sf); // c:277 stack = &sf
589
590 // c:279 — `f->calls++;`
591 {
592 let mut calls = CALLS.lock().unwrap();
593 calls[f.unwrap()].calls += 1; // c:279
594 }
595 // c:280-283 — read monotonic clock, compute prev (ms).
596 let mut ts = libc::timespec {
597 tv_sec: 0,
598 tv_nsec: 0,
599 }; // c:280
600 zgettime_monotonic_if_available(&mut ts); // c:281
601 sf.beg = (ts.tv_sec as f64) * 1000.0 + (ts.tv_nsec as f64) / 1_000_000.0; // c:282-283
602 prev = sf.beg; // c:282
603 // Update the stack-top copy we just pushed.
604 let mut st = STACK.lock().unwrap();
605 if let Some(top) = st.last_mut() {
606 top.beg = sf.beg;
607 }
608 }
609
610 // c:285 — `runshfunc(prog, w, name);` — the function-under-profile
611 // runs HERE, between the c:282 start-timestamp and the c:289 end
612 // read. Taken as the FnOnce runner (same shape as param_private's
613 // wrap_private, 90dfda9df7) so the timing actually brackets the
614 // call. A prior discarded placeholder meant every profiled time
615 // measured the wrapper's own overhead (~0ms) instead of the
616 // function.
617 let _ = (prog, w);
618 runshfunc(); // c:285
619
620 if active != 0 {
621 // c:286
622 if ZPROF_MODULE.load(Ordering::SeqCst) {
623 // c:287
624 let mut ts = libc::timespec {
625 tv_sec: 0,
626 tv_nsec: 0,
627 }; // c:288
628 zgettime_monotonic_if_available(&mut ts); // c:289
629 let now = (ts.tv_sec as f64) * 1000.0 + (ts.tv_nsec as f64) / 1_000_000.0; // c:291-292
630
631 // C's `sf` IS the top of the `stack` list (c:277 `stack = &sf;`),
632 // so every nested call's c:304 `stack->beg += now - prev` has
633 // been landing on THIS frame's `beg` — that is precisely how
634 // c:293/c:299 subtract child time out of self-time. The Rust
635 // port pushed a COPY of `sf` onto `STACK`, and c:304 wrote to
636 // the copy, so `sf.beg` here is the un-incremented start time.
637 // Read the live value back before using it.
638 sf.beg = {
639 let st = STACK.lock().unwrap();
640 st.last().map(|top| top.beg).unwrap_or(sf.beg)
641 };
642
643 // c:293 — `f->self += now - sf.beg;`
644 {
645 let mut calls = CALLS.lock().unwrap();
646 if let Some(idx) = f {
647 calls[idx].self_time += now - sf.beg; // c:293
648 }
649 }
650 // c:294 — recursion-detect: walk sf.prev looking for f.
651 let recursion: bool = {
652 // c:294
653 let st = STACK.lock().unwrap();
654 let cur_f = f.unwrap();
655 // sf.prev = the frame underneath sf — walk it down.
656 st.iter().rev().skip(1).any(|fr| fr.p == cur_f)
657 };
658 if !recursion {
659 // c:295
660 let mut calls = CALLS.lock().unwrap();
661 if let Some(idx) = f {
662 calls[idx].time += now - prev; // c:296
663 }
664 }
665 if let Some(arc_idx) = a {
666 // c:297
667 let mut arcs = ARCS.lock().unwrap();
668 arcs[arc_idx].calls += 1; // c:298
669 arcs[arc_idx].self_time += now - sf.beg; // c:299
670 }
671 // c:301 — `stack = sf.prev;`
672 {
673 let mut st = STACK.lock().unwrap();
674 st.pop(); // c:301
675 }
676 // c:303-307 — propagate elapsed up to caller frame.
677 let mut st = STACK.lock().unwrap();
678 if let Some(top) = st.last_mut() {
679 // c:303
680 top.beg += now - prev; // c:304
681 if let Some(arc_idx) = a {
682 // c:305
683 drop(st);
684 let mut arcs = ARCS.lock().unwrap();
685 arcs[arc_idx].time += now - prev; // c:306
686 }
687 }
688 } else {
689 // c:308
690 // c:309 — `stack = sf.prev;`
691 let mut st = STACK.lock().unwrap();
692 st.pop();
693 }
694 }
695 0 // c:311
696}
697
698// `bintab` — port of `static struct builtin bintab[]` (zprof.c:309).
699
700// `module_features` — port of `static struct features module_features`
701// from zprof.c:323.
702
703/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zprof.c:332`.
704/// C body: `zprof_module = m; return 0;`
705#[allow(unused_variables)]
706pub fn setup_(m: *const module) -> i32 {
707 // c:332
708 ZPROF_MODULE.store(true, Ordering::SeqCst); // c:340
709 0 // c:348
710}
711
712/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zprof.c:340`.
713/// C body: `*features = featuresarray(m, &module_features); return 0;`
714pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
715 // c:340
716 *features = featuresarray(m, module_features());
717 0 // c:355
718}
719
720/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zprof.c:348`.
721/// C body: `return handlefeatures(m, &module_features, enables);`
722pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
723 // c:348
724 handlefeatures(m, module_features(), enables) // c:355
725}
726
727/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zprof.c:355`.
728#[allow(unused_variables)]
729pub fn boot_(m: *const module) -> i32 {
730 // c:355
731 let mut calls = CALLS.lock().unwrap();
732 calls.clear(); // c:357 calls = NULL
733 NCALLS.store(0, Ordering::SeqCst); // c:358 ncalls = 0
734 let mut arcs = ARCS.lock().unwrap();
735 arcs.clear(); // c:359 arcs = NULL
736 NARCS.store(0, Ordering::SeqCst); // c:360 narcs = 0
737 STACK.lock().unwrap().clear(); // c:361 stack = NULL
738 drop(calls);
739 drop(arcs);
740 // c:362 — `return addwrapper(m, wrapper);`. The module arrives as a
741 // name because zshrs's module dispatcher passes a null
742 // `*const module` (module.rs:4140); see `addwrapper`'s WARNING.
743 //
744 // `wrapper` is C's file-static array at c:318-320:
745 // static struct funcwrap wrapper[] = { WRAPDEF(zprof_wrapper), };
746 // `WRAPDEF` (`Src/zsh.h:1370-1371`) expands to `{ NULL, 0, func, NULL }`.
747 // It's data, not a function, so it's built inline at its one use.
748 // `handler` stays `None` rather than holding `zprof_wrapper`: a
749 // `WrapFunc` (`Src/zsh.h:1359`) is `int (*)(Eprog, FuncWrap, char *)`,
750 // and this port's `zprof_wrapper` takes the function body as a
751 // delegate because a zshrs function body is a caller-supplied closure,
752 // not a walkable `Eprog` — see the `BodyWrap` doc block in
753 // `src/ported/exec.rs` (`Src/exec.c:6166` `runshfunc`).
754 // `exec::runshfunc` therefore dispatches on the owning module, and
755 // this node is the `addwrapper`/`deletewrapper` bookkeeping entry C
756 // keeps in the same list.
757 crate::ported::module::addwrapper(
758 "zsh/zprof",
759 funcwrap {
760 next: None, // c:1371 WRAPDEF
761 flags: 0, // c:1371
762 handler: None, // c:1371 — WRAPDEF(zprof_wrapper); see above
763 module: None, // c:1371
764 },
765 ) // c:362
766}
767
768/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zprof.c:367`.
769/// C body: free pfuncs + parcs, deletewrapper, setfeatureenables.
770pub fn cleanup_(m: *const module) -> i32 {
771 // c:367
772 let mut calls = CALLS.lock().unwrap();
773 freepfuncs(&mut calls); // c:369 freepfuncs(calls)
774 let mut arcs = ARCS.lock().unwrap();
775 freeparcs(&mut arcs); // c:370 freeparcs(arcs)
776 drop(calls);
777 drop(arcs);
778 ZPROF_MODULE.store(false, Ordering::SeqCst);
779 // c:371 — `deletewrapper(m, wrapper);` (return value discarded in C).
780 let _ = crate::ported::module::deletewrapper("zsh/zprof"); // c:371
781 setfeatureenables(m, module_features(), None) // c:372
782}
783
784/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zprof.c:377`.
785#[allow(unused_variables)]
786pub fn finish_(m: *const module) -> i32 {
787 // c:377
788 // C body c:379-380 — `return 0`. Faithful empty-body port; the
789 // profiling tables get freed by cleanup_ via
790 // setfeatureenables/zprof_cleanup.
791 0
792}
793
794// ---------------------------------------------------------------------------
795// Module loaders.
796// ---------------------------------------------------------------------------
797
798// =====================================================================
799// static struct builtin bintab[] c:309
800// static struct features module_features c:323
801// static struct funcwrap wrapper[] c:328
802// =====================================================================
803
804// ---------------------------------------------------------------------------
805// File-static globals — port of c:66-71.
806// ---------------------------------------------------------------------------
807
808/// Port of `static Pfunc calls;` from `Src/Modules/zprof.c:66`.
809/// Per-function aggregated table; the C linked list becomes a
810/// `Mutex<Vec<Pfunc>>` so `Pfunc *` becomes `usize` index.
811pub static CALLS: Mutex<Vec<Pfunc>> = Mutex::new(Vec::new()); // c:66
812
813/// Port of `static int ncalls;` from `Src/Modules/zprof.c:67`. Always
814/// equals `CALLS.lock().len()` — kept as an explicit counter to
815/// match C's `ncalls++` increment pattern.
816pub static NCALLS: AtomicI32 = AtomicI32::new(0); // c:67
817
818/// Port of `static Parc arcs;` from `Src/Modules/zprof.c:68`.
819pub static ARCS: Mutex<Vec<Parc>> = Mutex::new(Vec::new()); // c:68
820
821/// Port of `static int narcs;` from `Src/Modules/zprof.c:69`.
822pub static NARCS: AtomicI32 = AtomicI32::new(0); // c:69
823
824/// Port of `static Sfunc stack;` from `Src/Modules/zprof.c:70`. The
825/// C linked stack becomes a `Mutex<Vec<Sfunc>>` (top of stack at
826/// `last()`).
827pub static STACK: Mutex<Vec<Sfunc>> = Mutex::new(Vec::new()); // c:70
828
829/// Port of `static Module zprof_module;` from `Src/Modules/zprof.c:71`.
830/// C uses a `Module` (struct module *) pointer to track which module
831/// owns the wrapper; `zprof_wrapper` short-circuits when
832/// `MOD_UNLOAD` is set on it. Module is ported as
833/// `Box<crate::ported::zsh_h::module>` (zsh_h.rs:425) but recording
834/// the raw `*const module` would deadlock with Sync/Send for the
835/// static — `AtomicBool` captures the only state `zprof_wrapper`
836/// actually inspects (loaded vs. unloading), matching the C
837/// `MOD_UNLOAD` flag-check on the same pointer.
838pub static ZPROF_MODULE: AtomicBool = AtomicBool::new(false); // c:74
839
840static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
841
842// Local stubs for the per-module entry points. C uses generic
843// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
844// 3275/3370/3445) but those take `Builtin` + `Features` pointer
845// fields the Rust port doesn't carry. The hardcoded descriptor
846// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
847// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
848// C uses generic featuresarray/handlefeatures/setfeatureenables from
849// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
850// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
851fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
852 vec!["b:zprof".to_string()]
853}
854
855// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
856// C uses generic featuresarray/handlefeatures/setfeatureenables from
857// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
858// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
859fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
860 // c:3392 — the name-keyed variant in src/ported/module.rs; this
861 // module ships no `Features` descriptor tables for the per-feature
862 // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
863 crate::ported::module::handlefeatures("zsh/zprof", &featuresarray(m, f), enables)
864}
865
866// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
867// C uses generic featuresarray/handlefeatures/setfeatureenables from
868// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
869// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
870fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
871 0
872}
873
874// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
875// ─── RUST-ONLY ACCESSORS ───
876//
877// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
878// RwLock<T>>` globals declared above. C zsh uses direct global
879// access; Rust needs these wrappers because `OnceLock::get_or_init`
880// is the only way to lazily construct shared state. These ported sit
881// here so the body of this file reads in C source order without
882// the accessor wrappers interleaved between real port ported.
883// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
884
885// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
886// ─── RUST-ONLY ACCESSORS ───
887//
888// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
889// RwLock<T>>` globals declared above. C zsh uses direct global
890// access; Rust needs these wrappers because `OnceLock::get_or_init`
891// is the only way to lazily construct shared state. These ported sit
892// here so the body of this file reads in C source order without
893// the accessor wrappers interleaved between real port ported.
894// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
895
896// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
897// C uses generic featuresarray/handlefeatures/setfeatureenables from
898// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
899// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
900fn module_features() -> &'static Mutex<features> {
901 MODULE_FEATURES.get_or_init(|| {
902 Mutex::new(features {
903 bn_list: None,
904 bn_size: 1,
905 cd_list: None,
906 cd_size: 0,
907 mf_list: None,
908 mf_size: 0,
909 pd_list: None,
910 pd_size: 0,
911 n_abstract: 0,
912 })
913 })
914}
915
916#[cfg(test)]
917mod tests {
918 use super::*;
919 use crate::ported::zsh_h::funcstack;
920
921 /// Serialise tests that mutate the module-static globals so the
922 /// cargo-test parallel runner doesn't shred each other's state.
923 static TEST_LOCK: Mutex<()> = Mutex::new(());
924
925 fn reset_state() {
926 let mut c = CALLS.lock().unwrap();
927 c.clear();
928 let mut a = ARCS.lock().unwrap();
929 a.clear();
930 STACK.lock().unwrap().clear();
931 NCALLS.store(0, Ordering::SeqCst);
932 NARCS.store(0, Ordering::SeqCst);
933 }
934
935 /// Port of `bin_zprof(UNUSED(char *nam), UNUSED(char **args), Options ops, UNUSED(int func))` from `Src/Modules/zprof.c:139`.
936 /// Verifies `Pfunc` mirrors C `struct pfunc` field-for-field
937 /// (name/calls/time/self/num at c:40-44).
938 #[test]
939 fn pfunc_default_zeros() {
940 let _g = crate::test_util::global_state_lock();
941 let p = Pfunc::default();
942 assert_eq!(p.name, "");
943 assert_eq!(p.calls, 0);
944 assert_eq!(p.time, 0.0);
945 assert_eq!(p.self_time, 0.0);
946 assert_eq!(p.num, 0);
947 }
948
949 /// Verifies `freepfuncs` empties the table (c:78-82 zsfree+zfree).
950 #[test]
951 fn freepfuncs_clears() {
952 let _g = crate::test_util::global_state_lock();
953 let mut v = vec![Pfunc {
954 name: "a".into(),
955 ..Default::default()
956 }];
957 freepfuncs(&mut v);
958 assert!(v.is_empty());
959 }
960
961 /// Verifies `findpfunc` linear-scan match (c:101-103).
962 #[test]
963 fn findpfunc_matches_by_name() {
964 let _g = crate::test_util::global_state_lock();
965 let _g = TEST_LOCK.lock().unwrap();
966 reset_state();
967 CALLS.lock().unwrap().push(Pfunc {
968 name: "alpha".into(),
969 ..Default::default()
970 });
971 CALLS.lock().unwrap().push(Pfunc {
972 name: "beta".into(),
973 ..Default::default()
974 });
975 assert_eq!(findpfunc("alpha"), Some(0));
976 assert_eq!(findpfunc("beta"), Some(1));
977 assert_eq!(findpfunc("none"), None);
978 reset_state();
979 }
980
981 /// Verifies `findparc` matches (from, to) pair (c:113-115).
982 #[test]
983 fn findparc_matches_pair() {
984 let _g = crate::test_util::global_state_lock();
985 let _g = TEST_LOCK.lock().unwrap();
986 reset_state();
987 ARCS.lock().unwrap().push(Parc {
988 from: 0,
989 to: 1,
990 ..Default::default()
991 });
992 ARCS.lock().unwrap().push(Parc {
993 from: 0,
994 to: 2,
995 ..Default::default()
996 });
997 assert_eq!(findparc(0, 1), Some(0));
998 assert_eq!(findparc(0, 2), Some(1));
999 assert_eq!(findparc(1, 0), None);
1000 reset_state();
1001 }
1002
1003 /// Verifies `cmpsfuncs` is descending (c:121-124).
1004 #[test]
1005 fn cmpsfuncs_descending() {
1006 let _g = crate::test_util::global_state_lock();
1007 let a = Pfunc {
1008 self_time: 5.0,
1009 ..Default::default()
1010 };
1011 let b = Pfunc {
1012 self_time: 10.0,
1013 ..Default::default()
1014 };
1015 // descending: b should come before a → cmp(a, b) = Greater
1016 assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Greater);
1017 assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Less);
1018 }
1019
1020 /// Verifies `bin_zprof -c` clears state (c:141-147).
1021 #[test]
1022 fn bin_zprof_clear_resets_tables() {
1023 let _g = crate::test_util::global_state_lock();
1024 let _g = TEST_LOCK.lock().unwrap();
1025 reset_state();
1026 CALLS.lock().unwrap().push(Pfunc {
1027 name: "x".into(),
1028 ..Default::default()
1029 });
1030 ARCS.lock().unwrap().push(Parc {
1031 from: 0,
1032 to: 0,
1033 ..Default::default()
1034 });
1035 NCALLS.store(1, Ordering::SeqCst);
1036 NARCS.store(1, Ordering::SeqCst);
1037
1038 let mut ops = options {
1039 ind: [0u8; MAX_OPS],
1040 args: Vec::new(),
1041 argscount: 0,
1042 argsalloc: 0,
1043 };
1044 ops.ind[b'c' as usize] = 1;
1045 let r = bin_zprof("zprof", &["-c".to_string()], &ops, 0);
1046 assert_eq!(r, 0);
1047 assert!(CALLS.lock().unwrap().is_empty());
1048 assert!(ARCS.lock().unwrap().is_empty());
1049 assert_eq!(NCALLS.load(Ordering::SeqCst), 0);
1050 assert_eq!(NARCS.load(Ordering::SeqCst), 0);
1051 }
1052
1053 /// Verifies `zprof_wrapper` returns 0 (the static-link no-op
1054 /// path mirrors C's `return 0;` exit at c:311).
1055 #[test]
1056 fn zprof_wrapper_returns_zero() {
1057 let _g = crate::test_util::global_state_lock();
1058 assert_eq!(
1059 zprof_wrapper(std::ptr::null(), std::ptr::null(), "foo", || {}),
1060 0,
1061 );
1062 }
1063
1064 /// Verifies `name_for_anonymous_function` formats as
1065 /// `name [filename:lineno]` per c:224-232, reading filename
1066 /// and flineno from `funcstack[0]` per S1 rule.
1067 #[test]
1068 fn name_for_anonymous_function_format() {
1069 let _g = crate::test_util::global_state_lock();
1070 // Push a frame onto FUNCSTACK so the fn reads it.
1071 {
1072 let mut stack = FUNCSTACK.lock().unwrap();
1073 stack.clear();
1074 stack.push(funcstack {
1075 filename: Some("/tmp/foo.zsh".to_string()),
1076 flineno: 42,
1077 ..Default::default()
1078 });
1079 }
1080 let s = name_for_anonymous_function("anon");
1081 // Cleanup so subsequent tests aren't polluted.
1082 FUNCSTACK.lock().unwrap().clear();
1083 assert_eq!(s, "anon [/tmp/foo.zsh:42]");
1084 }
1085
1086 /// `name_for_anonymous_function` with an EMPTY funcstack must
1087 /// return `"name [:0]"` — empty filename + zero lineno — not
1088 /// panic on the unwrap. The C body's `funcstack[0].flineno`
1089 /// would segfault on an empty stack; the Rust port must defend
1090 /// because nothing in zsh prevents the profiler from being
1091 /// invoked before the first function frame is pushed (e.g.
1092 /// during init scripts that contain anonymous functions at top
1093 /// level).
1094 #[test]
1095 fn name_for_anonymous_function_empty_funcstack_defaults() {
1096 let _g = crate::test_util::global_state_lock();
1097 // Ensure stack is empty.
1098 FUNCSTACK.lock().unwrap().clear();
1099 // No panic on first().unwrap() — the fn uses Option chains.
1100 let s = std::panic::catch_unwind(|| name_for_anonymous_function("anon"))
1101 .expect("must not panic on empty funcstack");
1102 assert_eq!(
1103 s, "anon [:0]",
1104 "empty funcstack → empty filename + 0 lineno; got {:?}",
1105 s
1106 );
1107 }
1108
1109 /// c:97 — `findpfunc` on an empty table returns None. A
1110 /// regression that returns 0 (a valid index!) would silently
1111 /// corrupt every subsequent per-function profile accumulation.
1112 #[test]
1113 fn findpfunc_empty_table_returns_none() {
1114 let _g = crate::test_util::global_state_lock();
1115 let _g = TEST_LOCK.lock().unwrap();
1116 reset_state();
1117 assert!(findpfunc("never-called").is_none());
1118 }
1119
1120 /// c:97 — `findpfunc` after two inserts returns the right index.
1121 /// Pin the index-zero-based contract because the find result
1122 /// feeds back into CALLS[i].
1123 #[test]
1124 fn findpfunc_returns_correct_index_after_insert() {
1125 let _g = crate::test_util::global_state_lock();
1126 let _g = TEST_LOCK.lock().unwrap();
1127 reset_state();
1128 CALLS.lock().unwrap().push(Pfunc {
1129 name: "alpha".into(),
1130 ..Default::default()
1131 });
1132 CALLS.lock().unwrap().push(Pfunc {
1133 name: "beta".into(),
1134 ..Default::default()
1135 });
1136 assert_eq!(findpfunc("alpha"), Some(0));
1137 assert_eq!(findpfunc("beta"), Some(1));
1138 assert!(findpfunc("gamma").is_none());
1139 }
1140
1141 /// c:109 — `findparc(f, t)` on an empty arcs table → None.
1142 #[test]
1143 fn findparc_empty_table_returns_none() {
1144 let _g = crate::test_util::global_state_lock();
1145 let _g = TEST_LOCK.lock().unwrap();
1146 reset_state();
1147 assert!(findparc(0, 1).is_none());
1148 }
1149
1150 /// c:109 — `findparc` distinguishes (f1, t1) from (f1, t2):
1151 /// same `from`, different `to` is a different arc.
1152 #[test]
1153 fn findparc_distinguishes_to_field() {
1154 let _g = crate::test_util::global_state_lock();
1155 let _g = TEST_LOCK.lock().unwrap();
1156 reset_state();
1157 ARCS.lock().unwrap().push(Parc {
1158 from: 0,
1159 to: 1,
1160 ..Default::default()
1161 });
1162 ARCS.lock().unwrap().push(Parc {
1163 from: 0,
1164 to: 2,
1165 ..Default::default()
1166 });
1167 assert_eq!(findparc(0, 1), Some(0));
1168 assert_eq!(findparc(0, 2), Some(1));
1169 assert!(findparc(0, 99).is_none());
1170 assert!(findparc(99, 1).is_none());
1171 }
1172
1173 /// c:121 — `cmpsfuncs` compares by `self_time` DESCENDING (C
1174 /// source: `(int)((*b)->self < (*a)->self) - (int)((*a)->self < (*b)->self)`
1175 /// — i.e. higher self_time sorts first). Pin the direction
1176 /// because a regen that flips to ascending would silently
1177 /// invert the user-facing `zprof` output ordering.
1178 #[test]
1179 fn cmpsfuncs_compares_by_self_time_descending() {
1180 let _g = crate::test_util::global_state_lock();
1181 let high = Pfunc {
1182 name: "_".into(),
1183 self_time: 100.0,
1184 ..Default::default()
1185 };
1186 let low = Pfunc {
1187 name: "_".into(),
1188 self_time: 1.0,
1189 ..Default::default()
1190 };
1191 // higher self_time sorts FIRST → Ordering::Less for (high, low)
1192 assert_eq!(cmpsfuncs(&high, &low), std::cmp::Ordering::Less);
1193 assert_eq!(cmpsfuncs(&low, &high), std::cmp::Ordering::Greater);
1194 assert_eq!(cmpsfuncs(&high, &high), std::cmp::Ordering::Equal);
1195 }
1196
1197 /// c:74 — `freepfuncs` empties the input vec.
1198 #[test]
1199 fn freepfuncs_empties_input_vec() {
1200 let _g = crate::test_util::global_state_lock();
1201 let mut v = vec![
1202 Pfunc {
1203 name: "x".into(),
1204 ..Default::default()
1205 },
1206 Pfunc {
1207 name: "y".into(),
1208 ..Default::default()
1209 },
1210 ];
1211 freepfuncs(&mut v);
1212 assert!(v.is_empty(), "freepfuncs must clear the input vec");
1213 }
1214
1215 /// c:86 — `freeparcs` empties the input arc vec.
1216 #[test]
1217 fn freeparcs_empties_input_vec() {
1218 let _g = crate::test_util::global_state_lock();
1219 let mut v = vec![
1220 Parc {
1221 from: 0,
1222 to: 1,
1223 ..Default::default()
1224 },
1225 Parc {
1226 from: 2,
1227 to: 3,
1228 ..Default::default()
1229 },
1230 ];
1231 freeparcs(&mut v);
1232 assert!(v.is_empty(), "freeparcs must clear the input vec");
1233 }
1234
1235 /// c:121 vs c:127 — `cmpsfuncs` sorts by `self_time`, `cmptfuncs`
1236 /// sorts by `time` (cumulative). Pin they produce DIFFERENT
1237 /// orderings on an input where the two fields disagree, so a
1238 /// regen that aliases the field accessor in one of them gets
1239 /// caught.
1240 #[test]
1241 fn cmpsfuncs_and_cmptfuncs_differ_when_fields_disagree() {
1242 let _g = crate::test_util::global_state_lock();
1243 // `alpha` has high self_time but LOW cumulative time.
1244 // `beta` has low self_time but HIGH cumulative time.
1245 let alpha = Pfunc {
1246 name: "_".into(),
1247 self_time: 100.0,
1248 time: 1.0,
1249 ..Default::default()
1250 };
1251 let beta = Pfunc {
1252 name: "_".into(),
1253 self_time: 1.0,
1254 time: 100.0,
1255 ..Default::default()
1256 };
1257 let by_self = cmpsfuncs(&alpha, &beta);
1258 let by_time = cmptfuncs(&alpha, &beta);
1259 // by_self: alpha has higher self_time → alpha sorts first → Less
1260 // by_time: beta has higher cumulative time → alpha sorts after → Greater
1261 assert_ne!(
1262 by_self, by_time,
1263 "cmpsfuncs (self_time) and cmptfuncs (time) must differ when fields disagree"
1264 );
1265 }
1266
1267 // ─── zsh-corpus pins for zprof helpers ────────────────────────
1268
1269 /// `cmpsfuncs` sorts higher self_time first.
1270 #[test]
1271 fn zprof_corpus_cmpsfuncs_higher_self_time_first() {
1272 let _g = crate::test_util::global_state_lock();
1273 let high = Pfunc {
1274 name: "high".into(),
1275 self_time: 100.0,
1276 ..Default::default()
1277 };
1278 let low = Pfunc {
1279 name: "low".into(),
1280 self_time: 1.0,
1281 ..Default::default()
1282 };
1283 assert_eq!(
1284 cmpsfuncs(&high, &low),
1285 std::cmp::Ordering::Less,
1286 "higher self_time sorts first"
1287 );
1288 }
1289
1290 /// `cmptfuncs` sorts higher total time first.
1291 #[test]
1292 fn zprof_corpus_cmptfuncs_higher_time_first() {
1293 let _g = crate::test_util::global_state_lock();
1294 let high = Pfunc {
1295 name: "high".into(),
1296 time: 100.0,
1297 ..Default::default()
1298 };
1299 let low = Pfunc {
1300 name: "low".into(),
1301 time: 1.0,
1302 ..Default::default()
1303 };
1304 assert_eq!(
1305 cmptfuncs(&high, &low),
1306 std::cmp::Ordering::Less,
1307 "higher total time sorts first"
1308 );
1309 }
1310
1311 /// `cmpsfuncs` equal self_time → Equal.
1312 #[test]
1313 fn zprof_corpus_cmpsfuncs_equal_self_time() {
1314 let _g = crate::test_util::global_state_lock();
1315 let a = Pfunc {
1316 name: "a".into(),
1317 self_time: 5.0,
1318 ..Default::default()
1319 };
1320 let b = Pfunc {
1321 name: "b".into(),
1322 self_time: 5.0,
1323 ..Default::default()
1324 };
1325 // Equal self_time may or may not tie-break by name; pin: not Greater
1326 let o = cmpsfuncs(&a, &b);
1327 assert_ne!(o, std::cmp::Ordering::Greater);
1328 }
1329
1330 /// `findpfunc` on empty list returns None.
1331 #[test]
1332 fn zprof_corpus_findpfunc_empty_returns_none() {
1333 let _g = crate::test_util::global_state_lock();
1334 assert!(findpfunc("__never_seen_function__").is_none());
1335 }
1336
1337 /// `findparc` with arbitrary indexes returns None on empty arcs.
1338 #[test]
1339 fn zprof_corpus_findparc_empty_returns_none() {
1340 let _g = crate::test_util::global_state_lock();
1341 assert!(findparc(usize::MAX, usize::MAX).is_none());
1342 }
1343
1344 /// `name_for_anonymous_function("(anon)")` includes the input name
1345 /// and bracket form `[filename:lineno]`.
1346 #[test]
1347 fn zprof_corpus_name_for_anonymous_function_format() {
1348 let _g = crate::test_util::global_state_lock();
1349 let s = name_for_anonymous_function("(anon)");
1350 assert!(s.starts_with("(anon)"), "starts with name, got {s:?}");
1351 assert!(s.contains('['), "has opening bracket");
1352 assert!(s.contains(']'), "has closing bracket");
1353 assert!(s.contains(':'), "has line-number separator");
1354 }
1355
1356 // ═══════════════════════════════════════════════════════════════════
1357 // C-parity tests for Src/Modules/zprof.c cmp comparators + free ops.
1358 // ═══════════════════════════════════════════════════════════════════
1359
1360 fn mk_pfunc(name: &str, self_time: f64, time: f64) -> Pfunc {
1361 Pfunc {
1362 name: name.to_string(),
1363 calls: 0,
1364 time,
1365 self_time,
1366 num: 0,
1367 }
1368 }
1369
1370 fn mk_parc(from: usize, to: usize, time: f64) -> Parc {
1371 Parc {
1372 from,
1373 to,
1374 calls: 0,
1375 time,
1376 ..Default::default()
1377 }
1378 }
1379
1380 /// c:121 — `cmpsfuncs` is descending by self_time: a > b → Less
1381 /// (sort places larger first).
1382 #[test]
1383 fn cmpsfuncs_descending_by_self_time() {
1384 let a = mk_pfunc("a", 10.0, 0.0);
1385 let b = mk_pfunc("b", 5.0, 0.0);
1386 assert_eq!(
1387 cmpsfuncs(&a, &b),
1388 std::cmp::Ordering::Less,
1389 "larger self_time sorts first"
1390 );
1391 assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Greater);
1392 }
1393
1394 /// c:121 — equal self_time → Equal.
1395 #[test]
1396 fn cmpsfuncs_equal_returns_equal() {
1397 let a = mk_pfunc("a", 7.5, 0.0);
1398 let b = mk_pfunc("b", 7.5, 99.0); // different time, equal self_time
1399 assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Equal);
1400 }
1401
1402 /// c:127 — `cmptfuncs` descending by total time (ignores self_time).
1403 #[test]
1404 fn cmptfuncs_descending_by_total_time() {
1405 let a = mk_pfunc("a", 0.0, 10.0);
1406 let b = mk_pfunc("b", 99.0, 5.0); // larger self but smaller time
1407 assert_eq!(
1408 cmptfuncs(&a, &b),
1409 std::cmp::Ordering::Less,
1410 "larger total time sorts first regardless of self_time"
1411 );
1412 }
1413
1414 /// c:133 — `cmpparcs` descending by time.
1415 #[test]
1416 fn cmpparcs_descending_by_time() {
1417 let a = mk_parc(0, 1, 10.0);
1418 let b = mk_parc(0, 1, 5.0);
1419 assert_eq!(cmpparcs(&a, &b), std::cmp::Ordering::Less);
1420 assert_eq!(cmpparcs(&b, &a), std::cmp::Ordering::Greater);
1421 }
1422
1423 /// c:121 — NaN comparison → Equal (partial_cmp unwrap_or branch).
1424 #[test]
1425 fn cmpsfuncs_nan_returns_equal() {
1426 let a = mk_pfunc("a", f64::NAN, 0.0);
1427 let b = mk_pfunc("b", 5.0, 0.0);
1428 assert_eq!(
1429 cmpsfuncs(&a, &b),
1430 std::cmp::Ordering::Equal,
1431 "NaN partial_cmp returns None → Equal fallback"
1432 );
1433 }
1434
1435 /// c:74 — `freepfuncs` empties the vec.
1436 #[test]
1437 fn freepfuncs_clears_vec() {
1438 let mut v = vec![mk_pfunc("a", 0.0, 0.0), mk_pfunc("b", 0.0, 0.0)];
1439 freepfuncs(&mut v);
1440 assert!(v.is_empty(), "freepfuncs must clear the Vec");
1441 }
1442
1443 /// c:86 — `freeparcs` empties the vec.
1444 #[test]
1445 fn freeparcs_clears_vec() {
1446 let mut v = vec![mk_parc(0, 1, 0.0), mk_parc(1, 2, 0.0)];
1447 freeparcs(&mut v);
1448 assert!(v.is_empty());
1449 }
1450
1451 /// c:74 — `freepfuncs` on empty vec is a no-op.
1452 #[test]
1453 fn freepfuncs_empty_is_noop() {
1454 let mut v: Vec<Pfunc> = Vec::new();
1455 freepfuncs(&mut v);
1456 assert!(v.is_empty());
1457 }
1458
1459 /// c:97 — `findpfunc` is deterministic for the same lookup.
1460 #[test]
1461 fn findpfunc_is_deterministic_for_missing() {
1462 let _g = crate::test_util::global_state_lock();
1463 let first = findpfunc("__nope__");
1464 for _ in 0..10 {
1465 assert_eq!(findpfunc("__nope__"), first);
1466 }
1467 }
1468
1469 // ═══════════════════════════════════════════════════════════════════
1470 // Additional C-parity tests for Src/Modules/zprof.c
1471 // c:123 findpfunc / c:135 findparc / c:152-170 cmp* / c:418 name_for_anon /
1472 // c:509 zprof_wrapper / c:682+ lifecycle
1473 // ═══════════════════════════════════════════════════════════════════
1474
1475 /// c:135 — `findparc(0,0)` on empty table returns None.
1476 #[test]
1477 fn findparc_zero_zero_empty_table_returns_none() {
1478 let _g = crate::test_util::global_state_lock();
1479 reset_state();
1480 assert_eq!(findparc(0, 0), None);
1481 }
1482
1483 /// c:123 — `findpfunc("")` empty name returns None on empty table.
1484 #[test]
1485 fn findpfunc_empty_name_empty_table_returns_none() {
1486 let _g = crate::test_util::global_state_lock();
1487 reset_state();
1488 assert_eq!(findpfunc(""), None);
1489 }
1490
1491 /// c:152 — `cmpsfuncs(a, a)` is Equal (reflexive).
1492 #[test]
1493 fn cmpsfuncs_reflexive() {
1494 let a = mk_pfunc("x", 1.0, 2.0);
1495 assert_eq!(cmpsfuncs(&a, &a), std::cmp::Ordering::Equal);
1496 }
1497
1498 /// c:161 — `cmptfuncs(a, a)` is Equal (reflexive).
1499 #[test]
1500 fn cmptfuncs_reflexive() {
1501 let a = mk_pfunc("x", 1.0, 2.0);
1502 assert_eq!(cmptfuncs(&a, &a), std::cmp::Ordering::Equal);
1503 }
1504
1505 /// c:170 — `cmpparcs(a, a)` is Equal (reflexive).
1506 #[test]
1507 fn cmpparcs_reflexive() {
1508 let a = mk_parc(0, 1, 5.0);
1509 assert_eq!(cmpparcs(&a, &a), std::cmp::Ordering::Equal);
1510 }
1511
1512 /// c:152 — `cmpsfuncs` is antisymmetric: if a vs b is X, b vs a
1513 /// is X.reverse().
1514 #[test]
1515 fn cmpsfuncs_antisymmetric() {
1516 let a = mk_pfunc("a", 1.0, 5.0);
1517 let b = mk_pfunc("b", 2.0, 5.0);
1518 let ab = cmpsfuncs(&a, &b);
1519 let ba = cmpsfuncs(&b, &a);
1520 assert_eq!(ab.reverse(), ba, "must be antisymmetric");
1521 }
1522
1523 /// c:509 — `zprof_wrapper(null, null, "")` no panic.
1524 #[test]
1525 fn zprof_wrapper_empty_name_no_panic() {
1526 let _g = crate::test_util::global_state_lock();
1527 let _ = zprof_wrapper(std::ptr::null(), std::ptr::null(), "", || {});
1528 }
1529
1530 /// c:418 — `name_for_anonymous_function` is deterministic.
1531 #[test]
1532 fn name_for_anonymous_function_is_deterministic() {
1533 let _g = crate::test_util::global_state_lock();
1534 let first = name_for_anonymous_function("anon");
1535 for _ in 0..5 {
1536 assert_eq!(name_for_anonymous_function("anon"), first);
1537 }
1538 }
1539
1540 /// c:682-... — full lifecycle setup→features→enables→boot→cleanup→finish.
1541 #[test]
1542 fn zprof_full_lifecycle_returns_zero_for_all() {
1543 let _g = crate::test_util::global_state_lock();
1544 let null = std::ptr::null();
1545 assert_eq!(setup_(null), 0);
1546 let mut feats = Vec::new();
1547 let _ = features_(null, &mut feats);
1548 let mut enables: Option<Vec<i32>> = None;
1549 let _ = enables_(null, &mut enables);
1550 assert_eq!(boot_(null), 0);
1551 assert_eq!(cleanup_(null), 0);
1552 }
1553
1554 // ═══════════════════════════════════════════════════════════════════
1555 // Additional C-parity tests for Src/Modules/zprof.c
1556 // c:105 freepfuncs / c:113 freeparcs / c:123 findpfunc / c:135 findparc
1557 // c:152 cmpsfuncs / c:161 cmptfuncs / c:170 cmpparcs /
1558 // c:418 name_for_anonymous_function / c:509 zprof_wrapper
1559 // ═══════════════════════════════════════════════════════════════════
1560
1561 /// c:105 — `freepfuncs` returns void (compile-time pin).
1562 #[test]
1563 fn freepfuncs_returns_void() {
1564 let mut v: Vec<Pfunc> = vec![];
1565 let _: () = freepfuncs(&mut v);
1566 }
1567
1568 /// c:113 — `freeparcs` returns void.
1569 #[test]
1570 fn freeparcs_returns_void() {
1571 let mut v: Vec<Parc> = vec![];
1572 let _: () = freeparcs(&mut v);
1573 }
1574
1575 /// c:123 — `findpfunc` returns Option<usize> (compile-time pin).
1576 #[test]
1577 fn findpfunc_returns_option_usize_type() {
1578 let _g = crate::test_util::global_state_lock();
1579 let _: Option<usize> = findpfunc("anything");
1580 }
1581
1582 /// c:135 — `findparc(N, M)` returns Option<usize>.
1583 #[test]
1584 fn findparc_returns_option_usize_type() {
1585 let _g = crate::test_util::global_state_lock();
1586 let _: Option<usize> = findparc(0, 0);
1587 }
1588
1589 /// c:152 — `cmpsfuncs` returns Ordering (compile-time pin).
1590 #[test]
1591 fn cmpsfuncs_returns_ordering_type() {
1592 let a = mk_pfunc("a", 0.0, 0.0);
1593 let _: std::cmp::Ordering = cmpsfuncs(&a, &a);
1594 }
1595
1596 /// c:161 — `cmptfuncs` returns Ordering.
1597 #[test]
1598 fn cmptfuncs_returns_ordering_type() {
1599 let a = mk_pfunc("a", 0.0, 0.0);
1600 let _: std::cmp::Ordering = cmptfuncs(&a, &a);
1601 }
1602
1603 /// c:170 — `cmpparcs` returns Ordering.
1604 #[test]
1605 fn cmpparcs_returns_ordering_type() {
1606 let a = mk_parc(0, 1, 0.0);
1607 let _: std::cmp::Ordering = cmpparcs(&a, &a);
1608 }
1609
1610 /// c:418 — `name_for_anonymous_function` returns String type pin.
1611 #[test]
1612 fn name_for_anonymous_function_returns_string_type() {
1613 let _g = crate::test_util::global_state_lock();
1614 let _: String = name_for_anonymous_function("anon");
1615 }
1616
1617 /// c:509 — `zprof_wrapper` returns i32 type pin.
1618 #[test]
1619 fn zprof_wrapper_returns_i32_type() {
1620 let _g = crate::test_util::global_state_lock();
1621 let _: i32 = zprof_wrapper(std::ptr::null(), std::ptr::null(), "x", || {});
1622 }
1623
1624 /// c:105 — `freepfuncs` empties the vec.
1625 #[test]
1626 fn freepfuncs_empties_vec_pin() {
1627 let mut v = vec![mk_pfunc("a", 0.0, 0.0)];
1628 freepfuncs(&mut v);
1629 assert!(v.is_empty(), "freepfuncs must empty");
1630 }
1631
1632 /// c:113 — `freeparcs` empties the vec.
1633 #[test]
1634 fn freeparcs_empties_vec_pin() {
1635 let mut v = vec![mk_parc(0, 1, 0.0)];
1636 freeparcs(&mut v);
1637 assert!(v.is_empty(), "freeparcs must empty");
1638 }
1639
1640 // ═══════════════════════════════════════════════════════════════════
1641 // Additional C-parity pins for Src/Modules/zprof.c
1642 // c:123 findpfunc / c:135 findparc / c:152-170 cmp* / c:193 bin_zprof /
1643 // c:418 name_for_anonymous_function / c:682-719 lifecycle hooks
1644 // ═══════════════════════════════════════════════════════════════════
1645
1646 /// c:123 — `findpfunc("")` empty name doesn't panic.
1647 #[test]
1648 fn findpfunc_empty_name_no_panic() {
1649 let _g = crate::test_util::global_state_lock();
1650 let _ = findpfunc("");
1651 }
1652
1653 /// c:123 — `findpfunc` is pure (no observable mutation across calls).
1654 #[test]
1655 fn findpfunc_repeated_calls_deterministic() {
1656 let _g = crate::test_util::global_state_lock();
1657 let a = findpfunc("__never_real_pfn__");
1658 let b = findpfunc("__never_real_pfn__");
1659 assert_eq!(a, b, "findpfunc must be pure across calls");
1660 }
1661
1662 /// c:135 — `findparc(0, 0)` doesn't panic.
1663 #[test]
1664 fn findparc_zero_indices_no_panic() {
1665 let _g = crate::test_util::global_state_lock();
1666 let _ = findparc(0, 0);
1667 }
1668
1669 /// c:135 — `findparc(usize::MAX, usize::MAX)` doesn't panic.
1670 #[test]
1671 fn findparc_extreme_indices_no_panic() {
1672 let _g = crate::test_util::global_state_lock();
1673 let _ = findparc(usize::MAX, usize::MAX);
1674 }
1675
1676 /// c:152 — `cmpsfuncs` reflexivity (alt).
1677 #[test]
1678 fn cmpsfuncs_reflexive_alt() {
1679 let p = mk_pfunc("a", 1.0, 2.0);
1680 let p2 = p.clone();
1681 assert_eq!(
1682 cmpsfuncs(&p, &p2),
1683 std::cmp::Ordering::Equal,
1684 "cmpsfuncs(x, x.clone()) must be Equal"
1685 );
1686 }
1687
1688 /// c:161 — `cmptfuncs` reflexivity (alt).
1689 #[test]
1690 fn cmptfuncs_reflexive_alt() {
1691 let p = mk_pfunc("a", 1.0, 2.0);
1692 let p2 = p.clone();
1693 assert_eq!(
1694 cmptfuncs(&p, &p2),
1695 std::cmp::Ordering::Equal,
1696 "cmptfuncs(x, x.clone()) must be Equal"
1697 );
1698 }
1699
1700 /// c:170 — `cmpparcs` reflexivity (alt).
1701 #[test]
1702 fn cmpparcs_reflexive_alt() {
1703 let a = mk_parc(0, 1, 5.0);
1704 let b = a.clone();
1705 assert_eq!(cmpparcs(&a, &b), std::cmp::Ordering::Equal);
1706 }
1707
1708 /// c:193 — `bin_zprof` empty args non-negative.
1709 #[test]
1710 fn bin_zprof_empty_args_non_negative() {
1711 let _g = crate::test_util::global_state_lock();
1712 let ops = crate::ported::zsh_h::options {
1713 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1714 args: Vec::new(),
1715 argscount: 0,
1716 argsalloc: 0,
1717 };
1718 let r = bin_zprof("zprof", &[], &ops, 0);
1719 assert!(r >= 0, "bin_zprof empty must be ≥ 0, got {}", r);
1720 }
1721
1722 /// c:418 — `name_for_anonymous_function("")` returns String type (alt).
1723 #[test]
1724 fn name_for_anonymous_function_returns_string_type_alt() {
1725 let _g = crate::test_util::global_state_lock();
1726 let _: String = name_for_anonymous_function("");
1727 }
1728
1729 /// c:418 — synthesized name is non-empty.
1730 #[test]
1731 fn name_for_anonymous_function_returns_non_empty() {
1732 let _g = crate::test_util::global_state_lock();
1733 let n = name_for_anonymous_function("");
1734 assert!(!n.is_empty(), "synthesized name must not be empty");
1735 }
1736
1737 /// c:682 — `setup_` is idempotent.
1738 #[test]
1739 fn zprof_setup_idempotent_alt_pin() {
1740 let _g = crate::test_util::global_state_lock();
1741 for _ in 0..10 {
1742 assert_eq!(setup_(std::ptr::null()), 0);
1743 }
1744 }
1745
1746 /// c:719 — `cleanup_` is idempotent.
1747 #[test]
1748 fn zprof_cleanup_idempotent_alt_pin() {
1749 let _g = crate::test_util::global_state_lock();
1750 for _ in 0..10 {
1751 assert_eq!(cleanup_(std::ptr::null()), 0);
1752 }
1753 }
1754}