Skip to main content

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:293 — `f->self += now - sf.beg;`
632            {
633                let mut calls = CALLS.lock().unwrap();
634                if let Some(idx) = f {
635                    calls[idx].self_time += now - sf.beg; // c:293
636                }
637            }
638            // c:294 — recursion-detect: walk sf.prev looking for f.
639            let recursion: bool = {
640                // c:294
641                let st = STACK.lock().unwrap();
642                let cur_f = f.unwrap();
643                // sf.prev = the frame underneath sf — walk it down.
644                st.iter().rev().skip(1).any(|fr| fr.p == cur_f)
645            };
646            if !recursion {
647                // c:295
648                let mut calls = CALLS.lock().unwrap();
649                if let Some(idx) = f {
650                    calls[idx].time += now - prev; // c:296
651                }
652            }
653            if let Some(arc_idx) = a {
654                // c:297
655                let mut arcs = ARCS.lock().unwrap();
656                arcs[arc_idx].calls += 1; // c:298
657                arcs[arc_idx].self_time += now - sf.beg; // c:299
658            }
659            // c:301 — `stack = sf.prev;`
660            {
661                let mut st = STACK.lock().unwrap();
662                st.pop(); // c:301
663            }
664            // c:303-307 — propagate elapsed up to caller frame.
665            let mut st = STACK.lock().unwrap();
666            if let Some(top) = st.last_mut() {
667                // c:303
668                top.beg += now - prev; // c:304
669                if let Some(arc_idx) = a {
670                    // c:305
671                    drop(st);
672                    let mut arcs = ARCS.lock().unwrap();
673                    arcs[arc_idx].time += now - prev; // c:306
674                }
675            }
676        } else {
677            // c:308
678            // c:309 — `stack = sf.prev;`
679            let mut st = STACK.lock().unwrap();
680            st.pop();
681        }
682    }
683    0 // c:311
684}
685
686// `bintab` — port of `static struct builtin bintab[]` (zprof.c:309).
687
688// `module_features` — port of `static struct features module_features`
689// from zprof.c:323.
690
691/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zprof.c:332`.
692/// C body: `zprof_module = m; return 0;`
693#[allow(unused_variables)]
694pub fn setup_(m: *const module) -> i32 {
695    // c:332
696    ZPROF_MODULE.store(true, Ordering::SeqCst); // c:340
697    0 // c:348
698}
699
700/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zprof.c:340`.
701/// C body: `*features = featuresarray(m, &module_features); return 0;`
702pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
703    // c:340
704    *features = featuresarray(m, module_features());
705    0 // c:355
706}
707
708/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zprof.c:348`.
709/// C body: `return handlefeatures(m, &module_features, enables);`
710pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
711    // c:348
712    handlefeatures(m, module_features(), enables) // c:355
713}
714
715/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zprof.c:355`.
716#[allow(unused_variables)]
717pub fn boot_(m: *const module) -> i32 {
718    // c:355
719    let mut calls = CALLS.lock().unwrap();
720    calls.clear(); // c:367
721    NCALLS.store(0, Ordering::SeqCst); // c:367
722    let mut arcs = ARCS.lock().unwrap();
723    arcs.clear(); // c:367
724    NARCS.store(0, Ordering::SeqCst); // c:367
725    STACK.lock().unwrap().clear(); // c:367
726    0 // c:367 addwrapper return
727}
728
729/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zprof.c:367`.
730/// C body: free pfuncs + parcs, deletewrapper, setfeatureenables.
731pub fn cleanup_(m: *const module) -> i32 {
732    // c:367
733    let mut calls = CALLS.lock().unwrap();
734    freepfuncs(&mut calls); // c:377
735    let mut arcs = ARCS.lock().unwrap();
736    freeparcs(&mut arcs); // c:377
737    ZPROF_MODULE.store(false, Ordering::SeqCst);
738    setfeatureenables(m, module_features(), None) // c:377
739}
740
741/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zprof.c:377`.
742#[allow(unused_variables)]
743pub fn finish_(m: *const module) -> i32 {
744    // c:377
745    // C body c:379-380 — `return 0`. Faithful empty-body port; the
746    //                    profiling tables get freed by cleanup_ via
747    //                    setfeatureenables/zprof_cleanup.
748    0
749}
750
751// ---------------------------------------------------------------------------
752// Module loaders.
753// ---------------------------------------------------------------------------
754
755// =====================================================================
756// static struct builtin bintab[]                                    c:309
757// static struct features module_features                            c:323
758// static struct funcwrap wrapper[]                                  c:328
759// =====================================================================
760
761// ---------------------------------------------------------------------------
762// File-static globals — port of c:66-71.
763// ---------------------------------------------------------------------------
764
765/// Port of `static Pfunc calls;` from `Src/Modules/zprof.c:66`.
766/// Per-function aggregated table; the C linked list becomes a
767/// `Mutex<Vec<Pfunc>>` so `Pfunc *` becomes `usize` index.
768pub static CALLS: Mutex<Vec<Pfunc>> = Mutex::new(Vec::new()); // c:66
769
770/// Port of `static int ncalls;` from `Src/Modules/zprof.c:67`. Always
771/// equals `CALLS.lock().len()` — kept as an explicit counter to
772/// match C's `ncalls++` increment pattern.
773pub static NCALLS: AtomicI32 = AtomicI32::new(0); // c:67
774
775/// Port of `static Parc arcs;` from `Src/Modules/zprof.c:68`.
776pub static ARCS: Mutex<Vec<Parc>> = Mutex::new(Vec::new()); // c:68
777
778/// Port of `static int narcs;` from `Src/Modules/zprof.c:69`.
779pub static NARCS: AtomicI32 = AtomicI32::new(0); // c:69
780
781/// Port of `static Sfunc stack;` from `Src/Modules/zprof.c:70`. The
782/// C linked stack becomes a `Mutex<Vec<Sfunc>>` (top of stack at
783/// `last()`).
784pub static STACK: Mutex<Vec<Sfunc>> = Mutex::new(Vec::new()); // c:70
785
786/// Port of `static Module zprof_module;` from `Src/Modules/zprof.c:71`.
787/// C uses a `Module` (struct module *) pointer to track which module
788/// owns the wrapper; `zprof_wrapper` short-circuits when
789/// `MOD_UNLOAD` is set on it. Module is ported as
790/// `Box<crate::ported::zsh_h::module>` (zsh_h.rs:425) but recording
791/// the raw `*const module` would deadlock with Sync/Send for the
792/// static — `AtomicBool` captures the only state `zprof_wrapper`
793/// actually inspects (loaded vs. unloading), matching the C
794/// `MOD_UNLOAD` flag-check on the same pointer.
795pub static ZPROF_MODULE: AtomicBool = AtomicBool::new(false); // c:74
796
797static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
798
799// Local stubs for the per-module entry points. C uses generic
800// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
801// 3275/3370/3445) but those take `Builtin` + `Features` pointer
802// fields the Rust port doesn't carry. The hardcoded descriptor
803// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
804// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
805// C uses generic featuresarray/handlefeatures/setfeatureenables from
806// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
807// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
808fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
809    vec!["b:zprof".to_string()]
810}
811
812// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
813// C uses generic featuresarray/handlefeatures/setfeatureenables from
814// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
815// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
816fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
817    if enables.is_none() {
818        *enables = Some(vec![1; 1]);
819    }
820    0
821}
822
823// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
824// C uses generic featuresarray/handlefeatures/setfeatureenables from
825// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
826// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
827fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
828    0
829}
830
831// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
832// ─── RUST-ONLY ACCESSORS ───
833//
834// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
835// RwLock<T>>` globals declared above. C zsh uses direct global
836// access; Rust needs these wrappers because `OnceLock::get_or_init`
837// is the only way to lazily construct shared state. These ported sit
838// here so the body of this file reads in C source order without
839// the accessor wrappers interleaved between real port ported.
840// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
841
842// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
843// ─── RUST-ONLY ACCESSORS ───
844//
845// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
846// RwLock<T>>` globals declared above. C zsh uses direct global
847// access; Rust needs these wrappers because `OnceLock::get_or_init`
848// is the only way to lazily construct shared state. These ported sit
849// here so the body of this file reads in C source order without
850// the accessor wrappers interleaved between real port ported.
851// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
852
853// WARNING: NOT IN ZPROF.C — Rust-only module-framework shim.
854// C uses generic featuresarray/handlefeatures/setfeatureenables from
855// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
856// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
857fn module_features() -> &'static Mutex<features> {
858    MODULE_FEATURES.get_or_init(|| {
859        Mutex::new(features {
860            bn_list: None,
861            bn_size: 1,
862            cd_list: None,
863            cd_size: 0,
864            mf_list: None,
865            mf_size: 0,
866            pd_list: None,
867            pd_size: 0,
868            n_abstract: 0,
869        })
870    })
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876    use crate::ported::zsh_h::funcstack;
877
878    /// Serialise tests that mutate the module-static globals so the
879    /// cargo-test parallel runner doesn't shred each other's state.
880    static TEST_LOCK: Mutex<()> = Mutex::new(());
881
882    fn reset_state() {
883        let mut c = CALLS.lock().unwrap();
884        c.clear();
885        let mut a = ARCS.lock().unwrap();
886        a.clear();
887        STACK.lock().unwrap().clear();
888        NCALLS.store(0, Ordering::SeqCst);
889        NARCS.store(0, Ordering::SeqCst);
890    }
891
892    /// Port of `bin_zprof(UNUSED(char *nam), UNUSED(char **args), Options ops, UNUSED(int func))` from `Src/Modules/zprof.c:139`.
893    /// Verifies `Pfunc` mirrors C `struct pfunc` field-for-field
894    /// (name/calls/time/self/num at c:40-44).
895    #[test]
896    fn pfunc_default_zeros() {
897        let _g = crate::test_util::global_state_lock();
898        let p = Pfunc::default();
899        assert_eq!(p.name, "");
900        assert_eq!(p.calls, 0);
901        assert_eq!(p.time, 0.0);
902        assert_eq!(p.self_time, 0.0);
903        assert_eq!(p.num, 0);
904    }
905
906    /// Verifies `freepfuncs` empties the table (c:78-82 zsfree+zfree).
907    #[test]
908    fn freepfuncs_clears() {
909        let _g = crate::test_util::global_state_lock();
910        let mut v = vec![Pfunc {
911            name: "a".into(),
912            ..Default::default()
913        }];
914        freepfuncs(&mut v);
915        assert!(v.is_empty());
916    }
917
918    /// Verifies `findpfunc` linear-scan match (c:101-103).
919    #[test]
920    fn findpfunc_matches_by_name() {
921        let _g = crate::test_util::global_state_lock();
922        let _g = TEST_LOCK.lock().unwrap();
923        reset_state();
924        CALLS.lock().unwrap().push(Pfunc {
925            name: "alpha".into(),
926            ..Default::default()
927        });
928        CALLS.lock().unwrap().push(Pfunc {
929            name: "beta".into(),
930            ..Default::default()
931        });
932        assert_eq!(findpfunc("alpha"), Some(0));
933        assert_eq!(findpfunc("beta"), Some(1));
934        assert_eq!(findpfunc("none"), None);
935        reset_state();
936    }
937
938    /// Verifies `findparc` matches (from, to) pair (c:113-115).
939    #[test]
940    fn findparc_matches_pair() {
941        let _g = crate::test_util::global_state_lock();
942        let _g = TEST_LOCK.lock().unwrap();
943        reset_state();
944        ARCS.lock().unwrap().push(Parc {
945            from: 0,
946            to: 1,
947            ..Default::default()
948        });
949        ARCS.lock().unwrap().push(Parc {
950            from: 0,
951            to: 2,
952            ..Default::default()
953        });
954        assert_eq!(findparc(0, 1), Some(0));
955        assert_eq!(findparc(0, 2), Some(1));
956        assert_eq!(findparc(1, 0), None);
957        reset_state();
958    }
959
960    /// Verifies `cmpsfuncs` is descending (c:121-124).
961    #[test]
962    fn cmpsfuncs_descending() {
963        let _g = crate::test_util::global_state_lock();
964        let a = Pfunc {
965            self_time: 5.0,
966            ..Default::default()
967        };
968        let b = Pfunc {
969            self_time: 10.0,
970            ..Default::default()
971        };
972        // descending: b should come before a → cmp(a, b) = Greater
973        assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Greater);
974        assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Less);
975    }
976
977    /// Verifies `bin_zprof -c` clears state (c:141-147).
978    #[test]
979    fn bin_zprof_clear_resets_tables() {
980        let _g = crate::test_util::global_state_lock();
981        let _g = TEST_LOCK.lock().unwrap();
982        reset_state();
983        CALLS.lock().unwrap().push(Pfunc {
984            name: "x".into(),
985            ..Default::default()
986        });
987        ARCS.lock().unwrap().push(Parc {
988            from: 0,
989            to: 0,
990            ..Default::default()
991        });
992        NCALLS.store(1, Ordering::SeqCst);
993        NARCS.store(1, Ordering::SeqCst);
994
995        let mut ops = options {
996            ind: [0u8; MAX_OPS],
997            args: Vec::new(),
998            argscount: 0,
999            argsalloc: 0,
1000        };
1001        ops.ind[b'c' as usize] = 1;
1002        let r = bin_zprof("zprof", &["-c".to_string()], &ops, 0);
1003        assert_eq!(r, 0);
1004        assert!(CALLS.lock().unwrap().is_empty());
1005        assert!(ARCS.lock().unwrap().is_empty());
1006        assert_eq!(NCALLS.load(Ordering::SeqCst), 0);
1007        assert_eq!(NARCS.load(Ordering::SeqCst), 0);
1008    }
1009
1010    /// Verifies `zprof_wrapper` returns 0 (the static-link no-op
1011    /// path mirrors C's `return 0;` exit at c:311).
1012    #[test]
1013    fn zprof_wrapper_returns_zero() {
1014        let _g = crate::test_util::global_state_lock();
1015        assert_eq!(
1016            zprof_wrapper(std::ptr::null(), std::ptr::null(), "foo", || {}),
1017            0,
1018        );
1019    }
1020
1021    /// Verifies `name_for_anonymous_function` formats as
1022    /// `name [filename:lineno]` per c:224-232, reading filename
1023    /// and flineno from `funcstack[0]` per S1 rule.
1024    #[test]
1025    fn name_for_anonymous_function_format() {
1026        let _g = crate::test_util::global_state_lock();
1027        // Push a frame onto FUNCSTACK so the fn reads it.
1028        {
1029            let mut stack = FUNCSTACK.lock().unwrap();
1030            stack.clear();
1031            stack.push(funcstack {
1032                filename: Some("/tmp/foo.zsh".to_string()),
1033                flineno: 42,
1034                ..Default::default()
1035            });
1036        }
1037        let s = name_for_anonymous_function("anon");
1038        // Cleanup so subsequent tests aren't polluted.
1039        FUNCSTACK.lock().unwrap().clear();
1040        assert_eq!(s, "anon [/tmp/foo.zsh:42]");
1041    }
1042
1043    /// `name_for_anonymous_function` with an EMPTY funcstack must
1044    /// return `"name [:0]"` — empty filename + zero lineno — not
1045    /// panic on the unwrap. The C body's `funcstack[0].flineno`
1046    /// would segfault on an empty stack; the Rust port must defend
1047    /// because nothing in zsh prevents the profiler from being
1048    /// invoked before the first function frame is pushed (e.g.
1049    /// during init scripts that contain anonymous functions at top
1050    /// level).
1051    #[test]
1052    fn name_for_anonymous_function_empty_funcstack_defaults() {
1053        let _g = crate::test_util::global_state_lock();
1054        // Ensure stack is empty.
1055        FUNCSTACK.lock().unwrap().clear();
1056        // No panic on first().unwrap() — the fn uses Option chains.
1057        let s = std::panic::catch_unwind(|| name_for_anonymous_function("anon"))
1058            .expect("must not panic on empty funcstack");
1059        assert_eq!(
1060            s, "anon [:0]",
1061            "empty funcstack → empty filename + 0 lineno; got {:?}",
1062            s
1063        );
1064    }
1065
1066    /// c:97 — `findpfunc` on an empty table returns None. A
1067    /// regression that returns 0 (a valid index!) would silently
1068    /// corrupt every subsequent per-function profile accumulation.
1069    #[test]
1070    fn findpfunc_empty_table_returns_none() {
1071        let _g = crate::test_util::global_state_lock();
1072        let _g = TEST_LOCK.lock().unwrap();
1073        reset_state();
1074        assert!(findpfunc("never-called").is_none());
1075    }
1076
1077    /// c:97 — `findpfunc` after two inserts returns the right index.
1078    /// Pin the index-zero-based contract because the find result
1079    /// feeds back into CALLS[i].
1080    #[test]
1081    fn findpfunc_returns_correct_index_after_insert() {
1082        let _g = crate::test_util::global_state_lock();
1083        let _g = TEST_LOCK.lock().unwrap();
1084        reset_state();
1085        CALLS.lock().unwrap().push(Pfunc {
1086            name: "alpha".into(),
1087            ..Default::default()
1088        });
1089        CALLS.lock().unwrap().push(Pfunc {
1090            name: "beta".into(),
1091            ..Default::default()
1092        });
1093        assert_eq!(findpfunc("alpha"), Some(0));
1094        assert_eq!(findpfunc("beta"), Some(1));
1095        assert!(findpfunc("gamma").is_none());
1096    }
1097
1098    /// c:109 — `findparc(f, t)` on an empty arcs table → None.
1099    #[test]
1100    fn findparc_empty_table_returns_none() {
1101        let _g = crate::test_util::global_state_lock();
1102        let _g = TEST_LOCK.lock().unwrap();
1103        reset_state();
1104        assert!(findparc(0, 1).is_none());
1105    }
1106
1107    /// c:109 — `findparc` distinguishes (f1, t1) from (f1, t2):
1108    /// same `from`, different `to` is a different arc.
1109    #[test]
1110    fn findparc_distinguishes_to_field() {
1111        let _g = crate::test_util::global_state_lock();
1112        let _g = TEST_LOCK.lock().unwrap();
1113        reset_state();
1114        ARCS.lock().unwrap().push(Parc {
1115            from: 0,
1116            to: 1,
1117            ..Default::default()
1118        });
1119        ARCS.lock().unwrap().push(Parc {
1120            from: 0,
1121            to: 2,
1122            ..Default::default()
1123        });
1124        assert_eq!(findparc(0, 1), Some(0));
1125        assert_eq!(findparc(0, 2), Some(1));
1126        assert!(findparc(0, 99).is_none());
1127        assert!(findparc(99, 1).is_none());
1128    }
1129
1130    /// c:121 — `cmpsfuncs` compares by `self_time` DESCENDING (C
1131    /// source: `(int)((*b)->self < (*a)->self) - (int)((*a)->self < (*b)->self)`
1132    /// — i.e. higher self_time sorts first). Pin the direction
1133    /// because a regen that flips to ascending would silently
1134    /// invert the user-facing `zprof` output ordering.
1135    #[test]
1136    fn cmpsfuncs_compares_by_self_time_descending() {
1137        let _g = crate::test_util::global_state_lock();
1138        let high = Pfunc {
1139            name: "_".into(),
1140            self_time: 100.0,
1141            ..Default::default()
1142        };
1143        let low = Pfunc {
1144            name: "_".into(),
1145            self_time: 1.0,
1146            ..Default::default()
1147        };
1148        // higher self_time sorts FIRST → Ordering::Less for (high, low)
1149        assert_eq!(cmpsfuncs(&high, &low), std::cmp::Ordering::Less);
1150        assert_eq!(cmpsfuncs(&low, &high), std::cmp::Ordering::Greater);
1151        assert_eq!(cmpsfuncs(&high, &high), std::cmp::Ordering::Equal);
1152    }
1153
1154    /// c:74 — `freepfuncs` empties the input vec.
1155    #[test]
1156    fn freepfuncs_empties_input_vec() {
1157        let _g = crate::test_util::global_state_lock();
1158        let mut v = vec![
1159            Pfunc {
1160                name: "x".into(),
1161                ..Default::default()
1162            },
1163            Pfunc {
1164                name: "y".into(),
1165                ..Default::default()
1166            },
1167        ];
1168        freepfuncs(&mut v);
1169        assert!(v.is_empty(), "freepfuncs must clear the input vec");
1170    }
1171
1172    /// c:86 — `freeparcs` empties the input arc vec.
1173    #[test]
1174    fn freeparcs_empties_input_vec() {
1175        let _g = crate::test_util::global_state_lock();
1176        let mut v = vec![
1177            Parc {
1178                from: 0,
1179                to: 1,
1180                ..Default::default()
1181            },
1182            Parc {
1183                from: 2,
1184                to: 3,
1185                ..Default::default()
1186            },
1187        ];
1188        freeparcs(&mut v);
1189        assert!(v.is_empty(), "freeparcs must clear the input vec");
1190    }
1191
1192    /// c:121 vs c:127 — `cmpsfuncs` sorts by `self_time`, `cmptfuncs`
1193    /// sorts by `time` (cumulative). Pin they produce DIFFERENT
1194    /// orderings on an input where the two fields disagree, so a
1195    /// regen that aliases the field accessor in one of them gets
1196    /// caught.
1197    #[test]
1198    fn cmpsfuncs_and_cmptfuncs_differ_when_fields_disagree() {
1199        let _g = crate::test_util::global_state_lock();
1200        // `alpha` has high self_time but LOW cumulative time.
1201        // `beta`  has low self_time but HIGH cumulative time.
1202        let alpha = Pfunc {
1203            name: "_".into(),
1204            self_time: 100.0,
1205            time: 1.0,
1206            ..Default::default()
1207        };
1208        let beta = Pfunc {
1209            name: "_".into(),
1210            self_time: 1.0,
1211            time: 100.0,
1212            ..Default::default()
1213        };
1214        let by_self = cmpsfuncs(&alpha, &beta);
1215        let by_time = cmptfuncs(&alpha, &beta);
1216        // by_self: alpha has higher self_time → alpha sorts first → Less
1217        // by_time: beta has higher cumulative time → alpha sorts after → Greater
1218        assert_ne!(
1219            by_self, by_time,
1220            "cmpsfuncs (self_time) and cmptfuncs (time) must differ when fields disagree"
1221        );
1222    }
1223
1224    // ─── zsh-corpus pins for zprof helpers ────────────────────────
1225
1226    /// `cmpsfuncs` sorts higher self_time first.
1227    #[test]
1228    fn zprof_corpus_cmpsfuncs_higher_self_time_first() {
1229        let _g = crate::test_util::global_state_lock();
1230        let high = Pfunc {
1231            name: "high".into(),
1232            self_time: 100.0,
1233            ..Default::default()
1234        };
1235        let low = Pfunc {
1236            name: "low".into(),
1237            self_time: 1.0,
1238            ..Default::default()
1239        };
1240        assert_eq!(
1241            cmpsfuncs(&high, &low),
1242            std::cmp::Ordering::Less,
1243            "higher self_time sorts first"
1244        );
1245    }
1246
1247    /// `cmptfuncs` sorts higher total time first.
1248    #[test]
1249    fn zprof_corpus_cmptfuncs_higher_time_first() {
1250        let _g = crate::test_util::global_state_lock();
1251        let high = Pfunc {
1252            name: "high".into(),
1253            time: 100.0,
1254            ..Default::default()
1255        };
1256        let low = Pfunc {
1257            name: "low".into(),
1258            time: 1.0,
1259            ..Default::default()
1260        };
1261        assert_eq!(
1262            cmptfuncs(&high, &low),
1263            std::cmp::Ordering::Less,
1264            "higher total time sorts first"
1265        );
1266    }
1267
1268    /// `cmpsfuncs` equal self_time → Equal.
1269    #[test]
1270    fn zprof_corpus_cmpsfuncs_equal_self_time() {
1271        let _g = crate::test_util::global_state_lock();
1272        let a = Pfunc {
1273            name: "a".into(),
1274            self_time: 5.0,
1275            ..Default::default()
1276        };
1277        let b = Pfunc {
1278            name: "b".into(),
1279            self_time: 5.0,
1280            ..Default::default()
1281        };
1282        // Equal self_time may or may not tie-break by name; pin: not Greater
1283        let o = cmpsfuncs(&a, &b);
1284        assert_ne!(o, std::cmp::Ordering::Greater);
1285    }
1286
1287    /// `findpfunc` on empty list returns None.
1288    #[test]
1289    fn zprof_corpus_findpfunc_empty_returns_none() {
1290        let _g = crate::test_util::global_state_lock();
1291        assert!(findpfunc("__never_seen_function__").is_none());
1292    }
1293
1294    /// `findparc` with arbitrary indexes returns None on empty arcs.
1295    #[test]
1296    fn zprof_corpus_findparc_empty_returns_none() {
1297        let _g = crate::test_util::global_state_lock();
1298        assert!(findparc(usize::MAX, usize::MAX).is_none());
1299    }
1300
1301    /// `name_for_anonymous_function("(anon)")` includes the input name
1302    /// and bracket form `[filename:lineno]`.
1303    #[test]
1304    fn zprof_corpus_name_for_anonymous_function_format() {
1305        let _g = crate::test_util::global_state_lock();
1306        let s = name_for_anonymous_function("(anon)");
1307        assert!(s.starts_with("(anon)"), "starts with name, got {s:?}");
1308        assert!(s.contains('['), "has opening bracket");
1309        assert!(s.contains(']'), "has closing bracket");
1310        assert!(s.contains(':'), "has line-number separator");
1311    }
1312
1313    // ═══════════════════════════════════════════════════════════════════
1314    // C-parity tests for Src/Modules/zprof.c cmp comparators + free ops.
1315    // ═══════════════════════════════════════════════════════════════════
1316
1317    fn mk_pfunc(name: &str, self_time: f64, time: f64) -> Pfunc {
1318        Pfunc {
1319            name: name.to_string(),
1320            calls: 0,
1321            time,
1322            self_time,
1323            num: 0,
1324        }
1325    }
1326
1327    fn mk_parc(from: usize, to: usize, time: f64) -> Parc {
1328        Parc {
1329            from,
1330            to,
1331            calls: 0,
1332            time,
1333            ..Default::default()
1334        }
1335    }
1336
1337    /// c:121 — `cmpsfuncs` is descending by self_time: a > b → Less
1338    /// (sort places larger first).
1339    #[test]
1340    fn cmpsfuncs_descending_by_self_time() {
1341        let a = mk_pfunc("a", 10.0, 0.0);
1342        let b = mk_pfunc("b", 5.0, 0.0);
1343        assert_eq!(
1344            cmpsfuncs(&a, &b),
1345            std::cmp::Ordering::Less,
1346            "larger self_time sorts first"
1347        );
1348        assert_eq!(cmpsfuncs(&b, &a), std::cmp::Ordering::Greater);
1349    }
1350
1351    /// c:121 — equal self_time → Equal.
1352    #[test]
1353    fn cmpsfuncs_equal_returns_equal() {
1354        let a = mk_pfunc("a", 7.5, 0.0);
1355        let b = mk_pfunc("b", 7.5, 99.0); // different time, equal self_time
1356        assert_eq!(cmpsfuncs(&a, &b), std::cmp::Ordering::Equal);
1357    }
1358
1359    /// c:127 — `cmptfuncs` descending by total time (ignores self_time).
1360    #[test]
1361    fn cmptfuncs_descending_by_total_time() {
1362        let a = mk_pfunc("a", 0.0, 10.0);
1363        let b = mk_pfunc("b", 99.0, 5.0); // larger self but smaller time
1364        assert_eq!(
1365            cmptfuncs(&a, &b),
1366            std::cmp::Ordering::Less,
1367            "larger total time sorts first regardless of self_time"
1368        );
1369    }
1370
1371    /// c:133 — `cmpparcs` descending by time.
1372    #[test]
1373    fn cmpparcs_descending_by_time() {
1374        let a = mk_parc(0, 1, 10.0);
1375        let b = mk_parc(0, 1, 5.0);
1376        assert_eq!(cmpparcs(&a, &b), std::cmp::Ordering::Less);
1377        assert_eq!(cmpparcs(&b, &a), std::cmp::Ordering::Greater);
1378    }
1379
1380    /// c:121 — NaN comparison → Equal (partial_cmp unwrap_or branch).
1381    #[test]
1382    fn cmpsfuncs_nan_returns_equal() {
1383        let a = mk_pfunc("a", f64::NAN, 0.0);
1384        let b = mk_pfunc("b", 5.0, 0.0);
1385        assert_eq!(
1386            cmpsfuncs(&a, &b),
1387            std::cmp::Ordering::Equal,
1388            "NaN partial_cmp returns None → Equal fallback"
1389        );
1390    }
1391
1392    /// c:74 — `freepfuncs` empties the vec.
1393    #[test]
1394    fn freepfuncs_clears_vec() {
1395        let mut v = vec![mk_pfunc("a", 0.0, 0.0), mk_pfunc("b", 0.0, 0.0)];
1396        freepfuncs(&mut v);
1397        assert!(v.is_empty(), "freepfuncs must clear the Vec");
1398    }
1399
1400    /// c:86 — `freeparcs` empties the vec.
1401    #[test]
1402    fn freeparcs_clears_vec() {
1403        let mut v = vec![mk_parc(0, 1, 0.0), mk_parc(1, 2, 0.0)];
1404        freeparcs(&mut v);
1405        assert!(v.is_empty());
1406    }
1407
1408    /// c:74 — `freepfuncs` on empty vec is a no-op.
1409    #[test]
1410    fn freepfuncs_empty_is_noop() {
1411        let mut v: Vec<Pfunc> = Vec::new();
1412        freepfuncs(&mut v);
1413        assert!(v.is_empty());
1414    }
1415
1416    /// c:97 — `findpfunc` is deterministic for the same lookup.
1417    #[test]
1418    fn findpfunc_is_deterministic_for_missing() {
1419        let _g = crate::test_util::global_state_lock();
1420        let first = findpfunc("__nope__");
1421        for _ in 0..10 {
1422            assert_eq!(findpfunc("__nope__"), first);
1423        }
1424    }
1425
1426    // ═══════════════════════════════════════════════════════════════════
1427    // Additional C-parity tests for Src/Modules/zprof.c
1428    // c:123 findpfunc / c:135 findparc / c:152-170 cmp* / c:418 name_for_anon /
1429    // c:509 zprof_wrapper / c:682+ lifecycle
1430    // ═══════════════════════════════════════════════════════════════════
1431
1432    /// c:135 — `findparc(0,0)` on empty table returns None.
1433    #[test]
1434    fn findparc_zero_zero_empty_table_returns_none() {
1435        let _g = crate::test_util::global_state_lock();
1436        reset_state();
1437        assert_eq!(findparc(0, 0), None);
1438    }
1439
1440    /// c:123 — `findpfunc("")` empty name returns None on empty table.
1441    #[test]
1442    fn findpfunc_empty_name_empty_table_returns_none() {
1443        let _g = crate::test_util::global_state_lock();
1444        reset_state();
1445        assert_eq!(findpfunc(""), None);
1446    }
1447
1448    /// c:152 — `cmpsfuncs(a, a)` is Equal (reflexive).
1449    #[test]
1450    fn cmpsfuncs_reflexive() {
1451        let a = mk_pfunc("x", 1.0, 2.0);
1452        assert_eq!(cmpsfuncs(&a, &a), std::cmp::Ordering::Equal);
1453    }
1454
1455    /// c:161 — `cmptfuncs(a, a)` is Equal (reflexive).
1456    #[test]
1457    fn cmptfuncs_reflexive() {
1458        let a = mk_pfunc("x", 1.0, 2.0);
1459        assert_eq!(cmptfuncs(&a, &a), std::cmp::Ordering::Equal);
1460    }
1461
1462    /// c:170 — `cmpparcs(a, a)` is Equal (reflexive).
1463    #[test]
1464    fn cmpparcs_reflexive() {
1465        let a = mk_parc(0, 1, 5.0);
1466        assert_eq!(cmpparcs(&a, &a), std::cmp::Ordering::Equal);
1467    }
1468
1469    /// c:152 — `cmpsfuncs` is antisymmetric: if a vs b is X, b vs a
1470    /// is X.reverse().
1471    #[test]
1472    fn cmpsfuncs_antisymmetric() {
1473        let a = mk_pfunc("a", 1.0, 5.0);
1474        let b = mk_pfunc("b", 2.0, 5.0);
1475        let ab = cmpsfuncs(&a, &b);
1476        let ba = cmpsfuncs(&b, &a);
1477        assert_eq!(ab.reverse(), ba, "must be antisymmetric");
1478    }
1479
1480    /// c:509 — `zprof_wrapper(null, null, "")` no panic.
1481    #[test]
1482    fn zprof_wrapper_empty_name_no_panic() {
1483        let _g = crate::test_util::global_state_lock();
1484        let _ = zprof_wrapper(std::ptr::null(), std::ptr::null(), "", || {});
1485    }
1486
1487    /// c:418 — `name_for_anonymous_function` is deterministic.
1488    #[test]
1489    fn name_for_anonymous_function_is_deterministic() {
1490        let _g = crate::test_util::global_state_lock();
1491        let first = name_for_anonymous_function("anon");
1492        for _ in 0..5 {
1493            assert_eq!(name_for_anonymous_function("anon"), first);
1494        }
1495    }
1496
1497    /// c:682-... — full lifecycle setup→features→enables→boot→cleanup→finish.
1498    #[test]
1499    fn zprof_full_lifecycle_returns_zero_for_all() {
1500        let _g = crate::test_util::global_state_lock();
1501        let null = std::ptr::null();
1502        assert_eq!(setup_(null), 0);
1503        let mut feats = Vec::new();
1504        let _ = features_(null, &mut feats);
1505        let mut enables: Option<Vec<i32>> = None;
1506        let _ = enables_(null, &mut enables);
1507        assert_eq!(boot_(null), 0);
1508        assert_eq!(cleanup_(null), 0);
1509    }
1510
1511    // ═══════════════════════════════════════════════════════════════════
1512    // Additional C-parity tests for Src/Modules/zprof.c
1513    // c:105 freepfuncs / c:113 freeparcs / c:123 findpfunc / c:135 findparc
1514    // c:152 cmpsfuncs / c:161 cmptfuncs / c:170 cmpparcs /
1515    // c:418 name_for_anonymous_function / c:509 zprof_wrapper
1516    // ═══════════════════════════════════════════════════════════════════
1517
1518    /// c:105 — `freepfuncs` returns void (compile-time pin).
1519    #[test]
1520    fn freepfuncs_returns_void() {
1521        let mut v: Vec<Pfunc> = vec![];
1522        let _: () = freepfuncs(&mut v);
1523    }
1524
1525    /// c:113 — `freeparcs` returns void.
1526    #[test]
1527    fn freeparcs_returns_void() {
1528        let mut v: Vec<Parc> = vec![];
1529        let _: () = freeparcs(&mut v);
1530    }
1531
1532    /// c:123 — `findpfunc` returns Option<usize> (compile-time pin).
1533    #[test]
1534    fn findpfunc_returns_option_usize_type() {
1535        let _g = crate::test_util::global_state_lock();
1536        let _: Option<usize> = findpfunc("anything");
1537    }
1538
1539    /// c:135 — `findparc(N, M)` returns Option<usize>.
1540    #[test]
1541    fn findparc_returns_option_usize_type() {
1542        let _g = crate::test_util::global_state_lock();
1543        let _: Option<usize> = findparc(0, 0);
1544    }
1545
1546    /// c:152 — `cmpsfuncs` returns Ordering (compile-time pin).
1547    #[test]
1548    fn cmpsfuncs_returns_ordering_type() {
1549        let a = mk_pfunc("a", 0.0, 0.0);
1550        let _: std::cmp::Ordering = cmpsfuncs(&a, &a);
1551    }
1552
1553    /// c:161 — `cmptfuncs` returns Ordering.
1554    #[test]
1555    fn cmptfuncs_returns_ordering_type() {
1556        let a = mk_pfunc("a", 0.0, 0.0);
1557        let _: std::cmp::Ordering = cmptfuncs(&a, &a);
1558    }
1559
1560    /// c:170 — `cmpparcs` returns Ordering.
1561    #[test]
1562    fn cmpparcs_returns_ordering_type() {
1563        let a = mk_parc(0, 1, 0.0);
1564        let _: std::cmp::Ordering = cmpparcs(&a, &a);
1565    }
1566
1567    /// c:418 — `name_for_anonymous_function` returns String type pin.
1568    #[test]
1569    fn name_for_anonymous_function_returns_string_type() {
1570        let _g = crate::test_util::global_state_lock();
1571        let _: String = name_for_anonymous_function("anon");
1572    }
1573
1574    /// c:509 — `zprof_wrapper` returns i32 type pin.
1575    #[test]
1576    fn zprof_wrapper_returns_i32_type() {
1577        let _g = crate::test_util::global_state_lock();
1578        let _: i32 = zprof_wrapper(std::ptr::null(), std::ptr::null(), "x", || {});
1579    }
1580
1581    /// c:105 — `freepfuncs` empties the vec.
1582    #[test]
1583    fn freepfuncs_empties_vec_pin() {
1584        let mut v = vec![mk_pfunc("a", 0.0, 0.0)];
1585        freepfuncs(&mut v);
1586        assert!(v.is_empty(), "freepfuncs must empty");
1587    }
1588
1589    /// c:113 — `freeparcs` empties the vec.
1590    #[test]
1591    fn freeparcs_empties_vec_pin() {
1592        let mut v = vec![mk_parc(0, 1, 0.0)];
1593        freeparcs(&mut v);
1594        assert!(v.is_empty(), "freeparcs must empty");
1595    }
1596
1597    // ═══════════════════════════════════════════════════════════════════
1598    // Additional C-parity pins for Src/Modules/zprof.c
1599    // c:123 findpfunc / c:135 findparc / c:152-170 cmp* / c:193 bin_zprof /
1600    // c:418 name_for_anonymous_function / c:682-719 lifecycle hooks
1601    // ═══════════════════════════════════════════════════════════════════
1602
1603    /// c:123 — `findpfunc("")` empty name doesn't panic.
1604    #[test]
1605    fn findpfunc_empty_name_no_panic() {
1606        let _g = crate::test_util::global_state_lock();
1607        let _ = findpfunc("");
1608    }
1609
1610    /// c:123 — `findpfunc` is pure (no observable mutation across calls).
1611    #[test]
1612    fn findpfunc_repeated_calls_deterministic() {
1613        let _g = crate::test_util::global_state_lock();
1614        let a = findpfunc("__never_real_pfn__");
1615        let b = findpfunc("__never_real_pfn__");
1616        assert_eq!(a, b, "findpfunc must be pure across calls");
1617    }
1618
1619    /// c:135 — `findparc(0, 0)` doesn't panic.
1620    #[test]
1621    fn findparc_zero_indices_no_panic() {
1622        let _g = crate::test_util::global_state_lock();
1623        let _ = findparc(0, 0);
1624    }
1625
1626    /// c:135 — `findparc(usize::MAX, usize::MAX)` doesn't panic.
1627    #[test]
1628    fn findparc_extreme_indices_no_panic() {
1629        let _g = crate::test_util::global_state_lock();
1630        let _ = findparc(usize::MAX, usize::MAX);
1631    }
1632
1633    /// c:152 — `cmpsfuncs` reflexivity (alt).
1634    #[test]
1635    fn cmpsfuncs_reflexive_alt() {
1636        let p = mk_pfunc("a", 1.0, 2.0);
1637        let p2 = p.clone();
1638        assert_eq!(
1639            cmpsfuncs(&p, &p2),
1640            std::cmp::Ordering::Equal,
1641            "cmpsfuncs(x, x.clone()) must be Equal"
1642        );
1643    }
1644
1645    /// c:161 — `cmptfuncs` reflexivity (alt).
1646    #[test]
1647    fn cmptfuncs_reflexive_alt() {
1648        let p = mk_pfunc("a", 1.0, 2.0);
1649        let p2 = p.clone();
1650        assert_eq!(
1651            cmptfuncs(&p, &p2),
1652            std::cmp::Ordering::Equal,
1653            "cmptfuncs(x, x.clone()) must be Equal"
1654        );
1655    }
1656
1657    /// c:170 — `cmpparcs` reflexivity (alt).
1658    #[test]
1659    fn cmpparcs_reflexive_alt() {
1660        let a = mk_parc(0, 1, 5.0);
1661        let b = a.clone();
1662        assert_eq!(cmpparcs(&a, &b), std::cmp::Ordering::Equal);
1663    }
1664
1665    /// c:193 — `bin_zprof` empty args non-negative.
1666    #[test]
1667    fn bin_zprof_empty_args_non_negative() {
1668        let _g = crate::test_util::global_state_lock();
1669        let ops = crate::ported::zsh_h::options {
1670            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1671            args: Vec::new(),
1672            argscount: 0,
1673            argsalloc: 0,
1674        };
1675        let r = bin_zprof("zprof", &[], &ops, 0);
1676        assert!(r >= 0, "bin_zprof empty must be ≥ 0, got {}", r);
1677    }
1678
1679    /// c:418 — `name_for_anonymous_function("")` returns String type (alt).
1680    #[test]
1681    fn name_for_anonymous_function_returns_string_type_alt() {
1682        let _g = crate::test_util::global_state_lock();
1683        let _: String = name_for_anonymous_function("");
1684    }
1685
1686    /// c:418 — synthesized name is non-empty.
1687    #[test]
1688    fn name_for_anonymous_function_returns_non_empty() {
1689        let _g = crate::test_util::global_state_lock();
1690        let n = name_for_anonymous_function("");
1691        assert!(!n.is_empty(), "synthesized name must not be empty");
1692    }
1693
1694    /// c:682 — `setup_` is idempotent.
1695    #[test]
1696    fn zprof_setup_idempotent_alt_pin() {
1697        let _g = crate::test_util::global_state_lock();
1698        for _ in 0..10 {
1699            assert_eq!(setup_(std::ptr::null()), 0);
1700        }
1701    }
1702
1703    /// c:719 — `cleanup_` is idempotent.
1704    #[test]
1705    fn zprof_cleanup_idempotent_alt_pin() {
1706        let _g = crate::test_util::global_state_lock();
1707        for _ in 0..10 {
1708            assert_eq!(cleanup_(std::ptr::null()), 0);
1709        }
1710    }
1711}