diff options
Diffstat (limited to 'components/servo_tracing/lib.rs')
-rw-r--r-- | components/servo_tracing/lib.rs | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/components/servo_tracing/lib.rs b/components/servo_tracing/lib.rs new file mode 100644 index 00000000000..04e87ee6cc0 --- /dev/null +++ b/components/servo_tracing/lib.rs @@ -0,0 +1,394 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::Punct; +use quote::{ToTokens, TokenStreamExt, quote}; +use syn::parse::{Parse, Parser}; +use syn::punctuated::Punctuated; +use syn::token::Comma; +use syn::{Expr, ItemFn, Meta, MetaList, Token, parse_quote, parse2}; + +struct Fields(MetaList); +impl From<MetaList> for Fields { + fn from(value: MetaList) -> Self { + Fields(value) + } +} + +impl Fields { + fn create_with_servo_profiling() -> Self { + Fields(parse_quote! { fields(servo_profiling = true) }) + } + + fn inject_servo_profiling(&mut self) -> syn::Result<()> { + let metalist = std::mem::replace(&mut self.0, parse_quote! {field()}); + + let arguments: Punctuated<Meta, Comma> = + Punctuated::parse_terminated.parse2(metalist.tokens)?; + + let servo_profile_given = arguments + .iter() + .any(|arg| arg.path().is_ident("servo_profiling")); + + let metalist = if servo_profile_given { + parse_quote! { + fields(#arguments) + } + } else { + parse_quote! { + fields(servo_profiling=true, #arguments) + } + }; + + let _ = std::mem::replace(&mut self.0, metalist); + + Ok(()) + } +} + +impl ToTokens for Fields { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let items = &self.0; + tokens.append_all(quote! { #items }); + } +} +enum Directive { + Passthrough(Meta), + Level(Expr), + Fields(Fields), +} + +impl From<Fields> for Directive { + fn from(value: Fields) -> Self { + Directive::Fields(value) + } +} + +impl Directive { + fn is_level(&self) -> bool { + matches!(self, Directive::Level(..)) + } + + fn fields_mut(&mut self) -> Option<&mut Fields> { + match self { + Directive::Fields(fields) => Some(fields), + _ => None, + } + } +} + +impl ToTokens for Directive { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Directive::Passthrough(meta) => tokens.append_all(quote! { #meta }), + Directive::Level(level) => tokens.append_all(quote! { level = #level }), + Directive::Fields(fields) => tokens.append_all(quote! { #fields }), + }; + } +} + +impl ToTokens for InstrumentConfiguration { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.append_terminated(&self.0, Punct::new(',', proc_macro2::Spacing::Joint)); + } +} + +struct InstrumentConfiguration(Vec<Directive>); + +impl InstrumentConfiguration { + fn inject_servo_profiling(&mut self) -> syn::Result<()> { + let fields = self.0.iter_mut().find_map(Directive::fields_mut); + match fields { + None => { + self.0 + .push(Directive::from(Fields::create_with_servo_profiling())); + Ok(()) + }, + Some(fields) => fields.inject_servo_profiling(), + } + } + + fn inject_level(&mut self) { + if self.0.iter().any(|a| a.is_level()) { + return; + } + self.0.push(Directive::Level(parse_quote! { "trace" })); + } +} + +impl Parse for InstrumentConfiguration { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?; + let mut components = vec![]; + + for arg in args { + match arg { + Meta::List(meta_list) if meta_list.path.is_ident("fields") => { + components.push(Directive::Fields(meta_list.into())); + }, + Meta::NameValue(meta_name_value) if meta_name_value.path.is_ident("level") => { + components.push(Directive::Level(meta_name_value.value)); + }, + _ => { + components.push(Directive::Passthrough(arg)); + }, + } + } + Ok(InstrumentConfiguration(components)) + } +} + +fn instrument_internal( + attr: proc_macro2::TokenStream, + item: proc_macro2::TokenStream, +) -> syn::Result<proc_macro2::TokenStream> { + // Prepare passthrough arguments for tracing::instrument + let mut configuration: InstrumentConfiguration = parse2(attr)?; + let input_fn: ItemFn = parse2(item)?; + + configuration.inject_servo_profiling()?; + configuration.inject_level(); + + let output = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + #configuration + ) + )] + #input_fn + }; + + Ok(output) +} + +#[proc_macro_attribute] +/// Instruments a function with some sane defaults by automatically: +/// - setting the attribute behind the "tracing" flag +/// - adding `servo_profiling = true` in the `tracing::instrument(fields(...))` argument. +/// - setting `level = "trace"` if it is not given. +/// +/// This macro assumes the consuming crate has a `tracing` feature flag. +/// +/// We need to be able to set the following +/// ``` +/// #[cfg_attr( +/// feature = "tracing", +/// tracing::instrument( +/// name = "MyCustomName", +/// skip_all, +/// fields(servo_profiling = true), +/// level = "trace", +/// ) +/// )] +/// fn my_fn() { /* .... */ } +/// ``` +/// from a simpler macro, such as: +/// +/// ``` +/// #[servo_tracing::instrument(name = "MyCustomName", skip_all)] +/// fn my_fn() { /* .... */ } +/// ``` +pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream { + match instrument_internal(attr.into(), item.into()) { + Ok(stream) => stream.into(), + Err(err) => err.to_compile_error().into(), + } +} + +#[cfg(test)] +mod test { + use proc_macro2::TokenStream; + use quote::{ToTokens, quote}; + use syn::{Attribute, ItemFn}; + + use crate::instrument_internal; + + fn extract_instrument_attribute(item_fn: &mut ItemFn) -> TokenStream { + let attr: &Attribute = item_fn + .attrs + .iter() + .find(|attr| { + // because this is a very nested structure, it is easier to check + // by constructing the full path, and then doing a string comparison. + let p = attr.path().to_token_stream().to_string(); + p == "servo_tracing :: instrument" + }) + .expect("Attribute `servo_tracing::instrument` not found"); + + // we create a tokenstream of the actual internal contents of the attribute + let attr_args = attr + .parse_args::<TokenStream>() + .expect("Failed to parse attribute args"); + + // we remove the tracing attribute, this is to avoid passing it as an actual attribute to itself. + item_fn.attrs.retain(|attr| { + attr.path().to_token_stream().to_string() != "servo_tracing :: instrument" + }); + + attr_args + } + + /// To make test case generation easy, we parse a test_case as a function item + /// with its own attributes, including [`servo_tracing::instrument`]. + /// + /// We extract the [`servo_tracing::instrument`] attribute, and pass it as the first argument to + /// [`servo_tracing::instrument_internal`], + fn evaluate(function: TokenStream, test_case: TokenStream, expected: TokenStream) { + let test_case = quote! { + #test_case + #function + }; + let expected = quote! { + #expected + #function + }; + let function_str = function.to_string(); + let function_str = syn::parse_file(&function_str).expect("function to have valid syntax"); + let function_str = prettyplease::unparse(&function_str); + + let mut item_fn: ItemFn = + syn::parse2(test_case).expect("Failed to parse input as function"); + + let attr_args = extract_instrument_attribute(&mut item_fn); + let item_fn = item_fn.to_token_stream(); + + let generated = instrument_internal(attr_args, item_fn).expect("Generation to not fail."); + + let generated = syn::parse_file(generated.to_string().as_str()) + .expect("to have generated a valid function"); + let generated = prettyplease::unparse(&generated); + let expected = syn::parse_file(expected.to_string().as_str()) + .expect("to have been given a valid expected function"); + let expected = prettyplease::unparse(&expected); + + eprintln!( + "Generated:---------:\n{}--------\nExpected:----------\n{}", + &generated, &expected + ); + assert_eq!(generated, expected); + assert!( + generated.contains(&function_str), + "Expected generated code: {generated} to contain the function code: {function_str}" + ); + } + + fn function1() -> TokenStream { + quote! { + pub fn start( + state: (), + layout_factory: (), + random_pipeline_closure_probability: (), + random_pipeline_closure_seed: (), + hard_fail: (), + canvas_create_sender: (), + canvas_ipc_sender: (), + ) { + } + } + } + + fn function2() -> TokenStream { + quote! { + fn layout( + mut self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block_for_children: &ContainingBlock, + containing_block_for_table: &ContainingBlock, + depends_on_block_constraints: bool, + ) { + } + } + } + + #[test] + fn passing_servo_profiling_and_level_and_aux() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip(state, layout_factory), fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(skip(state, layout_factory),fields(servo_profiling = true),level = "trace",)] + }; + + evaluate(function, test_case, expected); + } + + #[test] + fn passing_servo_profiling_and_level() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(fields(servo_profiling = true),level = "trace",)] + }; + evaluate(function, test_case, expected); + } + + #[test] + fn passing_servo_profiling() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(fields(servo_profiling = true))] + }; + evaluate(function, test_case, expected); + } + + #[test] + fn inject_level_and_servo_profiling() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument(fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument()] + }; + evaluate(function, test_case, expected); + } + + #[test] + fn instrument_with_name() { + let function = function2(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "Table::layout", + skip_all, + fields(servo_profiling = true), + level = "trace", + ) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(name="Table::layout", skip_all)] + }; + + evaluate(function, test_case, expected); + } +} |