Wayland: bound the frame-callback wait before delivering RedrawRequested - #4675
Wayland: bound the frame-callback wait before delivering RedrawRequested#4675zh4ngx wants to merge 3 commits into
Conversation
8fb18b4 to
b3235a6
Compare
Compositors may delay wl_surface.frame callbacks for occluded windows for seconds or indefinitely (GNOME/mutter does this). While a callback is in flight, the Wayland backend suppresses RedrawRequested, so under ControlFlow::Poll the event loop degenerates into a zero-timeout epoll spin (rust-windowing#4668: one core burned, epoll_pwait(timeout=0) and read(eventfd)=EAGAIN at a 1:1 ratio), and apps that only re-arm their repaint schedule from the redraw handler wedge entirely. Track when the current frame callback was requested, then: 1. deliver the pending RedrawRequested once the callback has been in flight for FRAME_CALLBACK_STALL_TIMEOUT (250ms), and 2. while a redraw is gated behind a non-stalled callback, wait for the compositor instead of honoring a zero timeout: nothing but the compositor or the stall deadline can unblock that redraw. Verified on GNOME Wayland with an eframe 0.35 app against the same change backported to winit 0.30.13: stock winit pins the main thread at 75-100% with the reported syscall signature (~34k epoll_pwait(timeout=0) per second under strace, each paired with a read(eventfd)=EAGAIN); with the change, idle CPU is 0.2% of a core, the app keeps repainting at the stall cadence when callbacks are withheld entirely, and the X11 backend is unaffected (0.0%).
b3235a6 to
e0b23b3
Compare
|
Additional evidence for the multi-window case, since "what if one window's redraw is gated and another's isn't?" is the obvious review question. Setup: raw-winit two-window probe (no eframe),
Takeaways:
One behavioral note, measured and disclosed: while every pending redraw is gated and the compositor is completely silent, a |
Extract the per-window gate folding from WinitState::frame_callback_gate into fold_frame_callback_gate and cover its semantics with unit tests: all gated redraws wait for the nearest stall deadline, any un-gated redraw opens the gate (deliver now), and no pending redraws means no gate. The behavior of frame_callback_gate is unchanged.
|
don't want to understand what you AI wrote given that makes no sense to me. I don't really care what you use if you understand what you're doing. |
|
Sorry, agents gone haywire :D |
Fixes #4668 (the Wayland zero-timeout spin / wedged event loop).
Root cause
The reporter's fingerprint —
epoll_pwaitwith a zero timeout, paired 1:1 withread(eventfd) = -1 EAGAIN, one core burned while no frames are dispatched —reproduces deterministically on GNOME Wayland with winit 0.30.13 + eframe, and
instrumentation shows where the zero timeout comes from:
ControlFlow::Polltogetherwith
Window::request_redraw()when a repaint becomes due, and only re-armsits repaint schedule once
RedrawRequestedis delivered(
eframe/src/native/run.rs,check_redraw_requests).RedrawRequestedwhile a frame callback is inflight (
if window.frame_callback_state() == FrameCallbackState::Requested { return None; }in the redraw dispatch).wl_surface.framecallbacks — GNOME/mutter,for one, throttles them for occluded windows, and the reporter traced
mutter withholding them for 10–300 s at a time. While the callback is
pending, every iteration of the loop computes
Some(Duration::ZERO)fromControlFlow::Polland spins: ~34kepoll_pwait(timeout=0)/s under straceon my box, each iteration also paying
timerfd_settime+ twoepoll_ctlread(eventfd)=EAGAIN(polling's notifier clear). Between thewithheld callbacks the app never receives
RedrawRequested, so it alsonever re-arms its repaint schedule — that is the "no frames, no input"
wedge of Wayland: event loop spins at 100% CPU polling with a zero timeout; no frames, no input #4668, reproduced here with a 60-line eframe app whose logic/ui
pass counters freeze while the main thread sits at ~100%.
The same gate exists on master (
winit-wayland/src/event_loop/mod.rs), so thisis not specific to the 0.30 line.
The fix
Two small parts in the Wayland backend:
WindowStaterecords when the current frame callbackwas requested; once it has been in flight for
FRAME_CALLBACK_STALL_TIMEOUT(250 ms) the pending
RedrawRequestedis delivered anyway instead of beingsuppressed forever. Presenting without a frame callback is protocol-legal;
frame callbacks are advisory pacing, and a compositor that withholds them
must not be able to wedge the client.
poll_events_with_timeout, when thecomputed timeout is zero only because of
ControlFlow::Polland everypending redraw is gated behind a non-stalled frame callback, wait for the
remaining stall interval instead. Nothing except the compositor (or the
stall deadline) can unblock that redraw, so a zero timeout cannot make
progress; user events and socket traffic still wake the loop immediately
through their registered sources. Plain
ControlFlow::Pollwith no gatedredraw keeps its existing behavior.
The gate-folding semantics — all pending redraws gated → wait for the nearest
stall deadline; any un-gated pending redraw → deliver now — are covered by unit
tests (
fold_frame_callback_gateinwinit-wayland/src/state.rs).Verification
Measured on NixOS, GNOME Wayland, kernel 7.2.0, with a 60-line eframe 0.35
(wgpu/Vulkan) app that calls
ctx.request_repaint_after(100ms); CPU is/proc/<pid>/statutime+stime of the main thread, sampled over 5–15 s windows.The same change backported to winit 0.30.13 (branch
wayland-frame-callback-stall-v0.30)was used for the runs, since eframe pins winit 0.30:
Also verified on a real application, not just the probe — a large
eframe 0.35 + wgpu 29 (Vulkan) terminal app where we originally hit this
(the main-thread 93–95% idle case), same debug build and measurement
method, patch applied via a path override onto its pinned winit 0.30.13:
Multi-window probe (raw winit,
ControlFlow::Poll)A second probe with no eframe exercises the multi-window case under
ControlFlow::Poll— the exact configuration of #4668 — with two windows:one whose frame callbacks never arrive (headless weston, a surface that
never commits a buffer: a stand-in for an occluded window whose callbacks
the compositor is withholding) and which continuously requests redraws, and
one on-demand window that never arms callbacks and asks for a redraw every
100 ms.
15 s measurement windows, main-thread utime+stime:
Stock shows both reported variants of #4668 simultaneously in one process:
the zero-timeout spin burning a core and the starved window never getting
RedrawRequested("no frames"). With the fix, an un-gated pending redraw isnever delayed behind a gated sibling —
request_redraw()pings the loop'sawakener and the redraw is dispatched in the next pass (measured ~0 ms; the
all-gated→wait / any-un-gated→zero distinction in
frame_callback_gate()keeps that immediate even when another window is waiting out its stall
interval).
Behavioral note: while every pending redraw is gated and the compositor is
silent, a
Pollloop now sleeps to the stall boundary (≤250 ms) instead ofspinning. Any Wayland event — input, configure, a frame callback for a
visible window — still wakes it immediately. Apps that need precise timers
should keep using
WaitUntil/timer sources (as eframe does once re-armed);a
Pollloop whose only activity is a fully callback-starved window tradeswakeup granularity for an idle core.
Real-compositor rerun (GNOME 50.4 / mutter Wayland, live session)
The multi-window probe was re-run against a real logged-in GNOME Wayland
session (same probe source, only the winit path override differs; mutter
withholds frame callbacks for the never-committed surface exactly as the
headless-weston stand-in predicts — gated window stays gated for real):
ControlFlow::PollSame numbers as the weston table above, now on mutter itself. The large
eframe/wgpu app from #4668 was also re-measured on this session with the
revised gate: idle Wayland 0.2% main thread (stock in the calm case —
visible window, callbacks flowing, no pending redraw — is also ~0.2%,
consistent with the mechanism: the spin needs a redraw gated behind a
withheld callback, which is what occlusion/minimization produces).
Resize and activation paths on real mutter with the fix: the initial
compositor configure (
Resized 1600x1200) and a clientrequest_inner_size()round-trip (returnsSome(new_size), appliedimmediately) both behave identically to stock, and the example app's
xdg_activationflow — request token →ActivationTokenDonedelivered →create window with token — completes through the event loop.
Earlier single-window run under headless weston
The gate refinement (wait only while every pending redraw is gated; an
ungated pending redraw keeps its immediate dispatch) was additionally
exercised under headless weston: 0.4% idle with the app's logic/UI pass
counters advancing at the compositor's repaint clock — calm and healthy
rather than spinning or wedged.
strace
-f -cover 12 s, stock vs fixed (Wayland):Still not verified: interactive keyboard input and drag/maximize resize on
a focused window with the patch. Keyboard injection into the session works
(uinput), but no programmatic way to focus the probe window could deliver
focus on this mutter — self-issued
xdg_activationtokens complete butdon't steal focus, and Shell Introspect/Screenshot are portal-locked — so
the injected keys go to whatever the human user has focused. The dispatch
machinery those events ride (wayland socket dispatch, configure handling,
frame callbacks) is exercised by the measurements above; a human-driven
check on a focused window is the remaining step. Happy to run more
scenarios on request.
A 250 ms stall timeout means a fully callback-starved window repaints at ~4 fps
instead of never; normal visible windows (callbacks at vsync) are unaffected
because the wait ends when the callback arrives.
The same change backported onto the
v0.30.xline is onwayland-frame-callback-stall-v0.30(based on tag v0.30.13; that's the build the runtime numbers above were
measured with, since eframe and most of today's apps still pin 0.30). Happy to
open a backport PR against
v0.30.xif wanted.