Skip to content

perf: stop the graph and a full-window rebuild from stalling frames on integrated GPUs - #95

Merged
noahbclarkson merged 3 commits into
mainfrom
perf/integrated-gpu-frame-cost
Nov 4, 2026
Merged

noahbclarkson merged 3 commits into
mainfrom
perf/integrated-gpu-frame-cost

Conversation

@noahbclarkson

Copy link
Copy Markdown
Owner

Problem

On a laptop with an integrated GPU (Ryzen/Renoir, Hyprland, 60Hz) the app felt laggy: hover feedback trailed the pointer by 50–150ms and browsing commits dropped frames. Zed, on the same gpui renderer, was smooth. Two costs were stacking up on every frame:

  1. GPU: one full-window MSAA pass per path in view. gpui draws each run of paths it can't merge by rasterizing it into a full-window, 4× multisampled intermediate texture, then resolving and compositing that back. The graph drew each row's approach line, occluding ring and node dot as paths. Rows also painted 4–8px into their neighbours, which chained gpui's draw order down the list, so no two rows' paths could share a pass. A handful of visible rows meant 5–8 full-window passes per frame, about 40MB of memory traffic each, on a GPU that shares bandwidth with the CPU. Frames backed up in the GPU queue. The cost grew with window size, and hiding the graph didn't fix it.
  2. CPU: the whole window was rebuilt every frame. Nothing used gpui's view caching, so a hover that changed one row's background still re-rendered, laid out (taffy) and painted every panel: ~9–10ms of main-thread time on battery.

What changes

  • Graph dots and rings are quads (rgitui_graph/src/paint.rs). The approach line uses the existing lane quad. Only curves and the pinned-lane › chevron are still paths.
  • Nothing a row paints leaves the row. graph_cell_mask now clips top and bottom as well as the sides. Unrounded quads are not antialiased, so lane segments in adjacent rows still meet without a seam.
  • The main panels are cached views. The sidebar, graph, diff viewer, detail panel and commit panel each redraw only when that panel is notified (workspace/layout.rs).
  • Globals refresh the windows. Cached views don't see globals change, so the workspace now refreshes its windows when ThemeState or SettingsState changes. Language switches and focus changes already refresh.
  • Avatar images load in the resolver. gpui notifies only the first view that requested an image when it finishes loading. The resolver now starts each avatar's load itself and refreshes when it lands, so a cached graph can't miss an avatar the detail panel happened to request first.
  • GraphView::set_all_loaded now notifies. It was only ever called next to methods that do, so this is robustness rather than a fix.
  • Harness: a hover replay step and a hover-sweep scenario that sweeps the pointer over the sidebar, the graph rows and the window at 120Hz. Documented in docs/PERFORMANCE.md.

Visible change: the working-tree node's dashed outer ring now uses gpui's dashed border, so it has finer, lighter dashes than the previous 8 bold ones. Keeping the old dashes would add a path pass to every frame where that row is visible.

Measurements

--profile perf builds of main (cd9c4b4) and this branch, against this repository, laptop on charge, same window size. Path passes were counted with a temporary scene census in a local gpui copy, which isn't part of this PR.

Scenario Metric main This PR
graph-scroll path passes per frame 6–8 0–1
graph-scroll dropped frames (of ~580) 32 4
graph-scroll frame interval p95 26.1 ms 17.2 ms
graph-scroll draw p50 (main thread) 7.8 ms 5.5 ms
hover-sweep path passes per frame 5 1
hover-sweep draw p50 (main thread) 10.1 ms 1.5 ms
hover-sweep draw p95 (main thread) 11.0 ms 6.7 ms

Testing

  • Tested on Windows
  • Tested on macOS
  • Tested on Linux (NixOS, Hyprland/Wayland, AMD Renoir iGPU)
  • All existing tests pass (cargo test --workspace)
  • Code passes clippy (cargo clippy --workspace --all-targets -- -D warnings)
  • Code is formatted (cargo fmt --all -- --check)

Also checked:

  • Layout matches. Screenshots of main and this branch line up: line joins, curves and spacing are the same. With caching on and off, the screenshots match pixel for pixel apart from antialiasing noise.
  • Cached panels update live. Adding and removing an untracked file updated the cached sidebar's count (?2 → ?3 → ?2) with no interaction.
  • Hover is smooth by hand on the laptop, full-screen.

Not verified end to end: the avatar preload. While testing, GitHub's search API was rate-limited, so the avatars it affects couldn't resolve on main either.

🤖 Generated with Claude Code

noahbclarkson and others added 3 commits November 4, 2026 23:01
…s bounds

gpui rasterizes every run of paths it cannot merge into a full-window,
4x-multisampled intermediate texture, then resolves and composites it. The
graph painted each row's approach line, occluding ring and node as paths, and
let rows paint 4-8px into their neighbours, which chained draw order down the
list so no two rows' paths could share a pass. A frame ended up with one such
pass per path in view; on an integrated GPU that backed frames up in the GPU
queue and put hover feedback 50-150ms behind the pointer.

The approach line now uses the lane quad, discs and rings are quads (new
`paint` module), and the cell mask clips rows top and bottom as well as at the
sides. Unrounded quads are not antialiased, so lane segments in adjacent rows
still meet without a seam. Only curves and the pinned-lane chevron remain
paths.

`GraphView::set_all_loaded` now notifies, so it is correct regardless of what
it is called alongside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only that panel

Nothing used gpui's view caching, so every frame re-rendered, laid out and
painted the whole window: a hover changing one row's background cost a full
taffy layout of every panel. The sidebar, graph, diff viewer, detail panel and
commit panel are now cached views, reused until the panel itself is notified.

Cached views do not see globals change, so the workspace now refreshes its
windows when ThemeState or SettingsState changes.

Adds a `hover` replay step and a `hover-sweep` scenario to measure it. Main
thread draw time while hovering: p50 9.3ms -> 1.1ms, p95 10.4ms -> 6.1ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…raw when they land

When an image finishes loading, gpui notifies only the view that first asked
for it. With the main panels now cached views, a graph row whose avatar the
detail panel happened to request first would keep showing initials until
something else redrew the graph. The resolver now starts each avatar's image
load itself, under the same key `img(url)` uses, and refreshes the windows when
it completes, for avatars resolved now and ones remembered from an earlier
session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T10:36:25.426846Z 220195e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@noahbclarkson
noahbclarkson merged commit 70085d1 into main Nov 4, 2026
4 checks passed
@noahbclarkson
noahbclarkson deleted the perf/integrated-gpu-frame-cost branch November 4, 2026 10:53
noahbclarkson added a commit that referenced this pull request Nov 5, 2026
Changelog entry for everything since 0.5.0: #57, #89-#92 (already drafted
under [Unreleased]), plus new entries for #93, #95 and #98. The version is
bumped across the workspace, and the AppStream metainfo lists 0.5.1, which the
Linux metadata test requires.

The performance numbers are the ones measured in #95 and #98 with the
rgitui_perf scenarios on --profile perf builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant