render: the window stops repainting under sustained output on Linux #375
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis#375
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
On Linux the felis window stops repainting while a program writes output without pausing, and paints again only
once the output stops. Parsing keeps up; the frames are what go missing.
Observed during the cross-terminal benchmark run for PR #374:
run all show one flat color, while DOOM-fire reported 26,885 frames (1,680 fps).
kitty, alacritty, wezterm, ghostty tip and foot all painted continuously under the same workloads.
This matters beyond looks: a terminal that does not paint looks fast on the throughput suites. PR #374 drops felis's
Linux DOOM-fire result and marks its Linux throughput figures as upper bounds until this is fixed.
Environment
bc0e9a46(v0.1.0-rc.6 code),nix build .#felisNot checked on macOS or on other GPUs yet.
Reproduce
Watch the window (or screenshot it with
niri msg action screenshot-window --id <id>): it stays blank for the 16 sDOOM-fire runs. DOOM-fire does not use synchronized output (no DECSET 2026 in its stream), so a stuck BSU is not the
cause.
Expected
Under a continuous flood felis may drop frames, but it keeps presenting at some bounded interval, as the other
terminals in the field do.
Where to look
The client's redraw scheduling while daemon updates keep arriving: whether the event loop can stay busy draining
incoming updates so that
RedrawRequestedis never serviced, and whether any forced-frame deadline exists for aflood that never goes quiet.
Code reading: where the redraw loop can stall
Not reproduced under a debugger yet; this is what the source shows, with candidates for the trigger.
How a frame is produced
The local GUI window is pull-paced (
Hello.pull_paced, default for the GUI client):run_sessioncoalesces the effect drain toDRAIN_COALESCE(4 ms), butship_sub_atships nothing to a pull-paced subscriber until it holds a pull(
if sub.pull_pacing && !sub.pull_pending { return; },crates/felis-daemon/src/serve/session_task.rs).forward_frame,crates/felis-client/src/main.rs);RedrawSchedulercollapses them, andabout_to_waitcallswindow.request_redraw().RedrawRequestedrunson_redrawonly whenmay_paint_grid(has_surface, rehydrating, cycle_open)holds(
crates/felis-client/src/event_handler.rs:326,:1272); otherwise the event is dropped with no fallback.InputMsg::NextGridFrame) is sent only from insideon_redraw, after a successful render(
self.pull.take_due(),event_handler.rs:1255).So the loop is self-sustaining: no paint, no pull; no pull, nothing shipped; nothing shipped, nothing to paint.
It turns only while
RedrawRequestedkeeps arriving and passes the gate.No backstop
No client or daemon deadline forces a paint or a pull for grid content. The only daemon-side time slice in
docs/explanation/rendering/pipeline.md("Push fallback") applies to eager-push (SSH) subscribers, not to thepull-paced local window. Once one link in the loop above misses, nothing restarts it until the flood ends.
Candidates for the trigger
cycle_opengate.PullScheduler::cycle_open()is true from the first row of a compose cycle until itsGridMsg::CycleEnd. A full-screen cycle is split into severalRowDeltamessages(
OUTBOUND_BODY_BUDGET_BYTESinstreaming.rs), so aRedrawRequestedthat lands mid-cycle is dropped. If thenext
request_redrawis not raised afterCycleEndarrives, the pull is never re-armed.pre_present_notifyis never called (no hit incrates/). On Wayland, winit 0.30 alignsRedrawRequestedwith
wl_surface.framecallbacks only throughWindow::pre_present_notify; without it winit's frame-callbackthrottle never engages.
pipeline.md"Frame pacing" says felis mirrors the Wayland frame-callback model, whichthe code does not do at the winit level.
EventLoopProxycalloop channel in an unboundedtry_recvloop and replays every queued user event throughApp::user_eventbefore it reaches theRedrawRequested/AboutToWaitstep ofsingle_iteration. Under a continuous flood this is a plausible way foran iteration to be spent on IPC instead of painting.
Surface configuration for reference:
PresentMode::AutoVsync,desired_maximum_frame_latency: 2(
crates/felis-render-wgpu/src/lib.rs:396), rendered synchronously on the winit thread.Next step
Instrument the loop during the DOOM-fire repro: count
RedrawRequestedreceived vs. passed bymay_paint_grid,CycleEndreceived, andNextGridFramesent per second. That separates "the gate drops redraws" from "redraws neverarrive".
Root cause: the daemon's drain loses the
ParseCorelock to the parse threadReproduced and measured. The trigger is on the daemon side, not in the client's redraw loop.
blink-timer flushes kept firing), so it was idle while it waited for a cycle that never came.
felis-pty-parsewas runnable in 26 of 30 thread samples. The sessiontask's
drain_effectstook 40–70 ms per call and ran only a few times. The parse sink takes theParseCorelock once per swap buffer, and
std::sync::Mutexon Linux does not hand the lock off, so the thread that justunlocked takes it again before the woken session task runs. Under pull pacing a pull cannot be answered until
a drain lands, so the client never paints and never pulls again.
Swapping only that lock for
parking_lot::Mutex, which hands the lock to a waiter at least every ~0.5 ms, with the sameclient binary:
std::sync::Mutexparking_lot::MutexBulk
catof 150 MB (ascii, unicode and csi, 2×3 runs) shows no difference beyond run-to-run noise.The client-side candidates in the previous comment are not the cause. A fix plan is under review.