/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/servo_tracing/lib.rs
457 строк
14 KB
Narfinger
tracing: Implement instrument_all macro (#42578)
13 июл 2026, 11:34
Не верифицирован
13 июл 2026, 11:34
a3749ac
Код
Авторство
О чём код?
/* 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::{Ident, Punct, Span}; use quote::{ToTokens, TokenStreamExt, quote}; use syn::parse::{Parse, Parser}; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{ Expr, ImplItem, ImplItemFn, Item, ItemFn, Meta, MetaList, Token, parse_macro_input, 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(), } } #[proc_macro_attribute] /// This is only useful for `#[servo_tracing::instrument_all]` and has no /// functionality except in that context.` pub fn skip(_attr: TokenStream, item: TokenStream) -> TokenStream { item } fn should_apply_instrument_method(method: &mut ImplItemFn) -> bool { let ident = Ident::new("servo_tracing", Span::call_site()); // Test empty method.attrs.is_empty() || // Test not already have servo_tracing macro ! method .attrs .iter() .any(|attr| { attr.path().segments.first().map(|segment| &segment.ident) == Some(&ident) }) } #[proc_macro_attribute] /// Instruments a whole impl block with the default servo tracing. /// See the documentation for `#[servo_tracing::instrument]`. /// If you want special treatment of a specific function you can override it /// with the normal tracing macro /// ``` /// struct Foo {} /// #[servo_tracing::instrument_all(skip_all))] /// impl Foo { /// fn bar(&self) {} /// /// #[servo_tracing::instrument(name="Do baz")] /// fn baz(&self) {} /// /// fn bazz(&self) {} /// } /// ``` pub fn instrument_all(attr: TokenStream, item: TokenStream) -> TokenStream { let attr: proc_macro2::TokenStream = attr.into(); let parsed_item = parse_macro_input!(item as Item); match parsed_item { Item::Impl(mut impl_block) => { for item in impl_block.items.iter_mut() { if let ImplItem::Fn(method) = item && should_apply_instrument_method(method) { let transformed = instrument_internal(attr.clone(), method.to_token_stream()) .unwrap() .into(); *method = syn::parse_macro_input!(transformed as syn::ImplItemFn) } } TokenStream::from(quote!(#impl_block)) }, _ => { panic!("can only be applied to impl block"); }, } } #[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); } }