Bill a slow present to the compositor, not to the app - #508
Open
samoylenkodmitry wants to merge 1 commit into
Open
Bill a slow present to the compositor, not to the app#508samoylenkodmitry wants to merge 1 commit into
samoylenkodmitry wants to merge 1 commit into
Conversation
robot_idle_fps_after_tab_walk failed on a loaded host with "timed out after 1280577 iterations; needs_update=false, needs_redraw=true, has_animations=false, waiting_for_present=true", then passed in isolation and passed again in a full suite run on the same commit. 1.28M turns inside the 10s budget is a loop with all the CPU it asked for -- an outstanding idle wait pins the event loop at ControlFlow::Poll -- so the starvation floor did not apply and the wait was reported as an application defect. But its own diagnostics say composition had converged: no pending update, no animation. The only outstanding work was a frame the compositor had not handed back. The application was failed for a wait it does not own. The two waits have different owners and now have different budgets. A settled composition waiting on a present gets a clock of its own, armed on the turn it becomes the sole blocker, bounded by ROBOT_IDLE_PRESENT_TIMEOUT and reported as SurfaceNotPresenting against the surface -- naming the refused frames, the target generation and the current one, the way the standalone present wait already does. 30s keeps that under run_robot_test.sh's generic per-example timeout, so a window that truly never presents still reports a diagnostic instead of dying as an opaque exit 124; the 60s starvation ceiling would have collided with it. AppNotConverging keeps the soft budget and keeps every diagnostic field it carried. Headless runs are unaffected: they never arm a present target. cranpose-testing's headless wait_for_idle does not get the same treatment. That pump is synchronous work against an in-memory renderer with nothing to block on, so what a healthy app needs does not move with host load, and timing it would decide headless results by host load -- the flakiness this commit removes from the desktop side. Its defect was the opposite one: exhausting the budget returned quietly and handed back a half-settled tree. It now has a named budget, the reason it is counted in turns, and a loud failure. pump_robot_until_stable was a copy of the same loop with per-call-site bounds of 40 and 60; measurement says every call settles on turn 1, so it goes and its callers use wait_for_idle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
samoylenkodmitry
force-pushed
the
claude/stoic-franklin-d8ce3d
branch
from
August 27, 2026 16:07
6a4d472 to
fe7a2b8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
robot_idle_fps_after_tab_walkfailed on a loaded host (load_1m 24 on a 10-core machine) with:then passed in isolation and passed again in a full 121-test suite run on the same commit (a1f64e5).
The defect
The budget was already wall-clock (
ROBOT_IDLE_TIMEOUT = 10s);iterationsis a diagnostic, and enters the decision only as a floor that delays failure on a starved host. 1.28M turns inside that budget is a loop with all the CPU it asked for — an outstanding idle wait pins the event loop atControlFlow::Poll— so the floor did not apply and the wait was reported asAppNotConverging.But its own diagnostics say composition had converged:
needs_update=false,has_animations=false. The only outstanding work waswaiting_for_present— a frame the compositor had not handed back. The application was failed for a wait it does not own. The codebase already models compositor refusal separately (ROBOT_PRESENT_WAIT_TIMEOUT,unpresentable_frames_since_present); the idle path never consulted it.The fix
The two waits have different owners and now have different budgets. A settled composition waiting on a present gets its own clock, armed on the turn it becomes the sole blocker, bounded by
ROBOT_IDLE_PRESENT_TIMEOUT = 30sand reported asSurfaceNotPresentingagainst the surface — naming refused frames, target generation and current one, as the standalone present wait already does.30s keeps that under
run_robot_test.sh's generic 60s per-example timeout, so a window that truly never presents still reports a diagnostic instead of dying as an opaque exit 124; the 60s starvation ceiling would have collided with it.AppNotConvergingkeeps the soft budget and every diagnostic field it carried. Headless runs are unaffected — they never arm a present target.Three tests. Verified against the unfixed code: the two new ones fail with exactly the reported misattribution (
AppNotConvergingwhereSurfaceNotPresenting/Nonebelongs). The third pins that an app still churning with a present outstanding keeps being blamed, so the exemption cannot over-correct.cranpose-testing
The headless
wait_for_idledeliberately does not get the same treatment. That pump is synchronous work against an in-memory renderer with nothing to block on, so what a healthy app needs does not move with host load; timing it would decide headless results by host load — the flakiness this PR removes from the desktop side.Its defect was the opposite: exhausting the budget returned quietly and handed back a half-settled tree. It now has a named budget, the reason it is counted in turns, and a loud failure with a diagnostic.
pump_robot_until_stablewas a copy of the same loop with per-call-site bounds of 40 and 60; instrumented measurement says every call settles on turn 1, so it goes and its callers usewait_for_idle.Also
debug_toggles' non-UTF-8 fixture spelled a tmpfs path, which the workspace tmpfs hygiene gate reads as a test writing to/tmp—just testwas red on main before this branch (introduced by #495). The fixture is about byte preservation; the directory is arbitrary.Known, not addressed here
Poll, competing with the compositor it waits for. That is the mechanism behind the failure, but polling across "asking for a frame and being handed it" is documented as required or measured FPS clamps to vsync — changing it would break the frame-rate contracts this test family exists to check.RobotTestRule::wait_for_idleloops onneeds_redraw()(stale pixels + renderer warm-up), not pending work, so it is effectively "update once and return" — all 35 calls measured settle at turn 1. Fixing that means switching toframe_schedule()plus a shared animation-loop exit, and changes what every headless test observes. Filed separately; the misleading doc is corrected here.Local gates:
just fmt-check,just test,just clippyall clean.🤖 Generated with Claude Code