typst_timing/
lib.rs

1//! Performance timing for Typst.
2
3use 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/// Creates a timing scope around an expression.
12///
13/// The output of the expression is returned.
14///
15/// The scope will be named `name` and will have the span `span`. The span is
16/// optional.
17///
18/// ## Example
19///
20/// ```rs
21/// // With a scope name and span.
22/// timed!(
23///     "my scope",
24///     span = Span::detached(),
25///     std::thread::sleep(std::time::Duration::from_secs(1)),
26/// );
27///
28/// // With a scope name and no span.
29/// timed!(
30///     "my scope",
31///     std::thread::sleep(std::time::Duration::from_secs(1)),
32/// );
33/// ```
34#[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    /// Data that is initialized once per thread.
48    static THREAD_DATA: ThreadData = ThreadData {
49        id: {
50            // We only need atomicity and no synchronization of other
51            // operations, so `Relaxed` is fine.
52            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
60/// Whether the timer is enabled. Defaults to `false`.
61static ENABLED: AtomicBool = AtomicBool::new(false);
62
63/// The list of collected events.
64static EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
65
66/// Enable the timer.
67#[inline]
68pub fn enable() {
69    // We only need atomicity and no synchronization of other
70    // operations, so `Relaxed` is fine.
71    ENABLED.store(true, Ordering::Relaxed);
72}
73
74/// Whether the timer is enabled.
75#[inline]
76pub fn is_enabled() -> bool {
77    ENABLED.load(Ordering::Relaxed)
78}
79
80/// Clears the recorded events.
81#[inline]
82pub fn clear() {
83    EVENTS.lock().clear();
84}
85
86/// Export data as JSON for Chrome's tracing tool.
87///
88/// The `source` function is called for each span to get the source code
89/// location of the span. The first element of the tuple is the file path and
90/// the second element is the line number.
91pub 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
141/// A scope that records an event when it is dropped.
142pub struct TimingScope {
143    name: &'static str,
144    span: Option<NonZeroU64>,
145    thread_id: u64,
146}
147
148impl TimingScope {
149    /// Create a new scope if timing is enabled.
150    #[inline]
151    pub fn new(name: &'static str) -> Option<Self> {
152        Self::with_span(name, None)
153    }
154
155    /// Create a new scope with a span if timing is enabled.
156    ///
157    /// The span is a raw number because `typst-timing` can't depend on
158    /// `typst-syntax` (or else `typst-syntax` couldn't depend on
159    /// `typst-timing`).
160    #[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    /// Create a new scope without checking if timing is enabled.
169    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
196/// An event that has been recorded.
197struct Event {
198    /// Whether this is a start or end event.
199    kind: EventKind,
200    /// The time at which this event occurred.
201    timestamp: Timestamp,
202    /// The name of this event.
203    name: &'static str,
204    /// The raw value of the span of code that this event was recorded in.
205    span: Option<NonZeroU64>,
206    /// The thread ID of this event.
207    thread_id: u64,
208}
209
210/// Whether an event marks the start or end of a scope.
211#[derive(Debug, Clone, Copy, Eq, PartialEq)]
212enum EventKind {
213    Start,
214    End,
215}
216
217/// A cross-platform way to get the current time.
218#[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
261/// Per-thread data.
262struct ThreadData {
263    /// The thread's ID.
264    ///
265    /// In contrast to `std::thread::current().id()`, this is wasm-compatible
266    /// and also a bit cheaper to access because the std version does a bit more
267    /// stuff (including cloning an `Arc`).
268    id: u64,
269    /// A way to get the time from WebAssembly.
270    #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
271    timer: WasmTimer,
272}
273
274/// A way to get the time from WebAssembly.
275#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
276struct WasmTimer {
277    /// The cached JS performance handle for the thread.
278    perf: web_sys::Performance,
279    /// The cached JS time origin.
280    time_origin: f64,
281}
282
283#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
284impl WasmTimer {
285    fn new() -> Self {
286        // Retrieve `performance` from global object, either the window or
287        // globalThis.
288        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        // Every thread gets its own time origin. To make the results consistent
300        // across threads, we need to add this to each `now()` call.
301        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}