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}