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/// Disable the timer.
75#[inline]
76pub fn disable() {
77    // We only need atomicity and no synchronization of other
78    // operations, so `Relaxed` is fine.
79    ENABLED.store(false, Ordering::Relaxed);
80}
81
82/// Whether the timer is enabled.
83#[inline]
84pub fn is_enabled() -> bool {
85    ENABLED.load(Ordering::Relaxed)
86}
87
88/// Clears the recorded events.
89#[inline]
90pub fn clear() {
91    EVENTS.lock().clear();
92}
93
94/// Export data as JSON for Chrome's tracing tool.
95///
96/// The `source` function is called for each span to get the source code
97/// location of the span. The first element of the tuple is the file path and
98/// the second element is the line number.
99pub 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
149/// A scope that records an event when it is dropped.
150pub struct TimingScope {
151    name: &'static str,
152    span: Option<NonZeroU64>,
153    thread_id: u64,
154}
155
156impl TimingScope {
157    /// Create a new scope if timing is enabled.
158    #[inline]
159    pub fn new(name: &'static str) -> Option<Self> {
160        Self::with_span(name, None)
161    }
162
163    /// Create a new scope with a span if timing is enabled.
164    ///
165    /// The span is a raw number because `typst-timing` can't depend on
166    /// `typst-syntax` (or else `typst-syntax` couldn't depend on
167    /// `typst-timing`).
168    #[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    /// Create a new scope without checking if timing is enabled.
177    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
204/// An event that has been recorded.
205struct Event {
206    /// Whether this is a start or end event.
207    kind: EventKind,
208    /// The time at which this event occurred.
209    timestamp: Timestamp,
210    /// The name of this event.
211    name: &'static str,
212    /// The raw value of the span of code that this event was recorded in.
213    span: Option<NonZeroU64>,
214    /// The thread ID of this event.
215    thread_id: u64,
216}
217
218/// Whether an event marks the start or end of a scope.
219#[derive(Debug, Copy, Clone, Eq, PartialEq)]
220enum EventKind {
221    Start,
222    End,
223}
224
225/// A cross-platform way to get the current time.
226#[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
269/// Per-thread data.
270struct ThreadData {
271    /// The thread's ID.
272    ///
273    /// In contrast to `std::thread::current().id()`, this is wasm-compatible
274    /// and also a bit cheaper to access because the std version does a bit more
275    /// stuff (including cloning an `Arc`).
276    id: u64,
277    /// A way to get the time from WebAssembly.
278    #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
279    timer: WasmTimer,
280}
281
282/// A way to get the time from WebAssembly.
283#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
284struct WasmTimer {
285    /// The cached JS performance handle for the thread.
286    perf: web_sys::Performance,
287    /// The cached JS time origin.
288    time_origin: f64,
289}
290
291#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
292impl WasmTimer {
293    fn new() -> Self {
294        // Retrieve `performance` from global object, either the window or
295        // globalThis.
296        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        // Every thread gets its own time origin. To make the results consistent
308        // across threads, we need to add this to each `now()` call.
309        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}