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}