mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Adds a convenience macro that adds sane defaults to the tracing macro. Closes: #36534 Testing: - Pass in sample test cases and compare against expected behaviour. --------- Signed-off-by: Astraea Quinn Skoutelli <astraea.quinn.skoutelli@huawei.com>
394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
/* 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);
|
|
}
|
|
}
|