1use core::marker::PhantomData;
16
17#[cfg(all(feature = "probe", not(target_family = "wasm")))]
24mod imp {
25 use std::cell::RefCell;
26 use std::time::Instant;
27
28 thread_local! {
29 static EVENTS: RefCell<Vec<super::Event>> = const { RefCell::new(Vec::new()) };
30 }
31
32 pub struct Span {
34 pub(crate) name: &'static str,
35 pub(crate) start: Instant,
36 }
37
38 impl Drop for Span {
39 fn drop(&mut self) {
40 let dur_ns = self.start.elapsed().as_nanos() as u64;
41 let _ = EVENTS.try_with(|cell| {
46 cell.borrow_mut().push(super::Event {
47 name: self.name,
48 kind: super::EventKind::Span { dur_ns },
49 });
50 });
51 }
52 }
53
54 pub(super) fn open(name: &'static str) -> Span {
55 Span { name, start: Instant::now() }
56 }
57
58 pub(super) fn sample_rss(label: &'static str, bytes: u64) {
59 let _ = EVENTS.try_with(|cell| {
61 cell.borrow_mut().push(super::Event {
62 name: label,
63 kind: super::EventKind::Rss { bytes },
64 });
65 });
66 }
67
68 pub(super) fn drain() -> Vec<super::Event> {
69 EVENTS
70 .try_with(|cell| core::mem::take(&mut *cell.borrow_mut()))
71 .unwrap_or_default()
72 }
73
74 pub(super) fn drop_events() {
75 let _ = EVENTS.try_with(|cell| cell.borrow_mut().clear());
76 }
77
78 pub(super) fn peek_len() -> usize {
79 EVENTS.try_with(|cell| cell.borrow().len()).unwrap_or(0)
80 }
81
82 pub(super) fn enabled() -> bool {
83 true
84 }
85}
86
87#[cfg(any(not(feature = "probe"), target_family = "wasm"))]
88mod imp {
89 pub struct Span;
90
91 impl Drop for Span {
92 #[inline(always)]
93 fn drop(&mut self) {}
94 }
95
96 #[inline(always)]
97 pub(super) fn open(_name: &'static str) -> Span {
98 Span
99 }
100
101 #[inline(always)]
102 pub(super) fn sample_rss(_label: &'static str, _bytes: u64) {}
103
104 #[inline(always)]
105 pub(super) fn drain() -> Vec<super::Event> {
106 Vec::new()
107 }
108
109 #[inline(always)]
110 pub(super) fn drop_events() {}
111
112 #[inline(always)]
113 pub(super) fn peek_len() -> usize { 0 }
114
115 #[inline(always)]
116 pub(super) fn enabled() -> bool {
117 false
118 }
119}
120
121#[derive(Debug, Clone)]
124pub struct Event {
125 pub name: &'static str,
126 pub kind: EventKind,
127}
128
129#[derive(Debug, Clone)]
130pub enum EventKind {
131 Span { dur_ns: u64 },
133 Rss { bytes: u64 },
135}
136
137pub use imp::Span;
139
140pub struct Probe {
142 _no_construct: PhantomData<()>,
143}
144
145impl Probe {
146 #[inline(always)]
149 pub fn span(name: &'static str) -> Span {
150 imp::open(name)
151 }
152
153 #[inline(always)]
158 pub fn sample_rss(label: &'static str, bytes: u64) {
159 imp::sample_rss(label, bytes);
160 }
161
162 #[inline(always)]
164 pub fn drain() -> Vec<Event> {
165 imp::drain()
166 }
167
168 #[inline(always)]
173 pub fn drop_events() {
174 imp::drop_events();
175 }
176
177 #[inline(always)]
179 pub fn peek_len() -> usize {
180 imp::peek_len()
181 }
182
183 #[inline(always)]
185 pub fn enabled() -> bool {
186 imp::enabled()
187 }
188}
189
190#[inline]
194pub fn monotonic_now_nanos() -> u64 {
195 use std::sync::OnceLock;
196 use std::time::Instant;
197 static LAUNCH: OnceLock<Instant> = OnceLock::new();
198 let start = LAUNCH.get_or_init(Instant::now);
199 start.elapsed().as_nanos() as u64
200}
201
202pub fn print_drained_events(label: &str, events: &[Event]) {
214 use std::collections::BTreeMap;
215
216 if events.is_empty() {
217 if !Probe::enabled() {
218 eprintln!(
221 "[CPU] {label}: probe unavailable on this target (timings = ???)"
222 );
223 } else {
224 eprintln!("[CPU] {label}: no events recorded this pass");
225 }
226 return;
227 }
228
229 let mut spans: BTreeMap<&'static str, Vec<u64>> = BTreeMap::new();
230 let mut rss_marks: Vec<(&'static str, u64)> = Vec::new();
231 for ev in events {
232 match ev.kind {
233 EventKind::Span { dur_ns } => spans.entry(ev.name).or_default().push(dur_ns),
234 EventKind::Rss { bytes } => rss_marks.push((ev.name, bytes)),
235 }
236 }
237
238 let mut rows: Vec<(&'static str, usize, u64, u64, u64, u64)> = spans
239 .into_iter()
240 .map(|(name, mut ns)| {
241 ns.sort_unstable();
242 let n = ns.len();
243 let total: u128 = ns.iter().map(|&x| x as u128).sum();
244 let avg = (total / n.max(1) as u128) as u64;
245 let p99 = ns[(n.saturating_sub(1) * 99) / 100];
246 let max = *ns.last().unwrap();
247 (name, n, total as u64, avg, p99, max)
248 })
249 .collect();
250 rows.sort_by(|a, b| b.2.cmp(&a.2));
251
252 eprintln!("[CPU] === {label} ({} phases) ===", rows.len());
253 eprintln!(
254 "[CPU] {:<28} {:>5} {:>10} {:>9} {:>9} {:>9}",
255 "phase", "n", "total(µs)", "avg(µs)", "p99(µs)", "max(µs)"
256 );
257 for (name, n, total, avg, p99, max) in &rows {
258 eprintln!(
259 "[CPU] {:<28} {:>5} {:>10.1} {:>9.2} {:>9.2} {:>9.2}",
260 name,
261 n,
262 (*total as f64) / 1_000.0,
263 (*avg as f64) / 1_000.0,
264 (*p99 as f64) / 1_000.0,
265 (*max as f64) / 1_000.0,
266 );
267 }
268 if !rss_marks.is_empty() {
269 eprintln!("[CPU] -- RSS checkpoints (wall-clock order) --");
270 let mut prev: Option<u64> = None;
271 for (lbl, bytes) in &rss_marks {
272 let delta = prev
273 .map(|p| {
274 let diff = *bytes as i128 - p as i128;
275 if diff >= 0 {
276 format!(" (Δ +{:.2} MiB)", diff as f64 / 1048576.0)
277 } else {
278 format!(" (Δ -{:.2} MiB)", -diff as f64 / 1048576.0)
279 }
280 })
281 .unwrap_or_default();
282 eprintln!(
283 "[CPU] {:<28} {:.2} MiB{}",
284 lbl,
285 *bytes as f64 / 1048576.0,
286 delta
287 );
288 prev = Some(*bytes);
289 }
290 }
291}
292
293#[inline]
302pub fn sample_peak_rss(label: &'static str) {
303 #[cfg(feature = "probe")]
304 {
305 let (current, _virt) = current_rss_bytes();
306 let bytes = if current != 0 { current } else { peak_rss_bytes_self() };
307 Probe::sample_rss(label, bytes);
308 }
309 #[cfg(not(feature = "probe"))]
310 let _ = label;
311}
312
313#[cfg(feature = "probe")]
314pub fn peak_rss_bytes_pub() -> u64 { peak_rss_bytes_self() }
315
316#[cfg(feature = "probe")]
317fn peak_rss_bytes_self() -> u64 {
318 #[cfg(unix)]
319 unsafe {
320 let mut ru: libc::rusage = core::mem::zeroed();
321 if libc::getrusage(libc::RUSAGE_SELF, &mut ru) != 0 {
322 return 0;
323 }
324 let raw = ru.ru_maxrss as u64;
325 if cfg!(target_os = "macos") { raw } else { raw.saturating_mul(1024) }
326 }
327 #[cfg(not(unix))]
328 {
329 0
330 }
331}
332
333#[inline]
345pub fn hint_purge_allocator() {
346 #[cfg(feature = "allocator_mimalloc")]
347 {
348 unsafe {
350 libmimalloc_sys::mi_collect(true);
351 }
352 static PURGE_TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
353 if *PURGE_TRACE.get_or_init(azul_core::profile::memory_enabled) {
354 let (rss, _) = current_rss_bytes();
355 eprintln!("[PURGE] mi_collect(true) called — current rss={:.2} MiB", rss as f64 / 1048576.0);
356 }
357 return;
358 }
359 #[cfg(feature = "allocator_jemalloc")]
360 {
361 unsafe {
363 let _ = tikv_jemalloc_sys::mallctl(
364 b"arena.4096.purge\0".as_ptr() as *const _,
365 core::ptr::null_mut(),
366 core::ptr::null_mut(),
367 core::ptr::null_mut(),
368 0,
369 );
370 }
371 return;
372 }
373 #[cfg(all(target_os = "macos", not(any(feature = "allocator_mimalloc", feature = "allocator_jemalloc"))))]
374 {
375 extern "C" {
376 fn malloc_zone_pressure_relief(zone: *mut core::ffi::c_void, goal: usize) -> usize;
377 }
378 unsafe {
379 malloc_zone_pressure_relief(core::ptr::null_mut(), 0);
380 }
381 }
382}
383
384#[cfg(feature = "probe")]
394pub fn current_rss_bytes() -> (u64, u64) {
395 #[cfg(target_os = "macos")]
396 {
397 let pf = phys_footprint_bytes();
401 #[repr(C)]
402 struct MachTaskBasicInfo {
403 virtual_size: u64,
404 resident_size: u64,
405 resident_size_max: u64,
406 user_time: [u32; 2],
407 system_time: [u32; 2],
408 policy: i32,
409 suspend_count: i32,
410 }
411 const MACH_TASK_BASIC_INFO: u32 = 20;
412 extern "C" {
413 fn mach_task_self() -> u32;
414 fn task_info(
415 target: u32, flavor: u32,
416 info: *mut core::ffi::c_void, count: *mut u32,
417 ) -> i32;
418 }
419 unsafe {
420 let mut info: MachTaskBasicInfo = core::mem::zeroed();
421 let mut count = (core::mem::size_of::<MachTaskBasicInfo>() / 4) as u32;
422 let kr = task_info(
423 mach_task_self(),
424 MACH_TASK_BASIC_INFO,
425 &mut info as *mut _ as *mut core::ffi::c_void,
426 &mut count,
427 );
428 if kr == 0 {
429 let rss = if pf != 0 { pf } else { info.resident_size };
430 (rss, info.virtual_size)
431 } else {
432 (pf, 0)
433 }
434 }
435 }
436 #[cfg(not(target_os = "macos"))]
437 { (0, 0) }
438}
439
440#[cfg(feature = "probe")]
449pub fn malloc_heap_bytes() -> u64 {
450 #[cfg(target_os = "macos")]
451 {
452 #[repr(C)]
453 struct Mstats {
454 bytes_total: usize,
455 chunks_used: usize,
456 bytes_used: usize,
457 chunks_free: usize,
458 bytes_free: usize,
459 }
460 extern "C" {
461 fn mstats() -> Mstats;
462 }
463 unsafe { mstats().bytes_used as u64 }
464 }
465 #[cfg(not(target_os = "macos"))]
466 { 0 }
467}
468
469#[cfg(feature = "probe")]
481pub fn phys_footprint_bytes() -> u64 {
482 #[cfg(target_os = "macos")]
483 {
484 #[repr(C)]
488 struct TaskVmInfo {
489 virtual_size: u64,
490 region_count: u32,
491 page_size: u32,
492 resident_size: u64,
493 resident_size_peak: u64,
494 device: u64,
495 device_peak: u64,
496 internal: u64,
497 internal_peak: u64,
498 external: u64,
499 external_peak: u64,
500 reusable: u64,
501 reusable_peak: u64,
502 purgeable_volatile_pmap: u64,
503 purgeable_volatile_resident: u64,
504 purgeable_volatile_virtual: u64,
505 compressed: u64,
506 compressed_peak: u64,
507 compressed_lifetime: u64,
508 phys_footprint: u64,
509 _rest: [u64; 12],
511 }
512 const TASK_VM_INFO: u32 = 22;
513 extern "C" {
514 fn mach_task_self() -> u32;
515 fn task_info(
516 target: u32, flavor: u32,
517 info: *mut core::ffi::c_void, count: *mut u32,
518 ) -> i32;
519 }
520 unsafe {
521 let mut info: TaskVmInfo = core::mem::zeroed();
522 let mut count = (core::mem::size_of::<TaskVmInfo>() / 4) as u32;
523 let kr = task_info(
524 mach_task_self(),
525 TASK_VM_INFO,
526 &mut info as *mut _ as *mut core::ffi::c_void,
527 &mut count,
528 );
529 if kr == 0 { info.phys_footprint } else { 0 }
530 }
531 }
532 #[cfg(not(target_os = "macos"))]
533 { 0 }
534}
535
536#[cfg(feature = "probe")]
548pub fn start_peak_sampler() {
549 #[cfg(target_os = "macos")]
550 {
551 use std::sync::atomic::Ordering;
552 static STARTED: std::sync::atomic::AtomicBool =
554 std::sync::atomic::AtomicBool::new(false);
555 if STARTED.swap(true, Ordering::AcqRel) {
556 return;
557 }
558 std::thread::Builder::new()
559 .name("azul-peak-sampler".to_string())
560 .spawn(|| loop {
561 let now = phys_footprint_bytes();
562 let prev = PEAK_PHYS_FOOTPRINT.load(Ordering::Relaxed);
563 if now > prev {
564 PEAK_PHYS_FOOTPRINT.store(now, Ordering::Relaxed);
565 }
566 std::thread::sleep(std::time::Duration::from_micros(250));
567 })
568 .ok();
569 }
570}
571
572#[cfg(feature = "probe")]
573static PEAK_PHYS_FOOTPRINT: std::sync::atomic::AtomicU64 =
574 std::sync::atomic::AtomicU64::new(0);
575
576#[cfg(feature = "probe")]
579pub fn peak_phys_footprint_seen() -> u64 {
580 PEAK_PHYS_FOOTPRINT.load(std::sync::atomic::Ordering::Relaxed)
581}
582
583#[cfg(feature = "probe")]
589pub fn reset_peak() {
590 let now = phys_footprint_bytes();
591 PEAK_PHYS_FOOTPRINT.store(now, std::sync::atomic::Ordering::Relaxed);
592}
593
594#[cfg(feature = "probe")]
598#[inline]
599pub fn sample_phase_peak(label: &'static str) {
600 let peak = PEAK_PHYS_FOOTPRINT.load(std::sync::atomic::Ordering::Relaxed);
601 Probe::sample_rss(label, peak);
602}
603
604#[cfg(not(feature = "probe"))]
605#[inline(always)]
606pub fn reset_peak() {}
607
608#[cfg(not(feature = "probe"))]
609#[inline(always)]
610pub fn sample_phase_peak(_label: &'static str) {}
611
612#[cfg(not(feature = "probe"))]
613#[inline(always)]
614pub fn malloc_heap_bytes() -> u64 { 0 }
615
616#[cfg(feature = "probe")]
631pub fn emit_phase_heap(label: &str) {
632 use std::io::Write;
633 if !heap_jsonl_enabled() { return; }
634 let Some(p) = azul_core::profile::out_path() else { return };
635 static CALL_ID: std::sync::atomic::AtomicU64 =
636 std::sync::atomic::AtomicU64::new(0);
637 static CURRENT_CALL: std::sync::atomic::AtomicU64 =
641 std::sync::atomic::AtomicU64::new(0);
642 let call_id = if label == "start" {
643 let next = CALL_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
644 CURRENT_CALL.store(next, std::sync::atomic::Ordering::Relaxed);
645 next
646 } else {
647 CURRENT_CALL.load(std::sync::atomic::Ordering::Relaxed)
648 };
649 let heap = malloc_heap_bytes();
650 if let Ok(mut f) = std::fs::OpenOptions::new()
651 .create(true)
652 .append(true)
653 .open(p)
654 {
655 let _ = writeln!(
656 f,
657 r#"{{"ev":"phase","call":{},"label":"{}","heap":{}}}"#,
658 call_id, label, heap
659 );
660 }
661}
662
663#[cfg(not(feature = "probe"))]
664#[inline(always)]
665pub fn emit_phase_heap(_label: &str) {}
666
667#[cfg(feature = "probe")]
675pub fn emit_phase_heap_extra(label: &str, extra: u64) {
676 use std::io::Write;
677 if !heap_jsonl_enabled() { return; }
678 if !azul_core::profile::detail_enabled() { return; }
679 let Some(p) = azul_core::profile::out_path() else { return };
680 let heap = malloc_heap_bytes();
681 if let Ok(mut f) = std::fs::OpenOptions::new()
682 .create(true)
683 .append(true)
684 .open(p)
685 {
686 let _ = writeln!(
687 f,
688 r#"{{"ev":"phase","call":0,"label":"{}","heap":{},"extra":{}}}"#,
689 label, heap, extra
690 );
691 }
692}
693
694#[cfg(not(feature = "probe"))]
695#[inline(always)]
696pub fn emit_phase_heap_extra(_label: &str, _extra: u64) {}
697
698#[cfg(feature = "probe")]
701#[inline]
702fn heap_jsonl_enabled() -> bool {
703 let f = azul_core::profile::flags();
704 f.heap && f.jsonl
705}
706
707#[cfg(feature = "probe")]
711#[inline]
712pub fn detail_enabled() -> bool {
713 azul_core::profile::detail_enabled()
714}
715
716#[cfg(not(feature = "probe"))]
717#[inline(always)]
718pub fn detail_enabled() -> bool { false }