1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Unified profiling gate.
//!
//! Reads `AZ_PROFILE` once on first access, caches the result forever.
//! Value is a comma-separated list of tokens; unknown tokens are ignored,
//! whitespace is trimmed, matching is case-insensitive.
//!
//! Tokens:
//! - `memory` — heap-breakdown dumps (StyledDom, LayoutCache, text cache,
//! cascade maps, RSS). Printed to stderr once per frame.
//! - `cpu` — per-phase wall-clock timings from `Probe::span` (layout,
//! style, cascade, paint, callbacks, …), dumped once per
//! frame so stuttering frames are easy to spot.
//! - `cascade` — narrow diagnostic for prop-cache work: top-N CSS
//! properties by cascade-walk count per frame.
//! - `heap` — phase-boundary heap probes in `regenerate_layout`
//! (`emit_phase_heap`). By themselves print nothing —
//! pair with `jsonl` + `AZ_PROFILE_OUT` to persist.
//! - `jsonl` — format heap probes as JSONL to the file named by
//! `AZ_PROFILE_OUT=<path>`. Requires `heap` to do anything.
//! - `detail` — opt-in to the fine-grained per-step probes inside each
//! phase (e.g. `rf_*` labels inside
//! `rust_fontconfig::request_fonts`, and the `_extra`
//! cache-size payloads). Layered on top of `heap`.
//!
//! ## Examples
//! - `AZ_PROFILE=cpu` — per-phase CPU timings to stderr.
//! - `AZ_PROFILE=heap,jsonl AZ_PROFILE_OUT=/tmp/run.jsonl`
//! → coarse phase heap probes to JSONL.
//! - `AZ_PROFILE=heap,jsonl,detail AZ_PROFILE_OUT=/tmp/detail.jsonl`
//! → fine-grained (per-step) heap probes to JSONL.
//! - `AZ_PROFILE=cpu,cascade` — both dumps simultaneously.
//!
//! Tokens are independent flags, not mutually exclusive modes. Unset
//! or empty leaves every quick path silent.
//!
//! ## Path for jsonl output
//! `AZ_PROFILE_OUT` is read separately (not folded into `AZ_PROFILE`
//! because the value can contain `,` and `=` and a path is a different
//! shape from a flag). When `jsonl` is set but `AZ_PROFILE_OUT` is
//! unset, writers silently skip — no stderr fallback so benchmarks
//! don't get polluted.
//!
//! ## Portability
//! - **macOS / Linux**: full support. Span timings via `Instant`; RSS
//! checkpoints via `task_info` / `/proc/self/statm`.
//! - **Windows**: span timings work. RSS checkpoints silently read 0
//! (the RSS helpers in `azul_layout::probe` are `cfg(unix)`-gated).
//! - **WASM (`target_family = "wasm"`)**: `Instant::now()` panics on
//! browser WASM (no monotonic clock) and `libc::getrusage` isn't
//! available. The probe module detects WASM at compile time and
//! forces the no-op impl.
#[cfg(feature = "std")]
use std::sync::OnceLock;
/// Set of active `AZ_PROFILE` tokens. Parsed once from the env var.
// independent profile toggles parsed from the env var; a bitflags type would
// not improve this flat set of named booleans.
#[allow(clippy::struct_excessive_bools)]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct ProfileFlags {
pub memory: bool,
pub cpu: bool,
pub cascade: bool,
pub heap: bool,
pub jsonl: bool,
pub detail: bool,
}
impl ProfileFlags {
fn parse(value: &str) -> Self {
let mut f = Self::default();
for tok in value.split(',') {
let t = tok.trim();
if t.eq_ignore_ascii_case("memory") || t.eq_ignore_ascii_case("mem") {
f.memory = true;
} else if t.eq_ignore_ascii_case("cpu") || t.eq_ignore_ascii_case("perf") {
f.cpu = true;
} else if t.eq_ignore_ascii_case("cascade") || t.eq_ignore_ascii_case("css") {
f.cascade = true;
} else if t.eq_ignore_ascii_case("heap") {
f.heap = true;
} else if t.eq_ignore_ascii_case("jsonl") {
f.jsonl = true;
} else if t.eq_ignore_ascii_case("detail") {
f.detail = true;
}
}
f
}
}
#[cfg(feature = "std")]
#[inline]
pub fn flags() -> ProfileFlags {
static FLAGS: OnceLock<ProfileFlags> = OnceLock::new();
*FLAGS.get_or_init(|| {
let raw = std::env::var("AZ_PROFILE").ok();
let f = raw
.as_deref()
.map(ProfileFlags::parse)
.unwrap_or_default();
// A typo'd token must WARN and keep running, never silently parse to
// nothing - AZ_PROFILE=phases looked exactly like AZ_PROFILE unset,
// and "a zero is not a measurement".
if let Some(raw) = &raw {
let known = |t: &str| {
matches!(
t.to_ascii_lowercase().as_str(),
"memory" | "mem" | "cpu" | "perf" | "cascade" | "css" | "heap" | "jsonl"
| "detail" | ""
)
};
let mut unknown: Vec<&str> =
raw.split(',').map(str::trim).filter(|t| !known(t)).collect();
unknown.truncate(8); // a garbage value must not flood stderr
if !unknown.is_empty() {
eprintln!(
"[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"
);
}
}
// The announce table: a profile mode that silently emits NOTHING
// reads as "not looking" and has repeatedly burned real debugging
// time ("a zero is not a measurement"). One line, once, at the
// single point every mode resolves through.
if f.heap && !f.jsonl {
eprintln!(
"[azul][profile] AZ_PROFILE=heap alone emits nothing: use \
AZ_PROFILE=heap,jsonl with AZ_PROFILE_OUT=<file> for the \
per-phase heap table (and note builds without the `probe` \
feature report heap as 0)."
);
}
if f.heap && f.jsonl && std::env::var("AZ_PROFILE_OUT").is_err() {
eprintln!(
"[azul][profile] AZ_PROFILE=heap,jsonl is set but \
AZ_PROFILE_OUT is not — no destination, nothing will be \
written."
);
}
f
})
}
/// `no_std` builds have no environment; profiling is always off.
#[cfg(not(feature = "std"))]
#[inline]
pub fn flags() -> ProfileFlags {
let _ = ProfileFlags::parse;
ProfileFlags::default()
}
/// `AZ_PROFILE_OUT=<path>` — destination for JSONL heap probes.
/// Returns `None` if unset. Cached on first access.
#[cfg(feature = "std")]
#[inline]
pub fn out_path() -> Option<&'static str> {
static PATH: OnceLock<Option<String>> = OnceLock::new();
PATH.get_or_init(|| std::env::var("AZ_PROFILE_OUT").ok())
.as_deref()
}
/// `no_std` builds have no environment; no output path.
#[cfg(not(feature = "std"))]
#[inline]
pub fn out_path() -> Option<&'static str> {
None
}
#[inline]
#[must_use]
pub fn memory_enabled() -> bool {
flags().memory
}
#[inline]
#[must_use]
pub fn cpu_enabled() -> bool {
flags().cpu
}
#[inline]
#[must_use]
pub fn cascade_enabled() -> bool {
flags().cascade
}
#[inline]
#[must_use]
pub fn heap_enabled() -> bool {
flags().heap
}
#[inline]
#[must_use]
pub fn jsonl_enabled() -> bool {
flags().jsonl
}
#[inline]
#[must_use]
pub fn detail_enabled() -> bool {
flags().detail
}
#[cfg(test)]
#[path = "profile_test.rs"]
mod profile_test;