1use serde::Serialize;
4use std::borrow::Cow;
5use std::cell::RefCell;
6use std::collections::BTreeMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Mutex, OnceLock};
9use std::time::{Duration, Instant};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Format {
13 Human,
14 Structured,
15}
16
17#[derive(Clone)]
18struct ActiveSpan {
19 id: u64,
20 worker: Option<String>,
21}
22
23#[derive(Serialize)]
24#[serde(rename_all = "camelCase")]
25struct Event {
26 id: u64,
27 phase: Cow<'static, str>,
28 start_offset_ms: u128,
29 duration_ms: u128,
30 parent: Option<u64>,
31 worker: Option<String>,
32}
33
34struct State {
35 format: Format,
36 origin: Instant,
37 events: Vec<Event>,
38}
39
40static STATE: OnceLock<Mutex<Option<State>>> = OnceLock::new();
41static NEXT_ID: AtomicU64 = AtomicU64::new(1);
42
43thread_local! {
44 static ACTIVE: RefCell<Vec<ActiveSpan>> = const { RefCell::new(Vec::new()) };
45}
46
47pub fn set_format(format: Option<Format>, origin: Instant) {
48 *STATE.get_or_init(|| Mutex::new(None)).lock().expect("profile state lock poisoned") =
49 format.map(|format| State { format, origin, events: Vec::new() });
50 ACTIVE.with(|active| active.borrow_mut().clear());
51}
52
53pub struct Span {
54 id: u64,
55 name: Cow<'static, str>,
56 started: Option<Instant>,
57 start_offset: Duration,
58 parent: Option<u64>,
59 worker: Option<String>,
60 format: Option<Format>,
61}
62
63impl Span {
64 pub fn new(name: &'static str) -> Self {
65 Self::start(name.into(), None)
66 }
67
68 pub fn named(name: impl Into<String>) -> Self {
69 Self::start(name.into().into(), None)
70 }
71
72 pub fn named_worker(name: impl Into<String>, worker: impl Into<String>) -> Self {
73 Self::start(name.into().into(), Some(worker.into()))
74 }
75
76 fn start(name: Cow<'static, str>, worker_name: Option<String>) -> Self {
77 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
78 let mut format = None;
79 let mut start_offset = Duration::ZERO;
80 let mut worker = None;
81 if let Some(state) = STATE.get() {
82 let mut state = state.lock().expect("profile state lock poisoned");
83 if let Some(state) = state.as_mut() {
84 format = Some(state.format);
85 start_offset = state.origin.elapsed();
86 worker = worker_name;
87 }
88 }
89 let (parent, inherited_worker) = ACTIVE.with(|active| {
90 active.borrow().last().map_or((None, None), |span| (Some(span.id), span.worker.clone()))
91 });
92 worker = worker.or(inherited_worker);
93 let started = format.map(|_| Instant::now());
94 if started.is_some() {
95 ACTIVE.with(|active| active.borrow_mut().push(ActiveSpan { id, worker: worker.clone() }));
96 }
97 Self { id, name, started, start_offset, parent, worker, format }
98 }
99}
100
101impl Drop for Span {
102 fn drop(&mut self) {
103 let Some(started) = self.started else { return };
104 let duration = started.elapsed();
105 ACTIVE.with(|active| {
106 let mut active = active.borrow_mut();
107 if let Some(index) = active.iter().rposition(|span| span.id == self.id) {
108 active.remove(index);
109 }
110 });
111 match self.format {
112 Some(Format::Human) => {
113 eprintln!("PROFILE phase={} duration_ms={}", self.name, duration.as_millis());
114 }
115 Some(Format::Structured) => record(Event {
116 id: self.id,
117 phase: self.name.clone(),
118 start_offset_ms: self.start_offset.as_millis(),
119 duration_ms: duration.as_millis(),
120 parent: self.parent,
121 worker: self.worker.clone(),
122 }),
123 None => {}
124 }
125 }
126}
127
128pub fn record_duration(name: &'static str, start_offset: Duration, duration: Duration) {
129 let Some(state) = STATE.get() else { return };
130 let format =
131 state.lock().expect("profile state lock poisoned").as_ref().map(|state| state.format);
132 match format {
133 Some(Format::Human) => {
134 eprintln!("PROFILE phase={name} duration_ms={}", duration.as_millis());
135 }
136 Some(Format::Structured) => record(Event {
137 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
138 phase: name.into(),
139 start_offset_ms: start_offset.as_millis(),
140 duration_ms: duration.as_millis(),
141 parent: None,
142 worker: None,
143 }),
144 None => {}
145 }
146}
147
148fn record(event: Event) {
149 if let Some(state) = STATE.get()
150 && let Some(state) = state.lock().expect("profile state lock poisoned").as_mut()
151 {
152 state.events.push(event);
153 }
154}
155
156#[derive(Serialize)]
157#[serde(rename_all = "camelCase")]
158struct Summary<'a> {
159 phase: &'a str,
160 count: usize,
161 total_duration_ms: u128,
162}
163
164#[derive(Serialize)]
165#[serde(rename_all = "camelCase")]
166struct Envelope<'a> {
167 version: u8,
168 stability: &'static str,
169 events: &'a [Event],
170 summary: Vec<Summary<'a>>,
171}
172
173fn summarize(events: &[Event]) -> Vec<Summary<'_>> {
174 let mut totals = BTreeMap::<&str, (usize, u128)>::new();
175 for event in events {
176 let total = totals.entry(&event.phase).or_default();
177 total.0 += 1;
178 total.1 += event.duration_ms;
179 }
180 let mut summary = totals
181 .into_iter()
182 .map(|(phase, (count, total_duration_ms))| Summary { phase, count, total_duration_ms })
183 .collect::<Vec<_>>();
184 summary.sort_by(|left, right| {
185 right.total_duration_ms.cmp(&left.total_duration_ms).then(left.phase.cmp(right.phase))
186 });
187 summary
188}
189
190fn structured_value(state: &mut State) -> serde_json::Value {
191 state.events.sort_by_key(|event| (event.start_offset_ms, event.id));
192 let summary = summarize(&state.events);
193 serde_json::to_value(Envelope {
194 version: 1,
195 stability: "unstable",
196 events: &state.events,
197 summary,
198 })
199 .expect("profile envelope contains serializable values")
200}
201
202pub fn structured_snapshot() -> Option<serde_json::Value> {
204 let mut state = STATE.get()?.lock().expect("profile state lock poisoned");
205 let state = state.as_mut()?;
206 (state.format == Format::Structured).then(|| structured_value(state))
207}
208
209pub fn finish() {
210 let Some(state) = STATE.get() else { return };
211 let mut state = state.lock().expect("profile state lock poisoned");
212 let Some(mut state) = state.take() else { return };
213 if state.format != Format::Structured {
214 return;
215 }
216 eprintln!("{}", structured_value(&mut state));
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn structured_envelope_marks_instability_and_aggregates_slowest_first() {
225 set_format(Some(Format::Structured), Instant::now());
226 let parent = Span::named_worker("repeat", "host-a");
227 let parent_id = parent.id;
228 {
229 let _child = Span::new("repeat");
230 }
231 drop(parent);
232 let value = structured_snapshot().unwrap();
233 assert_eq!(structured_snapshot(), Some(value.clone()));
234 assert_eq!(value["stability"], "unstable");
235 assert_eq!(value["version"], 1);
236 let child = value["events"]
237 .as_array()
238 .unwrap()
239 .iter()
240 .find(|event| event["parent"] == parent_id)
241 .unwrap();
242 assert_eq!(child["worker"], "host-a");
243 assert_eq!(value["summary"][0]["count"], 2);
244 }
245}