This PR builds out GPUI's benchmark harness so render benchmarks measure
realistic frame costs using GPUI-owned, runtime-gated instrumentation.
It adds measurements for frame draw time, dirty-to-draw latency,
invalidation coalescing, and frame-budget overruns, and runs benchmark
workloads with production-like concurrency.
## How it works
**Frame timings flow through the GPUI profiler.** `Window::draw` emits a
`FrameTiming { window_id, dirty_at, invalidations, draw_start, draw_end
}` event into a global ring buffer in `gpui::profiler`, mirroring the
existing task-timing channel. Collection is runtime-gated by
`profiler::set_frame_trace_enabled` (one relaxed atomic load when
disabled — no `Instant::now` calls in production). `BenchReport` is a
pure listener: it drains events through a cursor-based
`FrameTimingCollector` and builds histograms in the bench layer.
`Window` carries no bench-only cfg fields, and the same event channel
can later feed the miniprofiler UI or an in-app frame-time HUD.
**`BenchDispatcher`** is a multithreaded `PlatformDispatcher` for
benchmarks: background tasks run on a worker pool (same priority queue
as `LinuxDispatcher`, with task-profiler hooks), timers fire in real
time on a dedicated thread, and foreground tasks queue until the bench
thread drains them with a blocking `run_until_idle()`. Unlike
`TestDispatcher`, work executes in parallel in real time, so wall-clock
measurements reflect production concurrency. In-flight accounting is
panic-safe via drop guards.
**`gpui::bench_platform()`** returns a per-process `TestPlatform` backed
by the `BenchDispatcher`, cached in a thread-local so worker threads
persist across Criterion calibration passes. This replaces the earlier
approach of constructing a real platform per invocation, which had
process-global singleton issues, never ran foreground tasks (no run loop
pumped the main queue), and couldn't open windows on headless CI.
**Text shaping** uses `NoopTextSystem`: deterministic across
machines/font installations and CI-portable. Measured cost of this
trade: ~10% of editor draw time vs `MacTextSystem` (Noop still emits one
glyph per character at fixed advances, so downstream layout/paint
structure is preserved).
**GPU coverage (macOS only for now).** `PlatformHeadlessRenderer` gained
`render_scene`, which encodes and submits the scene to Metal against a
cached offscreen target without blocking on completion or reading pixels
back — matching production `present()` CPU cost (`render_scene_to_image`
would overstate it: it waits for the GPU and copies pixels back).
`TestWindow::draw` forwards scenes to the renderer, the real
`MetalAtlas` means glyph/SVG rasterization happens during paint, and
`bench_renderer` presents after each measured update. Platforms without
a headless renderer degrade to discarding the scene.
## Example
```rust
#[gpui::bench]
fn editor_render(cx: &mut BenchAppContext) {
init_context(cx);
let buffer = cx.update(|cx| { /* build a MultiBuffer */ });
let mut window = cx.add_empty_window();
let editor = window.update(|window, cx| {
let editor = window.replace_root(cx, |window, cx| {
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
editor.set_style(editor::EditorStyle::default(), window, cx);
editor
});
window.focus(&editor.focus_handle(cx), cx);
editor
});
let mut move_down = true;
cx.bench_renderer(editor, move |editor, window, cx| {
if move_down {
editor.move_down(&MoveDown, window, cx);
} else {
editor.move_up(&MoveUp, window, cx);
}
move_down = !move_down;
});
}
```
## Example output (release, M-series)
```
editor_render time: [329.75 µs 330.17 µs 330.69 µs]
GPUI bench report (all observed iterations): editor_render
note: includes Criterion warmup/calibration
window dirty-to-draw:
samples: 31533
mean: 0.321ms
p50: 0.322ms
p90: 0.336ms
p95: 0.342ms
p99: 0.360ms
max: 0.504ms
frame budget overruns total: 0
frame budget overruns max: 0
window draw:
samples: 31533
mean: 0.295ms
p50: 0.295ms
p90: 0.307ms
p95: 0.313ms
p99: 0.330ms
max: 0.455ms
frame budget overruns total: 0
frame budget overruns max: 0
invalidations per frame: mean 5.00, max 5
```
(`invalidations per frame: mean 5.00` is real signal: each `move_down`
notifies the window five times before the draw.)
## Known limitations
- **Draw-per-flush**: the harness draws synchronously when effects flush
rather than coalescing invalidations to a vsync tick, so `dirty-to-draw`
excludes queueing delay, and `frame budget overruns` is a draw-time
budget proxy rather than actual missed presents. A frame-paced mode is
natural follow-up work.
- **GPU submission is measured on macOS only**; other platforms have no
headless renderer yet.
- The GPUI report includes Criterion warmup/calibration samples (noted
in the output); Criterion's `time` is the regression-gating number.
- `run_until_idle` waits for queued, running, and already-due work, but
not for timers that haven't reached their due time — the dispatcher runs
in real time and can't skip ahead like `TestDispatcher`'s virtual clock.
## Future work
- A vsync-like frame-pacing mode (suppress draw-on-flush; tick-driven
draw + present) so dirty-to-draw captures queueing delay
- Record present duration in `FrameTiming` so the report can split draw
vs present
- Benches that scroll through novel content (cold layout caches) and an
agent-panel render bench
- Headless renderers for Windows/Linux
- Move benches into a dedicated crate
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
140 lines
3.9 KiB
TOML
140 lines
3.9 KiB
TOML
[package]
|
|
name = "editor"
|
|
version = "0.1.0"
|
|
edition.workspace = true
|
|
publish.workspace = true
|
|
license = "GPL-3.0-or-later"
|
|
|
|
[lints]
|
|
workspace = true
|
|
|
|
[lib]
|
|
path = "src/editor.rs"
|
|
doctest = false
|
|
|
|
[features]
|
|
test-support = [
|
|
"text/test-support",
|
|
"language/test-support",
|
|
"gpui/test-support",
|
|
"multi_buffer/test-support",
|
|
"project/test-support",
|
|
"theme/test-support",
|
|
"util/test-support",
|
|
"workspace/test-support",
|
|
"tree-sitter-c",
|
|
"tree-sitter-rust",
|
|
"tree-sitter-typescript",
|
|
"tree-sitter-html",
|
|
"proptest",
|
|
"unindent",
|
|
]
|
|
|
|
[dependencies]
|
|
aho-corasick.workspace = true
|
|
anyhow.workspace = true
|
|
assets.workspace = true
|
|
base64.workspace = true
|
|
breadcrumbs.workspace = true
|
|
client.workspace = true
|
|
clock.workspace = true
|
|
collections.workspace = true
|
|
convert_case.workspace = true
|
|
dap.workspace = true
|
|
db.workspace = true
|
|
buffer_diff.workspace = true
|
|
emojis.workspace = true
|
|
feature_flags.workspace = true
|
|
file_icons.workspace = true
|
|
futures.workspace = true
|
|
futures-lite.workspace = true
|
|
fuzzy.workspace = true
|
|
fs.workspace = true
|
|
git.workspace = true
|
|
gpui.workspace = true
|
|
indoc.workspace = true
|
|
edit_prediction_types.workspace = true
|
|
itertools.workspace = true
|
|
language.workspace = true
|
|
linkify.workspace = true
|
|
log.workspace = true
|
|
lsp.workspace = true
|
|
markdown.workspace = true
|
|
menu.workspace = true
|
|
multi_buffer.workspace = true
|
|
ordered-float.workspace = true
|
|
parking_lot.workspace = true
|
|
pretty_assertions.workspace = true
|
|
project.workspace = true
|
|
proptest = { workspace = true, optional = true }
|
|
proptest-derive = { workspace = true, optional = true }
|
|
rand.workspace = true
|
|
regex.workspace = true
|
|
rpc.workspace = true
|
|
rope.workspace = true
|
|
schemars.workspace = true
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
settings.workspace = true
|
|
smallvec.workspace = true
|
|
snippet.workspace = true
|
|
sum_tree.workspace = true
|
|
task.workspace = true
|
|
telemetry.workspace = true
|
|
text.workspace = true
|
|
time.workspace = true
|
|
theme.workspace = true
|
|
theme_settings.workspace = true
|
|
tree-sitter-c = { workspace = true, optional = true }
|
|
tree-sitter-html = { workspace = true, optional = true }
|
|
tree-sitter-rust = { workspace = true, optional = true }
|
|
tree-sitter-typescript = { workspace = true, optional = true }
|
|
tree-sitter-python = { workspace = true, optional = true }
|
|
ztracing.workspace = true
|
|
tracing.workspace = true
|
|
unicode-segmentation.workspace = true
|
|
unicode-script.workspace = true
|
|
unindent = { workspace = true, optional = true }
|
|
ui.workspace = true
|
|
ui_input.workspace = true
|
|
url.workspace = true
|
|
util.workspace = true
|
|
uuid.workspace = true
|
|
vim_mode_setting.workspace = true
|
|
workspace.workspace = true
|
|
zed_actions.workspace = true
|
|
zlog.workspace = true
|
|
|
|
[dev-dependencies]
|
|
ctor.workspace = true
|
|
gpui = { workspace = true, features = ["test-support"] }
|
|
language = { workspace = true, features = ["test-support"] }
|
|
languages = {workspace = true, features = ["test-support"] }
|
|
lsp = { workspace = true, features = ["test-support"] }
|
|
markdown = { workspace = true, features = ["test-support"] }
|
|
multi_buffer = { workspace = true, features = ["test-support"] }
|
|
project = { workspace = true, features = ["test-support"] }
|
|
proptest.workspace = true
|
|
proptest-derive.workspace = true
|
|
release_channel.workspace = true
|
|
rand.workspace = true
|
|
semver.workspace = true
|
|
settings = { workspace = true, features = ["test-support"] }
|
|
|
|
text = { workspace = true, features = ["test-support"] }
|
|
theme = { workspace = true, features = ["test-support"] }
|
|
tree-sitter-c.workspace = true
|
|
tree-sitter-go.workspace = true
|
|
tree-sitter-html.workspace = true
|
|
tree-sitter-rust.workspace = true
|
|
tree-sitter-typescript.workspace = true
|
|
tree-sitter-yaml.workspace = true
|
|
tree-sitter-bash.workspace = true
|
|
tree-sitter-md.workspace = true
|
|
unicode-width.workspace = true
|
|
unindent.workspace = true
|
|
util = { workspace = true, features = ["test-support"] }
|
|
workspace = { workspace = true, features = ["test-support"] }
|
|
|
|
zlog.workspace = true
|