servo_tracing/
lib.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use proc_macro2::Punct;
8use quote::{ToTokens, TokenStreamExt, quote};
9use syn::parse::{Parse, Parser};
10use syn::punctuated::Punctuated;
11use syn::token::Comma;
12use syn::{Expr, ItemFn, Meta, MetaList, Token, parse_quote, parse2};
13
14struct Fields(MetaList);
15impl From<MetaList> for Fields {
16    fn from(value: MetaList) -> Self {
17        Fields(value)
18    }
19}
20
21impl Fields {
22    fn create_with_servo_profiling() -> Self {
23        Fields(parse_quote! { fields(servo_profiling = true) })
24    }
25
26    fn inject_servo_profiling(&mut self) -> syn::Result<()> {
27        let metalist = std::mem::replace(&mut self.0, parse_quote! {field()});
28
29        let arguments: Punctuated<Meta, Comma> =
30            Punctuated::parse_terminated.parse2(metalist.tokens)?;
31
32        let servo_profile_given = arguments
33            .iter()
34            .any(|arg| arg.path().is_ident("servo_profiling"));
35
36        let metalist = if servo_profile_given {
37            parse_quote! {
38                fields(#arguments)
39            }
40        } else {
41            parse_quote! {
42                fields(servo_profiling=true, #arguments)
43            }
44        };
45
46        let _ = std::mem::replace(&mut self.0, metalist);
47
48        Ok(())
49    }
50}
51
52impl ToTokens for Fields {
53    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
54        let items = &self.0;
55        tokens.append_all(quote! { #items });
56    }
57}
58enum Directive {
59    Passthrough(Meta),
60    Level(Expr),
61    Fields(Fields),
62}
63
64impl From<Fields> for Directive {
65    fn from(value: Fields) -> Self {
66        Directive::Fields(value)
67    }
68}
69
70impl Directive {
71    fn is_level(&self) -> bool {
72        matches!(self, Directive::Level(..))
73    }
74
75    fn fields_mut(&mut self) -> Option<&mut Fields> {
76        match self {
77            Directive::Fields(fields) => Some(fields),
78            _ => None,
79        }
80    }
81}
82
83impl ToTokens for Directive {
84    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
85        match self {
86            Directive::Passthrough(meta) => tokens.append_all(quote! { #meta }),
87            Directive::Level(level) => tokens.append_all(quote! { level = #level }),
88            Directive::Fields(fields) => tokens.append_all(quote! { #fields }),
89        };
90    }
91}
92
93impl ToTokens for InstrumentConfiguration {
94    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
95        tokens.append_terminated(&self.0, Punct::new(',', proc_macro2::Spacing::Joint));
96    }
97}
98
99struct InstrumentConfiguration(Vec<Directive>);
100
101impl InstrumentConfiguration {
102    fn inject_servo_profiling(&mut self) -> syn::Result<()> {
103        let fields = self.0.iter_mut().find_map(Directive::fields_mut);
104        match fields {
105            None => {
106                self.0
107                    .push(Directive::from(Fields::create_with_servo_profiling()));
108                Ok(())
109            },
110            Some(fields) => fields.inject_servo_profiling(),
111        }
112    }
113
114    fn inject_level(&mut self) {
115        if self.0.iter().any(|a| a.is_level()) {
116            return;
117        }
118        self.0.push(Directive::Level(parse_quote! { "trace" }));
119    }
120}
121
122impl Parse for InstrumentConfiguration {
123    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
124        let args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
125        let mut components = vec![];
126
127        for arg in args {
128            match arg {
129                Meta::List(meta_list) if meta_list.path.is_ident("fields") => {
130                    components.push(Directive::Fields(meta_list.into()));
131                },
132                Meta::NameValue(meta_name_value) if meta_name_value.path.is_ident("level") => {
133                    components.push(Directive::Level(meta_name_value.value));
134                },
135                _ => {
136                    components.push(Directive::Passthrough(arg));
137                },
138            }
139        }
140        Ok(InstrumentConfiguration(components))
141    }
142}
143
144fn instrument_internal(
145    attr: proc_macro2::TokenStream,
146    item: proc_macro2::TokenStream,
147) -> syn::Result<proc_macro2::TokenStream> {
148    // Prepare passthrough arguments for tracing::instrument
149    let mut configuration: InstrumentConfiguration = parse2(attr)?;
150    let input_fn: ItemFn = parse2(item)?;
151
152    configuration.inject_servo_profiling()?;
153    configuration.inject_level();
154
155    let output = quote! {
156        #[cfg_attr(
157            feature = "tracing",
158            tracing::instrument(
159                #configuration
160            )
161        )]
162        #input_fn
163    };
164
165    Ok(output)
166}
167
168#[proc_macro_attribute]
169/// Instruments a function with some sane defaults by automatically:
170///  - setting the attribute behind the "tracing" flag
171///  - adding `servo_profiling = true` in the `tracing::instrument(fields(...))` argument.
172///  - setting `level = "trace"` if it is not given.
173///
174/// This macro assumes the consuming crate has a `tracing` feature flag.
175///
176/// We need to be able to set the following
177/// ```
178/// #[cfg_attr(
179///         feature = "tracing",
180///         tracing::instrument(
181///             name = "MyCustomName",
182///             skip_all,
183///             fields(servo_profiling = true),
184///             level = "trace",
185///         )
186///     )]
187/// fn my_fn() { /* .... */ }
188/// ```
189/// from a simpler macro, such as:
190///
191/// ```
192/// #[servo_tracing::instrument(name = "MyCustomName", skip_all)]
193/// fn my_fn() { /* .... */ }
194/// ```
195pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream {
196    match instrument_internal(attr.into(), item.into()) {
197        Ok(stream) => stream.into(),
198        Err(err) => err.to_compile_error().into(),
199    }
200}
201
202#[cfg(test)]
203mod test {
204    use proc_macro2::TokenStream;
205    use quote::{ToTokens, quote};
206    use syn::{Attribute, ItemFn};
207
208    use crate::instrument_internal;
209
210    fn extract_instrument_attribute(item_fn: &mut ItemFn) -> TokenStream {
211        let attr: &Attribute = item_fn
212            .attrs
213            .iter()
214            .find(|attr| {
215                // because this is a very nested structure, it is easier to check
216                // by constructing the full path, and then doing a string comparison.
217                let p = attr.path().to_token_stream().to_string();
218                p == "servo_tracing :: instrument"
219            })
220            .expect("Attribute `servo_tracing::instrument` not found");
221
222        // we create a tokenstream of the actual internal contents of the attribute
223        let attr_args = attr
224            .parse_args::<TokenStream>()
225            .expect("Failed to parse attribute args");
226
227        // we remove the tracing attribute, this is to avoid passing it as an actual attribute to itself.
228        item_fn.attrs.retain(|attr| {
229            attr.path().to_token_stream().to_string() != "servo_tracing :: instrument"
230        });
231
232        attr_args
233    }
234
235    /// To make test case generation easy, we parse a test_case as a function item
236    /// with its own attributes, including [`servo_tracing::instrument`].
237    ///
238    /// We extract the [`servo_tracing::instrument`] attribute, and pass it as the first argument to
239    /// [`servo_tracing::instrument_internal`],
240    fn evaluate(function: TokenStream, test_case: TokenStream, expected: TokenStream) {
241        let test_case = quote! {
242            #test_case
243            #function
244        };
245        let expected = quote! {
246            #expected
247            #function
248        };
249        let function_str = function.to_string();
250        let function_str = syn::parse_file(&function_str).expect("function to have valid syntax");
251        let function_str = prettyplease::unparse(&function_str);
252
253        let mut item_fn: ItemFn =
254            syn::parse2(test_case).expect("Failed to parse input as function");
255
256        let attr_args = extract_instrument_attribute(&mut item_fn);
257        let item_fn = item_fn.to_token_stream();
258
259        let generated = instrument_internal(attr_args, item_fn).expect("Generation to not fail.");
260
261        let generated = syn::parse_file(generated.to_string().as_str())
262            .expect("to have generated a valid function");
263        let generated = prettyplease::unparse(&generated);
264        let expected = syn::parse_file(expected.to_string().as_str())
265            .expect("to have been given a valid expected function");
266        let expected = prettyplease::unparse(&expected);
267
268        eprintln!(
269            "Generated:---------:\n{}--------\nExpected:----------\n{}",
270            &generated, &expected
271        );
272        assert_eq!(generated, expected);
273        assert!(
274            generated.contains(&function_str),
275            "Expected generated code: {generated} to contain the function code: {function_str}"
276        );
277    }
278
279    fn function1() -> TokenStream {
280        quote! {
281            pub fn start(
282                state: (),
283                layout_factory: (),
284                random_pipeline_closure_probability: (),
285                random_pipeline_closure_seed: (),
286                hard_fail: (),
287                canvas_create_sender: (),
288                canvas_ipc_sender: (),
289            ) {
290            }
291        }
292    }
293
294    fn function2() -> TokenStream {
295        quote! {
296            fn layout(
297                mut self,
298                layout_context: &LayoutContext,
299                positioning_context: &mut PositioningContext,
300                containing_block_for_children: &ContainingBlock,
301                containing_block_for_table: &ContainingBlock,
302                depends_on_block_constraints: bool,
303            ) {
304            }
305        }
306    }
307
308    #[test]
309    fn passing_servo_profiling_and_level_and_aux() {
310        let function = function1();
311        let expected = quote! {
312            #[cfg_attr(
313                feature = "tracing",
314                tracing::instrument(skip(state, layout_factory), fields(servo_profiling = true), level = "trace",)
315            )]
316        };
317
318        let test_case = quote! {
319            #[servo_tracing::instrument(skip(state, layout_factory),fields(servo_profiling = true),level = "trace",)]
320        };
321
322        evaluate(function, test_case, expected);
323    }
324
325    #[test]
326    fn passing_servo_profiling_and_level() {
327        let function = function1();
328        let expected = quote! {
329            #[cfg_attr(
330                feature = "tracing",
331                tracing::instrument( fields(servo_profiling = true), level = "trace",)
332            )]
333        };
334
335        let test_case = quote! {
336            #[servo_tracing::instrument(fields(servo_profiling = true),level = "trace",)]
337        };
338        evaluate(function, test_case, expected);
339    }
340
341    #[test]
342    fn passing_servo_profiling() {
343        let function = function1();
344        let expected = quote! {
345            #[cfg_attr(
346                feature = "tracing",
347                tracing::instrument( fields(servo_profiling = true), level = "trace",)
348            )]
349        };
350
351        let test_case = quote! {
352            #[servo_tracing::instrument(fields(servo_profiling = true))]
353        };
354        evaluate(function, test_case, expected);
355    }
356
357    #[test]
358    fn inject_level_and_servo_profiling() {
359        let function = function1();
360        let expected = quote! {
361            #[cfg_attr(
362                feature = "tracing",
363                tracing::instrument(fields(servo_profiling = true), level = "trace",)
364            )]
365        };
366
367        let test_case = quote! {
368            #[servo_tracing::instrument()]
369        };
370        evaluate(function, test_case, expected);
371    }
372
373    #[test]
374    fn instrument_with_name() {
375        let function = function2();
376        let expected = quote! {
377            #[cfg_attr(
378                feature = "tracing",
379                tracing::instrument(
380                    name = "Table::layout",
381                    skip_all,
382                    fields(servo_profiling = true),
383                    level = "trace",
384                )
385            )]
386        };
387
388        let test_case = quote! {
389            #[servo_tracing::instrument(name="Table::layout", skip_all)]
390        };
391
392        evaluate(function, test_case, expected);
393    }
394}