edgefirst_hal/trace.rs
1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Trace capture for performance analysis.
5//!
6//! Provides a simple start/stop API for capturing [`tracing`]-based spans
7//! emitted by HAL crates into Chrome JSON trace files viewable at
8//! <https://ui.perfetto.dev/>.
9//!
10//! # Design
11//!
12//! Every HAL library crate — `edgefirst-tensor`, `edgefirst-codec`,
13//! `edgefirst-image`, `edgefirst-decoder`, and `edgefirst-tracker` — emits
14//! [`tracing::trace_span!`] spans on its hot paths. These have near-zero
15//! overhead when no subscriber is active (a single relaxed atomic load per
16//! span site).
17//!
18//! This module installs a **process-wide subscriber** consisting of a Chrome
19//! trace layer writing spans to a JSON file for Perfetto. Existing `log::*`
20//! output (via `env_logger`) continues independently to stderr.
21//!
22//! The subscriber is installed once on the first call to [`start_tracing`].
23//! Only one trace capture session is supported per process lifetime (this is
24//! a limitation of Rust's global subscriber model and is acceptable for
25//! profiling workflows where a single trace per run is the norm).
26//!
27//! # Usage from Rust
28//!
29//! ```no_run
30//! # #[cfg(feature = "tracing")]
31//! # {
32//! use edgefirst_hal::trace::{start_tracing, stop_tracing};
33//!
34//! start_tracing("/tmp/trace.json").expect("start tracing");
35//! // ... run inference pipeline ...
36//! stop_tracing(); // flushes and closes the trace file
37//! # }
38//! ```
39//!
40//! # Usage from Python
41//!
42//! ```python
43//! import edgefirst_hal as hal
44//!
45//! with hal.Tracing("/tmp/trace.json"):
46//! # ... run inference ...
47//! pass
48//! # trace file is flushed on __exit__
49//! ```
50//!
51//! # Usage from C
52//!
53//! ```c
54//! #include "edgefirst_hal.h"
55//! hal_start_tracing("/tmp/trace.json");
56//! // ... run inference ...
57//! hal_stop_tracing(); // flushes trace file
58//! ```
59
60use std::sync::atomic::{AtomicBool, Ordering};
61use std::sync::Mutex;
62
63use tracing_chrome::FlushGuard;
64use tracing_subscriber::prelude::*;
65
66/// Global flush guard for the active trace session.
67static GUARD: Mutex<Option<FlushGuard>> = Mutex::new(None);
68
69/// Tracks whether a session has ever been started (remains true after stop).
70static SESSION_USED: AtomicBool = AtomicBool::new(false);
71
72/// Errors from tracing operations.
73#[derive(Debug)]
74pub enum TracingError {
75 /// A trace capture session is already active.
76 AlreadyActive,
77 /// The single-use trace session was already started and stopped.
78 /// Only one session per process lifetime is supported.
79 SessionExhausted,
80 /// Failed to install the global subscriber (another was already set
81 /// by user code outside the HAL).
82 SubscriberInstallFailed(String),
83}
84
85impl std::fmt::Display for TracingError {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 Self::AlreadyActive => write!(f, "trace capture already active"),
89 Self::SessionExhausted => write!(
90 f,
91 "trace session already used (only one session per process lifetime)"
92 ),
93 Self::SubscriberInstallFailed(e) => {
94 write!(f, "failed to install tracing subscriber: {e}")
95 }
96 }
97 }
98}
99
100impl std::error::Error for TracingError {}
101
102/// Start trace capture, writing Chrome JSON to `path`.
103///
104/// Installs a global tracing subscriber (chrome layer only) on first call.
105/// The trace file is created immediately. All `tracing::trace_span!` spans
106/// emitted by HAL crates will be recorded until [`stop_tracing`] is called.
107///
108/// Only one session per process lifetime is supported (a limitation of
109/// Rust's global subscriber model).
110///
111/// # Errors
112///
113/// Returns [`TracingError::AlreadyActive`] if a session is currently capturing.
114/// Returns [`TracingError::SessionExhausted`] if a session was previously
115/// started and stopped (the global subscriber cannot be replaced).
116/// Returns [`TracingError::SubscriberInstallFailed`] if another tracing
117/// subscriber was installed by user code outside the HAL.
118pub fn start_tracing(path: &str) -> Result<(), TracingError> {
119 let mut lock = GUARD.lock().unwrap_or_else(|e| e.into_inner());
120 if lock.is_some() {
121 return Err(TracingError::AlreadyActive);
122 }
123 if SESSION_USED.load(Ordering::Relaxed) {
124 return Err(TracingError::SessionExhausted);
125 }
126
127 // Build chrome layer writing to the specified file.
128 let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new()
129 .file(path)
130 .include_args(true)
131 .build();
132
133 // Install only the chrome layer. Existing log::* output continues through
134 // env_logger to stderr independently — no conflict.
135 let subscriber = tracing_subscriber::registry().with(chrome_layer);
136
137 tracing::subscriber::set_global_default(subscriber)
138 .map_err(|e| TracingError::SubscriberInstallFailed(e.to_string()))?;
139
140 SESSION_USED.store(true, Ordering::Relaxed);
141 *lock = Some(guard);
142 Ok(())
143}
144
145/// Stop trace capture, flushing all buffered spans to the output file.
146///
147/// No-op if no session is active. After this call the trace file is complete
148/// and can be loaded into <https://ui.perfetto.dev/>.
149pub fn stop_tracing() {
150 let mut lock = GUARD.lock().unwrap_or_else(|e| e.into_inner());
151 // Dropping the FlushGuard flushes remaining spans and closes the file.
152 lock.take();
153}
154
155/// Returns `true` if a trace capture session is currently active.
156pub fn is_tracing_active() -> bool {
157 GUARD.lock().unwrap_or_else(|e| e.into_inner()).is_some()
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use std::path::Path;
164
165 // Single test because the global subscriber is per-process lifetime.
166 #[test]
167 fn test_trace_lifecycle() {
168 let dir = std::env::temp_dir();
169 let path = dir.join("hal_test_trace_lifecycle.json");
170 let path_str = path.to_str().unwrap();
171
172 // Clean up any previous test artifact
173 let _ = std::fs::remove_file(&path);
174
175 assert!(!is_tracing_active());
176
177 // First start should succeed
178 start_tracing(path_str).expect("start_tracing should succeed");
179 assert!(is_tracing_active());
180
181 // Second start while active should fail with AlreadyActive
182 let err = start_tracing(path_str).unwrap_err();
183 assert!(
184 matches!(err, TracingError::AlreadyActive),
185 "expected AlreadyActive, got: {err:?}"
186 );
187
188 // Emit a span to ensure the file gets content
189 {
190 let _span = tracing::trace_span!("hal.test_span", key = "value").entered();
191 }
192
193 // Stop should deactivate
194 stop_tracing();
195 assert!(!is_tracing_active());
196
197 // Trace file should exist with content
198 assert!(Path::new(path_str).exists());
199 let content = std::fs::read_to_string(&path).unwrap();
200 assert!(!content.is_empty(), "trace file should not be empty");
201
202 // Third start fails because session was already used
203 let err = start_tracing(path_str).unwrap_err();
204 assert!(
205 matches!(err, TracingError::SessionExhausted),
206 "expected SessionExhausted, got: {err:?}"
207 );
208
209 // Clean up
210 let _ = std::fs::remove_file(&path);
211 }
212}