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

            
53
#[cfg(feature = "std")]
54
use std::sync::OnceLock;
55

            
56
/// Set of active `AZ_PROFILE` tokens. Parsed once from the env var.
57
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
58
pub struct ProfileFlags {
59
    pub memory: bool,
60
    pub cpu: bool,
61
    pub cascade: bool,
62
    pub heap: bool,
63
    pub jsonl: bool,
64
    pub detail: bool,
65
}
66

            
67
impl ProfileFlags {
68
5
    fn parse(value: &str) -> Self {
69
5
        let mut f = Self::default();
70
12
        for tok in value.split(',') {
71
12
            let t = tok.trim();
72
12
            if t.eq_ignore_ascii_case("memory") || t.eq_ignore_ascii_case("mem") {
73
1
                f.memory = true;
74
11
            } else if t.eq_ignore_ascii_case("cpu") || t.eq_ignore_ascii_case("perf") {
75
3
                f.cpu = true;
76
8
            } else if t.eq_ignore_ascii_case("cascade") || t.eq_ignore_ascii_case("css") {
77
1
                f.cascade = true;
78
7
            } else if t.eq_ignore_ascii_case("heap") {
79
3
                f.heap = true;
80
4
            } else if t.eq_ignore_ascii_case("jsonl") {
81
2
                f.jsonl = true;
82
2
            } else if t.eq_ignore_ascii_case("detail") {
83
1
                f.detail = true;
84
1
            }
85
        }
86
5
        f
87
5
    }
88
}
89

            
90
#[cfg(feature = "std")]
91
#[inline]
92
3524
pub fn flags() -> ProfileFlags {
93
    static FLAGS: OnceLock<ProfileFlags> = OnceLock::new();
94
3524
    *FLAGS.get_or_init(|| {
95
926
        std::env::var("AZ_PROFILE")
96
926
            .map(|v| ProfileFlags::parse(&v))
97
926
            .unwrap_or_default()
98
926
    })
99
3524
}
100

            
101
/// `no_std` builds have no environment; profiling is always off.
102
#[cfg(not(feature = "std"))]
103
#[inline]
104
pub fn flags() -> ProfileFlags {
105
    let _ = ProfileFlags::parse;
106
    ProfileFlags::default()
107
}
108

            
109
/// `AZ_PROFILE_OUT=<path>` — destination for JSONL heap probes.
110
/// Returns `None` if unset. Cached on first access.
111
#[cfg(feature = "std")]
112
#[inline]
113
pub fn out_path() -> Option<&'static str> {
114
    static PATH: OnceLock<Option<String>> = OnceLock::new();
115
    PATH.get_or_init(|| std::env::var("AZ_PROFILE_OUT").ok())
116
        .as_deref()
117
}
118

            
119
/// `no_std` builds have no environment; no output path.
120
#[cfg(not(feature = "std"))]
121
#[inline]
122
pub fn out_path() -> Option<&'static str> {
123
    None
124
}
125

            
126
#[inline]
127
2188
pub fn memory_enabled() -> bool { flags().memory }
128

            
129
#[inline]
130
280
pub fn cpu_enabled() -> bool { flags().cpu }
131

            
132
#[inline]
133
1000
pub fn cascade_enabled() -> bool { flags().cascade }
134

            
135
#[inline]
136
pub fn heap_enabled() -> bool { flags().heap }
137

            
138
#[inline]
139
pub fn jsonl_enabled() -> bool { flags().jsonl }
140

            
141
#[inline]
142
pub fn detail_enabled() -> bool { flags().detail }
143

            
144
#[cfg(test)]
145
mod tests {
146
    use super::ProfileFlags;
147

            
148
    #[test]
149
1
    fn parse_single_token() {
150
1
        let f = ProfileFlags::parse("cpu");
151
1
        assert!(f.cpu && !f.memory && !f.heap);
152
1
    }
153

            
154
    #[test]
155
1
    fn parse_multiple_tokens() {
156
1
        let f = ProfileFlags::parse("heap,jsonl,detail");
157
1
        assert!(f.heap && f.jsonl && f.detail);
158
1
        assert!(!f.cpu && !f.memory);
159
1
    }
160

            
161
    #[test]
162
1
    fn parse_is_case_insensitive_and_trims() {
163
1
        let f = ProfileFlags::parse(" Heap , JSONL ");
164
1
        assert!(f.heap && f.jsonl);
165
1
    }
166

            
167
    #[test]
168
1
    fn parse_ignores_unknown_tokens() {
169
1
        let f = ProfileFlags::parse("cpu,bogus,heap");
170
1
        assert!(f.cpu && f.heap);
171
1
    }
172

            
173
    #[test]
174
1
    fn parse_accepts_aliases() {
175
1
        let f = ProfileFlags::parse("mem,perf,css");
176
1
        assert!(f.memory && f.cpu && f.cascade);
177
1
    }
178
}