Skip to main content

atomcode_tuix/
trace.rs

1// crates/atomcode-tuix/src/trace.rs
2//
3// Opt-in file logger for diagnosing event-loop / render timing issues.
4//
5// Enabled via env var `ATOMCODE_TUIX_LOG=/path/to/file`. When unset
6// every `tuix_trace!` call compiles into a no-op fast path (single
7// atomic load + branch predict), so leaving trace points scattered
8// through hot paths costs nothing in release.
9//
10// Why not env_logger / tracing? We write to stderr-redirected-to-tty
11// in raw mode; any sloppy write corrupts the display. A dedicated file
12// sink with explicit category tags keeps the noise off the terminal
13// and keeps the format parseable (one event per line, microsecond
14// monotonic timestamps).
15//
16// Format: `+{elapsed_us} [{CAT}] {tid} {message}`
17//   elapsed_us — microseconds since the first log event in this process
18//   CAT        — 2-4 char category (IN, KEY, PH, THR, REN, RD, QUE)
19//   tid        — short thread name or id (so `event_loop` vs `tuix-render`
20//                is visible at a glance)
21
22use std::fs::{File, OpenOptions};
23use std::io::Write;
24use std::sync::{Mutex, OnceLock};
25use std::time::Instant;
26
27static SINK: OnceLock<Option<Mutex<File>>> = OnceLock::new();
28static ORIGIN: OnceLock<Instant> = OnceLock::new();
29
30pub fn enabled() -> bool {
31    sink().is_some()
32}
33
34fn sink() -> Option<&'static Mutex<File>> {
35    // STRICTLY opt-in. Earlier versions defaulted to writing
36    // `/tmp/tuix.log` unconditionally so users didn't have to set an
37    // env var to produce diagnostic logs. That turned the trace
38    // infrastructure into a production bottleneck:
39    //   - every `tuix_trace!` call on the main thread AND on the
40    //     `tuix-render` worker thread contends the same `Mutex<File>`
41    //   - main thread emits RD+IN+KEY per keystroke (~3 traces),
42    //     worker emits FOOT+REN+THR per paint (~3 traces)
43    //   - under IME burst (8 chars in 100µs), that's 50+ mutex ops
44    //     from two threads. Lock queueing added 1-3ms of main-thread
45    //     stall per burst, which the user perceives as "吞字" —
46    //     characters logically accepted but visually delayed.
47    //
48    // Now: opt-in only. Default build ships no trace overhead at all
49    // (the macro's `if enabled()` short-circuits to a single atomic
50    // load). Set ATOMCODE_TUIX_LOG=/path to enable diagnosis.
51    SINK.get_or_init(|| {
52        let path = std::env::var("ATOMCODE_TUIX_LOG").ok()?;
53        if path.is_empty() {
54            return None;
55        }
56        // Truncate on each run so stale events from prior sessions
57        // don't confuse diagnosis.
58        let file = OpenOptions::new()
59            .create(true)
60            .write(true)
61            .truncate(true)
62            .open(&path)
63            .ok()?;
64        Some(Mutex::new(file))
65    })
66    .as_ref()
67}
68
69fn origin() -> Instant {
70    *ORIGIN.get_or_init(Instant::now)
71}
72
73/// Low-level write. Don't call directly — use the `tuix_trace!` macro.
74pub fn write_line(cat: &str, args: std::fmt::Arguments<'_>) {
75    let Some(sink) = sink() else {
76        return;
77    };
78    let us = origin().elapsed().as_micros();
79    let tid = std::thread::current().name().unwrap_or("?").to_string();
80    let line = format!("+{:>10}us [{:>3}] {:>14} {}\n", us, cat, tid, args);
81    if let Ok(mut f) = sink.lock() {
82        let _ = f.write_all(line.as_bytes());
83    }
84}
85
86/// `tuix_trace!("CAT", "fmt {}", args)` — compiles to a cheap `enabled()`
87/// check when the env var is unset. Use short 2-4 char categories so
88/// log lines remain grep-able by column:
89///   IN  — input event entering handle_input
90///   KEY — key-handler outcome (Redraw / Commit / NoOp)
91///   PH  — UiPhase transition (Streaming → Idle, etc.)
92///   QUE — type-ahead queue push / pop
93///   THR — InputThrottle paint/park decision
94///   REN — render worker command processed
95///   RD  — raw reader thread event
96#[macro_export]
97macro_rules! tuix_trace {
98    ($cat:expr, $($arg:tt)*) => {{
99        if $crate::trace::enabled() {
100            $crate::trace::write_line($cat, format_args!($($arg)*));
101        }
102    }};
103}