Skip to main content

teksilo_core/pointer/
trace.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Opt-in input tracing.
5//!
6//! An input bug is a *sequence* bug: the sample that mattered is three packets
7//! back, and by the time a widget misbehaves the evidence is gone. Rather than
8//! reach for a logging framework (`teksilo-core` has seven dependencies and
9//! intends to keep them), the input layer carries one environment-variable
10//! switch and one macro.
11//!
12//! ```text
13//! TEKSILO_TRACE_INPUT=samples   # every pointer and scroll sample
14//! TEKSILO_TRACE_INPUT=gestures  # recognizer transitions and arbitration
15//! TEKSILO_TRACE_INPUT=all       # both
16//! ```
17//!
18//! A line looks like:
19//!
20//! ```text
21//! [teksilo input] down PointerId(2) at Point { x: 120.0, y: 44.0 }
22//! ```
23//!
24//! # Cost when off
25//!
26//! The variable is read **once**, through a [`OnceLock`], so a
27//! [`trace_enabled`] call after the first is one relaxed load and a comparison.
28//! More importantly [`trace_input!`](crate::trace_input) guards its arguments: a trace call in a
29//! hot path formats nothing, allocates nothing and evaluates none of its
30//! argument expressions unless the level is on. That is what makes it
31//! acceptable to leave a trace call on the per-sample path.
32
33use std::sync::OnceLock;
34
35/// How much input tracing is on.
36///
37/// Ordered by inclusion: [`All`](Self::All) implies both of the others, so a
38/// call site asks "is *my* level on?" rather than matching every combination.
39#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
40pub enum TraceLevel {
41    /// Nothing is traced. The default, and what an unset or unrecognised
42    /// `TEKSILO_TRACE_INPUT` means.
43    #[default]
44    Off,
45    /// Raw samples entering the tree.
46    Samples,
47    /// Recognizer transitions, arbitration decisions and cancellations.
48    Gestures,
49    /// Both.
50    All,
51}
52
53/// The level parsed from the environment, resolved once per process.
54static LEVEL: OnceLock<TraceLevel> = OnceLock::new();
55
56/// Parse a `TEKSILO_TRACE_INPUT` value. Unrecognised values are
57/// [`TraceLevel::Off`] — a typo must not silently enable a different level, and
58/// must not be an error either.
59fn parse_level(raw: &str) -> TraceLevel {
60    match raw.trim().to_ascii_lowercase().as_str() {
61        "samples" | "sample" => TraceLevel::Samples,
62        "gestures" | "gesture" => TraceLevel::Gestures,
63        "all" | "1" | "true" => TraceLevel::All,
64        _ => TraceLevel::Off,
65    }
66}
67
68/// The active trace level.
69pub fn trace_level() -> TraceLevel {
70    *LEVEL.get_or_init(|| {
71        std::env::var("TEKSILO_TRACE_INPUT")
72            .ok()
73            .map(|raw| parse_level(&raw))
74            .unwrap_or_default()
75    })
76}
77
78/// Whether `level` is currently being traced.
79///
80/// [`TraceLevel::All`] enables both categories; asking for
81/// [`TraceLevel::Off`] is always `false` (there is nothing to trace at that
82/// level), so a caller cannot accidentally turn a guard into a no-op by
83/// passing it.
84pub fn trace_enabled(level: TraceLevel) -> bool {
85    match (trace_level(), level) {
86        (TraceLevel::Off, _) | (_, TraceLevel::Off) => false,
87        (TraceLevel::All, _) => true,
88        (active, wanted) => active == wanted,
89    }
90}
91
92/// Emit one trace line if the given level is on.
93///
94/// ```ignore
95/// trace_input!(Samples, "down {:?} at {:?}", id, position);
96/// ```
97///
98/// The first argument names a [`TraceLevel`] variant without its path. The rest
99/// is an ordinary `format!` argument list — and it is **not evaluated** unless
100/// the level is on, so an expensive `{:?}` on a large structure costs nothing
101/// in a normal run.
102#[macro_export]
103macro_rules! trace_input {
104    ($level:ident, $($arg:tt)*) => {
105        if $crate::pointer::trace::trace_enabled($crate::pointer::trace::TraceLevel::$level) {
106            eprintln!("[teksilo input] {}", format_args!($($arg)*));
107        }
108    };
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use std::cell::Cell;
115
116    #[test]
117    fn levels_parse_leniently_and_fail_closed() {
118        assert_eq!(parse_level("samples"), TraceLevel::Samples);
119        assert_eq!(parse_level("  SAMPLES "), TraceLevel::Samples);
120        assert_eq!(parse_level("gesture"), TraceLevel::Gestures);
121        assert_eq!(parse_level("all"), TraceLevel::All);
122        assert_eq!(parse_level("verbose"), TraceLevel::Off);
123        assert_eq!(parse_level(""), TraceLevel::Off);
124    }
125
126    #[test]
127    fn off_is_the_default() {
128        assert_eq!(TraceLevel::default(), TraceLevel::Off);
129    }
130
131    /// The whole point of the macro's guard: with tracing off, the argument
132    /// expressions must never run. A trace call on the per-sample path that
133    /// formatted its arguments regardless would be a per-sample allocation.
134    ///
135    /// The test suite runs without `TEKSILO_TRACE_INPUT` set, so this also
136    /// pins the "unset means off" default.
137    #[test]
138    fn tracing_off_does_not_evaluate_its_arguments() {
139        assert_eq!(
140            trace_level(),
141            TraceLevel::Off,
142            "the suite must run with TEKSILO_TRACE_INPUT unset"
143        );
144
145        let evaluated = Cell::new(0u32);
146        let bump = || {
147            evaluated.set(evaluated.get() + 1);
148            "argument"
149        };
150
151        trace_input!(Samples, "{}", bump());
152        trace_input!(Gestures, "{} {}", bump(), bump());
153
154        assert_eq!(
155            evaluated.get(),
156            0,
157            "arguments must be guarded, not formatted"
158        );
159    }
160
161    #[test]
162    fn asking_for_off_is_never_enabled() {
163        assert!(!trace_enabled(TraceLevel::Off));
164    }
165}