daemon: paint at the display rate under a sustained flood #380
No reviewers
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!380
Loading…
Reference in a new issue
No description provided.
Delete branch "daemon/one-lock-per-cycle"
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?
Closes #378.
A pull-paced window painted about 15 frames per second under a DOOM-fire flood. Three causes, each fixed and measured on this host (Linux, niri, 60 Hz, 188x82 grid, 16 s flood; GUI client pulls answered per second):
27c5fb13)5ae42481daemon: take the parse lock once per grid cycle3593a25adaemon: yield between flood drains so pulls are read off the socketfa722c21client: drop a redraw request the paint it waited for already satisfiedEvery cycle is now about 16.7 ms (p99 20 ms). DOOM-fire's own fps is unchanged within noise; the 150 MB
catpayloads are within noise or slightly faster (numbers in the commit bodies).What each commit does:
ParseCoreacquisitions, each waiting out a parking_lot fairness slice against the parse thread. Every cycle entry point now takes the guard once, helpers compose into buffers, and pushes / eviction / meta / image materialization run after the guard drops in the same wire order. Also fixes a latent bug: the command arm did not reset the row cache, so a second mirror could receive a stale row after an in-place edit (regression test included).CycleEndredraw request stayed pending after the paint that consumed it, and the redundant FIFO present cost a vsync every other cycle.Docs:
pipeline.mdrecords the one-acquisition rule (with its two exceptions) and the window stamping;ipc.mdthe row-cache lifetime;benchmarks.mdkeeps the75d08630run and its non-comparability note unchanged (a new figure belongs to the next published run); CHANGELOG under Rendering.Not verified: macOS, Windows, outputs above 60 Hz.
A pull-paced window painted only about 15 frames per second while a program flooded the terminal (DOOM-fire), even with the ParseCore lock handing off fairly. parking_lot's eventual fairness gives a parked waiter the lock only about every 0.5 ms while the parse thread relocks per swap buffer, and one answered pull made about six separate acquisitions: the effect drain (plus one per APC and per scroll into scrollback), two in the damage fan-out, notifications, facets, one compose per subscriber, and the meta refresh. The command arm that answers NextGridFrame made four. Each waited out its own fairness slice. Every cycle entry point now takes the guard once: the PTY drain, the command arm after each input, the EOF drain, subscribe, and resize. The helpers take a borrowed ParseCore and compose into buffers. Outbox pushes, eviction, the meta lock, image materialization and notification publishing run after the guard drops, in the same wire order, and a dead outbox is still removed as soon as a push fails. The lock is not reentrant. remove_sub reaches refresh_meta and recompute_presentation, which both relock, so no push may happen under the guard, and a compose failure is recorded and evicted in the tail. A cycle with an eviction therefore takes the lock a second time. The EOF arm still ships the final frame when the effect drain fails, and the PTY arm reports the drain error after the cycle has shipped. The command arm now resets the row cache before composing. Without the reset, a row edited in place (same geometry, same scroll sequence) after a drain cycle cached it was shipped stale to a second mirror whose pull the command arm answered, and that mirror's damage for the row was cleared. The new test reproduces this against the old code. The remaining lock_core calls are off the cycle path. Spawn setup runs before the task does. The PTY reconciles (focus, color scheme, 2048 resize report) and recompute_presentation run from the run loop or from remove_sub, which happens only after a guard drops. refresh_meta runs on subscribe, eviction and push_to_scope. Per-input-kind reads (key and mouse encoding, paste bracketing, a non-owner resize, viewport and prompt jumps) are not taken for NextGridFrame. Search, row streams, region replies and the size-ownership check in promote_active are commands of their own. Measured on Linux (i5-12400F, RTX 3080, niri, 60 Hz), before -> after: - measure_pull_cycles_under_flood (release, 10 s): 363, 285, 537 -> 635, 1020, 854 cycles; frame-file passes (parse throughput) are unchanged at about 600-650 - GUI client pulls per second over a 16 s DOOM-fire flood, bench-field font, 188x82 grid: 13.4, 11.9, 14.2 -> 26.4, 21.7, 25.7 (a 212x78 grid with the user config: 16.1, 21.4, 16.9, 13.6 -> 27.1, 31.9, 32.2) - DOOM-fire fps, same runs: 1,085-1,104 -> 1,062-1,095 (noise) - 150 MB cat, two runs of 3 each: ascii 1562/1572 -> 1563/1641 ms, unicode 880/985 -> 939/983 ms, csi 876/889 -> 908/903 ms (csi is 2-3% slower in both pairs; the others overlap) This is still short of the display rate. With one acquisition the daemon answers a pull in 1.8 ms median (p90 5.6 ms). In the slow cycles (over 34 ms, 62% of the flood time), 93% of the time falls between the daemon's ship and the client's next pull, which is client-side work that this change does not touch. Rejected: an unlock_fair in the sink only while a pull is pending. It shortens each wait but not the number of waits per cycle, so it is worth layering on only if a single acquisition turns out not to be enough. Part of #378 Assisted-by: Claude Code