gpui: Add bench_metrics for multi-metric Criterion benchmarks (#64753)

GPUI benchmarks measured only wall time, which is noisy enough on shared
CI runners that a regression must be large before Criterion flags it.
This PR adds a `bench_metrics` crate that records several metrics per
Criterion sample and reports them alongside Criterion's own analysis,
and wires GPUI's benchmark harness to it. Nothing in `bench_metrics`
depends on GPUI, so any Criterion benchmark in the workspace can adopt
it.

**What a benchmark now prints** (Linux, hardware counters available):

```
closed_inspector_grid/case/100-nodes-0-cycles
                        time:   [273.99 µs 274.78 µs 276.04 µs]
GPUI bench report (all observed iterations): closed_inspector_grid/case/100-nodes-0-cycles
  instructions per iteration: median 5.791 M instructions (min 5.788, max 5.880, samples 22)
  foreground instructions per iteration: median 5.791 M instructions (...)
  cycles per iteration: median 1.453 M cycles (...)
  branch misses per iteration: median 1.856 K branch misses (...)
  cache misses per iteration: median 123.008 cache misses (...)
  foreground context switches per iteration: median 0.000 context switches (...)
  page faults per iteration: median 0.000 page faults (...)
  IPC: median 3.996 (min 3.349, max 4.063, samples 22)
  foreground share of instructions: median 1.000 (...)
```

Criterion analyzes one metric (the *primary*; wall time by default) and
the rest are *secondaries* taken over the same iterations.
`BENCH_MEASUREMENT=instructions` makes process-wide retired instructions
the primary, and `BENCH_MEASUREMENT=foreground-instructions` the
benchmark thread's alone; both are near-deterministic, so a ~0.01%
interval is typical and small regressions are detectable, with wall time
demoted to the report. `BENCH_MEASUREMENT=wall-time` disables all
counters. Counters that can't be opened (no `CAP_PERFMON`, Docker
seccomp, VMs without a PMU, Intel Macs) are skipped with one note, so
`cargo bench` never fails by default; the instruction modes fail fast so
CI can't silently measure the wrong thing.

**Sources.** `HardwareCounter` is platform-neutral over per-OS backends.
On Linux it uses `perf_event_open`: counters are opened once per process
with `inherit` so every later thread is covered, read as deltas per
sample (~150 ns per counter), with hybrid-CPU encodings parsed from
sysfs and multiplexing scaled with a one-time warning. On Apple Silicon
it reads `proc_pid_rusage` and `thread_selfcounts`, which are
unprivileged, for instructions and cycles; branch and cache events need
`kperf` and stay Linux-only. `ResourceCounter` reads `getrusage` for
voluntary context switches on the foreground thread (the waits
instructions can't see) and minor page faults, with no privileges.

Foreground-thread instructions are counted separately from the process
total, so work shifting between GPUI's foreground and background threads
is visible even when the total is flat; with a headless renderer the gap
is driver-thread submission work (about 7% of a small Metal frame).

**Design.** `BenchMeasurement` type-erases the primary so
`#[gpui::bench]` functions and `BenchAppContext` need no type parameter;
`with_secondary`/`with_ratio` add metrics; `bench_group!` still
delegates to `criterion_group!`. Plain Criterion benches opt in with a
`config =` line and `MetricReport::iter` (documented in the gpui-bench
skill; wiring `rope` etc. is a follow-up). Reports are now emitted per
input for `inputs = ...` benchmarks, since per-iteration metrics blended
across inputs are meaningless. Criterion is bumped to 0.8.2, which also
randomizes stack alignment per sample.

**Verified** on Linux (hybrid Alder Lake) and an M-series Mac: every
mode on `inspector_render` and `editor_render`; counter denial via
seccomp on Linux; `bench_metrics` and `gpui` `bench_context` tests
including thread-scope inclusion/exclusion through real counters on both
platforms.

**Follow-ups:** counting-allocator metric (peak heap, allocations),
wiring non-GPUI benches, per-secondary baseline comparison.

Release Notes:

- [GPUI] Improved benchmarks to report retired instructions, cycles,
IPC, foreground-thread share, cache and branch misses, context switches,
and page faults per iteration alongside wall time, with
`BENCH_MEASUREMENT=instructions` or `=foreground-instructions` selecting
near-deterministic instruction counts as the Criterion-analyzed metric
for CI regression detection
This commit is contained in:
Anthony Eid 2026-09-27 17:37:29 +00:00 • committed by GitHub
parent 4c841aaf1c
commit e683fd7b46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2136 additions and 48 deletions

View file

@ -132,6 +132,58 @@ The attribute supports options such as:
Inspect the current macro before depending on an option because the API is evolving.
Register benchmark functions with `gpui::bench_group!` and `gpui::bench_main!` (drop-in for Criterion's `criterion_group!`/`criterion_main!`, including the `name = ...; config = ...; targets = ...` form). The generated functions take `&mut criterion::Criterion<gpui::BenchMeasurement>`; `bench_group!` supplies that measurement from the environment.
### Measurements: wall time, hardware counters, and rusage
`gpui::BenchMeasurement` (from the gpui-independent `bench_metrics` crate) records several metrics per Criterion sample. Criterion analyzes one of them (the *primary*, shown on its `time:` line with confidence intervals and baseline comparison); the rest are *secondaries*, printed per iteration in the GPUI bench report as median/min/max. `BENCH_MEASUREMENT` selects the arrangement:
| `BENCH_MEASUREMENT` | Criterion analyzes | Reported as secondaries |
|---|---|---|
| unset (default) | wall time | every counter the machine supports (below) |
| `instructions` | process-wide retired instructions; fails fast if counters are unavailable | the rest, plus wall time |
| `foreground-instructions` | the benchmark thread's retired instructions; fails fast if unavailable | the rest, plus wall time |
| `wall-time` | wall time | nothing; no counters are opened |
For a CI gate on frame cost prefer `foreground-instructions`: it excludes background parsing and GPU-driver submission threads, whose work varies run to run, and on Apple Silicon the process-wide count includes driver jitter that widens the interval (about ±0.6% versus ±0.01% for the foreground thread).
Default secondaries, each skipped with a one-time note when unavailable:
- **instructions** (process-wide) and **foreground instructions** (the benchmark thread only, where GPUI polls tasks, lays out, paints, and submits). Near-deterministic, so a ~0.1% spread is typical and small regressions are detectable. Blind to waits, memory stalls, and GPU shader work. Their ratio, **foreground share of instructions**, shows work moving between the foreground and background threads even when the total is flat; with a headless renderer it also exposes driver-thread submission work (about 7% of a small Metal frame).
- **cycles** and **IPC** (`instructions / cycles`). Falling IPC with flat instructions means worse cache or branch behavior, not more work.
- **branch misses** and **cache misses** (last-level), for diagnosing an IPC drop. Linux only.
- **foreground context switches**: voluntary switches on the benchmark thread per iteration, i.e. how many times a frame blocked on a lock, channel, or fence. This is the metric that catches the waits instructions cannot see. Linux only.
- **page faults**: minor faults per iteration, a privilege-free proxy for fresh memory touched.
Run one benchmark process per machine at a time. Hardware counters are per task, so instruction counts stay correct next to a concurrent `cargo bench`, but wall time, IPC, and cache misses share the hardware and degrade, and two processes requesting counters can exceed a core's counter slots, at which point the kernel multiplexes and the counts become scaled estimates (a warning is printed once). Within one process, benchmarks already run sequentially; process-wide counters can still attribute a previous benchmark's trailing background work to the next benchmark's first sample, which the foreground-thread metrics are immune to.
Platform support: on Linux, hardware counters come from `perf_event_open` and need `CAP_PERFMON` or `/proc/sys/kernel/perf_event_paranoid` at 2 or below; Docker's default seccomp profile and many cloud VMs block it, and the default mode then reports wall time plus the `getrusage` metrics. On Apple Silicon, instructions and cycles (both scopes) come from `proc_pid_rusage` and `thread_selfcounts` with no privileges; Intel Macs have no source. Criterion baselines are keyed by benchmark id, not measurement, so use distinct `--save-baseline` names when switching the primary on one machine.
Never present instructions alone for a latency claim; pair them with wall time and the frame metrics from the same run. Other Criterion measurements plug in with `criterion_group!` and `config = Criterion::default().with_measurement(gpui::BenchMeasurement::new(primary).with_secondary("name", other))`.
#### Benchmarks outside GPUI
`bench_metrics` has no GPUI dependency, so any Criterion benchmark in the workspace (`rope`, `sum_tree`, `text`, ...) gets the same metrics and the same `BENCH_MEASUREMENT` switch. Add `bench_metrics.workspace = true` to `[dev-dependencies]`, take `&mut Criterion<bench_metrics::BenchMeasurement>` in the benchmark functions, and register them with:
```rust
criterion_group! {
name = benches;
config = Criterion::default()
.with_measurement(bench_metrics::BenchMeasurement::from_env_or_exit());
targets = rope_benchmarks
}
```
That alone switches Criterion's analyzed metric with `BENCH_MEASUREMENT=instructions`. To also print the per-iteration secondaries, run each routine through a `MetricReport`, which counts iterations for you:
```rust
let report = bench_metrics::MetricReport::new();
group.bench_function("append", |b| report.iter(b, || rope.append(small.clone())));
report.print(" ");
```
Criterion's `Throughput` lines keep working alongside. Page faults per iteration are a useful allocation-pressure signal for data-structure crates even without a counting allocator.
### `bench_iter`: synchronous functions and compute work
Use `bench_iter` for synchronous application or compute code:
@ -197,8 +249,9 @@ On macOS, the headless renderer uses Metal without showing a window. `bench_rend
On platforms without a headless renderer, GPUI still performs CPU-side window work, but presenting discards the scene and does not measure real GPU submission. State this limitation in results.
Depending on the pinned revision, `BenchReport` can include:
`BenchReport` is printed once per benchmark, and once per input for `inputs = ...` benchmarks, labeled with the Criterion benchmark id so the two can be correlated. Depending on the pinned revision, it can include:
- secondary measurements per iteration (median, min, max): instructions, foreground instructions, cycles, IPC, branch and cache misses, foreground context switches, page faults;
- dirty-to-draw duration;
- draw and present intervals;
- invalidations per frame;
@ -359,6 +412,7 @@ Before accepting a GPUI benchmark, verify:
- [ ] Cache state is controlled and documented.
- [ ] A responsiveness signal competes with the heavy work when foreground starvation matters.
- [ ] Frame metrics and completion throughput are both reported.
- [ ] Instructions and IPC (Linux and Apple Silicon) and foreground context switches (Linux) are reported next to wall time, and none is presented alone as the whole result.
- [ ] Final correctness, ordering, and work counts are asserted.
- [ ] The workload completes under bounded smoke and measured runs.
- [ ] The same benchmark code runs on baseline and candidate.

1
.rules
View file

@ -148,6 +148,7 @@ When an agent opens or updates a pull request, it must:
- Use one bullet under `Release Notes:`:
- `- Added ...`, `- Fixed ...`, or `- Improved ...` for user-facing changes, or
- `- N/A` for docs-only and other non-user-facing changes.
- Changes to `gpui` (including its benchmark and test harnesses) must have a bullet prefixed with `[GPUI]` describing the improvement even when nothing is user-facing in Zed itself, since GPUI is consumed as a library (CI enforces this), for example `- [GPUI] Improved benchmarks to report retired CPU instructions alongside wall time on Linux`.
- Format release notes exactly with a blank line after the heading, for example:
```

84
Cargo.lock generated
View file

@ -657,6 +657,15 @@ dependencies = [
"equator",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@ -2112,6 +2121,16 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bench_metrics"
version = "0.1.0"
dependencies = [
"anyhow",
"criterion",
"libc",
"perf-event2",
]
[[package]]
name = "benchmarks"
version = "0.1.0"
@ -2187,7 +2206,7 @@ dependencies = [
"bitflags 2.13.1",
"cexpr",
"clang-sys",
"itertools 0.11.0",
"itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
@ -2592,6 +2611,12 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "c-enum"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8"
[[package]]
name = "cached"
version = "0.56.0"
@ -4341,25 +4366,24 @@ dependencies = [
[[package]]
name = "criterion"
version = "0.5.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
"itertools 0.13.0",
"num-traits",
"once_cell",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
@ -4367,12 +4391,12 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.5.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
dependencies = [
"cast",
"itertools 0.10.5",
"itertools 0.13.0",
]
[[package]]
@ -7507,6 +7531,7 @@ dependencies = [
"async-channel 2.5.0",
"async-task",
"backtrace",
"bench_metrics",
"bindgen",
"bitflags 2.13.1",
"chrono",
@ -9131,9 +9156,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.11.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@ -12965,6 +12990,41 @@ dependencies = [
"serde_json",
]
[[package]]
name = "perf-event-data"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1"
dependencies = [
"bitflags 2.13.1",
"c-enum",
"perf-event-open-sys2",
]
[[package]]
name = "perf-event-open-sys2"
version = "5.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c25955321465255e437600b54296983fab1feac2cd0c38958adeb26dbae49e"
dependencies = [
"libc",
"memoffset",
]
[[package]]
name = "perf-event2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f"
dependencies = [
"bitflags 2.13.1",
"c-enum",
"libc",
"memmap2",
"perf-event-data",
"perf-event-open-sys2",
]
[[package]]
name = "pest"
version = "2.8.3"

View file

@ -20,6 +20,7 @@ members = [
"crates/auto_update_ui",
"crates/aws_http_client",
"crates/bedrock",
"crates/bench_metrics",
"crates/benchmarks",
"crates/breadcrumbs",
"crates/buffer_diff",
@ -291,6 +292,7 @@ anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
audio = { path = "crates/audio" }
bench_metrics = { path = "crates/bench_metrics" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
@ -597,7 +599,8 @@ core-text = "21"
core-video = { version = "0.5.2", features = ["metal"] }
cpal = "0.17"
crash-handler = "0.8"
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { version = "0.8.2", features = ["html_reports"] }
perf-event = { package = "perf-event2", version = "0.7.4" }
crossbeam = "0.8.4"
ctor = "1.0.12"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" }

View file

@ -0,0 +1,24 @@
[package]
name = "bench_metrics"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/bench_metrics.rs"
doctest = false
[features]
test-support = []
[dependencies]
anyhow.workspace = true
criterion.workspace = true
libc.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
perf-event.workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use editor::{EditorStyle, MultiBuffer, display_map::*};
use gpui::{AppContext as _, HighlightStyle, Hsla, TestDispatcher, font, px};
use itertools::Itertools;
@ -6,6 +6,7 @@ use multi_buffer::MultiBufferOffset;
use project::project_settings::DiagnosticSeverity;
use rand::{Rng, SeedableRng, rngs::StdRng};
use settings::SettingsStore;
use std::hint::black_box;
use std::{num::NonZeroU32, time::Duration};
use text::Bias;
use util::RandomCharIter;

View file

@ -16,9 +16,7 @@ use benchmarks::bench_utils::{
RUST_FUNCTION_BODY_LINES, RUST_FUNCTION_LINES, RUST_MODULE_HEADER_LINES, random_rust_file,
rust_file_line_count, rust_identifier as identifier,
};
use criterion::{
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
};
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use editor::{Editor, EditorStyle};
use futures::{StreamExt as _, pin_mut, task::noop_waker};
use gpui::{
@ -32,6 +30,7 @@ use prompt_store::ProjectContext;
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
use serde_json::{Value, json};
use settings::{Settings as _, SettingsStore};
use std::hint::black_box;
use ui::IntoElement as _;
const SEED: u64 = 0x5EED_5EED;

View file

@ -1,5 +1,6 @@
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use edit_prediction::metrics::compute_kept_rate;
use std::hint::black_box;
fn repeated_function_lines(line_count: usize) -> String {
let mut text = String::with_capacity(line_count * 32);

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use edit_prediction::metrics::count_tree_sitter_errors;
use fs::FakeFs;
use gpui::{AppContext as _, TestAppContext};
@ -8,6 +8,7 @@ use language::{Buffer, BufferSnapshot, LanguageRegistry};
use languages::init as init_languages;
use node_runtime::NodeRuntime;
use settings::SettingsStore;
use std::hint::black_box;
struct ParsedCase {
label: String,

View file

@ -26,7 +26,7 @@ test-support = [
"x11",
"proptest",
]
bench-support = ["profiler", "dep:criterion"]
bench-support = ["profiler", "dep:criterion", "dep:bench_metrics"]
# Preserve the published feature name while consumers migrate to `bench-support`.
bench = ["bench-support"]
inspector = ["gpui_macros/inspector"]
@ -53,6 +53,7 @@ backtrace = { workspace = true, optional = true }
bitflags.workspace = true
collections.workspace = true
bench_metrics = { workspace = true, optional = true }
criterion = { workspace = true, optional = true }
ctor.workspace = true
derive_more.workspace = true
@ -131,13 +132,13 @@ objc2-metal = { version = "0.3", optional = true }
scap = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true, features = ["Win32_Foundation", "Win32_System_Power"] }
[dev-dependencies]
backtrace.workspace = true
bench_metrics = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui_platform = { workspace = true, features = ["font-kit", "wayland", "x11"] }

View file

@ -21,7 +21,10 @@ use slotmap::SlotMap;
pub use async_context::*;
#[cfg(feature = "bench-support")]
pub use bench_context::{BenchAppContext, BenchReport, BenchWindowContext, bench_platform};
pub use bench_context::{
BenchAppContext, BenchMeasurement, BenchReport, BenchWindowContext, MetricReport,
bench_platform,
};
use collections::{FxHashMap, FxHashSet, HashMap, TypeIdHashMap, TypeIdHashSet, VecDeque};
pub use context::*;
pub use entity_map::*;

View file

@ -10,6 +10,7 @@ use std::{
};
use anyhow::{Result, anyhow};
pub use bench_metrics::{BenchMeasurement, MetricReport};
use hdrhistogram::Histogram;
use crate::{
@ -101,6 +102,7 @@ pub struct ForegroundWorkSummary {
#[derive(Clone)]
pub struct BenchReport {
frame_snapshot: Rc<RefCell<WindowFrameSnapshot>>,
metrics: MetricReport,
frame_budget_nanos: u128,
}
@ -127,10 +129,18 @@ impl BenchReport {
);
Self {
frame_snapshot: Rc::new(RefCell::new(WindowFrameSnapshot::new())),
metrics: MetricReport::new(),
frame_budget_nanos,
}
}
/// Returns the per-iteration secondary metrics (retired instructions,
/// cycles, context switches, ...) recorded alongside Criterion's primary
/// measurement.
pub fn metrics(&self) -> &MetricReport {
&self.metrics
}
fn record_frame_timings<'i>(&self, events: impl IntoIterator<Item = &'i FrameEvent>) {
let mut snapshot = self.frame_snapshot.borrow_mut();
// `.ok()` on `record`: this operation is infallible (the histograms auto-resize).
@ -239,15 +249,15 @@ impl BenchReport {
}
/// Prints this report to stderr.
pub fn print(&self, benchmark_name: Option<&'static str>) {
pub fn print(&self, benchmark_name: &str) {
let frame_snapshot = self.frame_snapshot.borrow();
if frame_snapshot.is_empty() {
if frame_snapshot.is_empty() && self.metrics.is_empty() {
return;
}
let benchmark_name = benchmark_name.unwrap_or("unknown benchmark");
eprintln!("GPUI bench report (all observed iterations): {benchmark_name}");
eprintln!(" note: includes Criterion warmup/calibration");
self.metrics.print(" ");
self.print_histogram("window dirty-to-draw", &frame_snapshot.dirty_to_draw);
self.print_histogram("window draw", &frame_snapshot.draw);
self.print_histogram("window present interval", &frame_snapshot.present_interval);
@ -488,7 +498,7 @@ pub struct BenchAppContext<'a, 'measurement> {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
benchmark_name: Option<&'static str>,
bencher: Rc<RefCell<Option<&'a mut criterion::Bencher<'measurement>>>>,
bencher: Rc<RefCell<Option<&'a mut criterion::Bencher<'measurement, BenchMeasurement>>>>,
report: BenchReport,
}
@ -501,7 +511,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
pub fn new(
platform: Rc<dyn Platform>,
benchmark_name: Option<&'static str>,
bencher: &'a mut criterion::Bencher<'measurement>,
bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>,
) -> Self {
Self::build(platform, benchmark_name, bencher, BenchReport::default())
}
@ -515,7 +525,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
pub fn new_with_platform_and_report(
platform: Rc<dyn Platform>,
benchmark_name: Option<&'static str>,
bencher: &'a mut criterion::Bencher<'measurement>,
bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>,
report: BenchReport,
) -> Self {
Self::build(platform, benchmark_name, bencher, report)
@ -524,7 +534,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
fn build(
platform: Rc<dyn Platform>,
benchmark_name: Option<&'static str>,
bencher: &'a mut criterion::Bencher<'measurement>,
bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>,
report: BenchReport,
) -> Self {
let background_executor = platform.background_executor();
@ -662,12 +672,16 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
let bencher = self.take_bencher("bench_iter");
self.dispatch_pending_frames(|| true);
let collector = TraceScope::start(self.foreground_journal_collector());
self.report.metrics.discard_pending();
let mut iterations = 0;
let mut benchmark = || {
iterations += 1;
benchmark(self);
self.dispatch_pending_frames(|| true);
};
bencher.iter(&mut benchmark);
let events = collector.finish();
self.report.metrics.record_sample(iterations);
self.report.record_frame_timings(events.frame_events.iter());
self.report
.record_foreground_events(events.foreground_events());
@ -722,6 +736,8 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
let mut benchmark_context = self.clone();
let foreground_executor = self.foreground_executor.clone();
let report = self.report.clone();
report.metrics.discard_pending();
let iterations = std::cell::Cell::new(0);
bencher.iter_batched_ref(
|| {
@ -740,6 +756,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
}
},
|measured_input| {
iterations.set(iterations.get() + 1);
let task = benchmark(&mut measured_input.input, &mut benchmark_context);
benchmark_context.dispatch_pending_frames(|| true);
let output = Rc::new(RefCell::new(None));
@ -759,6 +776,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
},
criterion::BatchSize::PerIteration,
);
report.metrics.record_sample(iterations.get());
self.replace_bencher(bencher);
}
@ -789,7 +807,10 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
let dispatcher = self.background_executor.dispatcher().clone();
self.dispatch_pending_frames(|| true);
let collector = TraceScope::start(self.foreground_journal_collector());
self.report.metrics.discard_pending();
let mut iterations = 0;
let mut benchmark = || {
iterations += 1;
dispatcher
.as_threaded()
.expect("validated in BenchAppContext::build")
@ -808,6 +829,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
bencher.iter(&mut benchmark);
let events = collector.finish();
self.report.metrics.record_sample(iterations);
self.report.record_frame_timings(events.frame_events.iter());
self.report
.record_foreground_events(events.foreground_events());
@ -853,6 +875,8 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
.as_threaded()
.expect("validated in BenchAppContext::build");
let report = self.report.clone();
report.metrics.discard_pending();
let iterations = std::cell::Cell::new(0);
bencher.iter_batched_ref(
|| {
@ -867,6 +891,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
}
},
|measured_input| {
iterations.set(iterations.get() + 1);
let (state, window, stopped) = &mut measured_input.input;
let started = Instant::now();
let check_deadline = || {
@ -932,6 +957,7 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
},
criterion::BatchSize::PerIteration,
);
report.metrics.record_sample(iterations.get());
self.replace_bencher(bencher);
}
@ -1009,13 +1035,16 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> {
}
}
fn take_bencher(&self, benchmark_kind: &str) -> &'a mut criterion::Bencher<'measurement> {
fn take_bencher(
&self,
benchmark_kind: &str,
) -> &'a mut criterion::Bencher<'measurement, BenchMeasurement> {
self.bencher.borrow_mut().take().unwrap_or_else(|| {
panic!("cannot start {benchmark_kind}: benchmark measurement is already running")
})
}
fn replace_bencher(&self, bencher: &'a mut criterion::Bencher<'measurement>) {
fn replace_bencher(&self, bencher: &'a mut criterion::Bencher<'measurement, BenchMeasurement>) {
let previous = self.bencher.borrow_mut().replace(bencher);
assert!(
previous.is_none(),
@ -1323,11 +1352,96 @@ impl VisualContext for BenchWindowContext<'_, '_> {
#[cfg(test)]
mod tests {
use std::{rc::Rc, sync::Arc};
use std::{
rc::Rc,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use super::*;
use crate::profiler::journal::install_test_foreground_journal;
/// Runs `benchmark` under a wall-time primary with a fake counter as the
/// secondary metric and returns the per-iteration values the report
/// recorded for it.
fn secondary_values_per_iteration(
counter: Arc<AtomicU64>,
benchmark: impl Fn(&mut BenchAppContext, &Arc<AtomicU64>),
) -> Vec<u64> {
let platform = bench_platform(None, Arc::new(crate::NoopTextSystem::new()));
let report = BenchReport::default();
let mut criterion = criterion::Criterion::default()
.with_measurement(
BenchMeasurement::new(criterion::measurement::WallTime).with_secondary(
"fake counter",
bench_metrics::FakeCounter::new(counter.clone()),
),
)
.without_plots()
.sample_size(10)
.warm_up_time(Duration::from_millis(1))
.measurement_time(Duration::from_millis(1));
criterion.bench_function("secondary_values_per_iteration", |bencher| {
let mut cx = BenchAppContext::new_with_platform_and_report(
platform.clone(),
Some("secondary_values_per_iteration"),
bencher,
report.clone(),
);
// Fixture work outside the measured loop must not be attributed
// to any iteration.
counter.fetch_add(1_000_000, Ordering::SeqCst);
benchmark(&mut cx, &counter);
cx.teardown();
});
report
.metrics()
.values("fake counter")
.expect("the fake counter was recorded")
.into_iter()
.map(|value| value as u64)
.collect()
}
#[test]
fn secondary_metric_is_normalized_per_iteration_for_bench_iter() {
let values = secondary_values_per_iteration(Arc::new(AtomicU64::new(0)), |cx, counter| {
cx.bench_iter(|_| {
counter.fetch_add(11, Ordering::SeqCst);
});
});
assert!(!values.is_empty());
assert!(
values.iter().all(|value| *value == 11),
"each sample's total should be divided by its iteration count: {values:?}"
);
}
#[test]
fn secondary_metric_is_accumulated_across_per_iteration_batches() {
let values = secondary_values_per_iteration(Arc::new(AtomicU64::new(0)), |cx, counter| {
cx.bench_batched_task(
|_| {
// Setup runs outside every measured interval.
counter.fetch_add(1_000, Ordering::SeqCst);
},
|(), cx| {
counter.fetch_add(11, Ordering::SeqCst);
cx.background_spawn(async {})
},
);
});
assert!(!values.is_empty());
assert!(
values.iter().all(|value| *value == 11),
"PerIteration batches should sum to one per-iteration value: {values:?}"
);
}
#[test]
fn foreground_work_reports_long_task_without_window_draw() {
let (journal, _journal_guard) = install_test_foreground_journal(1024, 64);
@ -1421,6 +1535,7 @@ mod tests {
let name = "bench_task_reports_long_task_without_window";
let mut criterion = criterion::Criterion::default()
.with_measurement(BenchMeasurement::new(criterion::measurement::WallTime))
.without_plots()
.sample_size(10)
.warm_up_time(Duration::from_millis(1))

View file

@ -112,14 +112,35 @@ pub use spring::*;
/// Defines a Criterion benchmark group for benchmarks annotated with [`gpui::bench`].
///
/// This mirrors `criterion::criterion_group!` so GPUI benchmark files can keep the
/// same shape as ordinary Criterion benchmarks.
/// This mirrors `criterion::criterion_group!`, but the group measures with the
/// `gpui::BenchMeasurement` configured by `BENCH_MEASUREMENT` (see
/// `BenchMeasurement::from_env`). By default Criterion analyzes wall time
/// while retired instructions, cycles, IPC, context switches, and the other
/// counters this machine supports are printed per iteration in the GPUI bench
/// report. `BENCH_MEASUREMENT=instructions` makes Criterion analyze
/// process-wide instructions instead, `foreground-instructions` the benchmark
/// thread's alone, and `wall-time` disables all counters. A
/// `config = ...` expression may set any other Criterion option; its
/// measurement is replaced. To measure with something else, call
/// `criterion::criterion_group!` directly with
/// `config = criterion::Criterion::default().with_measurement(gpui::BenchMeasurement::new(...))`.
///
/// [`gpui::bench`]: crate::bench
#[macro_export]
macro_rules! bench_group {
($($tokens:tt)*) => {
criterion::criterion_group!($($tokens)*);
(name = $name:ident; config = $config:expr; targets = $($target:path),+ $(,)?) => {
criterion::criterion_group! {
name = $name;
config = ($config).with_measurement($crate::BenchMeasurement::from_env_or_exit());
targets = $($target),+
}
};
($name:ident, $($target:path),+ $(,)?) => {
$crate::bench_group! {
name = $name;
config = criterion::Criterion::default();
targets = $($target),+
}
};
}

View file

@ -83,10 +83,13 @@ pub fn bench(args: TokenStream, function: TokenStream) -> TokenStream {
let sample_size =
sample_size.map(|sample_size| quote! { group.sample_size(#sample_size); });
quote! {
let report = #report_expr;
let mut group = criterion.benchmark_group(#group_name);
#sample_size
for input in #inputs {
// One report per input: per-iteration metrics differ across
// inputs, so blending them would make the summary meaningless.
let report = #report_expr;
let report_name = format!("{}/{}/{}", #group_name, #input_name, input);
group.bench_with_input(criterion::BenchmarkId::new(#input_name, &input), &input, {
let report = report.clone();
move |bencher, input| {
@ -105,9 +108,9 @@ pub fn bench(args: TokenStream, function: TokenStream) -> TokenStream {
cx.teardown();
}
});
report.print(&report_name);
}
group.finish();
report.print(Some(stringify!(#outer_fn_name)));
}
} else {
if let Some(input_name) = input_name {
@ -148,14 +151,14 @@ pub fn bench(args: TokenStream, function: TokenStream) -> TokenStream {
cx.teardown();
}
});
report.print(Some(stringify!(#outer_fn_name)));
report.print(stringify!(#outer_fn_name));
}
};
TokenStream::from(quote! {
#inner_fn
fn #outer_fn_name(criterion: &mut criterion::Criterion) {
fn #outer_fn_name(criterion: &mut criterion::Criterion<gpui::BenchMeasurement>) {
#benchmark
}

View file

@ -1,6 +1,7 @@
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use gpui::rgba;
use language::build_highlight_map;
use std::hint::black_box;
use theme::SyntaxTheme;
fn syntax_theme(highlight_names: &[&str]) -> SyntaxTheme {

View file

@ -1,11 +1,10 @@
use std::ops::Range;
use criterion::{
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
};
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use rand::prelude::*;
use rand::rngs::StdRng;
use rope::{Point, Rope};
use std::hint::black_box;
use sum_tree::Bias;
use util::RandomCharIter;

View file

@ -1,7 +1,6 @@
use criterion::{
BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main,
};
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
use std::hint::black_box;
use streaming_diff::StreamingDiff;
const SEED: u64 = 0x5EED_5EED;