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