1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Don't busy-loop a CPU core while waiting for a redraw (#8398)

Closes #8326 

`check_redraw_requests` switched the event loop to `ControlFlow::Poll`
every time it called `request_redraw`, and only ever restored a sleeping
control flow when a *timed* repaint was still pending. Once the last
scheduled repaint had been consumed the `Poll` was never undone, so the
loop kept spinning.

This is most visible on Wayland, where `RedrawRequested` is only
delivered after the compositor sends a frame callback: between the
request and the callback eframe burns 100% of a CPU core, so simply
moving the mouse over a reactive app pegs a core.

`request_redraw` already wakes the event loop on its own, so the `Poll`
is not needed. Drop it, and always set an explicit sleeping control flow
at the end of `check_redraw_requests`: `WaitUntil` for the earliest
scheduled repaint, `Wait` when nothing is scheduled.

**Measured effect of this patch**

Two byte-identical eframe apps (a 400-row scrolling page, free-running
at 60fps), toggling only whether `eframe` resolves to stock 0.36.0 or
this patch. Same machine and session, native Wayland (niri,
wgpu/Vulkan). Whole-process CPU is `utime+stime` from `/proc/self/stat`,
so it counts every thread — what a system monitor sees.

| configuration | whole process |
|---|---|
| eframe 0.34.3, Wayland | ~12% of a core |
| stock 0.36.0, Wayland | **99–100% of a core** |
| stock 0.36.0, XWayland (same binary) | ~15% of a core |
| **0.36.0 + this patch, Wayland** | **15–16% of a core** |

The patch restores the 0.34 baseline and matches the XWayland figure for
the same binary — ~6.5× less CPU — with frame delivery unchanged at
60fps.

Two details worth noting: the same 0.36 binary is already fine on
XWayland, so this isn't application repaint behaviour; and the per-frame
*closure* cost rises slightly (1.55 to 2.15 ms) because those frames now
run on a CPU that isn't being held at max clocks by the spin loop.

* [X] I have followed the instructions in the PR template
This commit is contained in:
42Pupusas
2026-08-11 05:52:12 -06:00
committed by GitHub
parent 46ba6405bf
commit b42d2ef4f0

View File

@@ -207,7 +207,12 @@ impl<T: WinitApp> WinitAppWrapper<T> {
invisible_window_ids.push(*window_id);
} else {
log::trace!("request_redraw for {window_id:?}");
event_loop.set_control_flow(ControlFlow::Poll);
// Don't switch to `ControlFlow::Poll` here. `request_redraw`
// is enough to wake the event loop, and on Wayland the
// `RedrawRequested` event is only delivered once the
// compositor sends a frame callback. Polling in the meantime
// busy-loops a whole CPU core.
// See https://github.com/emilk/egui/issues/8326.
window.request_redraw();
}
} else {
@@ -237,10 +242,16 @@ impl<T: WinitApp> WinitAppWrapper<T> {
}
}
// Always set an explicit, sleeping control flow. Previously we only set
// `WaitUntil` when a repaint was already scheduled, which meant that a
// `ControlFlow::Poll` set earlier was never undone once the last timed
// repaint had been consumed, leaving the loop spinning.
// See https://github.com/emilk/egui/issues/8326.
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
}
event_loop.set_control_flow(match next_repaint_time {
Some(next_repaint_time) => ControlFlow::WaitUntil(next_repaint_time),
None => ControlFlow::Wait,
});
}
}