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
53#[cfg(feature = "std")]
54use std::sync::OnceLock;
55
56/// Set of active `AZ_PROFILE` tokens. Parsed once from the env var.
57// independent profile toggles parsed from the env var; a bitflags type would
58// not improve this flat set of named booleans.
59#[allow(clippy::struct_excessive_bools)]
60#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
61pub struct ProfileFlags {
62 pub memory: bool,
63 pub cpu: bool,
64 pub cascade: bool,
65 pub heap: bool,
66 pub jsonl: bool,
67 pub detail: bool,
68}
69
70impl ProfileFlags {
71 fn parse(value: &str) -> Self {
72 let mut f = Self::default();
73 for tok in value.split(',') {
74 let t = tok.trim();
75 if t.eq_ignore_ascii_case("memory") || t.eq_ignore_ascii_case("mem") {
76 f.memory = true;
77 } else if t.eq_ignore_ascii_case("cpu") || t.eq_ignore_ascii_case("perf") {
78 f.cpu = true;
79 } else if t.eq_ignore_ascii_case("cascade") || t.eq_ignore_ascii_case("css") {
80 f.cascade = true;
81 } else if t.eq_ignore_ascii_case("heap") {
82 f.heap = true;
83 } else if t.eq_ignore_ascii_case("jsonl") {
84 f.jsonl = true;
85 } else if t.eq_ignore_ascii_case("detail") {
86 f.detail = true;
87 }
88 }
89 f
90 }
91}
92
93#[cfg(feature = "std")]
94#[inline]
95pub fn flags() -> ProfileFlags {
96 static FLAGS: OnceLock<ProfileFlags> = OnceLock::new();
97 *FLAGS.get_or_init(|| {
98 let raw = std::env::var("AZ_PROFILE").ok();
99 let f = raw
100 .as_deref()
101 .map(ProfileFlags::parse)
102 .unwrap_or_default();
103 // A typo'd token must WARN and keep running, never silently parse to
104 // nothing - AZ_PROFILE=phases looked exactly like AZ_PROFILE unset,
105 // and "a zero is not a measurement".
106 if let Some(raw) = &raw {
107 let known = |t: &str| {
108 matches!(
109 t.to_ascii_lowercase().as_str(),
110 "memory" | "mem" | "cpu" | "perf" | "cascade" | "css" | "heap" | "jsonl"
111 | "detail" | ""
112 )
113 };
114 let mut unknown: Vec<&str> =
115 raw.split(',').map(str::trim).filter(|t| !known(t)).collect();
116 unknown.truncate(8); // a garbage value must not flood stderr
117 if !unknown.is_empty() {
118 eprintln!(
119 "[azul][profile] AZ_PROFILE={raw:?}: unknown token(s) {unknown:?} ignored - valid values are cpu (perf), memory (mem), cascade (css), heap, jsonl, detail; combine with commas, e.g. AZ_PROFILE=cpu,memory"
120 );
121 }
122 }
123 // The announce table: a profile mode that silently emits NOTHING
124 // reads as "not looking" and has repeatedly burned real debugging
125 // time ("a zero is not a measurement"). One line, once, at the
126 // single point every mode resolves through.
127 if f.heap && !f.jsonl {
128 eprintln!(
129 "[azul][profile] AZ_PROFILE=heap alone emits nothing: use \
130 AZ_PROFILE=heap,jsonl with AZ_PROFILE_OUT=<file> for the \
131 per-phase heap table (and note builds without the `probe` \
132 feature report heap as 0)."
133 );
134 }
135 if f.heap && f.jsonl && std::env::var("AZ_PROFILE_OUT").is_err() {
136 eprintln!(
137 "[azul][profile] AZ_PROFILE=heap,jsonl is set but \
138 AZ_PROFILE_OUT is not — no destination, nothing will be \
139 written."
140 );
141 }
142 f
143 })
144}
145
146/// `no_std` builds have no environment; profiling is always off.
147#[cfg(not(feature = "std"))]
148#[inline]
149pub fn flags() -> ProfileFlags {
150 let _ = ProfileFlags::parse;
151 ProfileFlags::default()
152}
153
154/// `AZ_PROFILE_OUT=<path>` — destination for JSONL heap probes.
155/// Returns `None` if unset. Cached on first access.
156#[cfg(feature = "std")]
157#[inline]
158pub fn out_path() -> Option<&'static str> {
159 static PATH: OnceLock<Option<String>> = OnceLock::new();
160 PATH.get_or_init(|| std::env::var("AZ_PROFILE_OUT").ok())
161 .as_deref()
162}
163
164/// `no_std` builds have no environment; no output path.
165#[cfg(not(feature = "std"))]
166#[inline]
167pub fn out_path() -> Option<&'static str> {
168 None
169}
170
171#[inline]
172#[must_use]
173pub fn memory_enabled() -> bool {
174 flags().memory
175}
176
177#[inline]
178#[must_use]
179pub fn cpu_enabled() -> bool {
180 flags().cpu
181}
182
183#[inline]
184#[must_use]
185pub fn cascade_enabled() -> bool {
186 flags().cascade
187}
188
189#[inline]
190#[must_use]
191pub fn heap_enabled() -> bool {
192 flags().heap
193}
194
195#[inline]
196#[must_use]
197pub fn jsonl_enabled() -> bool {
198 flags().jsonl
199}
200
201#[inline]
202#[must_use]
203pub fn detail_enabled() -> bool {
204 flags().detail
205}
206
207#[cfg(test)]
208#[path = "profile_test.rs"]
209mod profile_test;