nmbrs_runtime/readouts/builtins/
trace.rs1use std::fmt::Write as _;
29
30use crate::lifecycle::SubjectKind;
31use crate::readouts::buf::ReadoutBuf;
32use crate::readouts::context::ReadoutContext;
33use crate::readouts::readout::{ContentMode, Lod, Readout, ReadoutOptions};
34
35pub struct Trace;
36
37impl Readout for Trace {
38 fn name(&self) -> &'static str {
39 "trace"
40 }
41 fn accepts(&self) -> &'static [SubjectKind] {
42 &[
43 SubjectKind::Session,
44 SubjectKind::Phase,
45 SubjectKind::Iteration,
46 SubjectKind::Scope,
47 ]
48 }
49
50 fn render(
51 &self,
52 ctx: &dyn ReadoutContext,
53 lod: Lod,
54 mode: ContentMode,
55 _opts: &ReadoutOptions,
56 out: &mut dyn ReadoutBuf,
57 ) -> usize {
58 if matches!(mode, ContentMode::Explanation) {
64 return render_explanation(ctx, out);
65 }
66 let mut tmp = String::with_capacity(512);
67
68 let _ = write!(
72 &mut tmp,
73 "event={slot} lod={lod:?} mode={mode:?}",
74 slot = ctx.event().slot_name(),
75 );
76
77 let _ = write!(&mut tmp, "\n refresh_tick={t}", t = ctx.refresh_tick(),);
81 let _ = write!(&mut tmp, "\n phase_name={n:?}", n = ctx.subject_name(),);
82 let _ = write!(&mut tmp, "\n activity_name={n:?}", n = ctx.activity_name(),);
83 match ctx.subject_seq() {
84 Some((i, t)) => {
85 let _ = write!(&mut tmp, "\n phase_seq=({i}/{t})");
86 }
87 None => {
88 let _ = write!(&mut tmp, "\n phase_seq=None");
89 }
90 }
91 let _ = write!(&mut tmp, "\n phase_labels={l:?}", l = ctx.subject_labels(),);
92 let _ = write!(
93 &mut tmp,
94 "\n cycles={c}/{t}",
95 c = ctx.cycles_completed(),
96 t = ctx.cycles_total(),
97 );
98 let _ = write!(
99 &mut tmp,
100 "\n ops_started={s} ops_ok={ok} errors={e} retries={r}",
101 s = ctx.ops_started(),
102 ok = ctx.ops_ok(),
103 e = ctx.errors(),
104 r = ctx.retries(),
105 );
106 let _ = write!(&mut tmp, "\n concurrency={c}", c = ctx.concurrency(),);
107 let _ = write!(&mut tmp, "\n consumed={c}", c = ctx.consumed(),);
108 let _ = write!(&mut tmp, "\n elapsed_secs={e:.3}", e = ctx.elapsed_secs(),);
109 match ctx.eta_secs() {
110 Some(s) => {
111 let _ = write!(&mut tmp, "\n eta_secs={s:.3}");
112 }
113 None => {
114 let _ = write!(&mut tmp, "\n eta_secs=None");
115 }
116 }
117 let chips = ctx.status_metric_chips();
118 if !chips.is_empty() {
119 let _ = write!(&mut tmp, "\n status_metric_chips={chips:?}");
120 }
121 let adapter = ctx.adapter_counters_text();
122 if !adapter.is_empty() {
123 let _ = write!(&mut tmp, "\n adapter_counters_text={adapter:?}");
124 }
125 let batch = ctx.batch_info_text();
126 if !batch.is_empty() {
127 let _ = write!(&mut tmp, "\n batch_info_text={batch:?}");
128 }
129 let _ = write!(
130 &mut tmp,
131 "\n depth_indent_len={d}",
132 d = ctx.depth_indent().len(),
133 );
134 let _ = write!(&mut tmp, "\n use_color={c}", c = ctx.use_color(),);
135
136 let len = tmp.len();
137 let _ = out.write_str(&tmp);
138 len
139 }
140}
141
142fn render_explanation(ctx: &dyn ReadoutContext, out: &mut dyn ReadoutBuf) -> usize {
146 let s = "event=on_<event-slot> lod=<density> mode=<value|explanation>\n \
147refresh_tick: monotonic counter advanced once per refresh fire\n \
148phase_name: bare phase identifier (no coord suffix)\n \
149activity_name: full activity name with leaf coord\n \
150phase_seq: pre-map (idx, total) numbering\n \
151phase_labels: root-first scope coordinate path\n \
152cycles: completed/total\n \
153ops_started ops_ok errors retries: cumulative counters\n \
154concurrency: effective fiber count\n \
155consumed: items pulled from the source factory\n \
156elapsed_secs: wallclock since phase start\n \
157eta_secs: estimated remaining time, when computable\n \
158status_metric_chips: workload-emphasised metric tail\n \
159adapter_counters_text: per-dispenser counter chips\n \
160batch_info_text: rows-per-batch summary\n \
161depth_indent_len: scope-tree indent width\n \
162use_color: surface accepts ANSI styling"
163 .to_string();
164 let _ = out.write_str(&s);
165 let _ = ctx; s.len()
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::lifecycle::EventType;
173 use crate::readouts::buf::StringBuf;
174
175 #[derive(Default)]
176 struct TestCtx {
177 event: Option<EventType>,
178 refresh_tick: u64,
179 phase_name: String,
180 activity_name: Option<String>,
181 phase_seq: Option<(usize, usize)>,
182 phase_labels: String,
183 cycles_completed: u64,
184 cycles_total: u64,
185 ops_started: u64,
186 ops_ok: u64,
187 errors: u64,
188 retries: u64,
189 concurrency: usize,
190 consumed: u64,
191 elapsed_secs: f64,
192 eta_secs: Option<f64>,
193 chips: String,
194 adapter: String,
195 batch: String,
196 depth_indent: String,
197 use_color: bool,
198 }
199
200 impl ReadoutContext for TestCtx {
201 fn subject_name(&self) -> &str {
202 &self.phase_name
203 }
204 fn activity_name(&self) -> &str {
205 self.activity_name.as_deref().unwrap_or(&self.phase_name)
206 }
207 fn subject_seq(&self) -> Option<(usize, usize)> {
208 self.phase_seq
209 }
210 fn subject_labels(&self) -> &str {
211 &self.phase_labels
212 }
213 fn cycles_completed(&self) -> u64 {
214 self.cycles_completed
215 }
216 fn cycles_total(&self) -> u64 {
217 self.cycles_total
218 }
219 fn ops_started(&self) -> u64 {
220 self.ops_started
221 }
222 fn ops_ok(&self) -> u64 {
223 self.ops_ok
224 }
225 fn errors(&self) -> u64 {
226 self.errors
227 }
228 fn retries(&self) -> u64 {
229 self.retries
230 }
231 fn concurrency(&self) -> usize {
232 self.concurrency
233 }
234 fn elapsed_secs(&self) -> f64 {
235 self.elapsed_secs
236 }
237 fn eta_secs(&self) -> Option<f64> {
238 self.eta_secs
239 }
240 fn consumed(&self) -> u64 {
241 self.consumed
242 }
243 fn status_metric_chips(&self) -> String {
244 self.chips.clone()
245 }
246 fn adapter_counters_text(&self) -> String {
247 self.adapter.clone()
248 }
249 fn batch_info_text(&self) -> String {
250 self.batch.clone()
251 }
252 fn depth_indent(&self) -> &str {
253 &self.depth_indent
254 }
255 fn use_color(&self) -> bool {
256 self.use_color
257 }
258 fn event(&self) -> EventType {
259 self.event.unwrap_or(EventType::PhaseEnd)
260 }
261 fn refresh_tick(&self) -> u64 {
262 self.refresh_tick
263 }
264 }
265
266 fn render(ctx: &TestCtx) -> String {
267 let mut s = String::new();
268 let mut buf = StringBuf::new(&mut s);
269 Trace.render(
270 ctx,
271 Lod::Labeled,
272 ContentMode::Value,
273 &ReadoutOptions::new(),
274 &mut buf,
275 );
276 s
277 }
278
279 #[test]
280 fn covers_every_event_class() {
281 for ev in [
285 EventType::SessionStart,
286 EventType::SessionEnd,
287 EventType::PhaseStart,
288 EventType::PhaseEnd,
289 EventType::EachStart,
290 EventType::EachEnd,
291 EventType::ScopeStart,
292 EventType::ScopeEnd,
293 EventType::Update,
294 ] {
295 let ctx = TestCtx {
296 event: Some(ev),
297 phase_name: "p".into(),
298 ..Default::default()
299 };
300 let out = render(&ctx);
301 assert!(
302 out.starts_with(&format!("event={}", ev.slot_name())),
303 "event {ev:?} not surfaced in trace header: {out}"
304 );
305 }
306 }
307
308 #[test]
309 fn dumps_all_listed_fields() {
310 let ctx = TestCtx {
311 event: Some(EventType::PhaseEnd),
312 refresh_tick: 7,
313 phase_name: "ann_query".into(),
314 activity_name: Some("ann_query (k=10)".into()),
315 phase_seq: Some((3, 9)),
316 phase_labels: "(profile=alpha)".into(),
317 cycles_completed: 100,
318 cycles_total: 100,
319 ops_started: 100,
320 ops_ok: 99,
321 errors: 1,
322 retries: 0,
323 concurrency: 4,
324 consumed: 100,
325 elapsed_secs: 1.234,
326 eta_secs: Some(0.5),
327 chips: " recall_at_10:79.62%".into(),
328 adapter: " rows/s=12.5K".into(),
329 batch: " r/b=12.5".into(),
330 depth_indent: " ".into(), use_color: true,
332 };
333 let out = render(&ctx);
334 for needle in [
336 "event=on_phase_end",
337 "refresh_tick=7",
338 "phase_name=\"ann_query\"",
339 "activity_name=\"ann_query (k=10)\"",
340 "phase_seq=(3/9)",
341 "phase_labels=\"(profile=alpha)\"",
342 "cycles=100/100",
343 "ops_started=100 ops_ok=99 errors=1 retries=0",
344 "concurrency=4",
345 "consumed=100",
346 "elapsed_secs=1.234",
347 "eta_secs=0.500",
348 "status_metric_chips=\" recall_at_10:79.62%\"",
349 "adapter_counters_text=\" rows/s=12.5K\"",
350 "batch_info_text=\" r/b=12.5\"",
351 "depth_indent_len=4",
352 "use_color=true",
353 ] {
354 assert!(
355 out.contains(needle),
356 "trace missing {needle:?} — actual: {out}"
357 );
358 }
359 }
360
361 #[test]
362 fn omits_empty_string_fields() {
363 let ctx = TestCtx {
364 phase_name: "x".into(),
365 ..Default::default()
366 };
367 let out = render(&ctx);
368 assert!(
370 !out.contains("status_metric_chips="),
371 "empty chips should not surface: {out}"
372 );
373 assert!(
374 !out.contains("adapter_counters_text="),
375 "empty adapter text should not surface: {out}"
376 );
377 assert!(
378 !out.contains("batch_info_text="),
379 "empty batch text should not surface: {out}"
380 );
381 }
382}