Skip to main content

cranpose_core/
env_flags.rs

1//! Cached reads of the `CRANPOSE_*` diagnostic environment variables.
2//!
3//! Every diagnostic switch in the framework is read from the environment, and
4//! several of them sit on paths that run once per frame, once per state write,
5//! or once per laid-out node. That is a problem, because reading an
6//! environment variable is not a cheap lookup: `getenv` takes the environ
7//! lock and walks the whole variable array, comparing each entry's name
8//! prefix, until it finds a match or runs off the end. A switch that is
9//! *turned off* is the worst case — it never matches, so every read pays for
10//! a full scan of the process environment just to learn there is nothing to
11//! print. On a device that showed up as a measurable share of an idle frame,
12//! spent entirely inside `strncmp`.
13//!
14//! None of these switches can change after startup. Android's are seeded from
15//! `debug.cranpose.*` system properties before the app shell exists, and on
16//! every other platform they come from the process environment, which the
17//! framework never mutates. So each one is read once and cached.
18//!
19//! Use [`env_flag!`] for presence switches and [`env_threshold_ms!`] for the
20//! millisecond thresholds. Both expand to a `OnceLock` owned by the call site,
21//! which costs an acquire load once warm.
22
23/// Reads a presence-style environment switch once and caches the answer.
24///
25/// Expands to a `bool` that is `true` when the variable is set to anything at
26/// all, including the empty string. The backing `OnceLock` belongs to the
27/// expansion, so two call sites naming the same variable get their own cache
28/// rather than sharing one; that keeps the macro usable from any crate without
29/// a registry, at the cost of one pointer-sized static each.
30///
31/// ```ignore
32/// if env_flag!("CRANPOSE_SCENE_UPDATE_DIAG") {
33///     eprintln!("[scene-update-diag] dirty={dirty_nodes:?}");
34/// }
35/// ```
36#[macro_export]
37macro_rules! env_flag {
38    ($name:expr) => {{
39        static CRANPOSE_ENV_FLAG: ::std::sync::OnceLock<bool> = ::std::sync::OnceLock::new();
40        *CRANPOSE_ENV_FLAG.get_or_init(|| ::std::env::var_os($name).is_some())
41    }};
42}
43
44/// Reads a millisecond-threshold environment variable once and caches it.
45///
46/// Expands to an `Option<f64>`: `None` when the variable is unset or does not
47/// parse, `Some(threshold)` otherwise. A threshold of `0` is preserved rather
48/// than treated as absent, so `0` means "report every frame" — the setting
49/// used to capture a full trace.
50///
51/// ```ignore
52/// if let Some(threshold) = env_threshold_ms!("CRANPOSE_FRAME_STAGE_TELEMETRY_MS") {
53///     if elapsed_ms >= threshold {
54///         log::info!("[frame-stage] {elapsed_ms:.2}ms");
55///     }
56/// }
57/// ```
58#[macro_export]
59macro_rules! env_threshold_ms {
60    ($name:expr) => {{
61        static CRANPOSE_ENV_THRESHOLD: ::std::sync::OnceLock<Option<f64>> =
62            ::std::sync::OnceLock::new();
63        *CRANPOSE_ENV_THRESHOLD.get_or_init(|| {
64            ::std::env::var($name)
65                .ok()
66                .and_then(|value| value.trim().parse::<f64>().ok())
67                .filter(|threshold| *threshold >= 0.0)
68        })
69    }};
70}
71
72#[cfg(test)]
73mod tests {
74    #[test]
75    fn an_unset_switch_reads_false_and_stays_false() {
76        assert!(!env_flag!("CRANPOSE_ENV_FLAG_THAT_NOBODY_SETS"));
77        assert!(!env_flag!("CRANPOSE_ENV_FLAG_THAT_NOBODY_SETS"));
78    }
79
80    #[test]
81    fn an_unset_threshold_reads_none() {
82        assert_eq!(env_threshold_ms!("CRANPOSE_ENV_MS_THAT_NOBODY_SETS"), None);
83    }
84
85    #[test]
86    fn each_call_site_caches_the_value_it_read_first() {
87        let first = env_flag!("CRANPOSE_ENV_FLAG_CACHE_PROBE");
88        let second = env_flag!("CRANPOSE_ENV_FLAG_CACHE_PROBE");
89        assert_eq!(first, second);
90    }
91}