Skip to content

Commit 7820586

Browse files
authored
tests: Add an automatic perf profiler (#38543)
Add an auto-profiler for our tests, to hopefully allow better triage of performance impacts resulting from code changes. Comprehensive usage docs are in the code. Currently, it uses hyperfine under the hood and prints markdown to the command line for all crates with relevant tests enabled. We may want to expand this to allow outputting json in the future to allow e.g. automatically comparing the difference between two runs on different commits, and in general a lot of functionality could be added (maybe measuring memory usage?). It's enabled (mostly as an example) on two tests inside `gpui` and a bunch of those inside `vim`. I'd have happily used `cargo bench`, but that's nightly-only. Release Notes: - N/A
1 parent be77682 commit 7820586

File tree

12 files changed

+393
-5
lines changed

12 files changed

+393
-5
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
44

55
[alias]
66
xtask = "run --package xtask --"
7+
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--config", "target.'cfg(true)'.runner='target/release/perf'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
78

89
[target.x86_64-unknown-linux-gnu]
910
linker = "clang"

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ members = [
220220
#
221221

222222
"tooling/workspace-hack",
223-
"tooling/xtask",
223+
"tooling/xtask", "tooling/perf",
224224
]
225225
default-members = ["crates/zed"]
226226

crates/gpui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ resvg = { version = "0.45.0", default-features = false, features = [
110110
"memmap-fonts",
111111
] }
112112
usvg = { version = "0.45.0", default-features = false }
113+
util_macros.workspace = true
113114
schemars.workspace = true
114115
seahash = "4.1"
115116
semantic_version.workspace = true

crates/gpui/src/style.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,7 +1300,9 @@ mod tests {
13001300

13011301
use super::*;
13021302

1303-
#[test]
1303+
use util_macros::perf;
1304+
1305+
#[perf]
13041306
fn test_basic_highlight_style_combination() {
13051307
let style_a = HighlightStyle::default();
13061308
let style_b = HighlightStyle::default();
@@ -1385,7 +1387,7 @@ mod tests {
13851387
);
13861388
}
13871389

1388-
#[test]
1390+
#[perf]
13891391
fn test_combine_highlights() {
13901392
assert_eq!(
13911393
combine_highlights(

crates/util_macros/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ doctest = false
1717
quote.workspace = true
1818
syn.workspace = true
1919
workspace-hack.workspace = true
20+
21+
[features]
22+
perf-enabled = []

crates/util_macros/src/util_macros.rs

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#![cfg_attr(not(target_os = "windows"), allow(unused))]
2+
#![allow(clippy::test_attr_in_doctest)]
23

34
use proc_macro::TokenStream;
4-
use quote::quote;
5-
use syn::{LitStr, parse_macro_input};
5+
use quote::{ToTokens, quote};
6+
use syn::{ItemFn, LitStr, parse_macro_input, parse_quote};
67

78
/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces
89
/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is
@@ -87,3 +88,145 @@ pub fn line_endings(input: TokenStream) -> TokenStream {
8788
#text
8889
})
8990
}
91+
92+
/// Inner data for the perf macro.
93+
struct PerfArgs {
94+
/// How many times to loop a test before rerunning the test binary.
95+
/// If left empty, the test harness will auto-determine this value.
96+
iterations: Option<syn::Expr>,
97+
}
98+
99+
impl syn::parse::Parse for PerfArgs {
100+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
101+
if input.is_empty() {
102+
return Ok(PerfArgs { iterations: None });
103+
}
104+
105+
let mut iterations = None;
106+
// In principle we only have one possible argument, but leave this as
107+
// a loop in case we expand this in the future.
108+
for meta in
109+
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated(input)?
110+
{
111+
match &meta {
112+
syn::Meta::NameValue(meta_name_value) => {
113+
if meta_name_value.path.is_ident("iterations") {
114+
iterations = Some(meta_name_value.value.clone());
115+
} else {
116+
return Err(syn::Error::new_spanned(
117+
&meta_name_value.path,
118+
"unexpected argument, expected 'iterations'",
119+
));
120+
}
121+
}
122+
_ => {
123+
return Err(syn::Error::new_spanned(
124+
meta,
125+
"expected name-value argument like 'iterations = 1'",
126+
));
127+
}
128+
}
129+
}
130+
131+
Ok(PerfArgs { iterations })
132+
}
133+
}
134+
135+
/// Marks a test as perf-sensitive, to be triaged when checking the performance
136+
/// of a build. This also automatically applies `#[test]`.
137+
///
138+
/// By default, the number of iterations when profiling this test is auto-determined.
139+
/// If this needs to be overwritten, pass the desired iteration count to the macro
140+
/// as a parameter (`#[perf(iterations = n)]`). Note that the actual profiler may still
141+
/// run the test an arbitrary number times; this flag just sets the number of executions
142+
/// before the process is restarted and global state is reset.
143+
///
144+
/// # Usage notes
145+
/// This should probably not be applied to tests that do any significant fs IO, as
146+
/// locks on files may not be released in time when repeating a test many times. This
147+
/// might lead to spurious failures.
148+
///
149+
/// # Examples
150+
/// ```rust
151+
/// use util_macros::perf;
152+
///
153+
/// #[perf]
154+
/// fn expensive_computation_test() {
155+
/// // Test goes here.
156+
/// }
157+
/// ```
158+
///
159+
/// This also works with `#[gpui::test]`s, though in most cases it shouldn't
160+
/// be used with automatic iterations.
161+
/// ```rust,ignore
162+
/// use util_macros::perf;
163+
///
164+
/// #[perf(iterations = 1)]
165+
/// #[gpui::test]
166+
/// fn oneshot_test(_cx: &mut gpui::TestAppContext) {
167+
/// // Test goes here.
168+
/// }
169+
/// ```
170+
#[proc_macro_attribute]
171+
pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
172+
// If any of the below constants are changed, make sure to also update the perf
173+
// profiler to match!
174+
175+
/// The suffix on tests marked with `#[perf]`.
176+
const SUF_NORMAL: &str = "__ZED_PERF";
177+
/// The suffix on tests marked with `#[perf(iterations = n)]`.
178+
const SUF_FIXED: &str = "__ZED_PERF_FIXEDITER";
179+
/// The env var in which we pass the iteration count to our tests.
180+
const ITER_ENV_VAR: &str = "ZED_PERF_ITER";
181+
182+
let iter_count = parse_macro_input!(our_attr as PerfArgs).iterations;
183+
184+
let ItemFn {
185+
mut attrs,
186+
vis,
187+
mut sig,
188+
block,
189+
} = parse_macro_input!(input as ItemFn);
190+
attrs.push(parse_quote!(#[test]));
191+
attrs.push(parse_quote!(#[allow(non_snake_case)]));
192+
193+
let block: Box<syn::Block> = if cfg!(perf_enabled) {
194+
// Make the ident obvious when calling, for the test parser.
195+
let mut new_ident = sig.ident.to_string();
196+
if iter_count.is_some() {
197+
new_ident.push_str(SUF_FIXED);
198+
} else {
199+
new_ident.push_str(SUF_NORMAL);
200+
}
201+
202+
let new_ident = syn::Ident::new(&new_ident, sig.ident.span());
203+
sig.ident = new_ident;
204+
// If we have a preset iteration count, just use that.
205+
if let Some(iter_count) = iter_count {
206+
parse_quote!({
207+
for _ in 0..#iter_count {
208+
#block
209+
}
210+
})
211+
} else {
212+
// Otherwise, the perf harness will pass us the value in an env var.
213+
parse_quote!({
214+
let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::<usize>().unwrap();
215+
for _ in 0..iter_count {
216+
#block
217+
}
218+
})
219+
}
220+
} else {
221+
block
222+
};
223+
224+
ItemFn {
225+
attrs,
226+
vis,
227+
sig,
228+
block,
229+
}
230+
.into_token_stream()
231+
.into()
232+
}

crates/vim/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ theme.workspace = true
4646
tokio = { version = "1.15", features = ["full"], optional = true }
4747
ui.workspace = true
4848
util.workspace = true
49+
util_macros.workspace = true
4950
vim_mode_setting.workspace = true
5051
workspace.workspace = true
5152
zed_actions.workspace = true

0 commit comments

Comments
 (0)