Skip to main content

azul_core/
profile.rs

1//! Unified profiling gate.
2//!
3//! Reads `AZ_PROFILE` once on first access, caches the result forever.
4//! Value is a comma-separated list of tokens; unknown tokens are ignored,
5//! whitespace is trimmed, matching is case-insensitive.
6//!
7//! Tokens:
8//! - `memory`  — heap-breakdown dumps (StyledDom, LayoutCache, text cache,
9//!               cascade maps, RSS). Printed to stderr once per frame.
10//! - `cpu`     — per-phase wall-clock timings from `Probe::span` (layout,
11//!               style, cascade, paint, callbacks, …), dumped once per
12//!               frame so stuttering frames are easy to spot.
13//! - `cascade` — narrow diagnostic for prop-cache work: top-N CSS
14//!               properties by cascade-walk count per frame.
15//! - `heap`    — phase-boundary heap probes in `regenerate_layout`
16//!               (`emit_phase_heap`). By themselves print nothing —
17//!               pair with `jsonl` + `AZ_PROFILE_OUT` to persist.
18//! - `jsonl`   — format heap probes as JSONL to the file named by
19//!               `AZ_PROFILE_OUT=<path>`. Requires `heap` to do anything.
20//! - `detail`  — opt-in to the fine-grained per-step probes inside each
21//!               phase (e.g. `rf_*` labels inside
22//!               `rust_fontconfig::request_fonts`, and the `_extra`
23//!               cache-size payloads). Layered on top of `heap`.
24//!
25//! ## Examples
26//! - `AZ_PROFILE=cpu` — per-phase CPU timings to stderr.
27//! - `AZ_PROFILE=heap,jsonl AZ_PROFILE_OUT=/tmp/run.jsonl`
28//!     → coarse phase heap probes to JSONL.
29//! - `AZ_PROFILE=heap,jsonl,detail AZ_PROFILE_OUT=/tmp/detail.jsonl`
30//!     → fine-grained (per-step) heap probes to JSONL.
31//! - `AZ_PROFILE=cpu,cascade` — both dumps simultaneously.
32//!
33//! Tokens are independent flags, not mutually exclusive modes. Unset
34//! or empty leaves every quick path silent.
35//!
36//! ## Path for jsonl output
37//! `AZ_PROFILE_OUT` is read separately (not folded into `AZ_PROFILE`
38//! because the value can contain `,` and `=` and a path is a different
39//! shape from a flag). When `jsonl` is set but `AZ_PROFILE_OUT` is
40//! unset, writers silently skip — no stderr fallback so benchmarks
41//! don't get polluted.
42//!
43//! ## Portability
44//! - **macOS / Linux**: full support. Span timings via `Instant`; RSS
45//!   checkpoints via `task_info` / `/proc/self/statm`.
46//! - **Windows**: span timings work. RSS checkpoints silently read 0
47//!   (the RSS helpers in `azul_layout::probe` are `cfg(unix)`-gated).
48//! - **WASM (`target_family = "wasm"`)**: `Instant::now()` panics on
49//!   browser WASM (no monotonic clock) and `libc::getrusage` isn't
50//!   available. The probe module detects WASM at compile time and
51//!   forces the no-op impl.
52
53use std::sync::OnceLock;
54
55/// Set of active `AZ_PROFILE` tokens. Parsed once from the env var.
56#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
57pub struct ProfileFlags {
58    pub memory: bool,
59    pub cpu: bool,
60    pub cascade: bool,
61    pub heap: bool,
62    pub jsonl: bool,
63    pub detail: bool,
64}
65
66impl ProfileFlags {
67    fn parse(value: &str) -> Self {
68        let mut f = Self::default();
69        for tok in value.split(',') {
70            let t = tok.trim();
71            if t.eq_ignore_ascii_case("memory") || t.eq_ignore_ascii_case("mem") {
72                f.memory = true;
73            } else if t.eq_ignore_ascii_case("cpu") || t.eq_ignore_ascii_case("perf") {
74                f.cpu = true;
75            } else if t.eq_ignore_ascii_case("cascade") || t.eq_ignore_ascii_case("css") {
76                f.cascade = true;
77            } else if t.eq_ignore_ascii_case("heap") {
78                f.heap = true;
79            } else if t.eq_ignore_ascii_case("jsonl") {
80                f.jsonl = true;
81            } else if t.eq_ignore_ascii_case("detail") {
82                f.detail = true;
83            }
84        }
85        f
86    }
87}
88
89#[inline]
90pub fn flags() -> ProfileFlags {
91    static FLAGS: OnceLock<ProfileFlags> = OnceLock::new();
92    *FLAGS.get_or_init(|| {
93        std::env::var("AZ_PROFILE")
94            .map(|v| ProfileFlags::parse(&v))
95            .unwrap_or_default()
96    })
97}
98
99/// `AZ_PROFILE_OUT=<path>` — destination for JSONL heap probes.
100/// Returns `None` if unset. Cached on first access.
101#[inline]
102pub fn out_path() -> Option<&'static str> {
103    static PATH: OnceLock<Option<String>> = OnceLock::new();
104    PATH.get_or_init(|| std::env::var("AZ_PROFILE_OUT").ok())
105        .as_deref()
106}
107
108#[inline]
109pub fn memory_enabled() -> bool { flags().memory }
110
111#[inline]
112pub fn cpu_enabled() -> bool { flags().cpu }
113
114#[inline]
115pub fn cascade_enabled() -> bool { flags().cascade }
116
117#[inline]
118pub fn heap_enabled() -> bool { flags().heap }
119
120#[inline]
121pub fn jsonl_enabled() -> bool { flags().jsonl }
122
123#[inline]
124pub fn detail_enabled() -> bool { flags().detail }
125
126#[cfg(test)]
127mod tests {
128    use super::ProfileFlags;
129
130    #[test]
131    fn parse_single_token() {
132        let f = ProfileFlags::parse("cpu");
133        assert!(f.cpu && !f.memory && !f.heap);
134    }
135
136    #[test]
137    fn parse_multiple_tokens() {
138        let f = ProfileFlags::parse("heap,jsonl,detail");
139        assert!(f.heap && f.jsonl && f.detail);
140        assert!(!f.cpu && !f.memory);
141    }
142
143    #[test]
144    fn parse_is_case_insensitive_and_trims() {
145        let f = ProfileFlags::parse(" Heap , JSONL ");
146        assert!(f.heap && f.jsonl);
147    }
148
149    #[test]
150    fn parse_ignores_unknown_tokens() {
151        let f = ProfileFlags::parse("cpu,bogus,heap");
152        assert!(f.cpu && f.heap);
153    }
154
155    #[test]
156    fn parse_accepts_aliases() {
157        let f = ProfileFlags::parse("mem,perf,css");
158        assert!(f.memory && f.cpu && f.cascade);
159    }
160}