perf: Greatly expand profiler (#38584)
Expands on #38543 (notably allows setting importance categories and weights on tests, and a lot of internal refactoring) because I couldn't help myself. Also allows exporting runs to json and comparing across them. See code for docs. Release Notes: - N/A
This commit is contained in:
@@ -16,6 +16,7 @@ doctest = false
|
||||
[dependencies]
|
||||
quote.workspace = true
|
||||
syn.workspace = true
|
||||
perf.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![cfg_attr(not(target_os = "windows"), allow(unused))]
|
||||
#![allow(clippy::test_attr_in_doctest)]
|
||||
|
||||
use perf::*;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{ItemFn, LitStr, parse_macro_input, parse_quote};
|
||||
@@ -90,68 +91,81 @@ pub fn line_endings(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
/// Inner data for the perf macro.
|
||||
#[derive(Default)]
|
||||
struct PerfArgs {
|
||||
/// How many times to loop a test before rerunning the test binary.
|
||||
/// If left empty, the test harness will auto-determine this value.
|
||||
/// How many times to loop a test before rerunning the test binary. If left
|
||||
/// empty, the test harness will auto-determine this value.
|
||||
iterations: Option<syn::Expr>,
|
||||
/// How much this test's results should be weighed when comparing across runs.
|
||||
/// If unspecified, defaults to `WEIGHT_DEFAULT` (50).
|
||||
weight: Option<syn::Expr>,
|
||||
/// How relevant a benchmark is to overall performance. See docs on the enum
|
||||
/// for details. If unspecified, `Average` is selected.
|
||||
importance: Importance,
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for PerfArgs {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
if input.is_empty() {
|
||||
return Ok(PerfArgs { iterations: None });
|
||||
#[warn(clippy::all, clippy::pedantic)]
|
||||
impl PerfArgs {
|
||||
/// Parses attribute arguments into a `PerfArgs`.
|
||||
fn parse_into(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> {
|
||||
if meta.path.is_ident("iterations") {
|
||||
self.iterations = Some(meta.value()?.parse()?);
|
||||
} else if meta.path.is_ident("weight") {
|
||||
self.weight = Some(meta.value()?.parse()?);
|
||||
} else if meta.path.is_ident("critical") {
|
||||
self.importance = Importance::Critical;
|
||||
} else if meta.path.is_ident("important") {
|
||||
self.importance = Importance::Important;
|
||||
} else if meta.path.is_ident("average") {
|
||||
// This shouldn't be specified manually, but oh well.
|
||||
self.importance = Importance::Average;
|
||||
} else if meta.path.is_ident("iffy") {
|
||||
self.importance = Importance::Iffy;
|
||||
} else if meta.path.is_ident("fluff") {
|
||||
self.importance = Importance::Fluff;
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(meta.path, "unexpected identifier"));
|
||||
}
|
||||
|
||||
let mut iterations = None;
|
||||
// In principle we only have one possible argument, but leave this as
|
||||
// a loop in case we expand this in the future.
|
||||
for meta in
|
||||
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?
|
||||
{
|
||||
match &meta {
|
||||
syn::Meta::NameValue(meta_name_value) => {
|
||||
if meta_name_value.path.is_ident("iterations") {
|
||||
iterations = Some(meta_name_value.value.clone());
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&meta_name_value.path,
|
||||
"unexpected argument, expected 'iterations'",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
meta,
|
||||
"expected name-value argument like 'iterations = 1'",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PerfArgs { iterations })
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a test as perf-sensitive, to be triaged when checking the performance
|
||||
/// of a build. This also automatically applies `#[test]`.
|
||||
///
|
||||
/// By default, the number of iterations when profiling this test is auto-determined.
|
||||
/// If this needs to be overwritten, pass the desired iteration count to the macro
|
||||
/// as a parameter (`#[perf(iterations = n)]`). Note that the actual profiler may still
|
||||
/// run the test an arbitrary number times; this flag just sets the number of executions
|
||||
/// before the process is restarted and global state is reset.
|
||||
///
|
||||
/// # Usage notes
|
||||
/// This should probably not be applied to tests that do any significant fs IO, as
|
||||
/// locks on files may not be released in time when repeating a test many times. This
|
||||
/// might lead to spurious failures.
|
||||
/// # Usage
|
||||
/// Applying this attribute to a test marks it as average importance by default.
|
||||
/// There are 4 levels of importance (`Critical`, `Important`, `Average`, `Fluff`);
|
||||
/// see the documentation on `Importance` for details. Add the importance as a
|
||||
/// parameter to override the default (e.g. `#[perf(important)]`).
|
||||
///
|
||||
/// Each test also has a weight factor. This is irrelevant on its own, but is considered
|
||||
/// when comparing results across different runs. By default, this is set to 50;
|
||||
/// pass `weight = n` as a parameter to override this. Note that this value is only
|
||||
/// relevant within its importance category.
|
||||
///
|
||||
/// By default, the number of iterations when profiling this test is auto-determined.
|
||||
/// If this needs to be overwritten, pass the desired iteration count as a parameter
|
||||
/// (`#[perf(iterations = n)]`). Note that the actual profiler may still run the test
|
||||
/// an arbitrary number times; this flag just sets the number of executions before the
|
||||
/// process is restarted and global state is reset.
|
||||
///
|
||||
/// This attribute should probably not be applied to tests that do any significant
|
||||
/// disk IO, as locks on files may not be released in time when repeating a test many
|
||||
/// times. This might lead to spurious failures.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use util_macros::perf;
|
||||
///
|
||||
/// #[perf]
|
||||
/// fn expensive_computation_test() {
|
||||
/// fn generic_test() {
|
||||
/// // Test goes here.
|
||||
/// }
|
||||
///
|
||||
/// #[perf(fluff, weight = 30)]
|
||||
/// fn cold_path_test() {
|
||||
/// // Test goes here.
|
||||
/// }
|
||||
/// ```
|
||||
@@ -161,72 +175,108 @@ impl syn::parse::Parse for PerfArgs {
|
||||
/// ```rust,ignore
|
||||
/// use util_macros::perf;
|
||||
///
|
||||
/// #[perf(iterations = 1)]
|
||||
/// #[perf(iterations = 1, critical)]
|
||||
/// #[gpui::test]
|
||||
/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
|
||||
/// // Test goes here.
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
#[warn(clippy::all, clippy::pedantic)]
|
||||
pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
// If any of the below constants are changed, make sure to also update the perf
|
||||
// profiler to match!
|
||||
|
||||
/// The suffix on tests marked with `#[perf]`.
|
||||
const SUF_NORMAL: &str = "__ZED_PERF";
|
||||
/// The suffix on tests marked with `#[perf(iterations = n)]`.
|
||||
const SUF_FIXED: &str = "__ZED_PERF_FIXEDITER";
|
||||
/// The env var in which we pass the iteration count to our tests.
|
||||
const ITER_ENV_VAR: &str = "ZED_PERF_ITER";
|
||||
|
||||
let iter_count = parse_macro_input!(our_attr as PerfArgs).iterations;
|
||||
let mut args = PerfArgs::default();
|
||||
let parser = syn::meta::parser(|meta| PerfArgs::parse_into(&mut args, meta));
|
||||
parse_macro_input!(our_attr with parser);
|
||||
|
||||
let ItemFn {
|
||||
mut attrs,
|
||||
attrs: mut attrs_main,
|
||||
vis,
|
||||
mut sig,
|
||||
sig: mut sig_main,
|
||||
block,
|
||||
} = parse_macro_input!(input as ItemFn);
|
||||
attrs.push(parse_quote!(#[test]));
|
||||
attrs.push(parse_quote!(#[allow(non_snake_case)]));
|
||||
attrs_main.push(parse_quote!(#[test]));
|
||||
attrs_main.push(parse_quote!(#[allow(non_snake_case)]));
|
||||
|
||||
let fns = if cfg!(perf_enabled) {
|
||||
#[allow(clippy::wildcard_imports, reason = "We control the other side")]
|
||||
use consts::*;
|
||||
|
||||
let block: Box<syn::Block> = if cfg!(perf_enabled) {
|
||||
// Make the ident obvious when calling, for the test parser.
|
||||
let mut new_ident = sig.ident.to_string();
|
||||
if iter_count.is_some() {
|
||||
new_ident.push_str(SUF_FIXED);
|
||||
} else {
|
||||
new_ident.push_str(SUF_NORMAL);
|
||||
}
|
||||
// Also set up values for the second metadata-returning "test".
|
||||
let mut new_ident_main = sig_main.ident.to_string();
|
||||
let mut new_ident_meta = new_ident_main.clone();
|
||||
new_ident_main.push_str(SUF_NORMAL);
|
||||
new_ident_meta.push_str(SUF_MDATA);
|
||||
|
||||
let new_ident = syn::Ident::new(&new_ident, sig.ident.span());
|
||||
sig.ident = new_ident;
|
||||
// If we have a preset iteration count, just use that.
|
||||
if let Some(iter_count) = iter_count {
|
||||
parse_quote!({
|
||||
for _ in 0..#iter_count {
|
||||
#block
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Otherwise, the perf harness will pass us the value in an env var.
|
||||
let new_ident_main = syn::Ident::new(&new_ident_main, sig_main.ident.span());
|
||||
sig_main.ident = new_ident_main;
|
||||
|
||||
// We don't want any nonsense if the original test had a weird signature.
|
||||
let new_ident_meta = syn::Ident::new(&new_ident_meta, sig_main.ident.span());
|
||||
let sig_meta = parse_quote!(fn #new_ident_meta());
|
||||
let attrs_meta = parse_quote!(#[test] #[allow(non_snake_case)]);
|
||||
|
||||
// Make the test loop as the harness instructs it to.
|
||||
let block_main = {
|
||||
// The perf harness will pass us the value in an env var. Even if we
|
||||
// have a preset value, just do this to keep the code paths unified.
|
||||
parse_quote!({
|
||||
let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
|
||||
for _ in 0..iter_count {
|
||||
#block
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
let importance = format!("{}", args.importance);
|
||||
let block_meta = {
|
||||
// This function's job is to just print some relevant info to stdout,
|
||||
// based on the params this attr is passed. It's not an actual test.
|
||||
// Since we use a custom attr set on our metadata fn, it shouldn't
|
||||
// cause problems with xfail tests.
|
||||
let q_iter = if let Some(iter) = args.iterations {
|
||||
quote! {
|
||||
println!("{} {} {}", #MDATA_LINE_PREF, #ITER_COUNT_LINE_NAME, #iter);
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let weight = args
|
||||
.weight
|
||||
.unwrap_or_else(|| parse_quote! { #WEIGHT_DEFAULT });
|
||||
parse_quote!({
|
||||
#q_iter
|
||||
println!("{} {} {}", #MDATA_LINE_PREF, #WEIGHT_LINE_NAME, #weight);
|
||||
println!("{} {} {}", #MDATA_LINE_PREF, #IMPORTANCE_LINE_NAME, #importance);
|
||||
println!("{} {} {}", #MDATA_LINE_PREF, #VERSION_LINE_NAME, #MDATA_VER);
|
||||
})
|
||||
};
|
||||
|
||||
vec![
|
||||
// The real test.
|
||||
ItemFn {
|
||||
attrs: attrs_main,
|
||||
vis: vis.clone(),
|
||||
sig: sig_main,
|
||||
block: block_main,
|
||||
},
|
||||
// The fake test.
|
||||
ItemFn {
|
||||
attrs: attrs_meta,
|
||||
vis,
|
||||
sig: sig_meta,
|
||||
block: block_meta,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
block
|
||||
vec![ItemFn {
|
||||
attrs: attrs_main,
|
||||
vis,
|
||||
sig: sig_main,
|
||||
block,
|
||||
}]
|
||||
};
|
||||
|
||||
ItemFn {
|
||||
attrs,
|
||||
vis,
|
||||
sig,
|
||||
block,
|
||||
}
|
||||
.into_token_stream()
|
||||
.into()
|
||||
fns.into_iter()
|
||||
.flat_map(|f| TokenStream::from(f.into_token_stream()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user