aboutsummaryrefslogtreecommitdiffstats
path: root/components/servo_tracing/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/servo_tracing/lib.rs')
-rw-r--r--components/servo_tracing/lib.rs394
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);
+ }
+}