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}