mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
[tracing] Add convenience macro for function tracing (#36573)
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>
This commit is contained in:
parent
a57ab6deca
commit
b6967fc4c8
7 changed files with 431 additions and 4 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -4321,6 +4321,7 @@ dependencies = [
|
|||
"servo-media",
|
||||
"servo-media-dummy",
|
||||
"servo-media-gstreamer",
|
||||
"servo-tracing",
|
||||
"servo_allocator",
|
||||
"servo_config",
|
||||
"servo_geometry",
|
||||
|
@ -6734,6 +6735,16 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "servo-tracing"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "servo_allocator"
|
||||
version = "0.0.1"
|
||||
|
@ -6891,6 +6902,7 @@ dependencies = [
|
|||
"raw-window-handle",
|
||||
"rustls",
|
||||
"serde_json",
|
||||
"servo-tracing",
|
||||
"servo_allocator",
|
||||
"shellwords",
|
||||
"sig",
|
||||
|
|
|
@ -125,6 +125,7 @@ serde_json = "1.0"
|
|||
servo-media = { git = "https://github.com/servo/media" }
|
||||
servo-media-dummy = { git = "https://github.com/servo/media" }
|
||||
servo-media-gstreamer = { git = "https://github.com/servo/media" }
|
||||
servo-tracing = { path = "components/servo_tracing" }
|
||||
servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-03-15" }
|
||||
smallbitvec = "2.6.0"
|
||||
smallvec = "1.15"
|
||||
|
|
|
@ -104,6 +104,7 @@ serde = { workspace = true }
|
|||
servo-media = { workspace = true }
|
||||
servo-media-dummy = { workspace = true }
|
||||
servo-media-gstreamer = { workspace = true, optional = true }
|
||||
servo-tracing = { workspace = true }
|
||||
servo_allocator = { path = "../allocator" }
|
||||
servo_config = { path = "../config" }
|
||||
servo_geometry = { path = "../geometry" }
|
||||
|
|
|
@ -246,10 +246,7 @@ impl webrender_api::RenderNotifier for RenderNotifier {
|
|||
}
|
||||
|
||||
impl Servo {
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(skip(builder), fields(servo_profiling = true), level = "trace",)
|
||||
)]
|
||||
#[servo_tracing::instrument(skip(builder))]
|
||||
fn new(builder: ServoBuilder) -> Self {
|
||||
// Global configuration options, parsed from the command line.
|
||||
let opts = builder.opts.map(|opts| *opts);
|
||||
|
|
21
components/servo_tracing/Cargo.toml
Normal file
21
components/servo_tracing/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "servo-tracing"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
quote = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
syn = { version = "2", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
proc-macro = true
|
||||
doctest = false
|
||||
|
||||
[dev-dependencies]
|
||||
prettyplease = "0.2.32"
|
394
components/servo_tracing/lib.rs
Normal file
394
components/servo_tracing/lib.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ mime_guess = { workspace = true }
|
|||
url = { workspace = true }
|
||||
raw-window-handle = { workspace = true }
|
||||
rustls = { workspace = true, features = ["aws-lc-rs"] }
|
||||
servo-tracing = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
tracing-subscriber = { workspace = true, optional = true, features = ["env-filter"] }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue