flight_debug/debug_timing.rs
1//! Debug timing spans and frame markers (TS `debugTiming.ts`), gated on an active debug session so
2//! instrumentation left in shipping code costs one boolean check. Timestamps are injected (D5).
3
4use std::collections::HashMap;
5use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
6use std::sync::Arc;
7
8use flight_log::{
9 LogData, LogStateRef, LogTimer, LogValue, end_log_timer, log_debug, start_log_timer,
10};
11
12use crate::debug::is_debug_enabled;
13use crate::types::DebugStateRef;
14
15/// Opens a named timing span only while a debug session is active (TS `beginDebugSpan`): a running
16/// `LogTimer` to hand to `end_debug_span`, or `None` when debug is disabled (nothing is allocated).
17// flight-lint: allow(conform-params, f64) D5: the caller-owned debug state and injected now_ms replace the source module global and clock; D1: a JS-number millisecond timestamp
18pub fn begin_debug_span(
19 state: &DebugStateRef,
20 now_ms: f64,
21 name: &str,
22 channel: Option<String>,
23) -> Option<LogTimer> {
24 if !is_debug_enabled(state) {
25 return None;
26 }
27 return Some(start_log_timer(name, channel, now_ms));
28}
29
30/// Closes a span opened by `begin_debug_span` (TS `endDebugSpan`), emitting its elapsed-time Debug
31/// entry and returning the elapsed milliseconds. A `None` timer (debug was disabled when the span
32/// opened) is a no-op returning -1, the "not measured" sentinel.
33// flight-lint: allow(conform-params, f64) D5: caller-owned log state and injected now_ms replace the source singleton and clock; D1: JS-number milliseconds
34pub fn end_debug_span(log_state: &LogStateRef, now_ms: f64, timer: Option<&LogTimer>) -> f64 {
35 let Some(timer) = timer else {
36 return -1.0;
37 };
38 return end_log_timer(log_state, now_ms, timer);
39}
40
41/// Emits a frame-boundary marker while a debug session is active (TS `markDebugFrame`): a Debug entry
42/// `{ frame: label }`, or, unlabeled, the next value of the state's ascending frame counter, which a
43/// new session does not reset. A no-op, with no counter advance, when debug is disabled.
44// flight-lint: allow(conform-params, f64) D5: caller-owned debug and log state and injected now_ms replace the source module globals and clock; D1: a JS-number millisecond timestamp
45pub fn mark_debug_frame(
46 state: &DebugStateRef,
47 log_state: &LogStateRef,
48 now_ms: f64,
49 label: Option<&str>,
50 channel: Option<String>,
51) {
52 if !is_debug_enabled(state) {
53 return;
54 }
55 let frame = match label {
56 Some(label) => LogValue::Str(label.to_string()),
57 None => {
58 let mut current = state.write().unwrap();
59 current.frame_number += 1;
60 LogValue::Int(current.frame_number)
61 }
62 };
63 let mut data = HashMap::new();
64 data.insert("frame".to_string(), frame);
65 log_debug(log_state, now_ms, LogData::Record(data).into(), channel);
66}
67
68/// Times a synchronous function as one span while a debug session is active (TS
69/// `measureDebugSpan`) and returns its result. With debug disabled the function still runs and only
70/// the timing is skipped. The span is closed, and its entry emitted, even when the function panics
71/// (the source's throw), and the panic then resumes. `clock` is read once when the span opens and
72/// once when it closes.
73// flight-lint: allow(conform-params, contract-vocabulary, f64) D5: caller-owned debug and log state and an injected clock replace the source module globals and clock; the source `fn: () => T` is run once inline and never stored; D1: JS-number milliseconds
74pub fn measure_debug_span<T>(
75 state: &DebugStateRef,
76 log_state: &LogStateRef,
77 clock: &Arc<dyn Fn() -> f64 + Send + Sync>,
78 name: &str,
79 run: impl FnOnce() -> T,
80 channel: Option<String>,
81) -> T {
82 let timer = begin_debug_span(state, clock(), name, channel);
83 let result = catch_unwind(AssertUnwindSafe(run));
84 if timer.is_some() {
85 end_debug_span(log_state, clock(), timer.as_ref());
86 }
87 return match result {
88 Ok(value) => value,
89 Err(error) => resume_unwind(error),
90 };
91}