Skip to main content

vantage_diorama/
debug.rs

1//! The official per-datasource debug stream.
2//!
3//! A [`DebugTap`] is carried by every [`Lens`](crate::Lens) and reached from
4//! every task the lens spawns. Enabled per datasource via
5//! [`LensBuilder::debug_datasource`](crate::lens::LensBuilder::debug_datasource),
6//! it emits at `info` level under the single target `vantage_diorama::debug`
7//! — visible in a default log with no `RUST_LOG` required. Every line carries
8//! `ds=<datasource>`; every per-dio line also carries `dio=<master table
9//! name>`. Off — the default — nothing is emitted and nothing is paid: every
10//! call site checks [`DebugTap::enabled`] before doing any work the line
11//! itself needs, not just before formatting it.
12//!
13//! This stream is the mechanism for demonstrating the cache's efficiency
14//! and its resilience to backend faults: every master round trip, every
15//! cache mutation, every consumer open/close (the *census*), every status
16//! transition — attributable, correlated (`req=N`), and greppable.
17//!
18//! # Line shape
19//!
20//! Every line is `<datasource>  <tag>  <clause>` — a scannable left edge and
21//! one clause of plain English. Units are human (`3.0s`, `24KB`, `200,000`,
22//! `0.1%`), and a field is omitted rather than printed empty.
23//!
24//! The **tag** is the grep anchor and comes from a closed set:
25//!
26//! | Tag | Says |
27//! |---|---|
28//! | `dio` | a Dio was created; a fetch was asked for, came back, or failed (`fetch #N`, `list #N`) |
29//! | `source` | what the master can and cannot do, and how this view loads — once, at open |
30//! | `scenery` | a view opened; a load-state transition; row positions dropped |
31//! | `census` | a consumer attached or detached, with live counts and RSS |
32//! | `viewport` | the range a consumer declared, and how many scroll events coalesced into it |
33//! | `cache` | rows committed, or a viewport served locally with no fetch |
34//! | `payload` | columns received against columns displayed, and the bytes |
35//! | `total` | the grand total changed, and what decided it |
36//! | `sort` / `search` | the query changed, and whether it was pushed to the source |
37//! | `hydrate` | a two-pass detail sweep queued its pending ids |
38//! | `derive` | a `vantage-diorama-aggregate` layer recomputed |
39//! | `summary` | the end-of-session ledger (see [`stats::emit_debug_summary`](crate::stats::emit_debug_summary)) |
40//!
41//! `fetch #N` / `list #N` is a per-dio counter tying a request to its outcome;
42//! it is allocated only when the tap is enabled. Lines from one load are not
43//! emitted in a fixed order — the cache commit and the state transition happen
44//! inside the operation the return line closes — so correlate on the id rather
45//! than on adjacency.
46//!
47//! A `derive()`'s first load emits two `derive` lines: an eager compute that
48//! seeds the derived Vista's schema, then the engine's own seed pass over the
49//! same rows, which reports `unchanged` because it reads what the eager pass
50//! just published. Both are real recomputations.
51
52use std::sync::Arc;
53use std::sync::LazyLock;
54use std::time::Instant;
55
56/// Per-datasource debug switch. Cheap to clone, cheap to check.
57#[derive(Debug, Clone, Default)]
58pub struct DebugTap {
59    /// `Some(name)` = enabled for that datasource; `None` = off.
60    ds: Option<Arc<str>>,
61}
62
63impl DebugTap {
64    /// The disabled tap — the default for every Lens.
65    pub fn off() -> Self {
66        Self { ds: None }
67    }
68
69    /// An enabled tap tagged with the datasource's name; it prefixes every
70    /// line the tap emits.
71    pub fn for_datasource(name: impl Into<String>) -> Self {
72        ANY_TAP_ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
73        // Start the session clock here rather than at the first reader, so
74        // "session" spans the whole of the debug stream instead of beginning
75        // at whatever happened to look at it first.
76        LazyLock::force(&PROCESS_START);
77        Self {
78            ds: Some(Arc::from(name.into())),
79        }
80    }
81
82    pub fn enabled(&self) -> bool {
83        self.ds.is_some()
84    }
85
86    /// The datasource name, or `""` when the tap is off. Only meaningful
87    /// inside a `tapline!` (which never fires when off).
88    pub fn ds(&self) -> &str {
89        self.ds.as_deref().unwrap_or("")
90    }
91}
92
93/// Emit one debug-stream line, only when the tap is enabled.
94///
95/// `tapline!(tap, "tag", "clause {}", value)` renders as
96/// `<datasource>  <tag>  <clause>` — a scannable left edge (which source,
97/// which kind of event) followed by one clause of plain English. The whole
98/// invocation, arguments included, sits behind the enabled check, so a
99/// disabled tap pays for nothing it would otherwise format.
100macro_rules! tapline {
101    ($tap:expr, $tag:literal, $($arg:tt)*) => {
102        if $tap.enabled() {
103            tracing::info!(
104                target: "vantage_diorama::debug",
105                "{:<10} {:<8} {}",
106                $tap.ds(),
107                $tag,
108                format_args!($($arg)*),
109            );
110        }
111    };
112}
113pub(crate) use tapline;
114
115/// A duration in the unit a reader thinks in: `840ms`, `3.0s`, `1m12s`.
116///
117/// Public because the stream is a shared format: a crate that writes into
118/// `vantage_diorama::debug` — `vantage-diorama-aggregate` does — has to print
119/// its numbers the same way, or the reader meets two conventions in one log.
120pub fn dur(ms: u64) -> String {
121    if ms < 1_000 {
122        format!("{ms}ms")
123    } else if ms < 60_000 {
124        format!("{:.1}s", ms as f64 / 1000.0)
125    } else {
126        format!("{}m{:02}s", ms / 60_000, (ms % 60_000) / 1000)
127    }
128}
129
130/// A byte count as `812B`, `24KB`, `1.2MB`.
131pub fn bytes(n: usize) -> String {
132    const KB: usize = 1024;
133    const MB: usize = KB * 1024;
134    if n < KB {
135        format!("{n}B")
136    } else if n < MB {
137        format!("{}KB", n / KB)
138    } else {
139        format!("{:.1}MB", n as f64 / MB as f64)
140    }
141}
142
143/// A count with thousands separators — `200,000` reads, `200000` doesn't.
144pub fn num(n: usize) -> String {
145    let s = n.to_string();
146    let mut out = String::with_capacity(s.len() + s.len() / 3);
147    for (i, c) in s.chars().enumerate() {
148        if i > 0 && (s.len() - i).is_multiple_of(3) {
149            out.push(',');
150        }
151        out.push(c);
152    }
153    out
154}
155
156/// `held` of `total` as a percentage, precise enough to stay honest at the
157/// small end: 200 of 200,000 is `0.1%`, not `0%`.
158pub fn pct(held: usize, total: usize) -> String {
159    if total == 0 {
160        return "—".into();
161    }
162    let p = held as f64 / total as f64 * 100.0;
163    if p >= 10.0 {
164        format!("{p:.0}%")
165    } else {
166        format!("{p:.1}%")
167    }
168}
169
170/// Wall/CPU/memory snapshot for census lines and the exit summary.
171#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
172pub struct ProcessStats {
173    /// Milliseconds since the debug stream was armed (the first
174    /// [`DebugTap::for_datasource`]), not since process start — anything
175    /// before the first datasource opted in is not measured.
176    pub uptime_ms: u64,
177    /// User + system CPU time consumed by the process, in milliseconds.
178    pub cpu_ms: u64,
179    /// Peak resident set size, in bytes. 0 where unsupported.
180    pub peak_rss_bytes: u64,
181}
182
183/// Set the first time any datasource opts in. The exit summary consults it
184/// so an embedder can call `emit_debug_summary()` unconditionally on quit
185/// without printing a ledger nobody asked for.
186static ANY_TAP_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
187
188/// Whether any datasource enabled the debug stream in this process.
189pub fn any_tap_enabled() -> bool {
190    ANY_TAP_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
191}
192
193static PROCESS_START: LazyLock<Instant> = LazyLock::new(Instant::now);
194
195/// Snapshot process wall-clock, CPU time, and peak RSS.
196///
197/// Unix only (`getrusage`); other platforms report uptime and zeros.
198pub fn process_stats() -> ProcessStats {
199    let uptime_ms = PROCESS_START.elapsed().as_millis() as u64;
200    #[cfg(unix)]
201    {
202        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
203        if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
204            let tv_ms = |tv: libc::timeval| tv.tv_sec as u64 * 1000 + tv.tv_usec as u64 / 1000;
205            // ru_maxrss is bytes on macOS, kilobytes on Linux.
206            #[cfg(target_os = "macos")]
207            let peak = usage.ru_maxrss as u64;
208            #[cfg(not(target_os = "macos"))]
209            let peak = usage.ru_maxrss as u64 * 1024;
210            return ProcessStats {
211                uptime_ms,
212                cpu_ms: tv_ms(usage.ru_utime) + tv_ms(usage.ru_stime),
213                peak_rss_bytes: peak,
214            };
215        }
216    }
217    ProcessStats {
218        uptime_ms,
219        ..Default::default()
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn tap_is_off_by_default_and_carries_the_datasource_name() {
229        let off = DebugTap::default();
230        assert!(!off.enabled());
231        assert_eq!(off.ds(), "");
232        let on = DebugTap::for_datasource("librarian");
233        assert!(on.enabled());
234        assert_eq!(on.ds(), "librarian");
235    }
236
237    #[test]
238    fn process_stats_reports_nonzero_cpu_and_rss() {
239        // Burn a little CPU so utime is measurable.
240        let mut x = 0u64;
241        for i in 0..5_000_000u64 {
242            x = x.wrapping_add(i);
243        }
244        std::hint::black_box(x);
245        let s = process_stats();
246        #[cfg(unix)]
247        {
248            assert!(
249                s.peak_rss_bytes > 0,
250                "peak RSS should be measurable on unix"
251            );
252            assert!(s.cpu_ms > 0, "cpu time should be nonzero after busy loop");
253        }
254        let _ = s.uptime_ms; // monotonic, may be 0 in a fast test — presence is enough
255    }
256}