Description
Edited twice on 2026-08-19. The original report claimed a ptrace attach
recovered it and that resume from suspend was the trigger; neither holds, and both are
retracted below. The second edit adds the important part: it reproduces with a forty
line eframe app, whose full source is included, wedged in the same session with an
identical syscall cycle.
Summary
An eframe/winit app on Wayland stops dispatching. The window keeps its last frame, no
input ever reaches the app again, and the main thread pins one of my 16 cores until
something outside the process changes. Three occurrences so far.
While wedged, the loop calls epoll_pwait with a zero timeout about 9,400 times per
second, gets zero events, re-arms its registrations, repeats. Nothing is ever ready; the
poller never blocks and the loop never dispatches. Hence no paint, no input.
Versions
| crate |
version |
| winit |
0.30.13 |
| calloop |
0.13.0 |
| polling |
3.11.0 |
| wayland-client |
0.31.15 |
| smithay-client-toolkit |
0.19.2 |
| eframe |
0.36.1 |
| egui |
0.35.0 |
| wgpu |
29.0.4 |
Ubuntu kernel 7.0.0-22-generic (PREEMPT_DYNAMIC), GNOME Shell (--mode=ubuntu) on
Wayland. Hybrid graphics: AMD Radeon 680M drives the display, NVIDIA RTX 2050 alongside
(driver 580.173.02). Debug build, wgpu backend.
The Loop
strace -c, 10 s, while wedged:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
39.85 0.466938 2 188915 epoll_ctl
21.00 0.246064 2 94458 timerfd_settime
19.82 0.232295 2 94457 94457 read
19.33 0.226567 2 94457 epoll_pwait
Every iteration is identical:
timerfd_settime(6, 0, {it_interval={0,0}, it_value={0,0}}) = 0 # disarm the timer
epoll_ctl(4, EPOLL_CTL_MOD, 6, {EPOLLIN|EPOLLPRI|EPOLLERR|EPOLLHUP|EPOLLONESHOT}) = 0
epoll_pwait(4, [], 1024, 0, NULL, 1024) = 0 # timeout 0, no events
read(5, 0x7ffe8fadfc70, 8) = -1 EAGAIN # notifier empty
epoll_ctl(4, EPOLL_CTL_MOD, 5, {EPOLLIN|EPOLLPRI|EPOLLERR|EPOLLHUP|EPOLLONESHOT}) = 0
fd 4 is the epoll, fd 6 the timerfd, fd 5 an eventfd; the 8 byte read is polling's
wakeup notifier and it is empty every time (24,554 EAGAIN in 24,555 calls over a
separate 5 s sample).
A zero timeout forever is what a ControlFlow::WaitUntil deadline stuck in the past looks
like from underneath: deadline - now saturates to zero on every pass, so polling polls
instead of waiting, and nothing advances the deadline because that needs a dispatch which
never happens. I cannot tell from here which deadline it is, or what keeps it in the past.
The Compositor Misbehaves At The Same Moments
gnome-shell logs a known mutter assertion, and the two largest clusters in the whole
boot land on my first two wedges:
gnome-shell[8114]: meta_window_set_stack_position_no_sync:
assertion 'window->stack_position >= 0' failed
| when |
assertions that minute |
app |
| 01:23 |
8 |
first wedge begins |
| 11:38-11:39 |
9 |
second wedge begins |
| 11:45-11:47 |
9 |
still wedged, then recovers |
| rest of boot |
1 to 5, scattered |
normal |
That is GNOME/mutter#1647, open and Critical, reported for years with triggers filed
against several unrelated apps (fullscreen mpv, Evolution, Nautilus, Telegram Desktop).
Telegram runs here and its D-Bus IdleMonitor calls appear minutes before both clusters,
so I suspect my compositor's stacking state is corrupted by something other than my app.
Not filing this as "mutter's fault, never mind". The compositor is the trigger; the bug I
report is the response. A client that loses the compositor's cooperation should idle or
complain, not burn a core indefinitely and silently stop accepting input.
Incidents
| # |
began |
ended how |
| 1 |
~01:23, about 8 min after resume from a 7 h s2idle suspend |
~10:10, unclear; coincided with a gdb attach that did not reproduce as a cure |
| 2 |
~11:38, ordinary session, no suspend |
~11:47, spontaneously, no attach, no interaction |
| 3 |
same day, same shape |
not instrumented |
Incident 1 ran about 9 hours: ps showed CPUTIME 08:58:20 over ELAPSED 18:40:12, and
the main thread held 3,203,667 of the process's 3,230,074 jiffies.
Two retractions follow. Incident 2 survived five gdb attach and detach cycles and
kept spinning, so ptrace does not recover this. Incident 2 had no suspend, so suspend is
one route in, not the cause.
While wedged: state=R, utime rising every sample. Recovered: state=S, counters static.
What The Protocol Trace Shows
With WAYLAND_DEBUG=1 on the probe, the mild form is periodic and unmistakable. Before
each stall the client commits and asks for a callback:
[4220513.488] -> wl_surface#27.frame(new id wl_callback#122)
[4220513.670] -> wp_fifo_v1#87.set_barrier()
[4220513.673] -> wp_fifo_v1#87.wait_barrier()
[4220513.675] -> wl_surface#27.commit()
Then silence, ended by exactly one event, after which the app resumes in the same
millisecond:
[4243046.668] wl_callback#122.done(61602155)
[ 301.0s] logic=1317 ui=1316
Every stall in the session ends that way:
| stall |
ended by |
| 101.7s to 120.6s (18.9s) |
wl_callback#122.done |
| 120.6s to 143.2s (22.6s) |
wl_callback#122.done |
| 143.2s to 177.7s (34.5s) |
wl_callback#122.done |
| 177.7s to 188.3s (10.6s) |
wl_callback#122.done |
| 188.3s to 210.9s (22.6s) |
wl_callback#122.done |
| 210.9s to 233.4s (22.5s) |
wl_callback#122.done |
The loop is alive during the stalls. Mid gap the client receives and answers a compositor
ping, and receives pointer input:
[4242578.492] xdg_wm_base#14.ping(61601687)
[4242578.502] wl_pointer#23.motion(61601687, 11.22656250, 303.56640625)
[4242578.544] -> xdg_wm_base#14.pong(61601687)
[4242585.910] wl_pointer#107.leave(64440, wl_surface#27)
So input is delivered and dispatched; it is simply never processed, because no egui pass
runs until the frame callback arrives. From the outside that is "the window is drawn and
clicks do nothing", which is exactly what I see.
wp_fifo_v1 is in use here (set_barrier, wait_barrier), which may matter.
Reproduced With A Minimal App
I left a forty line eframe probe running beside the real application. Both wedged in the
same session, minutes apart, and the loop is the same in both:
|
probe |
my app |
| iterations per 10 s |
91,613 |
94,458 |
epoll_pwait timeout |
0 |
0 |
eventfd read failures |
91,612 of 91,612 |
94,457 of 94,457 |
| fds |
4 epoll, 6 timerfd, 5 eventfd |
same |
One difference worth noting: the probe crawls rather than stopping dead. It managed one
pass per 85 s or so while wedged, where my application produced none at all. Whatever
starves them is not perfectly total.
The probe counts logic and ui separately, because eframe 0.36 runs logic without an
egui pass while it considers the window hidden. Both counters stall together here, so that
path does not appear to be the explanation.
//! Minimal eframe app for the winit/Wayland zero-timeout spin.
//!
//! Counts `logic` and `ui` passes separately. eframe 0.36 runs `logic` but no egui
//! pass while it considers the window hidden, so if a wedge shows logic climbing
//! while ui stalls, the hidden-window path is implicated; if both stall, the loop
//! never reaches the app at all.
//!
//! It also requests a timed repaint every pass, the way a worker-polling app does.
use eframe::{App, Frame};
struct Probe {
logic_passes: u64,
ui_passes: u64,
started: std::time::Instant,
last_report: std::time::Instant,
}
impl App for Probe {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
self.logic_passes += 1;
if self.last_report.elapsed() >= std::time::Duration::from_secs(1) {
eprintln!(
"[{:>7.1}s] logic={} ui={}",
self.started.elapsed().as_secs_f32(),
self.logic_passes,
self.ui_passes,
);
self.last_report = std::time::Instant::now();
}
// The pattern an app uses to poll background workers.
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) {
self.ui_passes += 1;
ui.heading("winit zero-timeout spin probe");
ui.label(format!("logic passes: {}", self.logic_passes));
ui.label(format!("ui passes: {}", self.ui_passes));
ui.label("SIGSTOP this process, wait, then SIGCONT.");
}
}
fn main() -> eframe::Result<()> {
eframe::run_native(
"winit-spin-repro",
eframe::NativeOptions::default(),
Box::new(|_cc| {
Ok(Box::new(Probe {
logic_passes: 0,
ui_passes: 0,
started: std::time::Instant::now(),
last_report: std::time::Instant::now(),
}))
}),
)
}
A stall alone does not do it. I SIGSTOPped this probe for 120 s and SIGCONTed it: the
log shows the expected 120.5 s gap, then it resumed within a second and stayed healthy. So
a clock jump by itself is not sufficient, which argues against my own guess above being
the whole story.
Ruled Out
- GPU fault or device lost: no
NVRM: Xid, no drm errors, no GPU call on the stack.
- Compositor restart: the socket peer is still
gnome-shell pid 8114, started before the
app; ss shows ESTAB.
- Unread input backing up: every socket shows RecvQ 0.
- A pathological timeout from the app: every
request_repaint_after is 16 to 100 ms plus
a bounded debounce remainder. Nothing near Duration::MAX, nothing derived from a
pre-suspend Instant.
Questions
- While waiting for a
wl_surface.frame callback, should the backend block in the
poller rather than poll with a zero timeout? The waiting itself looks correct; burning
a core for the duration does not.
- Is a compositor that withholds frame callbacks for tens of seconds, or hours, something
winit can reasonably detect and report, rather than presenting as a frozen app?
- Does
wp_fifo_v1 change anything here? The barrier is set and waited on immediately
before each stall.
Reproduction
No procedure yet, only a specimen that eventually wedges: run the probe above and wait.
Four occurrences across two processes so far. SIGSTOP/SIGCONT is ruled out as a
trigger, as noted above.
A second probe is now running with WAYLAND_DEBUG=1, so the next wedge will show which
protocol message stops arriving; the wedges so far were not logged that way. I am also
willing to provoke mutter#1647 deliberately to see whether the assertion alone is enough.
Happy to run anything against a live wedged process.
Debugging output
No WAYLAND_DEBUG=1 output. The app was launched without it and the wedges were not
anticipated; it is enabled from now on. What I have instead:
Stack while spinning (main thread, in the timer re-arm step):
#6 rustix::backend::event::syscalls::epoll_mod (linux_raw/event/syscalls.rs:260)
#7 rustix::event::epoll::modify (event/epoll.rs:161)
#8 polling::epoll::Poller::modify (polling/src/epoll.rs:138)
#9 polling::epoll::Poller::wait_deadline (polling/src/epoll.rs:199)
#10 polling::Poller::wait_impl (polling/src/lib.rs:754)
#12 calloop::sys::Poll::poll (calloop/src/sys.rs:233)
#13 calloop::loop_logic::EventLoop::dispatch_events<winit::..::wayland::state::WinitState>
#15 winit::..::wayland::event_loop::EventLoop::loop_dispatch (mod.rs:585)
#16 winit::..::wayland::event_loop::EventLoop::poll_events_with_timeout (mod.rs:289)
#17 winit::..::wayland::event_loop::EventLoop::pump_events (mod.rs:226)
#22 eframe::native::run::run_and_return
No application frame appears on it; update() is never called again.
After recovery, five stacks one second apart are identical and parked rather than
spinning:
#1 rustix::backend::event::syscalls::epoll_wait
#2 rustix::event::epoll::wait (event/epoll.rs:213)
#3 polling::epoll::Poller::wait_deadline (polling/src/epoll.rs:219)
#4 polling::Poller::wait_impl (polling/src/lib.rs:754)
Not the "windows are not shown unless you draw" case: the window drew and presented
normally for about 8 minutes after resume, with three timestamped autosaves in the app's
own log, before it stopped.
Window isn't shown unless you draw
Winit version
0.30.13 (full matrix in the description)
Description
Summary
An
eframe/winitapp on Wayland stops dispatching. The window keeps its last frame, noinput ever reaches the app again, and the main thread pins one of my 16 cores until
something outside the process changes. Three occurrences so far.
While wedged, the loop calls
epoll_pwaitwith a zero timeout about 9,400 times persecond, gets zero events, re-arms its registrations, repeats. Nothing is ever ready; the
poller never blocks and the loop never dispatches. Hence no paint, no input.
Versions
Ubuntu kernel
7.0.0-22-generic(PREEMPT_DYNAMIC), GNOME Shell (--mode=ubuntu) onWayland. Hybrid graphics: AMD Radeon 680M drives the display, NVIDIA RTX 2050 alongside
(driver 580.173.02). Debug build,
wgpubackend.The Loop
strace -c, 10 s, while wedged:Every iteration is identical:
fd 4 is the epoll, fd 6 the timerfd, fd 5 an eventfd; the 8 byte read is
polling'swakeup notifier and it is empty every time (24,554
EAGAINin 24,555 calls over aseparate 5 s sample).
A zero timeout forever is what a
ControlFlow::WaitUntildeadline stuck in the past lookslike from underneath:
deadline - nowsaturates to zero on every pass, sopollingpollsinstead of waiting, and nothing advances the deadline because that needs a dispatch which
never happens. I cannot tell from here which deadline it is, or what keeps it in the past.
The Compositor Misbehaves At The Same Moments
gnome-shelllogs a known mutter assertion, and the two largest clusters in the wholeboot land on my first two wedges:
That is GNOME/mutter#1647, open and Critical, reported for years with triggers filed
against several unrelated apps (fullscreen mpv, Evolution, Nautilus, Telegram Desktop).
Telegram runs here and its D-Bus
IdleMonitorcalls appear minutes before both clusters,so I suspect my compositor's stacking state is corrupted by something other than my app.
Not filing this as "mutter's fault, never mind". The compositor is the trigger; the bug I
report is the response. A client that loses the compositor's cooperation should idle or
complain, not burn a core indefinitely and silently stop accepting input.
Incidents
s2idlesuspendgdbattach that did not reproduce as a cureIncident 1 ran about 9 hours:
psshowedCPUTIME 08:58:20overELAPSED 18:40:12, andthe main thread held 3,203,667 of the process's 3,230,074 jiffies.
Two retractions follow. Incident 2 survived five
gdbattach and detach cycles andkept spinning, so
ptracedoes not recover this. Incident 2 had no suspend, so suspend isone route in, not the cause.
While wedged:
state=R,utimerising every sample. Recovered:state=S, counters static.What The Protocol Trace Shows
With
WAYLAND_DEBUG=1on the probe, the mild form is periodic and unmistakable. Beforeeach stall the client commits and asks for a callback:
Then silence, ended by exactly one event, after which the app resumes in the same
millisecond:
Every stall in the session ends that way:
wl_callback#122.donewl_callback#122.donewl_callback#122.donewl_callback#122.donewl_callback#122.donewl_callback#122.doneThe loop is alive during the stalls. Mid gap the client receives and answers a compositor
ping, and receives pointer input:
So input is delivered and dispatched; it is simply never processed, because no egui pass
runs until the frame callback arrives. From the outside that is "the window is drawn and
clicks do nothing", which is exactly what I see.
wp_fifo_v1is in use here (set_barrier,wait_barrier), which may matter.Reproduced With A Minimal App
I left a forty line
eframeprobe running beside the real application. Both wedged in thesame session, minutes apart, and the loop is the same in both:
epoll_pwaittimeoutreadfailuresOne difference worth noting: the probe crawls rather than stopping dead. It managed one
pass per 85 s or so while wedged, where my application produced none at all. Whatever
starves them is not perfectly total.
The probe counts
logicanduiseparately, because eframe 0.36 runslogicwithout anegui pass while it considers the window hidden. Both counters stall together here, so that
path does not appear to be the explanation.
A stall alone does not do it. I
SIGSTOPped this probe for 120 s andSIGCONTed it: thelog shows the expected 120.5 s gap, then it resumed within a second and stayed healthy. So
a clock jump by itself is not sufficient, which argues against my own guess above being
the whole story.
Ruled Out
NVRM: Xid, nodrmerrors, no GPU call on the stack.gnome-shellpid 8114, started before theapp;
ssshowsESTAB.request_repaint_afteris 16 to 100 ms plusa bounded debounce remainder. Nothing near
Duration::MAX, nothing derived from apre-suspend
Instant.Questions
wl_surface.framecallback, should the backend block in thepoller rather than poll with a zero timeout? The waiting itself looks correct; burning
a core for the duration does not.
winit can reasonably detect and report, rather than presenting as a frozen app?
wp_fifo_v1change anything here? The barrier is set and waited on immediatelybefore each stall.
Reproduction
No procedure yet, only a specimen that eventually wedges: run the probe above and wait.
Four occurrences across two processes so far.
SIGSTOP/SIGCONTis ruled out as atrigger, as noted above.
A second probe is now running with
WAYLAND_DEBUG=1, so the next wedge will show whichprotocol message stops arriving; the wedges so far were not logged that way. I am also
willing to provoke mutter#1647 deliberately to see whether the assertion alone is enough.
Happy to run anything against a live wedged process.
Debugging output
No
WAYLAND_DEBUG=1output. The app was launched without it and the wedges were notanticipated; it is enabled from now on. What I have instead:
Stack while spinning (main thread, in the timer re-arm step):
No application frame appears on it;
update()is never called again.After recovery, five stacks one second apart are identical and parked rather than
spinning:
Not the "windows are not shown unless you draw" case: the window drew and presented
normally for about 8 minutes after resume, with three timestamped autosaves in the app's
own log, before it stopped.
Window isn't shown unless you draw
Winit version
0.30.13 (full matrix in the description)