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>(
100 writer: W,
101 mut source: impl FnMut(NonZeroU64) -> (String, u32),
102) -> Result<(), String> {
103 #[derive(Serialize)]
104 struct Entry {
105 name: &'static str,
106 cat: &'static str,
107 ph: &'static str,
108 ts: f64,
109 pid: u64,
110 tid: u64,
111 args: Option<Args>,
112 }
113
114 #[derive(Serialize)]
115 struct Args {
116 file: String,
117 line: u32,
118 }
119
120 let lock = EVENTS.lock();
121 let events = lock.as_slice();
122
123 let mut serializer = serde_json::Serializer::new(writer);
124 let mut seq = serializer
125 .serialize_seq(Some(events.len()))
126 .map_err(|e| format!("failed to serialize events: {e}"))?;
127
128 for event in events.iter() {
129 seq.serialize_element(&Entry {
130 name: event.name,
131 cat: "typst",
132 ph: match event.kind {
133 EventKind::Start => "B",
134 EventKind::End => "E",
135 },
136 ts: event.timestamp.micros_since(events[0].timestamp),
137 pid: 1,
138 tid: event.thread_id,
139 args: event.span.map(&mut source).map(|(file, line)| Args { file, line }),
140 })
141 .map_err(|e| format!("failed to serialize event: {e}"))?;
142 }
143
144 seq.end().map_err(|e| format!("failed to serialize events: {e}"))?;
145
146 Ok(())
147}
148
149pub struct TimingScope {
151 name: &'static str,
152 span: Option<NonZeroU64>,
153 thread_id: u64,
154}
155
156impl TimingScope {
157 #[inline]
159 pub fn new(name: &'static str) -> Option<Self> {
160 Self::with_span(name, None)
161 }
162
163 #[inline]
169 pub fn with_span(name: &'static str, span: Option<NonZeroU64>) -> Option<Self> {
170 if is_enabled() {
171 return Some(Self::new_impl(name, span));
172 }
173 None
174 }
175
176 fn new_impl(name: &'static str, span: Option<NonZeroU64>) -> Self {
178 let (thread_id, timestamp) =
179 THREAD_DATA.with(|data| (data.id, Timestamp::now_with(data)));
180 EVENTS.lock().push(Event {
181 kind: EventKind::Start,
182 timestamp,
183 name,
184 span,
185 thread_id,
186 });
187 Self { name, span, thread_id }
188 }
189}
190
191impl Drop for TimingScope {
192 fn drop(&mut self) {
193 let timestamp = Timestamp::now();
194 EVENTS.lock().push(Event {
195 kind: EventKind::End,
196 timestamp,
197 name: self.name,
198 span: self.span,
199 thread_id: self.thread_id,
200 });
201 }
202}
203
204struct Event {
206 kind: EventKind,
208 timestamp: Timestamp,
210 name: &'static str,
212 span: Option<NonZeroU64>,
214 thread_id: u64,
216}
217
218#[derive(Debug, Copy, Clone, Eq, PartialEq)]
220enum EventKind {
221 Start,
222 End,
223}
224
225#[derive(Copy, Clone)]
227struct Timestamp {
228 #[cfg(not(target_arch = "wasm32"))]
229 inner: std::time::SystemTime,
230 #[cfg(target_arch = "wasm32")]
231 inner: f64,
232}
233
234impl Timestamp {
235 fn now() -> Self {
236 #[cfg(target_arch = "wasm32")]
237 return THREAD_DATA.with(Self::now_with);
238
239 #[cfg(not(target_arch = "wasm32"))]
240 Self { inner: std::time::SystemTime::now() }
241 }
242
243 #[allow(unused_variables)]
244 fn now_with(data: &ThreadData) -> Self {
245 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
246 return Self { inner: data.timer.now() };
247
248 #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
249 return Self { inner: 0.0 };
250
251 #[cfg(not(target_arch = "wasm32"))]
252 Self::now()
253 }
254
255 fn micros_since(self, start: Self) -> f64 {
256 #[cfg(target_arch = "wasm32")]
257 return (self.inner - start.inner) * 1000.0;
258
259 #[cfg(not(target_arch = "wasm32"))]
260 (self
261 .inner
262 .duration_since(start.inner)
263 .unwrap_or(std::time::Duration::ZERO)
264 .as_nanos() as f64
265 / 1_000.0)
266 }
267}
268
269struct ThreadData {
271 id: u64,
277 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
279 timer: WasmTimer,
280}
281
282#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
284struct WasmTimer {
285 perf: web_sys::Performance,
287 time_origin: f64,
289}
290
291#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
292impl WasmTimer {
293 fn new() -> Self {
294 let perf = web_sys::window()
297 .and_then(|window| window.performance())
298 .or_else(|| {
299 use web_sys::wasm_bindgen::JsCast;
300 web_sys::js_sys::global()
301 .dyn_into::<web_sys::WorkerGlobalScope>()
302 .ok()
303 .and_then(|scope| scope.performance())
304 })
305 .expect("failed to get JS performance handle");
306
307 let time_origin = perf.time_origin();
310
311 Self { perf, time_origin }
312 }
313
314 fn now(&self) -> f64 {
315 self.time_origin + self.perf.now()
316 }
317}