Skip to main content

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.
99///
100/// Will also internally clear the recorded events.
101pub 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    // We take out the events to avoid a potential deadlock if `source` happens
123    // to invoke the timed things.
124    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
152/// A scope that records an event when it is dropped.
153pub struct TimingScope {
154    name: &'static str,
155    span: Option<NonZeroU64>,
156    thread_id: u64,
157}
158
159impl TimingScope {
160    /// Create a new scope if timing is enabled.
161    #[inline]
162    pub fn new(name: &'static str) -> Option<Self> {
163        Self::with_span(name, None)
164    }
165
166    /// Create a new scope with a span if timing is enabled.
167    ///
168    /// The span is a raw number because `typst-timing` can't depend on
169    /// `typst-syntax` (or else `typst-syntax` couldn't depend on
170    /// `typst-timing`).
171    #[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    /// Create a new scope without checking if timing is enabled.
180    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
207/// An event that has been recorded.
208struct Event {
209    /// Whether this is a start or end event.
210    kind: EventKind,
211    /// The time at which this event occurred.
212    timestamp: Timestamp,
213    /// The name of this event.
214    name: &'static str,
215    /// The raw value of the span of code that this event was recorded in.
216    span: Option<NonZeroU64>,
217    /// The thread ID of this event.
218    thread_id: u64,
219}
220
221/// Whether an event marks the start or end of a scope.
222#[derive(Debug, Copy, Clone, Eq, PartialEq)]
223enum EventKind {
224    Start,
225    End,
226}
227
228/// A cross-platform way to get the current time.
229#[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
272/// Per-thread data.
273struct ThreadData {
274    /// The thread's ID.
275    ///
276    /// In contrast to `std::thread::current().id()`, this is wasm-compatible
277    /// and also a bit cheaper to access because the std version does a bit more
278    /// stuff (including cloning an `Arc`).
279    id: u64,
280    /// A way to get the time from WebAssembly.
281    #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
282    timer: WasmTimer,
283}
284
285/// A way to get the time from WebAssembly.
286#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
287struct WasmTimer {
288    /// The cached JS performance handle for the thread.
289    perf: web_sys::Performance,
290    /// The cached JS time origin.
291    time_origin: f64,
292}
293
294#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
295impl WasmTimer {
296    fn new() -> Self {
297        // Retrieve `performance` from global object, either the window or
298        // globalThis.
299        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        // Every thread gets its own time origin. To make the results consistent
311        // across threads, we need to add this to each `now()` call.
312        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}