Skip to content

perf(gpui): stop the display link while a window has nothing to draw - #2725

Merged
richiemcilroy merged 3 commits into
mainfrom
perf/gpui-idle-frame-source
Oct 20, 2026
Merged

richiemcilroy merged 3 commits into
mainfrom
perf/gpui-idle-frame-source

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

What

An idle GPUI window on macOS still woke the main thread on every display refresh (121×/s on a 120 Hz display), because gpui keeps its CVDisplayLink subscription for as long as the window is visible, even when nothing is drawing.

This adds apps/desktop-gpui/patches/zed-idle-frames.patch, applied last on top of the existing gpui patch stack:

  • gpui: a FramePacer per window. After 100 ms of frame ticks with nothing to do (window clean, no next-frame callbacks, nothing to present), it asks the platform to stop frame requests. Any invalidation (invalidate_view, set_dirty(true) / refresh) or on_next_frame / request_animation_frame restarts them. 100 ms is long enough that steady low-rate redraws (30 fps video, timers) never stop and restart the link between frames.
  • PlatformWindow::frame_request_toggle() defaults to None, so Windows, Linux and the test platform behave exactly as before.
  • gpui_macos: the toggle unsubscribes the window from the display link and resubscribes it. On resume it also draws the waking frame right away on the main thread. Otherwise the restarted link only fires a refresh or two later (measured 17 ms wake→frame without this, 1.2 ms with it). While paused, occlusion, screen-change and activation handlers don't restart the link; the next invalidation does.

scripts/prepare-gpui-dependency.sh lists the new patch. Its tests gain a fourth fixture patch plus a case proving that a checkout prepared before a patch was added is moved onto the new stack automatically (local edits are still never overwritten).

Measured (M4 Max, release, 120 Hz display, 15 s windows)

Scenario main this PR
Main window visible, idle 0.80–0.91% CPU, 121 wakeups/s 0.01–0.02% CPU, 1 wakeup/s
Editor open, paused (4K project, audio stream idle-paused), 3 runs each 0.81–0.96% CPU, 121 wakeups/s 0.02–0.03% CPU, 1 wakeup/s
Editor playback, 4K60 59.8–60.1 fps, ~37–39% CPU same fps and frame count, ~37% CPU

Idle main window and paused editor now both match the tray-only baseline (0.02%).

Checks:

  • A fresh preparation from the pinned rev produces a tree byte-identical to the one measured.
  • node --test scripts/prepare-gpui-dependency.test.mjs: 8/8 pass.
  • During playback the pacer never pauses: busy ticks per second are identical with and without it.
  • After playback ends, ticks stop within 100 ms.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge, with the previous test-coverage concern addressed.

Summary

Stops macOS frame requests after a window has nothing to draw for 100 ms, then restarts them when work arrives.

  • Idle GPUI windows stop requesting frames until work returns.
  • GPUI preparation now includes the frame-pacing patch.

Reviews (2) · Last reviewed commit: "test(gpui): cover when the frame pacer s..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cap-status Ready Ready Preview Oct 20, 2026 2:37am UTC

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 1 other finding in Capy.

Open in Capy

Comment on lines +61 to +70
+ fn frame_finished(&self, busy: bool, now: Instant) {
+ let Some(toggle) = &self.toggle else {
+ return;
+ };
+ if busy {
+ self.busy_at.set(now);
+ } else if now.duration_since(self.busy_at.get()) >= Self::IDLE_DELAY
+ && !self.paused.replace(true)
+ {
+ toggle(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 FramePacer behavior lacks tests

The added tests cover patch installation, not whether a window stops and resumes frame requests correctly. Add focused tests with a recording toggle for the 100 ms cutoff, busy frames resetting the delay, repeated idle ticks stopping only once, and wake() restarting only once. Without these checks, changes could leave idle windows ticking or prevent them from waking while the tests still pass.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/patches/zed-idle-frames.patch
Line: 61-70

Comment:
**`FramePacer` behavior lacks tests**

The added tests cover patch installation, not whether a window stops and resumes frame requests correctly. Add focused tests with a recording toggle for the 100 ms cutoff, busy frames resetting the delay, repeated idle ticks stopping only once, and `wake()` restarting only once. Without these checks, changes could leave idle windows ticking or prevent them from waking while the tests still pass.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 76f950a: frame_pacer_tests in the patch covers the 100 ms cutoff, a busy frame restarting the delay, repeated idle ticks stopping once, wake() restarting once and only when stopped, a woken window waiting a full delay before stopping again, and the no-toggle (non-macOS) path. Cap CI does not run gpui's own tests, so I ran them in the zed workspace: cargo test -p gpui --lib -F gpui_platform/runtime_shaders → 212 passed, including the 6 new ones.

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

This branch was successfully deployed

2 active deployments
Preview – cap-web — 76f950af Deployed Oct 20, 2026 by vercel[bot]
Preview – cap-status — 76f950af Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant