Skip to content

perf(gpui): stop vblank redraws of idle windows on Windows and X11 - #3104

Open
richiemcilroy wants to merge 5 commits into
mainfrom
perf/idle-vblank-win-x11
Open

richiemcilroy wants to merge 5 commits into
mainfrom
perf/idle-vblank-win-x11

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Problem

zed-idle-frames.patch stops the frame source of an idle window only on macOS. frame_request_toggle defaults to None (apps/desktop-gpui/patches/zed-idle-frames.patch:31), so on Windows and Linux the frame pacer does nothing and every visible window keeps waking the main thread once per vblank while it has nothing to draw:

  • Windows: the VSyncProvider thread calls RedrawWindow(RDW_INVALIDATE) on every window after every vblank (patched zed crates/gpui_windows/src/platform.rs:342-346). Each one becomes a WM_PAINT that runs gpui's frame callback.
  • X11: each visible window has a calloop timer at the display refresh rate (patched zed crates/gpui_linux/src/linux/x11/client.rs:2000-2036).

Fix

Implement frame_request_toggle for Windows and X11 in zed-idle-frames.patch. The gpui-side pacer is unchanged.

  • Windows: the platform keeps a list of paused HWNDs. Stop adds the window; Start and Once remove it. The vsync loop skips paused windows when it calls RedrawWindow, and that is the only change to the loop. The thread still wakes every vblank, so check_device_lost / handle_gpu_device_lost run exactly as before. Device-lost recovery reaches paused windows because it sends WM_GPUI_GPU_DEVICE_LOST and WM_GPUI_FORCE_UPDATE_WINDOW to every window with SendMessageW. Once resumes the vsync redraws instead of drawing directly. The pacer stops them again after the frame, and until a frame arrives the window is redrawn every vblank as before, so a hidden or minimized window can't miss its frame. A new window clears any stale entry for its handle, and a dropped window removes its own.
  • Windows touchpad gestures: draw_window is what pumps Direct Manipulation, which carries precision-touchpad two-finger scrolling and pinch. DM_POINTERHITTEST takes the window off the paused list, and Stop is refused from a successful SetContact until the viewport is READY again after the gesture and its inertia.
  • X11: Stop removes the window's refresh timer; Start and Once restart it (an immediate tick), and the pacer stops it again after the frame. The toggle runs deferred on the foreground executor because the pacer calls it from inside frames and event handlers that may hold the client state. An Expose event on a paused window restarts the timer so the exposed area is presented. A window still owed force_render_after_recovery, or with an Expose still waiting to be presented, keeps its timer until that frame has run.
  • Wayland is left alone, since its first configure and can_draw depend on the frame callbacks.

Removed lines: the vsync loop's for hwnd ... RedrawWindow block (same call, now over the unpaused windows), the X11 update_refresh_loop match head, the Expose loop's binding name, the SetContact call (now also records the contact) and the DM_POINTERHITTEST dispatch (now passes the handle). All of them are upstream zed lines; none come from zed-windows.patch or zed-linux.patch. The device-loss code, force_render_after_recovery, skip_draws / mark_drawable, invalidate_devices and the #2472 display, DPI, fit_window_rect and IsWindowVisible hunks are untouched.

ci.yml: the gpui-patch-tests filter now includes frame_pacer_tests (it ran only stream before) and, on Windows, the new frame_request_tests and direct_manipulation tests.

Results

Same runner for base (main) and head, each idle window measured for 60 s, 3 rounds, alternating order, after an 8 s (probe) or 20 s (app) settle. Median (min to max). Main-thread wakeups are context switches of the process's first thread.

Real Cap app (cap-gpui main window, fresh CAP_GPUI_APP_DATA_DIR, onboarding skipped), run 40100331504

main-thread wakeups/s process wakeups/s process CPU
Linux X11, base 62.50 (62.50-62.50) 62.63 (62.63-62.63) 0.643 ms/s (0.559-0.688)
Linux X11, head 0.00 (0.00-0.00) 0.13 (0.13-0.13) 0.009 ms/s (0.009-0.009)

I couldn't measure the real app idle on the hosted Windows runner. There, both base and head redraw the main window continuously: about 38 main-thread wakeups/s and about 3.8 cores of CPU, almost all of it outside the main thread, with no difference between the two. A window that never goes idle never pauses, so this change can't affect it. The Windows numbers below come from the minimal window instead.

Minimal gpui window built against the same patched gpui (idle_probe), run 40096753616

main-thread wakeups/s process wakeups/s process CPU main-thread CPU
Windows idle, base 44.47 (44.45-45.30) 87.41 (87.36-88.90) 7.49 Mcycles/s (7.45-7.62) 3.63 Mcycles/s (3.61-3.74)
Windows idle, head 0.00 (0.00-0.00) 42.47 (42.03-42.50) 1.65 Mcycles/s (1.62-1.68) 0.00
Windows 1 Hz clock, base 44.13 (44.06-45.76) 100.76 (100.19-102.38) 42.8 Mcycles/s (39.1-45.4) 6.86 Mcycles/s (4.51-8.31)
Windows 1 Hz clock, head 2.07 (2.00-2.13) 57.89 (57.76-58.06) 34.5 Mcycles/s (33.7-35.7) 1.14 Mcycles/s (1.12-1.22)
Linux X11 idle, base 62.50 (62.50-62.50) 62.50 (62.50-62.50) 0.322 ms/s (0.308-0.337) 0.322 ms/s
Linux X11 idle, head 0.00 (0.00-0.00) 0.00 (0.00-0.00) 0.000 ms/s 0.000 ms/s
Linux X11 1 Hz clock, base 68.03 (67.86-68.30) 123.36 (123.23-124.26) 2.92 ms/s (2.91-3.01) 0.70 ms/s (0.68-0.70)
Linux X11 1 Hz clock, head 5.77 (5.43-6.03) 62.76 (61.60-63.00) 2.72 ms/s (2.59-2.75) 0.47 ms/s (0.46-0.49)

The remaining ~42 process wakeups/s on Windows are the VSyncProvider thread, which keeps ticking on purpose so device-loss checks still run every vblank. The "1 Hz clock" variant redraws once a second, like the recording bar timer.

In absolute terms the CPU saved is small (the idle Windows window drops from about 7.5 to 1.7 Mcycles/s, roughly 0.3% to 0.07% of one core). The main win is that an idle Cap window no longer wakes the UI thread 44-62 times a second, which keeps the CPU out of deep idle states.

Repaint on demand. idle_probe check runs on both variants and both platforms. It waits past the pacer's idle delay before each step, then checks:

  • six notifies on an idle window each draw a frame, and a screenshot of the window shows the new color (12/12 pixel checks pass on both platforms). On X11 the check first covers and uncovers the window 60 to 160 ms after each change, around when the pacer pauses, so the pixels also prove the Expose was presented (run 40106160444);
  • nothing redraws while idle;
  • an animation started from idle runs continuously (Windows 43 frames/s head vs 45 base; Linux 22 vs 22 on the software renderer);
  • on_next_frame and Window::resize on an idle window both draw.

Repaint latency is unchanged or lower: Windows 15.5-31 ms on both (vblank-aligned), X11 2.0 ms head vs 4.1 ms base.

Tests. frame_request_tests::a_paused_window_skips_vsync_redraws_but_still_gets_the_device_lost_forced_render (Windows, WARP device) pauses a window, checks the vsync loop skips it, runs the real handle_gpu_device_lost and checks the paused window still receives WM_GPUI_GPU_DEVICE_LOST and WM_GPUI_FORCE_UPDATE_WINDOW, then checks a frame request resumes the redraws and the paused list empties. a_manipulation_lasts_through_its_inertia_until_the_viewport_is_ready drives the Direct Manipulation event handler through RUNNING, INERTIA and READY.

How it was measured

Scratch branch ci/idle-vblank-ab (runs linked above):

  • Windows runs on GitHub-hosted windows-2022, because Blacksmith Windows runs jobs in session 0, which has no desktop.
  • Linux runs on blacksmith-8vcpu-ubuntu-2404 under Xvfb with Mesa lavapipe.
  • Each job builds base, then re-applies the head branch's patches to the same ../zed-cap and rebuilds.
  • Counters come from NtQuerySystemInformation and QueryThreadCycleTime on Windows, and from /proc/<pid>/task/*/status and schedstat on Linux.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no actionable new issues were found.

Summary

Stops idle-window redraw requests on Windows and X11 while keeping on-demand redraws available.

  • Idle Windows windows stop getting routine vblank redraws.
  • Idle X11 windows stop getting refresh-timer redraws.

Tests were not run during this review.

Reviews (3) · Last reviewed commit: "ci: run the Direct Manipulation test wit..." · Reviewed by Greptile

@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 2 potential issues (2 medium).

View 1 other finding in Capy.

Open in Capy

Comment thread apps/desktop-gpui/patches/zed-idle-frames.patch
Comment thread apps/desktop-gpui/patches/zed-idle-frames.patch
@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

Comment thread apps/desktop-gpui/patches/zed-idle-frames.patch Outdated
@vercel

vercel Bot commented Oct 24, 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 24, 2026 3:16pm UTC

@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@richiemcilroy

Copy link
Copy Markdown
Member Author

Real Cap app on Windows (GitHub-hosted windows-2022, WARP, main window with onboarding skipped via the first_run path, 3 rounds alternating, 25 s settle then 30 s measured; runs 40104582510 and 40107488101):

Main window Main-thread wakeups/s Process wakeups/s Process Mcycles/s Main-thread Mcycles/s
main 44.16 (43.93-44.36) 87.9 (87.4-88.4) 8.48 (8.42-8.63) 4.34 (4.33-4.42)
this PR 0.03 (0.03-0.13) 42.6 (42.5-42.7) 1.91 (1.90-1.92) 0.010 (0.008-0.016)

On main the 44 wakeups/s are vblank frame callbacks with no draws. The earlier "never idles" Windows reading was the first-run Welcome window, whose ambient animation is meant to run while it's active.

@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 2 other findings in Capy.

Open in Capy

This branch was successfully deployed

2 active deployments
Preview – cap-web — f9aaa87e Deployed Oct 24, 2026 by vercel[bot]
Preview – cap-status — f9aaa87e Deployed Oct 24, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant