Compare commits

...

7 Commits

7 changed files with 83 additions and 41 deletions

2
Cargo.lock generated
View File

@ -3247,7 +3247,7 @@ dependencies = [
name = "typst-timing" name = "typst-timing"
version = "0.13.1" version = "0.13.1"
dependencies = [ dependencies = [
"indexmap 2.7.1", "ecow",
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -46,7 +46,7 @@ impl i64 {
/// or smaller than the minimum 64-bit signed integer. /// or smaller than the minimum 64-bit signed integer.
/// ///
/// - Booleans are converted to `0` or `1`. /// - Booleans are converted to `0` or `1`.
/// - Floats and decimals are truncated to the next 64-bit integer. /// - Floats and decimals are rounded to the next 64-bit integer towards zero.
/// - Strings are parsed in base 10. /// - Strings are parsed in base 10.
/// ///
/// ```example /// ```example

View File

@ -20,8 +20,8 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// A mathematical equation. /// A mathematical equation.
/// ///
/// Can be displayed inline with text or as a separate block. An equation /// Can be displayed inline with text or as a separate block. An equation
/// becomes block-level through the presence of at least one space after the /// becomes block-level through the presence of whitespace after the opening
/// opening dollar sign and one space before the closing dollar sign. /// dollar sign and whitespace before the closing dollar sign.
/// ///
/// # Example /// # Example
/// ```example /// ```example
@ -41,9 +41,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// ///
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: Write mathematical markup within /// This function also has dedicated syntax: Write mathematical markup within
/// dollar signs to create an equation. Starting and ending the equation with at /// dollar signs to create an equation. Starting and ending the equation with
/// least one space lifts it into a separate block that is centered /// whitespace lifts it into a separate block that is centered horizontally.
/// horizontally. For more details about math syntax, see the /// For more details about math syntax, see the
/// [main math page]($category/math). /// [main math page]($category/math).
#[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)] #[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)]
pub struct EquationElem { pub struct EquationElem {

View File

@ -92,7 +92,7 @@ pub(super) fn define(global: &mut Scope) {
/// ``` /// ```
#[elem(Debug, Construct, PlainText, Repr)] #[elem(Debug, Construct, PlainText, Repr)]
pub struct TextElem { pub struct TextElem {
/// A font family descriptor or priority list of font family descriptor. /// A font family descriptor or priority list of font family descriptors.
/// ///
/// A font family descriptor can be a plain string representing the family /// A font family descriptor can be a plain string representing the family
/// name or a dictionary with the following keys: /// name or a dictionary with the following keys:

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens}; use quote::{format_ident, quote, ToTokens};
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
@ -17,12 +19,12 @@ pub struct Meta {
pub span: Option<syn::Expr>, pub span: Option<syn::Expr>,
pub callsite: Option<syn::Expr>, pub callsite: Option<syn::Expr>,
pub func: Option<syn::Expr>, pub func: Option<syn::Expr>,
pub extras: Vec<(String, Mode, syn::Expr)>, pub extras: Vec<(syn::Ident, Mode, syn::Expr)>,
} }
impl Parse for Meta { impl Parse for Meta {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
Ok(Self { let out = Self {
name: parse_string::<kw::name>(input)?, name: parse_string::<kw::name>(input)?,
span: parse_key_value::<kw::span, syn::Expr>(input)?, span: parse_key_value::<kw::span, syn::Expr>(input)?,
callsite: parse_key_value::<kw::callsite, syn::Expr>(input)?, callsite: parse_key_value::<kw::callsite, syn::Expr>(input)?,
@ -39,11 +41,26 @@ impl Parse for Meta {
let value = input.parse()?; let value = input.parse()?;
eat_comma(input); eat_comma(input);
pairs.push((key.to_string(), mode, value)); pairs.push((key, mode, value));
} }
pairs pairs
}, },
}) };
let mut keys = HashSet::new();
keys.insert("name".to_string());
keys.insert("span".to_string());
keys.insert("callsite".to_string());
keys.insert("func".to_string());
// Check that the keys are unique.
for (key, _, _) in &out.extras {
if !keys.insert(key.to_string()) {
bail!(key, "Duplicate key in #[time(..)]: `{}`", key);
}
}
Ok(out)
} }
} }
@ -98,6 +115,7 @@ fn create(meta: Meta, mut item: syn::ItemFn) -> Result<TokenStream> {
Mode::Serialize => (format_ident!("with_arg"), None), Mode::Serialize => (format_ident!("with_arg"), None),
}; };
let key = key.to_string();
extras.push(quote! { .#method(#key, (#value) #transform) }); extras.push(quote! { .#method(#key, (#value) #transform) });
if matches!(mode, Mode::Serialize) { if matches!(mode, Mode::Serialize) {
let error_msg = format!("failed to serialize {key}"); let error_msg = format!("failed to serialize {key}");

View File

@ -13,7 +13,7 @@ keywords = { workspace = true }
readme = { workspace = true } readme = { workspace = true }
[dependencies] [dependencies]
indexmap = { workspace = true } ecow = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }

View File

@ -6,11 +6,10 @@ use std::io::Write;
use std::num::NonZeroU64; use std::num::NonZeroU64;
use std::ops::Not; use std::ops::Not;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use indexmap::IndexMap; use ecow::EcoVec;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::ser::SerializeSeq; use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
/// Creates a timing scope around an expression. /// Creates a timing scope around an expression.
@ -105,8 +104,11 @@ pub fn export_json<W: Write>(
ts: f64, ts: f64,
pid: u64, pid: u64,
tid: u64, tid: u64,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
args: Option<IndexMap<Cow<'a, str>, Cow<'a, serde_json::Value>>>, skip_serializing_if = "Option::is_none",
serialize_with = "serialize_vec_as_map"
)]
args: Option<EcoVec<(Cow<'a, str>, Cow<'a, serde_json::Value>)>>,
} }
let lock = EVENTS.lock(); let lock = EVENTS.lock();
@ -118,9 +120,9 @@ pub fn export_json<W: Write>(
.map_err(|e| format!("failed to serialize events: {e}"))?; .map_err(|e| format!("failed to serialize events: {e}"))?;
for event in events.iter() { for event in events.iter() {
let mut args = IndexMap::new(); let mut args = EcoVec::with_capacity(event.arguments.len() * 2 + 1);
if let Some(func) = event.func.as_ref() { if let Some(func) = event.func.as_ref() {
args.insert("func".into(), Cow::Owned(serde_json::json!(func))); args.push(("func".into(), Cow::Owned(serde_json::json!(func))));
} }
for (key, arg) in event.arguments.iter() { for (key, arg) in event.arguments.iter() {
@ -143,7 +145,7 @@ pub fn export_json<W: Write>(
.map_err(|e| format!("failed to serialize event: {e}"))?; .map_err(|e| format!("failed to serialize event: {e}"))?;
} }
seq.end().map_err(|e| format!("failed to serialize events: {e}"))?; SerializeSeq::end(seq).map_err(|e| format!("failed to serialize events: {e}"))?;
Ok(()) Ok(())
} }
@ -153,7 +155,7 @@ pub fn export_json<W: Write>(
pub struct TimingScope { pub struct TimingScope {
name: &'static str, name: &'static str,
func: Option<String>, func: Option<String>,
args: IndexMap<&'static str, EventArgument>, args: EcoVec<(&'static str, EventArgument)>,
} }
impl TimingScope { impl TimingScope {
@ -161,7 +163,7 @@ impl TimingScope {
#[inline] #[inline]
pub fn new(name: &'static str) -> Option<Self> { pub fn new(name: &'static str) -> Option<Self> {
if is_enabled() { if is_enabled() {
Some(Self { name, func: None, args: IndexMap::new() }) Some(Self { name, func: None, args: EcoVec::new() })
} else { } else {
None None
} }
@ -173,17 +175,17 @@ impl TimingScope {
} }
pub fn with_span(mut self, span: NonZeroU64) -> Self { pub fn with_span(mut self, span: NonZeroU64) -> Self {
self.args.insert("span", EventArgument::Span(span)); self.args.push(("span", EventArgument::Span(span)));
self self
} }
pub fn with_callsite(mut self, callsite: NonZeroU64) -> Self { pub fn with_callsite(mut self, callsite: NonZeroU64) -> Self {
self.args.insert("callsite", EventArgument::Span(callsite)); self.args.push(("callsite", EventArgument::Span(callsite)));
self self
} }
pub fn with_named_span(mut self, name: &'static str, span: NonZeroU64) -> Self { pub fn with_named_span(mut self, name: &'static str, span: NonZeroU64) -> Self {
self.args.insert(name, EventArgument::Span(span)); self.args.push((name, EventArgument::Span(span)));
self self
} }
@ -203,7 +205,7 @@ impl TimingScope {
value: impl Serialize, value: impl Serialize,
) -> Result<Self, serde_json::Error> { ) -> Result<Self, serde_json::Error> {
let value = serde_json::to_value(value)?; let value = serde_json::to_value(value)?;
self.args.insert(arg, EventArgument::Value(value)); self.args.push((arg, EventArgument::Value(value)));
Ok(self) Ok(self)
} }
@ -217,7 +219,7 @@ impl TimingScope {
name: self.name, name: self.name,
func: self.func.clone(), func: self.func.clone(),
thread_id, thread_id,
arguments: Arc::new(self.args), arguments: self.args.clone(),
}; };
EVENTS.lock().push(event.clone()); EVENTS.lock().push(event.clone());
TimingScopeGuard { scope: Some(event) } TimingScopeGuard { scope: Some(event) }
@ -240,6 +242,7 @@ impl Drop for TimingScopeGuard {
} }
} }
#[derive(Clone)]
enum EventArgument { enum EventArgument {
Span(NonZeroU64), Span(NonZeroU64),
Value(serde_json::Value), Value(serde_json::Value),
@ -250,45 +253,45 @@ impl EventArgument {
&'a self, &'a self,
mut source: impl FnMut(NonZeroU64) -> (String, u32), mut source: impl FnMut(NonZeroU64) -> (String, u32),
key: &'static str, key: &'static str,
out: &mut IndexMap<Cow<'static, str>, Cow<'a, serde_json::Value>>, out: &mut EcoVec<(Cow<'static, str>, Cow<'a, serde_json::Value>)>,
) -> Result<(), serde_json::Error> { ) -> Result<(), serde_json::Error> {
match self { match self {
EventArgument::Span(span) => { EventArgument::Span(span) => {
let (file, line) = source(*span); let (file, line) = source(*span);
// Insert file and line information for the span // Insert file and line information for the span
if key == "span" { if key == "span" {
out.insert("file".into(), Cow::Owned(serde_json::json!(file))); out.push(("file".into(), Cow::Owned(serde_json::json!(file))));
out.insert("line".into(), Cow::Owned(serde_json::json!(line))); out.push(("line".into(), Cow::Owned(serde_json::json!(line))));
return Ok(()); return Ok(());
} }
// Small optimization for callsite // Small optimization for callsite
if key == "callsite" { if key == "callsite" {
out.insert( out.push((
"callsite_file".into(), "callsite_file".into(),
Cow::Owned(serde_json::json!(file)), Cow::Owned(serde_json::json!(file)),
); ));
out.insert( out.push((
"callsite_line".into(), "callsite_line".into(),
Cow::Owned(serde_json::json!(line)), Cow::Owned(serde_json::json!(line)),
); ));
return Ok(()); return Ok(());
} }
out.insert( out.push((
format!("{key}_file").into(), format!("{key}_file").into(),
Cow::Owned(serde_json::json!(file)), Cow::Owned(serde_json::json!(file)),
); ));
out.insert( out.push((
format!("{key}_line").into(), format!("{key}_line").into(),
Cow::Owned(serde_json::json!(line)), Cow::Owned(serde_json::json!(line)),
); ));
} }
EventArgument::Value(value) => { EventArgument::Value(value) => {
out.insert(key.into(), Cow::Borrowed(value)); out.push((key.into(), Cow::Borrowed(value)));
} }
} }
@ -306,7 +309,7 @@ struct Event {
/// The name of this event. /// The name of this event.
name: &'static str, name: &'static str,
/// The additional arguments of this event. /// The additional arguments of this event.
arguments: Arc<IndexMap<&'static str, EventArgument>>, arguments: EcoVec<(&'static str, EventArgument)>,
/// The function being called (if any). /// The function being called (if any).
func: Option<String>, func: Option<String>,
/// The thread ID of this event. /// The thread ID of this event.
@ -413,3 +416,24 @@ impl WasmTimer {
self.time_origin + self.perf.now() self.time_origin + self.perf.now()
} }
} }
// Custom serialization function for handling `EcoVec` as a map in JSON.
fn serialize_vec_as_map<S>(
data: &Option<EcoVec<(Cow<str>, Cow<serde_json::Value>)>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let Some(data) = data.as_deref() else {
// Should not happen, but if it does, we turn it into a `null` value.
return serializer.serialize_none();
};
let mut map = serializer.serialize_map(Some(data.len()))?;
for (key, value) in data {
map.serialize_entry(key, value)?;
}
map.end()
}