1use core::marker::PhantomData;
16
17#[cfg(all(
33 feature = "probe",
34 not(target_family = "wasm"),
35 not(feature = "web_lift")
36))]
37mod imp {
38 use std::cell::RefCell;
39 use std::time::Instant;
40
41 thread_local! {
42 static EVENTS: RefCell<Vec<super::Event>> = const { RefCell::new(Vec::new()) };
43 }
44
45 pub struct Span {
47 pub(crate) name: &'static str,
48 pub(crate) start: Instant,
49 }
50
51 impl Drop for Span {
52 fn drop(&mut self) {
53 let dur_ns = self.start.elapsed().as_nanos() as u64;
54 let _ = EVENTS.try_with(|cell| {
59 cell.borrow_mut().push(super::Event {
60 name: self.name,
61 kind: super::EventKind::Span { dur_ns },
62 });
63 });
64 }
65 }
66
67 pub(super) fn open(name: &'static str) -> Span {
68 Span { name, start: Instant::now() }
69 }
70
71 pub(super) fn sample_rss(label: &'static str, bytes: u64) {
72 let _ = EVENTS.try_with(|cell| {
74 cell.borrow_mut().push(super::Event {
75 name: label,
76 kind: super::EventKind::Rss { bytes },
77 });
78 });
79 }
80
81 pub(super) fn drain() -> Vec<super::Event> {
82 EVENTS
83 .try_with(|cell| core::mem::take(&mut *cell.borrow_mut()))
84 .unwrap_or_default()
85 }
86
87 pub(super) fn drop_events() {
88 let _ = EVENTS.try_with(|cell| cell.borrow_mut().clear());
89 }
90
91 pub(super) fn peek_len() -> usize {
92 EVENTS.try_with(|cell| cell.borrow().len()).unwrap_or(0)
93 }
94
95 pub(super) fn enabled() -> bool {
96 true
97 }
98}
99
100#[cfg(any(
101 not(feature = "probe"),
102 target_family = "wasm",
103 feature = "web_lift"
104))]
105mod imp {
106 #[derive(Debug)]
107 pub struct Span;
108
109 impl Drop for Span {
110 #[inline]
111 fn drop(&mut self) {}
112 }
113
114 #[inline]
115 pub(super) const fn open(_name: &'static str) -> Span {
116 Span
117 }
118
119 #[inline]
120 pub(super) const fn sample_rss(_label: &'static str, _bytes: u64) {}
121
122 #[inline]
123 pub(super) const fn drain() -> Vec<super::Event> {
124 Vec::new()
125 }
126
127 #[inline]
128 pub(super) const fn drop_events() {}
129
130 #[inline]
131 pub(super) const fn peek_len() -> usize { 0 }
132
133 #[inline]
134 pub(super) const fn enabled() -> bool {
135 false
136 }
137}
138
139#[derive(Copy, Debug, Clone)]
142pub struct Event {
143 pub name: &'static str,
144 pub kind: EventKind,
145}
146
147#[derive(Copy, Debug, Clone)]
148pub enum EventKind {
149 Span { dur_ns: u64 },
151 Rss { bytes: u64 },
153}
154
155pub use imp::Span;
157
158#[derive(Copy, Clone, Debug)]
160pub struct Probe {
161 _no_construct: PhantomData<()>,
162}
163
164impl Probe {
165 #[inline]
168 #[allow(clippy::missing_const_for_fn)]
170 #[must_use] pub fn span(name: &'static str) -> Span {
171 imp::open(name)
172 }
173
174 #[inline]
179 #[allow(clippy::missing_const_for_fn)]
181 pub fn sample_rss(label: &'static str, bytes: u64) {
182 imp::sample_rss(label, bytes);
183 }
184
185 #[inline]
187 #[allow(clippy::missing_const_for_fn)]
189 #[must_use] pub fn drain() -> Vec<Event> {
190 imp::drain()
191 }
192
193 #[inline]
198 #[allow(clippy::missing_const_for_fn)]
200 pub fn drop_events() {
201 imp::drop_events();
202 }
203
204 #[inline]
206 #[allow(clippy::missing_const_for_fn)]
208 #[must_use] pub fn peek_len() -> usize {
209 imp::peek_len()
210 }
211
212 #[inline]
214 #[allow(clippy::missing_const_for_fn)]
216 #[must_use] pub fn enabled() -> bool {
217 imp::enabled()
218 }
219}
220
221#[inline]
225#[allow(clippy::cast_possible_truncation)] pub fn monotonic_now_nanos() -> u64 {
227 use std::sync::OnceLock;
228 use std::time::Instant;
229 static LAUNCH: OnceLock<Instant> = OnceLock::new();
230 let start = LAUNCH.get_or_init(Instant::now);
231 start.elapsed().as_nanos() as u64
232}
233
234#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] pub fn print_drained_events(label: &str, events: &[Event]) {
250 use std::collections::BTreeMap;
251
252 if events.is_empty() {
253 if Probe::enabled() {
254 eprintln!("[CPU] {label}: no events recorded this pass");
255 } else {
256 eprintln!(
259 "[CPU] {label}: probe unavailable on this target (timings = ???)"
260 );
261 }
262 return;
263 }
264
265 let mut spans: BTreeMap<&'static str, Vec<u64>> = BTreeMap::new();
266 let mut rss_marks: Vec<(&'static str, u64)> = Vec::new();
267 for ev in events {
268 match ev.kind {
269 EventKind::Span { dur_ns } => spans.entry(ev.name).or_default().push(dur_ns),
270 EventKind::Rss { bytes } => rss_marks.push((ev.name, bytes)),
271 }
272 }
273
274 let mut rows: Vec<(&'static str, usize, u64, u64, u64, u64)> = spans
275 .into_iter()
276 .map(|(name, mut ns)| {
277 ns.sort_unstable();
278 let n = ns.len();
279 let total: u128 = ns.iter().map(|&x| u128::from(x)).sum();
280 let avg = (total / n.max(1) as u128) as u64;
281 let p99 = ns[(n.saturating_sub(1) * 99) / 100];
282 let max = *ns.last().unwrap();
283 (name, n, total as u64, avg, p99, max)
284 })
285 .collect();
286 rows.sort_by(|a, b| b.2.cmp(&a.2));
287
288 eprintln!("[CPU] === {label} ({} phases) ===", rows.len());
289 eprintln!(
290 "[CPU] {:<28} {:>5} {:>10} {:>9} {:>9} {:>9}",
291 "phase", "n", "total(µs)", "avg(µs)", "p99(µs)", "max(µs)"
292 );
293 for (name, n, total, avg, p99, max) in &rows {
294 eprintln!(
295 "[CPU] {:<28} {:>5} {:>10.1} {:>9.2} {:>9.2} {:>9.2}",
296 name,
297 n,
298 (*total as f64) / 1_000.0,
299 (*avg as f64) / 1_000.0,
300 (*p99 as f64) / 1_000.0,
301 (*max as f64) / 1_000.0,
302 );
303 }
304 if !rss_marks.is_empty() {
305 eprintln!("[CPU] -- RSS checkpoints (wall-clock order) --");
306 let mut prev: Option<u64> = None;
307 for (lbl, bytes) in &rss_marks {
308 let delta = prev
309 .map(|p| {
310 let diff = i128::from(*bytes) - i128::from(p);
311 if diff >= 0 {
312 format!(" (Δ +{:.2} MiB)", diff as f64 / 1_048_576.0)
313 } else {
314 format!(" (Δ -{:.2} MiB)", -diff as f64 / 1_048_576.0)
315 }
316 })
317 .unwrap_or_default();
318 eprintln!(
319 "[CPU] {:<28} {:.2} MiB{}",
320 lbl,
321 *bytes as f64 / 1_048_576.0,
322 delta
323 );
324 prev = Some(*bytes);
325 }
326 }
327}
328
329#[inline]
338#[allow(clippy::missing_const_for_fn)]
340pub fn sample_peak_rss(label: &'static str) {
341 #[cfg(all(feature = "probe", not(feature = "web_lift")))]
345 {
346 let (current, _virt) = current_rss_bytes();
347 let bytes = if current != 0 { current } else { peak_rss_bytes_self() };
348 Probe::sample_rss(label, bytes);
349 }
350 #[cfg(any(not(feature = "probe"), feature = "web_lift"))]
351 let _ = label;
352}
353
354#[cfg(feature = "probe")]
355pub fn peak_rss_bytes_pub() -> u64 { peak_rss_bytes_self() }
356
357#[cfg(feature = "probe")]
358fn peak_rss_bytes_self() -> u64 {
359 #[cfg(unix)]
360 unsafe {
361 let mut ru: libc::rusage = core::mem::zeroed();
362 if libc::getrusage(libc::RUSAGE_SELF, &mut ru) != 0 {
363 return 0;
364 }
365 let raw = ru.ru_maxrss as u64;
366 if cfg!(target_os = "macos") { raw } else { raw.saturating_mul(1024) }
367 }
368 #[cfg(not(unix))]
369 {
370 0
371 }
372}
373
374#[inline]
386#[allow(clippy::missing_const_for_fn)]
389pub fn hint_purge_allocator() {
390 #[cfg(feature = "allocator_mimalloc")]
391 {
392 unsafe {
394 libmimalloc_sys::mi_collect(true);
395 }
396 static PURGE_TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
397 if *PURGE_TRACE.get_or_init(azul_core::profile::memory_enabled) {
398 let (rss, _) = current_rss_bytes();
399 eprintln!("[PURGE] mi_collect(true) called — current rss={:.2} MiB", rss as f64 / 1048576.0);
400 }
401 return;
402 }
403 #[cfg(feature = "allocator_jemalloc")]
404 {
405 unsafe {
407 let _ = tikv_jemalloc_sys::mallctl(
408 b"arena.4096.purge\0".as_ptr() as *const _,
409 core::ptr::null_mut(),
410 core::ptr::null_mut(),
411 core::ptr::null_mut(),
412 0,
413 );
414 }
415 return;
416 }
417 #[cfg(all(target_os = "macos", not(miri), not(any(feature = "allocator_mimalloc", feature = "allocator_jemalloc"))))]
418 {
419 extern "C" {
420 fn malloc_zone_pressure_relief(zone: *mut core::ffi::c_void, goal: usize) -> usize;
421 }
422 unsafe {
423 malloc_zone_pressure_relief(core::ptr::null_mut(), 0);
424 }
425 }
426 #[cfg(all(
430 target_os = "linux",
431 target_env = "gnu",
432 not(miri),
433 not(any(feature = "allocator_mimalloc", feature = "allocator_jemalloc"))
434 ))]
435 {
436 extern "C" {
441 fn malloc_trim(pad: usize) -> core::ffi::c_int;
442 }
443 unsafe {
444 malloc_trim(0);
445 }
446 }
447}
448
449#[cfg(feature = "probe")]
459pub fn current_rss_bytes() -> (u64, u64) {
460 #[cfg(miri)]
463 return (0, 0);
464 #[cfg(all(target_os = "macos", not(miri)))]
465 {
466 let pf = phys_footprint_bytes();
470 #[repr(C)]
471 struct MachTaskBasicInfo {
472 virtual_size: u64,
473 resident_size: u64,
474 resident_size_max: u64,
475 user_time: [u32; 2],
476 system_time: [u32; 2],
477 policy: i32,
478 suspend_count: i32,
479 }
480 const MACH_TASK_BASIC_INFO: u32 = 20;
481 extern "C" {
482 fn mach_task_self() -> u32;
483 fn task_info(
484 target: u32, flavor: u32,
485 info: *mut core::ffi::c_void, count: *mut u32,
486 ) -> i32;
487 }
488 unsafe {
489 let mut info: MachTaskBasicInfo = core::mem::zeroed();
490 let mut count = (core::mem::size_of::<MachTaskBasicInfo>() / 4) as u32;
491 let kr = task_info(
492 mach_task_self(),
493 MACH_TASK_BASIC_INFO,
494 &mut info as *mut _ as *mut core::ffi::c_void,
495 &mut count,
496 );
497 if kr == 0 {
498 let rss = if pf != 0 { pf } else { info.resident_size };
499 (rss, info.virtual_size)
500 } else {
501 (pf, 0)
502 }
503 }
504 }
505 #[cfg(all(target_os = "linux", not(miri)))]
511 {
512 let Ok(statm) = std::fs::read_to_string("/proc/self/statm") else {
514 return (0, 0);
515 };
516 let mut it = statm.split_ascii_whitespace();
517 let size: u64 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
518 let resident: u64 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
519 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
520 let page = if page > 0 { page as u64 } else { 4096 };
521 (
522 resident.saturating_mul(page),
523 size.saturating_mul(page),
524 )
525 }
526 #[cfg(not(any(target_os = "macos", all(target_os = "linux", not(miri)))))]
527 { (0, 0) }
528}
529
530#[cfg(feature = "probe")]
556pub fn malloc_heap_bytes() -> u64 {
557 #[cfg(target_os = "macos")]
558 {
559 #[repr(C)]
560 struct Mstats {
561 bytes_total: usize,
562 chunks_used: usize,
563 bytes_used: usize,
564 chunks_free: usize,
565 bytes_free: usize,
566 }
567 extern "C" {
568 fn mstats() -> Mstats;
569 }
570 unsafe { mstats().bytes_used as u64 }
571 }
572 #[cfg(all(target_os = "linux", target_env = "gnu", not(miri)))]
573 {
574 type Mallinfo2Fn = unsafe extern "C" fn() -> libc::mallinfo2;
575 static MALLINFO2: std::sync::OnceLock<Option<Mallinfo2Fn>> =
576 std::sync::OnceLock::new();
577 let resolved = MALLINFO2.get_or_init(|| unsafe {
578 let sym = libc::dlsym(
581 core::ptr::null_mut(),
582 b"mallinfo2\0".as_ptr().cast::<core::ffi::c_char>(),
583 );
584 if sym.is_null() {
585 None
586 } else {
587 Some(core::mem::transmute::<
588 *mut core::ffi::c_void,
589 Mallinfo2Fn,
590 >(sym))
591 }
592 });
593 return match resolved {
594 Some(mallinfo2) => unsafe { mallinfo2().uordblks as u64 },
595 None => unsafe { libc::mallinfo().uordblks.max(0) as u64 },
598 };
599 }
600 #[cfg(not(any(
601 target_os = "macos",
602 all(target_os = "linux", target_env = "gnu", not(miri))
603 )))]
604 { 0 }
605}
606
607#[cfg(feature = "probe")]
619pub fn phys_footprint_bytes() -> u64 {
620 #[cfg(miri)]
622 return 0;
623 #[cfg(all(target_os = "macos", not(miri)))]
624 {
625 #[repr(C)]
629 struct TaskVmInfo {
630 virtual_size: u64,
631 region_count: u32,
632 page_size: u32,
633 resident_size: u64,
634 resident_size_peak: u64,
635 device: u64,
636 device_peak: u64,
637 internal: u64,
638 internal_peak: u64,
639 external: u64,
640 external_peak: u64,
641 reusable: u64,
642 reusable_peak: u64,
643 purgeable_volatile_pmap: u64,
644 purgeable_volatile_resident: u64,
645 purgeable_volatile_virtual: u64,
646 compressed: u64,
647 compressed_peak: u64,
648 compressed_lifetime: u64,
649 phys_footprint: u64,
650 _rest: [u64; 12],
652 }
653 const TASK_VM_INFO: u32 = 22;
654 extern "C" {
655 fn mach_task_self() -> u32;
656 fn task_info(
657 target: u32, flavor: u32,
658 info: *mut core::ffi::c_void, count: *mut u32,
659 ) -> i32;
660 }
661 unsafe {
662 let mut info: TaskVmInfo = core::mem::zeroed();
663 let mut count = (core::mem::size_of::<TaskVmInfo>() / 4) as u32;
664 let kr = task_info(
665 mach_task_self(),
666 TASK_VM_INFO,
667 &mut info as *mut _ as *mut core::ffi::c_void,
668 &mut count,
669 );
670 if kr == 0 { info.phys_footprint } else { 0 }
671 }
672 }
673 #[cfg(not(target_os = "macos"))]
674 { 0 }
675}
676
677#[cfg(feature = "probe")]
689pub fn start_peak_sampler() {
690 #[cfg(target_os = "macos")]
691 {
692 use std::sync::atomic::Ordering;
693 static STARTED: std::sync::atomic::AtomicBool =
695 std::sync::atomic::AtomicBool::new(false);
696 if STARTED.swap(true, Ordering::AcqRel) {
697 return;
698 }
699 std::thread::Builder::new()
700 .name("azul-peak-sampler".to_string())
701 .spawn(|| loop {
702 let now = phys_footprint_bytes();
703 let prev = PEAK_PHYS_FOOTPRINT.load(Ordering::Relaxed);
704 if now > prev {
705 PEAK_PHYS_FOOTPRINT.store(now, Ordering::Relaxed);
706 }
707 std::thread::sleep(std::time::Duration::from_micros(250));
708 })
709 .ok();
710 }
711}
712
713#[cfg(feature = "probe")]
714static PEAK_PHYS_FOOTPRINT: std::sync::atomic::AtomicU64 =
715 std::sync::atomic::AtomicU64::new(0);
716
717#[cfg(feature = "probe")]
720pub fn peak_phys_footprint_seen() -> u64 {
721 PEAK_PHYS_FOOTPRINT.load(std::sync::atomic::Ordering::Relaxed)
722}
723
724#[cfg(feature = "probe")]
730pub fn reset_peak() {
731 let now = phys_footprint_bytes();
732 PEAK_PHYS_FOOTPRINT.store(now, std::sync::atomic::Ordering::Relaxed);
733}
734
735#[cfg(feature = "probe")]
739#[inline]
740pub fn sample_phase_peak(label: &'static str) {
741 let peak = PEAK_PHYS_FOOTPRINT.load(std::sync::atomic::Ordering::Relaxed);
742 Probe::sample_rss(label, peak);
743}
744
745#[cfg(not(feature = "probe"))]
746#[inline]
747pub const fn reset_peak() {}
748
749#[cfg(not(feature = "probe"))]
750#[inline]
751pub const fn sample_phase_peak(_label: &'static str) {}
752
753#[cfg(not(feature = "probe"))]
754#[inline]
755#[must_use] pub const fn malloc_heap_bytes() -> u64 { 0 }
756
757#[cfg(feature = "probe")]
772pub fn emit_phase_heap(label: &str) {
773 use std::io::Write;
774 if !heap_jsonl_enabled() { return; }
775 let Some(p) = azul_core::profile::out_path() else { return };
776 static CALL_ID: std::sync::atomic::AtomicU64 =
777 std::sync::atomic::AtomicU64::new(0);
778 static CURRENT_CALL: std::sync::atomic::AtomicU64 =
782 std::sync::atomic::AtomicU64::new(0);
783 let call_id = if label == "start" {
784 let next = CALL_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
785 CURRENT_CALL.store(next, std::sync::atomic::Ordering::Relaxed);
786 next
787 } else {
788 CURRENT_CALL.load(std::sync::atomic::Ordering::Relaxed)
789 };
790 let heap = malloc_heap_bytes();
791 if let Ok(mut f) = std::fs::OpenOptions::new()
792 .create(true)
793 .append(true)
794 .open(p)
795 {
796 let _ = writeln!(
797 f,
798 r#"{{"ev":"phase","call":{},"label":"{}","heap":{}}}"#,
799 call_id, label, heap
800 );
801 }
802}
803
804#[cfg(not(feature = "probe"))]
805#[inline]
806pub const fn emit_phase_heap(_label: &str) {}
807
808#[cfg(feature = "probe")]
816pub fn emit_phase_heap_extra(label: &str, extra: u64) {
817 use std::io::Write;
818 if !heap_jsonl_enabled() { return; }
819 if !azul_core::profile::detail_enabled() { return; }
820 let Some(p) = azul_core::profile::out_path() else { return };
821 let heap = malloc_heap_bytes();
822 if let Ok(mut f) = std::fs::OpenOptions::new()
823 .create(true)
824 .append(true)
825 .open(p)
826 {
827 let _ = writeln!(
828 f,
829 r#"{{"ev":"phase","call":0,"label":"{}","heap":{},"extra":{}}}"#,
830 label, heap, extra
831 );
832 }
833}
834
835#[cfg(not(feature = "probe"))]
836#[inline]
837pub const fn emit_phase_heap_extra(_label: &str, _extra: u64) {}
838
839#[cfg(feature = "probe")]
842#[inline]
843fn heap_jsonl_enabled() -> bool {
844 let f = azul_core::profile::flags();
845 f.heap && f.jsonl
846}
847
848#[cfg(feature = "probe")]
852#[inline]
853pub fn detail_enabled() -> bool {
854 azul_core::profile::detail_enabled()
855}
856
857#[cfg(not(feature = "probe"))]
858#[inline]
859#[must_use] pub const fn detail_enabled() -> bool { false }
860
861#[cfg(test)]
862#[allow(let_underscore_drop, clippy::too_many_lines)]
863mod autotest_generated {
864 use super::*;
865
866 fn leak(s: String) -> &'static str {
870 Box::leak(s.into_boxed_str())
871 }
872
873 fn reset() {
877 Probe::drop_events();
878 assert_eq!(Probe::peek_len(), 0, "drop_events must leave an empty buffer");
879 }
880
881 fn span_ns(ev: &Event) -> Option<u64> {
882 match ev.kind {
883 EventKind::Span { dur_ns } => Some(dur_ns),
884 EventKind::Rss { .. } => None,
885 }
886 }
887
888 fn rss_bytes(ev: &Event) -> Option<u64> {
889 match ev.kind {
890 EventKind::Rss { bytes } => Some(bytes),
891 EventKind::Span { .. } => None,
892 }
893 }
894
895 #[test]
900 fn enabled_matches_the_compiled_imp() {
901 let expected = cfg!(all(
905 feature = "probe",
906 not(target_family = "wasm"),
907 not(feature = "web_lift")
908 ));
909 assert_eq!(Probe::enabled(), expected);
910 assert_eq!(imp::enabled(), expected);
911 }
912
913 #[test]
914 fn enabled_is_pure_and_idempotent() {
915 let first = Probe::enabled();
916 for _ in 0..1000 {
917 assert_eq!(Probe::enabled(), first);
918 }
919 }
920
921 #[test]
926 fn span_round_trips_name_through_drain() {
927 reset();
928 {
929 let _g = Probe::span("autotest_span_round_trip");
930 }
931 let events = Probe::drain();
932 if Probe::enabled() {
933 assert_eq!(events.len(), 1);
934 assert_eq!(events[0].name, "autotest_span_round_trip");
935 assert!(span_ns(&events[0]).is_some(), "span guard must emit EventKind::Span");
936 } else {
937 assert!(events.is_empty(), "no-op imp must never buffer events");
938 }
939 assert_eq!(Probe::peek_len(), 0, "drain must empty the buffer");
940 }
941
942 #[test]
943 fn nested_spans_drop_inner_first_and_outer_duration_is_the_larger() {
944 reset();
945 {
946 let _outer = Probe::span("outer");
947 {
948 let _inner = Probe::span("inner");
949 }
950 }
951 let events = Probe::drain();
952 if !Probe::enabled() {
953 assert!(events.is_empty());
954 return;
955 }
956 assert_eq!(events.len(), 2);
957 assert_eq!(events[0].name, "inner");
959 assert_eq!(events[1].name, "outer");
960 let inner = span_ns(&events[0]).expect("inner is a span");
961 let outer = span_ns(&events[1]).expect("outer is a span");
962 assert!(
964 outer >= inner,
965 "outer span ({outer} ns) must cover the inner one ({inner} ns)"
966 );
967 }
968
969 #[test]
970 fn forgotten_span_guard_records_nothing() {
971 reset();
972 core::mem::forget(Probe::span("forgotten"));
973 let events = Probe::drain();
974 assert!(
975 events.is_empty(),
976 "a leaked guard never runs Drop, so it must not emit an event"
977 );
978 }
979
980 #[test]
981 fn many_spans_do_not_lose_or_reorder_events() {
982 reset();
983 const N: usize = 10_000;
984 let names: Vec<&'static str> = (0..N).map(|i| leak(format!("phase_{i}"))).collect();
985 for &name in &names {
986 drop(Probe::span(name));
987 }
988 if Probe::enabled() {
989 assert_eq!(Probe::peek_len(), N);
990 } else {
991 assert_eq!(Probe::peek_len(), 0);
992 }
993 let events = Probe::drain();
994 if Probe::enabled() {
995 assert_eq!(events.len(), N);
996 for (i, ev) in events.iter().enumerate() {
997 assert_eq!(ev.name, names[i], "event order must be emission order");
998 }
999 } else {
1000 assert!(events.is_empty());
1001 }
1002 assert_eq!(Probe::peek_len(), 0);
1003 }
1004
1005 #[test]
1006 fn span_survives_hostile_unicode_and_huge_names() {
1007 reset();
1008 let hostile: Vec<&'static str> = vec![
1009 "",
1010 "\0embedded\0nul\0",
1011 "\n\r\t",
1012 "{}{:?}{0}%s%n", "🦀👨👩👧👦🇩🇪", "مرحبا بالعالم", "e\u{0301}\u{0301}\u{0301}", leak("A".repeat(100_000)), leak("\u{1F4A9}".repeat(10_000)),
1018 ];
1019 for &name in &hostile {
1020 drop(Probe::span(name));
1021 }
1022 let events = Probe::drain();
1023 if Probe::enabled() {
1024 assert_eq!(events.len(), hostile.len());
1025 for (ev, name) in events.iter().zip(hostile.iter()) {
1026 assert_eq!(ev.name, *name, "name must round-trip byte-for-byte");
1027 }
1028 print_drained_events("hostile-names", &events);
1030 } else {
1031 assert!(events.is_empty());
1032 }
1033 }
1034
1035 #[test]
1036 fn drain_is_empty_the_second_time() {
1037 reset();
1038 drop(Probe::span("once"));
1039 let first = Probe::drain();
1040 let second = Probe::drain();
1041 if Probe::enabled() {
1042 assert_eq!(first.len(), 1);
1043 }
1044 assert!(second.is_empty(), "a drained buffer must stay drained");
1045 }
1046
1047 #[test]
1052 fn sample_rss_round_trips_every_numeric_boundary() {
1053 reset();
1054 let boundaries: [u64; 8] = [
1055 0,
1056 1,
1057 u64::from(u32::MAX),
1058 u64::from(u32::MAX) + 1,
1059 1 << 63,
1060 u64::MAX - 1,
1061 u64::MAX,
1062 0xDEAD_BEEF_DEAD_BEEF,
1063 ];
1064 for b in boundaries {
1065 Probe::sample_rss("bytes", b);
1066 }
1067 let events = Probe::drain();
1068 if !Probe::enabled() {
1069 assert!(events.is_empty());
1070 return;
1071 }
1072 assert_eq!(events.len(), boundaries.len());
1073 for (ev, expected) in events.iter().zip(boundaries.iter()) {
1074 assert_eq!(
1075 rss_bytes(ev),
1076 Some(*expected),
1077 "RSS byte counts must survive the buffer unchanged (no saturation)"
1078 );
1079 }
1080 }
1081
1082 #[test]
1083 fn sample_rss_zero_is_recorded_not_skipped() {
1084 reset();
1085 Probe::sample_rss("zero", 0);
1086 let events = Probe::drain();
1087 if Probe::enabled() {
1088 assert_eq!(events.len(), 1, "a 0-byte checkpoint is still a checkpoint");
1089 assert_eq!(rss_bytes(&events[0]), Some(0));
1090 assert_eq!(events[0].name, "zero");
1091 } else {
1092 assert!(events.is_empty());
1093 }
1094 }
1095
1096 #[test]
1101 fn peek_len_tracks_pushes_and_drop_events_clears() {
1102 reset();
1103 assert_eq!(Probe::peek_len(), 0);
1104 for i in 0..64u64 {
1105 Probe::sample_rss("tick", i);
1106 }
1107 if Probe::enabled() {
1108 assert_eq!(Probe::peek_len(), 64);
1109 } else {
1110 assert_eq!(Probe::peek_len(), 0);
1111 }
1112 Probe::drop_events();
1113 assert_eq!(Probe::peek_len(), 0, "drop_events must clear the buffer");
1114 assert!(
1115 Probe::drain().is_empty(),
1116 "drop_events must discard, not stash, the events"
1117 );
1118 }
1119
1120 #[test]
1121 fn drop_events_on_an_empty_buffer_is_a_no_op() {
1122 reset();
1123 for _ in 0..100 {
1124 Probe::drop_events();
1125 assert_eq!(Probe::peek_len(), 0);
1126 }
1127 }
1128
1129 #[test]
1130 fn peek_len_is_side_effect_free() {
1131 reset();
1132 Probe::sample_rss("keep", 7);
1133 let expected = if Probe::enabled() { 1 } else { 0 };
1134 for _ in 0..100 {
1135 assert_eq!(Probe::peek_len(), expected, "peek must not consume events");
1136 }
1137 let events = Probe::drain();
1138 assert_eq!(events.len(), expected);
1139 }
1140
1141 #[test]
1146 fn event_buffer_is_per_thread() {
1147 reset();
1148 Probe::sample_rss("main_thread", 1);
1149
1150 let child_len = std::thread::spawn(|| {
1151 assert_eq!(Probe::peek_len(), 0, "buffers must not be shared across threads");
1154 Probe::sample_rss("child_thread", 2);
1155 let drained = Probe::drain();
1156 for ev in &drained {
1157 assert_eq!(ev.name, "child_thread", "child must only see its own events");
1158 }
1159 drained.len()
1160 })
1161 .join()
1162 .expect("probe calls must not panic on a spawned thread");
1163
1164 let events = Probe::drain();
1165 if Probe::enabled() {
1166 assert_eq!(child_len, 1);
1167 assert_eq!(events.len(), 1, "the child's drain must not touch our buffer");
1168 assert_eq!(events[0].name, "main_thread");
1169 } else {
1170 assert_eq!(child_len, 0);
1171 assert!(events.is_empty());
1172 }
1173 }
1174
1175 #[test]
1180 fn imp_facade_parity() {
1181 reset();
1182 {
1183 let _g = imp::open("imp_open");
1184 }
1185 imp::sample_rss("imp_rss", u64::MAX);
1186 let len = imp::peek_len();
1187 assert_eq!(len, Probe::peek_len());
1188 let events = imp::drain();
1189 assert_eq!(events.len(), len);
1190 assert_eq!(imp::peek_len(), 0);
1191 if Probe::enabled() {
1192 assert_eq!(events[0].name, "imp_open");
1193 assert_eq!(rss_bytes(&events[1]), Some(u64::MAX));
1194 } else {
1195 assert!(events.is_empty());
1196 }
1197 imp::drop_events();
1198 assert_eq!(imp::peek_len(), 0);
1199 }
1200
1201 #[test]
1206 fn print_drained_events_empty_slice_does_not_panic() {
1207 print_drained_events("empty", &[]);
1211 print_drained_events("", &[]);
1212 }
1213
1214 #[test]
1215 fn print_drained_events_rss_only_has_no_span_rows() {
1216 let events = [
1219 Event { name: "a", kind: EventKind::Rss { bytes: 0 } },
1220 Event { name: "b", kind: EventKind::Rss { bytes: u64::MAX } },
1221 Event { name: "c", kind: EventKind::Rss { bytes: 1 } },
1222 ];
1223 print_drained_events("rss-only", &events);
1224 }
1225
1226 #[test]
1227 fn print_drained_events_p99_index_is_in_bounds_for_every_sample_count() {
1228 for n in [1usize, 2, 3, 99, 100, 101, 199, 200, 201, 1000] {
1231 let events: Vec<Event> = (0..n)
1232 .map(|i| Event {
1233 name: "phase",
1234 kind: EventKind::Span { dur_ns: i as u64 },
1235 })
1236 .collect();
1237 print_drained_events("p99", &events);
1238 }
1239 }
1240
1241 #[test]
1242 fn print_drained_events_saturating_totals_do_not_panic() {
1243 let events = [
1247 Event { name: "huge", kind: EventKind::Span { dur_ns: u64::MAX } },
1248 Event { name: "huge", kind: EventKind::Span { dur_ns: u64::MAX } },
1249 Event { name: "huge", kind: EventKind::Span { dur_ns: u64::MAX } },
1250 Event { name: "zero", kind: EventKind::Span { dur_ns: 0 } },
1251 ];
1252 print_drained_events("overflowing-total", &events);
1253 }
1254
1255 #[test]
1256 fn print_drained_events_rss_delta_handles_full_u64_swing() {
1257 let events = [
1260 Event { name: "peak", kind: EventKind::Rss { bytes: u64::MAX } },
1261 Event { name: "trough", kind: EventKind::Rss { bytes: 0 } },
1262 Event { name: "peak_again", kind: EventKind::Rss { bytes: u64::MAX } },
1263 ];
1264 print_drained_events("delta-swing", &events);
1265 }
1266
1267 #[test]
1268 fn print_drained_events_hostile_labels_and_names() {
1269 let big = leak("x".repeat(65_536));
1270 let events = [
1271 Event { name: "", kind: EventKind::Span { dur_ns: 1 } },
1272 Event { name: "{}{:?}", kind: EventKind::Span { dur_ns: 2 } },
1273 Event { name: big, kind: EventKind::Span { dur_ns: u64::MAX } },
1274 Event { name: "🦀\u{0301}\0", kind: EventKind::Rss { bytes: 1 } },
1275 ];
1276 print_drained_events(big, &events);
1277 print_drained_events("\0\n{}", &events);
1278 }
1279
1280 #[test]
1281 fn print_drained_events_accepts_a_real_drain() {
1282 reset();
1283 {
1284 let _a = Probe::span("layout");
1285 let _b = Probe::span("layout");
1286 }
1287 Probe::sample_rss("after", 4096);
1288 let events = Probe::drain();
1289 print_drained_events("real-drain", &events);
1290 }
1291
1292 #[test]
1297 fn monotonic_now_nanos_never_goes_backwards() {
1298 let mut prev = monotonic_now_nanos();
1299 for _ in 0..10_000 {
1300 let now = monotonic_now_nanos();
1301 assert!(now >= prev, "clock went backwards: {prev} -> {now}");
1302 prev = now;
1303 }
1304 }
1305
1306 #[test]
1307 fn monotonic_now_nanos_is_monotonic_across_threads() {
1308 let before = monotonic_now_nanos();
1311 let mid = std::thread::spawn(monotonic_now_nanos)
1312 .join()
1313 .expect("monotonic_now_nanos must not panic off the main thread");
1314 let after = monotonic_now_nanos();
1315 assert!(before <= mid && mid <= after, "{before} <= {mid} <= {after}");
1316 }
1317
1318 #[test]
1323 fn sample_peak_rss_emits_exactly_one_labelled_event() {
1324 reset();
1325 sample_peak_rss("autotest_peak_rss");
1326 let events = Probe::drain();
1327 if Probe::enabled() {
1328 assert_eq!(events.len(), 1);
1329 assert_eq!(events[0].name, "autotest_peak_rss");
1330 assert!(
1331 rss_bytes(&events[0]).is_some(),
1332 "sample_peak_rss must emit an Rss-kind event"
1333 );
1334 } else {
1335 assert!(events.is_empty());
1336 }
1337 }
1338
1339 #[test]
1340 fn sample_phase_peak_emits_exactly_one_labelled_event() {
1341 reset();
1342 sample_phase_peak("autotest_phase_peak");
1343 let events = Probe::drain();
1344 if Probe::enabled() {
1345 assert_eq!(events.len(), 1);
1346 assert_eq!(events[0].name, "autotest_phase_peak");
1347 assert!(rss_bytes(&events[0]).is_some());
1348 } else {
1349 assert!(events.is_empty());
1350 }
1351 }
1352
1353 #[test]
1354 fn reset_peak_is_repeatable_and_side_effect_free_on_the_event_buffer() {
1355 reset();
1356 for _ in 0..100 {
1357 reset_peak();
1358 }
1359 assert_eq!(
1360 Probe::peek_len(),
1361 0,
1362 "reset_peak touches an atomic, it must not push events"
1363 );
1364 }
1365
1366 #[test]
1367 fn hint_purge_allocator_is_repeatable_and_emits_nothing() {
1368 reset();
1369 for _ in 0..50 {
1370 hint_purge_allocator();
1371 }
1372 assert_eq!(Probe::peek_len(), 0, "purging must not push probe events");
1373 }
1374
1375 const HEAP_BYTES_IS_REAL: bool = cfg!(all(
1383 feature = "probe",
1384 any(
1385 target_os = "macos",
1386 all(target_os = "linux", target_env = "gnu")
1387 ),
1388 not(miri)
1389 ));
1390
1391 #[test]
1392 fn malloc_heap_bytes_actually_tracks_live_heap() {
1393 if !HEAP_BYTES_IS_REAL {
1394 assert_eq!(malloc_heap_bytes(), 0);
1397 assert_eq!(malloc_heap_bytes(), 0);
1398 return;
1399 }
1400
1401 const BLOCK: usize = 64 * 1024;
1410 const BLOCKS: usize = 128;
1411 const TOTAL: u64 = (BLOCK * BLOCKS) as u64;
1412
1413 let before = malloc_heap_bytes();
1414 assert!(before > 0, "a live process holds a non-zero heap");
1415
1416 let mut ballast: Vec<Vec<u8>> = Vec::with_capacity(BLOCKS);
1417 for _ in 0..BLOCKS {
1418 ballast.push(vec![0xAB_u8; BLOCK]);
1420 }
1421 let during = malloc_heap_bytes();
1422
1423 drop(ballast);
1424 let after = malloc_heap_bytes();
1425
1426 assert!(
1427 during >= before + TOTAL / 2,
1428 "allocating {TOTAL} B moved the probe by only {} B \
1429 (before={before}, during={during}) — it is not measuring the heap",
1430 during.saturating_sub(before),
1431 );
1432 assert!(
1433 after < during - TOTAL / 2,
1434 "freeing {TOTAL} B left the probe at {after} B (during={during}) — \
1435 it does not see frees, so it cannot distinguish a leak from churn",
1436 );
1437 }
1438
1439 #[test]
1440 fn detail_enabled_is_deterministic() {
1441 let first = detail_enabled();
1442 for _ in 0..100 {
1443 assert_eq!(detail_enabled(), first, "flag reads are cached, must not flap");
1444 }
1445 if !cfg!(feature = "probe") {
1446 assert!(!first, "the no-probe stub is a const `false`");
1447 }
1448 }
1449
1450 #[test]
1456 fn emit_phase_heap_survives_hostile_labels() {
1457 reset();
1458 let huge = "L".repeat(65_536);
1459 let labels: Vec<&str> = vec![
1460 "",
1461 "start",
1462 "start", "end",
1464 "\"quote\"", "back\\slash",
1466 "new\nline",
1467 "\0nul",
1468 "🦀 unicode",
1469 &huge,
1470 ];
1471 for l in &labels {
1472 emit_phase_heap(l);
1473 }
1474 assert_eq!(Probe::peek_len(), 0, "JSONL emission must not touch the span buffer");
1475 }
1476
1477 #[test]
1478 fn emit_phase_heap_extra_survives_numeric_boundaries() {
1479 reset();
1480 for extra in [0u64, 1, u64::MAX / 2, u64::MAX - 1, u64::MAX] {
1481 emit_phase_heap_extra("autotest_extra", extra);
1482 emit_phase_heap_extra("", extra);
1483 }
1484 assert_eq!(Probe::peek_len(), 0);
1485 }
1486
1487 #[test]
1492 fn event_is_copy_and_clone_preserving_payload() {
1493 let span = Event { name: "n", kind: EventKind::Span { dur_ns: u64::MAX } };
1494 let rss = Event { name: "n", kind: EventKind::Rss { bytes: u64::MAX } };
1495 let span_copy = span; #[allow(clippy::clone_on_copy)]
1497 let rss_clone = rss.clone();
1498 assert_eq!(span_ns(&span_copy), Some(u64::MAX));
1499 assert_eq!(rss_bytes(&rss_clone), Some(u64::MAX));
1500 assert!(span_ns(&rss_clone).is_none());
1502 assert!(rss_bytes(&span_copy).is_none());
1503 let _ = format!("{span:?}{rss:?}");
1505 }
1506
1507 #[cfg(feature = "probe")]
1512 #[test]
1513 fn peak_rss_bytes_is_monotonic_and_agrees_with_the_pub_wrapper() {
1514 let first = peak_rss_bytes_self();
1516 let pubbed = peak_rss_bytes_pub();
1517 let second = peak_rss_bytes_self();
1518 assert!(pubbed >= first, "peak RSS must never decrease: {first} -> {pubbed}");
1519 assert!(second >= pubbed, "peak RSS must never decrease: {pubbed} -> {second}");
1520 if cfg!(unix) && !cfg!(miri) {
1521 assert!(first > 0, "getrusage on a live unix process must report some RSS");
1522 }
1523 }
1524
1525 #[cfg(feature = "probe")]
1526 #[test]
1527 fn current_rss_bytes_does_not_panic_and_is_self_consistent() {
1528 let (footprint, virt) = current_rss_bytes();
1529 if cfg!(all(target_os = "macos", not(miri))) {
1530 assert!(footprint > 0, "macOS must report a non-zero footprint");
1531 assert!(virt >= footprint || virt == 0);
1532 }
1533 for _ in 0..100 {
1535 let _ = current_rss_bytes();
1536 }
1537 }
1538
1539 #[cfg(feature = "probe")]
1540 #[test]
1541 fn phys_footprint_bytes_is_zero_off_macos() {
1542 let v = phys_footprint_bytes();
1543 if cfg!(all(target_os = "macos", not(miri))) {
1544 assert!(v > 0);
1545 } else {
1546 assert_eq!(v, 0, "documented: returns 0 on non-macOS / under miri");
1547 }
1548 }
1549
1550 #[cfg(feature = "probe")]
1551 #[test]
1552 fn start_peak_sampler_is_idempotent() {
1553 for _ in 0..200 {
1556 start_peak_sampler();
1557 }
1558 let _ = peak_phys_footprint_seen();
1559 }
1560
1561 #[cfg(feature = "probe")]
1562 #[test]
1563 fn peak_phys_footprint_seen_is_readable_without_a_sampler() {
1564 let seen = peak_phys_footprint_seen();
1568 if !cfg!(target_os = "macos") {
1569 assert_eq!(seen, 0, "no phys_footprint source off macOS => peak stays 0");
1570 }
1571 }
1572
1573 #[cfg(feature = "probe")]
1574 #[test]
1575 fn heap_jsonl_enabled_matches_the_profile_flags() {
1576 let f = azul_core::profile::flags();
1577 assert_eq!(
1578 heap_jsonl_enabled(),
1579 f.heap && f.jsonl,
1580 "either token alone must be a no-op"
1581 );
1582 let first = heap_jsonl_enabled();
1583 for _ in 0..100 {
1584 assert_eq!(heap_jsonl_enabled(), first, "flags are cached, must not flap");
1585 }
1586 }
1587}