Skip to main content

typst_kit/
timer.rs

1//! Recording and writing of performance timing files.
2//!
3//! This can be used to record performance events via [`typst_timing`] and write
4//! them to disk.
5
6#![cfg(feature = "timer")]
7
8use std::fs::File;
9use std::io::BufWriter;
10use std::path::{Path, PathBuf};
11
12use typst_library::World;
13use typst_library::diag::{StrResult, bail};
14use typst_syntax::{Span, SpanKind};
15
16/// Allows to record timings of function executions.
17pub struct Timer {
18    /// Where to save the recorded timings of each compilation step.
19    path: Option<PathBuf>,
20    /// The current watch iteration.
21    iter: usize,
22}
23
24impl Timer {
25    /// Creates a timer that can be used to record timings for a specific
26    /// function invocation.
27    ///
28    /// Will also internally enable event collection in [`typst_timing`].
29    ///
30    /// If the path contains the string `{n}`, it is replaced with a per
31    /// recording index. If recording multiple events, the path _must_ contain
32    /// this string.
33    pub fn new(path: PathBuf) -> Self {
34        // Enable event collection.
35        typst_timing::enable();
36        Self { path: Some(path), iter: 0 }
37    }
38
39    /// Returns a placeholder that does not record any actual timings. This can
40    /// be useful to have uniform code paths with `timer.record` regardless of
41    /// whether timings are enabled at runtime.
42    pub fn placeholder() -> Self {
43        Self { path: None, iter: 0 }
44    }
45
46    /// Creates a proper timer if the `path` is `Some(_)` or a placeholder timer
47    /// if the path is `None`.
48    pub fn new_or_placeholder(path: Option<PathBuf>) -> Self {
49        match path {
50            Some(path) => Self::new(path),
51            None => Self::placeholder(),
52        }
53    }
54
55    /// Records all timings in `f` and writes them to disk as JSON compatible
56    /// with Chrome's tracing tool.
57    pub fn record<W: World, T>(
58        &mut self,
59        world: &mut W,
60        f: impl FnOnce(&mut W) -> T,
61    ) -> StrResult<T> {
62        let Some(path) = &self.path else {
63            return Ok(f(world));
64        };
65
66        typst_timing::clear();
67
68        let string = path.to_str().unwrap_or_default();
69        let numbered = string.contains("{n}");
70        if !numbered && self.iter > 0 {
71            bail!("cannot export multiple recordings without `{{n}}` in path");
72        }
73
74        let storage;
75        let path = if numbered {
76            storage = string.replace("{n}", &self.iter.to_string());
77            Path::new(&storage)
78        } else {
79            path.as_path()
80        };
81
82        let output = f(world);
83        self.iter += 1;
84
85        let file =
86            File::create(path).map_err(|e| format!("failed to create file: {e}"))?;
87        let writer = BufWriter::with_capacity(1 << 20, file);
88
89        typst_timing::export_json(writer, |span| {
90            resolve_span(world, Span::from_raw(span))
91                .unwrap_or_else(|| ("unknown".to_string(), 0))
92        })?;
93
94        Ok(output)
95    }
96}
97
98/// Turns a span into a (file, line) pair.
99fn resolve_span<W: World>(world: &W, span: Span) -> Option<(String, u32)> {
100    let (id, line) = match span.get() {
101        SpanKind::Detached => return None,
102        SpanKind::Number { id, num } => {
103            let source = world.source(id).ok()?;
104            let range = source.range(num, None)?;
105            let line = source.lines().byte_to_line(range.start)?;
106            (id, line)
107        }
108        SpanKind::Range { id, range } => {
109            let file = world.file(id).ok()?;
110            let lines = file.lines().ok()?;
111            let line = lines.byte_to_line(range.start)?;
112            (id, line)
113        }
114    };
115    Some((format!("{id:?}"), line as u32 + 1))
116}