1use std::io::Write;
4use std::num::NonZeroU64;
5use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
6
7use parking_lot::Mutex;
8use serde::ser::SerializeSeq;
9use serde::{Serialize, Serializer};
10
11#[macro_export]
35macro_rules! timed {
36 ($name:expr, span = $span:expr, $body:expr $(,)?) => {{
37 let __scope = $crate::TimingScope::with_span($name, Some($span));
38 $body
39 }};
40 ($name:expr, $body:expr $(,)?) => {{
41 let __scope = $crate::TimingScope::new($name);
42 $body
43 }};
44}
45
46thread_local! {
47 static THREAD_DATA: ThreadData = ThreadData {
49 id: {
50 static COUNTER: AtomicU64 = AtomicU64::new(1);
53 COUNTER.fetch_add(1, Ordering::Relaxed)
54 },
55 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
56 timer: WasmTimer::new(),
57 };
58}
59
60static ENABLED: AtomicBool = AtomicBool::new(false);
62
63static EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
65
66#[inline]
68pub fn enable() {
69 ENABLED.store(true, Ordering::Relaxed);
72}
73
74#[inline]
76pub fn disable() {
77 ENABLED.store(false, Ordering::Relaxed);
80}
81
82#[inline]
84pub fn is_enabled() -> bool {
85 ENABLED.load(Ordering::Relaxed)
86}
87
88#[inline]
90pub fn clear() {
91 EVENTS.lock().clear();
92}
93
94pub fn export_json<W: Write>(
102 writer: W,
103 mut source: impl FnMut(NonZeroU64) -> (String, u32),
104) -> Result<(), String> {
105 #[derive(Serialize)]
106 struct Entry {
107 name: &'static str,
108 cat: &'static str,
109 ph: &'static str,
110 ts: f64,
111 pid: u64,
112 tid: u64,
113 args: Option<Args>,
114 }
115
116 #[derive(Serialize)]
117 struct Args {
118 file: String,
119 line: u32,
120 }
121
122 let events = std::mem::take(&mut *EVENTS.lock());
125
126 let mut serializer = serde_json::Serializer::new(writer);
127 let mut seq = serializer
128 .serialize_seq(Some(events.len()))
129 .map_err(|e| format!("failed to serialize events: {e}"))?;
130
131 for event in events.iter() {
132 seq.serialize_element(&Entry {
133 name: event.name,
134 cat: "typst",
135 ph: match event.kind {
136 EventKind::Start => "B",
137 EventKind::End => "E",
138 },
139 ts: event.timestamp.micros_since(events[0].timestamp),
140 pid: 1,
141 tid: event.thread_id,
142 args: event.span.map(&mut source).map(|(file, line)| Args { file, line }),
143 })
144 .map_err(|e| format!("failed to serialize event: {e}"))?;
145 }
146
147 seq.end().map_err(|e| format!("failed to serialize events: {e}"))?;
148
149 Ok(())
150}
151
152pub struct TimingScope {
154 name: &'static str,
155 span: Option<NonZeroU64>,
156 thread_id: u64,
157}
158
159impl TimingScope {
160 #[inline]
162 pub fn new(name: &'static str) -> Option<Self> {
163 Self::with_span(name, None)
164 }
165
166 #[inline]
172 pub fn with_span(name: &'static str, span: Option<NonZeroU64>) -> Option<Self> {
173 if is_enabled() {
174 return Some(Self::new_impl(name, span));
175 }
176 None
177 }
178
179 fn new_impl(name: &'static str, span: Option<NonZeroU64>) -> Self {
181 let (thread_id, timestamp) =
182 THREAD_DATA.with(|data| (data.id, Timestamp::now_with(data)));
183 EVENTS.lock().push(Event {
184 kind: EventKind::Start,
185 timestamp,
186 name,
187 span,
188 thread_id,
189 });
190 Self { name, span, thread_id }
191 }
192}
193
194impl Drop for TimingScope {
195 fn drop(&mut self) {
196 let timestamp = Timestamp::now();
197 EVENTS.lock().push(Event {
198 kind: EventKind::End,
199 timestamp,
200 name: self.name,
201 span: self.span,
202 thread_id: self.thread_id,
203 });
204 }
205}
206
207struct Event {
209 kind: EventKind,
211 timestamp: Timestamp,
213 name: &'static str,
215 span: Option<NonZeroU64>,
217 thread_id: u64,
219}
220
221#[derive(Debug, Copy, Clone, Eq, PartialEq)]
223enum EventKind {
224 Start,
225 End,
226}
227
228#[derive(Copy, Clone)]
230struct Timestamp {
231 #[cfg(not(target_arch = "wasm32"))]
232 inner: std::time::SystemTime,
233 #[cfg(target_arch = "wasm32")]
234 inner: f64,
235}
236
237impl Timestamp {
238 fn now() -> Self {
239 #[cfg(target_arch = "wasm32")]
240 return THREAD_DATA.with(Self::now_with);
241
242 #[cfg(not(target_arch = "wasm32"))]
243 Self { inner: std::time::SystemTime::now() }
244 }
245
246 #[allow(unused_variables)]
247 fn now_with(data: &ThreadData) -> Self {
248 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
249 return Self { inner: data.timer.now() };
250
251 #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
252 return Self { inner: 0.0 };
253
254 #[cfg(not(target_arch = "wasm32"))]
255 Self::now()
256 }
257
258 fn micros_since(self, start: Self) -> f64 {
259 #[cfg(target_arch = "wasm32")]
260 return (self.inner - start.inner) * 1000.0;
261
262 #[cfg(not(target_arch = "wasm32"))]
263 (self
264 .inner
265 .duration_since(start.inner)
266 .unwrap_or(std::time::Duration::ZERO)
267 .as_nanos() as f64
268 / 1_000.0)
269 }
270}
271
272struct ThreadData {
274 id: u64,
280 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
282 timer: WasmTimer,
283}
284
285#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
287struct WasmTimer {
288 perf: web_sys::Performance,
290 time_origin: f64,
292}
293
294#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
295impl WasmTimer {
296 fn new() -> Self {
297 let perf = web_sys::window()
300 .and_then(|window| window.performance())
301 .or_else(|| {
302 use web_sys::wasm_bindgen::JsCast;
303 web_sys::js_sys::global()
304 .dyn_into::<web_sys::WorkerGlobalScope>()
305 .ok()
306 .and_then(|scope| scope.performance())
307 })
308 .expect("failed to get JS performance handle");
309
310 let time_origin = perf.time_origin();
313
314 Self { perf, time_origin }
315 }
316
317 fn now(&self) -> f64 {
318 self.time_origin + self.perf.now()
319 }
320}