Commit Graph
9 Commits
Author SHA1 Message Date
Kirill Bulatov b6490c5fbd Stop re-resolving editorconfig sections on every language settings lookup (#63323)
Closes https://github.com/zed-industries/zed/issues/59689

Symbolicating the reporter's profile (21s of typing, Windows) showed
`Window::draw` taking 63% of the main thread at ~62 redraws/s (p95 draw
= 40ms), and 30.9% of the *entire* main thread inside
`EditorconfigStore::properties()`:
every `LanguageSettings` lookup — several per frame via
`DisplayMap::tab_size`, `MultiBuffer::language_settings`, indent guides,
and `LspStore::supports_range_formatting` — re-ran the full editorconfig
resolution (ancestor walk plus `ec4rs::Section::apply_to` over every key
of every section) and then cloned the whole `LanguageSettings` struct.
Real-world `.editorconfig` files carry hundreds of irrelevant keys
(`dotnet_*`, `ij_*`, ...), so the per-call map assembly dominated:
`RtlAllocateHeap` 7.3%, `memcmp` 7.0%, `memcpy` 6.6%,
`Properties::find_idx` 5.9% self time.

## Commit 1: resolve only the editorconfig properties Zed reads

- `EditorconfigStore::properties()` now applies only the 7 keys Zed
consumes (`indent_style`, `indent_size`, `tab_width`, `end_of_line`,
`max_line_length`, `insert_final_newline`, `trim_trailing_whitespace`)
instead of materializing every key of every matching section.
- Worktrees with no editorconfig at all now return `None` early instead
of allocating an empty `Properties` and forcing a `LanguageSettings`
clone on every lookup.
- Adds an `editor_render_with_editorconfig` benchmark: maximized window,
file-backed buffer at `src/app/components/editor_pane.ts`, a root
`.editorconfig` shaped after [the second
report](https://github.com/zed-industries/zed/issues/59689#issuecomment-5475124891)
(no `root = true`, a `[*]` section with ~3200 keys, 25 sections with
large `{...}` alternation lists) plus a 4-level nested chain, and a
typing workload (insert/delete + cursor moves).

```
main (f034409b6a):      time: [37.905 ms 38.005 ms 38.107 ms]
                        time: [38.069 ms 38.157 ms 38.248 ms]
this commit:            time: [2.8547 ms 2.8618 ms 2.8702 ms]
                        time: [2.8372 ms 2.8435 ms 2.8510 ms]
```

The candidate is ~13x faster.
GPUI frame report over the same workload: window draw p50 12.71 ms →
0.76 ms, p99 23.38 ms → 1.22 ms; frame budget overruns 556 → 0.

## Commit 2: cache resolved `LanguageSettings` per buffer

- `Buffer` and `BufferSnapshot` now hold `resolved_settings:
Arc<LanguageSettings>`, recomputed by a `SettingsStore` global observer.
- Global methods for retrieving the settings are still there, but buffer
only uses them for multi buffer's `language_settings_at` with an
injected language different from the buffer's primary; cold scopes such
as extension API, LSP workspace configuration, server startup still use
the global resolution

Same benchmark, same method, on top of commit 1 (alternated runs, quiet
machine):

```
commit 1:               time: [2.8547 ms 2.8618 ms 2.8702 ms]
                        time: [2.8372 ms 2.8435 ms 2.8510 ms]
this commit:            time: [1.6256 ms 1.6325 ms 1.6403 ms]
                        change: [-42.685% -42.270% -41.854%] (p = 0.00 < 0.05)
                        time: [1.6221 ms 1.6284 ms 1.6352 ms]
```

Window draw p50 0.76 ms → 0.47 ms, p99 1.22 ms → 0.69 ms, max 1.02 ms;
zero frame-budget overruns on both sides.
Combined, the benchmark's per-keystroke cost drops ~23x vs `main`
(37.9–38.2 ms → 1.62–1.64 ms).


Release Notes:

- Improved typing and rendering performance in projects that use
`.editorconfig` files.
2026-08-31 11:58:50 +00:00
09440c85cb lsp: Add client support for LSP 3.18 markdown diagnostic messages (#61030)
LSP 3.18 allows servers to send [`MarkupContent` diagnostic
messages](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#diagnostic),
guarded by the client capability
`textDocument.diagnostic.markupMessageSupport`.

- Advertise `markup_message_support` only while pull diagnostics are
enabled because `textDocument.diagnostic` is the pull-model capability.
  LSP 3.18 currently has no equivalent capability for push diagnostics:

https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#diagnosticClientCapabilities
  https://github.com/microsoft/language-server-protocol/issues/2246
- Handle `string | MarkupContent` messages in both push and pull
diagnostic paths: markdown-kind messages populate the existing
`Diagnostic::markdown` field (rendered in hover popovers and the
diagnostics panel), plaintext-kind markup remains plain text, and plain
string messages keep going through the adapter's
`diagnostic_message_to_markdown`.
- Preserve server-provided markup for code actions sent back to the
originating server, avoid forwarding markup diagnostics to other
servers, and render diagnostic metadata without altering the
server-provided markdown.
- Carry the markup kind over RPC via a new optional
`LspDiagnostic.markup_message_kind` proto field so remote/collab
projects round-trip `MarkupContent` messages.

Depends on the lsp-types change that turns `Diagnostic.message` into a
`DiagnosticMessage` enum (`string | MarkupContent`):
https://github.com/zed-industries/lsp-types/pull/15
Once merged, Cargo.toml can point to the upstream commit.

Note that Zed already rendered some plain-string diagnostics as Markdown
through language-specific adapter hooks, including `rust-analyzer`. This
adds protocol-level support for diagnostics explicitly sent as LSP 3.18
`MarkupContent`, while preserving the existing adapter fallbacks.

As a result, this does not introduce a new diagnostics rendering
pipeline.
Server-provided Markdown is routed through the existing
`Diagnostic::markdown` path, with small fixes to handle Markdown-only
updates and block content correctly.

## Showcase

Example from [JETLS.jl](github.com/aviatesk/JETLS.jl) (a Julia language
server):

> Before
<img width="924" height="160" alt="Screenshot 2026-07-15 at 16 05 40"
src="https://github.com/user-attachments/assets/b19fe9e3-50e3-44e4-9b4d-ac8bd438d3d6"
/>

> After
<img width="924" height="160" alt="Screenshot 2026-07-15 at 14 13 05"
src="https://github.com/user-attachments/assets/359c3382-118d-4fa6-aa91-6bd3661d3d28"
/>

---

Release Notes:

- Added support for rendering LSP 3.18 Markdown diagnostic messages from
language servers.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2026-08-27 12:55:42 +00:00
Anthony Eid 328ad39817 gpui_platform: Expose headless renderer through bench support (#63244)
GPUI's `bench-support` feature provides the benchmark context and
headless-renderer interface without enabling `test-support`, but the
platform implementation remained available only through
`gpui_platform/test-support`. As a result, consumers could not use
`#[gpui::bench]` with a fully isolated feature graph because the
generated benchmark harness calls
`gpui_platform::current_headless_renderer()`.

This threads `bench-support` through `gpui_platform`, `gpui_macos`, and
`gpui_apple`, exposing the existing Metal headless renderer without
widening unrelated test window or prompt APIs. The existing benchmark
package now requests the platform's canonical `bench-support` feature.
Its other test-backed fixtures are intentionally unchanged and remain
outside this focused dependency fix.

Validation:

- Confirmed `gpui_apple/bench-support`, `gpui_macos/bench-support`, and
`gpui_platform/bench-support` resolve no `test-support` feature with
`cargo tree --locked --no-default-features --features bench-support -e
no-dev,features`.
- Ran `cargo check --locked -p benchmarks --benches` to compile every
existing `#[gpui::bench]` target against the real platform headless
renderer.
- Ran `cargo nextest run --locked -p gpui_apple -p gpui_macos -p
gpui_platform`.
- Ran `./script/clippy` for each changed platform package with
`--no-default-features --features bench-support`.
- Ran `cargo fmt --check`.

Release Notes:

- N/A
2026-08-26 11:00:10 +00:00
Anthony Eid 1f1220db93 gpui: Separate benchmark support from test support (#63039)
`gpui::bench` previously required GPUI's full `test-support` feature.
Any benchmark using the supported GPUI harness therefore compiled
test-only APIs and fakes, even when the benchmark otherwise followed
production dependency paths.

This change introduces `bench-support` as GPUI's canonical benchmark
feature and keeps `bench` as a compatibility alias for existing
consumers. The benchmark feature now exposes only the existing GPUI
internals that `BenchAppContext` needs: the real threaded dispatcher,
profiler integration, and a headless platform surface. Benchmark HTTP
requests use the production `BlockedHttpClient`, so accidental network
access fails instead of silently entering a fake response path.

The current headless implementation reuses the cfg-stripped core of
`TestPlatform`. Test-only prompt state, simulation APIs, and test
executors remain unavailable under `bench-support`. Cargo feature-tree
checks confirm that both `bench-support` and the compatibility alias
remain independent of `test-support`.

This also adds a `gpui-bench` agent skill covering production-shaped
fixtures, frame and foreground responsiveness, macOS headless Metal
rendering, deterministic correctness guards, Criterion measurement,
Instruments symbolication, and the requirement that a benchmark's
complete dependency graph contain no `test-support` feature.

Two follow-ups are intentionally deferred to keep this feature-isolation
change reviewable:

- Extract the shared headless implementation into
`HeadlessPlatformCore`, with separate `BenchPlatform` and `TestPlatform`
wrappers. This will remove benchmark-related cfg branches from the
test-facing type and make their supported surfaces explicit.
- Add a shared scripted HTTP client that compiles independently under
either `http_client/test-support` or `http_client/bench-support`. Tests
and benchmarks could then serve predefined responses through the same
deterministic transport boundary without either support feature enabling
the other; the default benchmark context should remain network-blocked.

This PR only isolates GPUI itself. Migrating downstream benchmark
fixtures that explicitly request other crates' `test-support` features
remains separate work.

Testing performed:

- `cargo tree --offline --package gpui --no-default-features --features
bench-support --edges features --invert gpui`
- `cargo tree --offline --package gpui --no-default-features --features
bench --edges features --invert gpui`
- `cargo check -p gpui --no-default-features --features bench-support`
- `cargo check -p gpui --no-default-features --features bench`
- `git diff --check`

Release Notes:

- N/A
2026-08-26 09:51:50 +00:00
Neel 255880598d Switch from cargo-machete to cargo-shear (#62643)
This results in ~110 crates being removed, and some orphaned files also.

---

Release Notes:

- N/A

Signed-off-by: Neel <neel@zed.dev>
2026-08-20 10:08:48 +00:00
Kirill Bulatov 4f3ea0998f Reuse char-scan invisibles detection in highlight_invisibles (#62715)
Follow-up to
https://github.com/zed-industries/zed/pull/62478#discussion_r3769810809

New bench results:

| corpus | old | new | speedup |
|---|---|---|---|
| ascii, no invisibles | 83 MB/s | 580 MB/s | **7.0x** |
| unicode, no invisibles | 88 MB/s | 442 MB/s | **5.0x** |
| sparse invisibles | 63 MB/s | 431 MB/s | **6.9x** |
| dense invisibles | 82 MB/s | 109 MB/s | 1.3x |


Release Notes:

- N/A
2026-08-17 09:45:39 +00:00
Anthony EidandAnant Goel 21a80e7a53 perf: Add initial benchmark for markdown element (#59524)
## Summary

This PR adds an initial markdown element renderer benchmark, so we could
later expand this to benchmark search in markdown, and reparsing. This
is important because the agent panel renders a lot of markdown elements,
so I want to use these benchmarks to ensure our markdown element
performance is good, and later expand them to include the agent panel

I added a `bench_util.rs` file that has methods to generate random rust
modules, this will later be used by the editor benchmarks, and is
currently used by the markdown and edit_file_tool benchmarks.

Finally, I made `cx.bench_renderer` also account for ready foreground
tasks and poll them. This more accurately mimics gpui dispatcher. (It's
not perfect, but it's good enough for a benchmark)


## 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 adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: Anant Goel <anant@zed.dev>
2026-06-18 02:47:10 +00:00
RaniandAnthony Eid a41ccd1206 editor: Speed up multi cursor editing (#58510)
Early draft for #32051 (multi-cursor editing is very slow, and basically
hangs at high cursor counts). Opening it early like @Anthony-Eid
suggested so we can agree on direction before I go further.

## Root cause

Typing with a cursor on every line is mostly O(N)-per-keystroke work
spread across a few places. I profiled it with `sample` and per-phase
timers around `handle_input`. At 1000 cursors (~24ms before this PR):
the post-edit display-map sync is ~11.8ms, the CRDT edit
(`apply_local_edit`) ~3.7ms, resolving all N selections plus the
per-cursor input loop ~3.5ms, the post-edit selection round-trip ~2.6ms,
and `change_selections` plus transact machinery ~2.4ms. The display sync
re-runs per-edit `SumTree` work through every layer even when nothing
transforms (the plain-text case), and selections get re-resolved through
the full display round-trip several times per keystroke.

## What this does

- Display fast paths: `InlayMap::sync` (no inlays) and
`WrapMap::interpolate` (no soft-wrap) skip the O(edits) transform-tree
rebuild and return the passthrough snapshot, gated so a pending inlay
splice or existing wraps still take the slow path.
- Selection fast path: when nothing collapses buffer content
(`!has_folds() && !has_replacement_blocks()`), resolve `Anchor` to
`Point` to `Offset` batched and skip the per-selection display
round-trip (the `todo(lw)`).
- Render and autoscroll resolve only the first/last/newest selections
instead of all N per frame.
- A parameterized `Multi-cursor input/cursors/{1000,10000,100000}`
benchmark.

## Results

- Typing (`handle_input`): ~2.1x faster (24ms to 11.7ms at 1k).
- Type plus two word-deletes (criterion): 29% faster (89.9 to 63.8ms) at
1k, 37% (959 to 606ms) at 10k.
- Post-edit display sync alone: 11.8 to 3.8ms at 1k, 133 to 45ms at 10k.

All `editor` (759), `display_map` (67), `multi_buffer` (58), and `text`
(36) tests pass, and `./script/clippy` is clean.

## Direction

This is ~2x, and I can get the current architecture to roughly 3.5-4.5x
with a few more safe changes (hoisting the per-cursor
language/editability checks, cheaper snapshot clones). Genuine VS Code
numbers (~1-2us/cursor) aren't reachable while the buffer is a CRDT rope
of fragments with anchor selections and a 5-layer transform stack. That
would need a plain-offset cursor model and/or decoupling display layout
from the edit path, which is a bigger effort I'd want to design with
you. One thing I'd need your call on: `set_active_selections` sends an
`UpdateSelections` collab op every keystroke (building N anchors); can
that be debounced to transaction end, or do presence/follow-mode
features rely on per-keystroke cursor broadcast?

Release Notes:

- editor: Improved multi cursor editing performance

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
2026-06-17 07:57:05 +00:00
Anthony Eid 6d9ddb96b2 Bench app context phase 2 (#58202)
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
2026-06-10 09:26:50 +00:00