Skip to main content

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}