Skip to content

fix(render): release sleep landing mid-frame never cleared output - #294

Merged
hyperb1iss merged 2 commits into
mainfrom
nova/capture-demand-transitions
Sep 16, 2026
Merged

hyperb1iss merged 2 commits into
mainfrom
nova/capture-demand-transitions

Conversation

@hyperb1iss

@hyperb1iss hyperb1iss commented Sep 15, 2026

Copy link
Copy Markdown
Owner

What this changes

Two Windows CI failures on PR #292, neither caused by that branch. One was a test asserting scheduler luck; the other was a real daemon bug the loaded runner exposed.

Release sleep landing mid-frame never released devices. When the session goes to sleep after a frame's power snapshot was taken, the frame executor switches to a static sleep frame late: it writes one off-colour frame to every device, publishes populated zones, and latches the sleep frame as pushed. That shortcut only satisfies the static off behaviour. Under the release behaviour the throttle path is what clears the published zones and stops driving devices, and it returns early once the latch is set, so it never ran. The late switch now applies only to static sleep; a release sleep lets the admitted frame finish and the next frame takes the release path, one frame later than the static case.

Capture-gating tests asserted the startup race. Four render-thread tests required a demand-gated source to see exactly [false, true, false]. The leading false is a race: start_all clears the input manager's cached demand and the input publication worker reconciles on its first pass, before the first frame publishes the scene's authoritative demand. When the worker wins it applies "inactive" to an already-inactive source; when the first frame wins, the first application is already true. Both orderings are correct, so a shared helper now requires one activation followed by deactivation and tolerates the optional startup no-op.

Why

The first symptom was output_sleep_keeps_reactive_input_capture_live timing out at the full ten-second deadline waiting for the published zones to empty after a release sleep. Wider frames on a loaded runner widen the window in which the power flip lands mid-frame, so Windows CI hit it while Linux rarely does. Outside the test, a user whose session sleeps with the release behaviour could have devices held at the off colour instead of handed back.

The second was audio_capture_enabled_when_any_active_zone_is_reactive failing with [true, false]; the same shape was recorded once on main on Aug 30 and re-run past.

🛠️ How it works

should_switch_to_late_sleep_frame now takes the latest power state's effective off behaviour into account and answers true only for static sleep. Nothing else in the two late-sleep sites changes. The next frame sees output_power.sleeping() at the top, the sleep flag is still clear, and maybe_sleep_throttle runs its release branch: clear zones, publish the cleared frame, latch, throttle.

🧪 Validation

  • cargo test -p hypercolor-daemon --lib -- late_sleep: both predicate tests pass, including the new one asserting release sleep does not take the shortcut.

  • cargo clippy -p hypercolor-daemon --features persistence-test-hooks --tests -- -D warnings: clean.

  • The four gating tests (output_sleep_keeps_reactive_input_capture_live, audio_capture_enabled_when_any_active_zone_is_reactive, render_thread_gates_audio_capture_to_audio_reactive_effects, render_thread_gates_screen_capture_to_screen_reactive_scene_groups) ran twenty times back to back after the change: 80 of 80 pass.

  • Failing-before evidence is the PR feat(ci): ship releases without Apple signing credentials #292 Windows job: the release-sleep timeout on run 35020518159 and [true, false] on the first attempt of run 35006142665. Neither reproduces on a lightly loaded Linux box, so the local proof is the predicate test plus the loop, not a reproduction of the CI timing.

  • Added or updated tests

  • Added or updated docs (not applicable)

  • just verify passes locally (daemon clippy with -D warnings and the affected test targets; the full recipe was not run)

  • just deny passes (not applicable)

  • just ui-test and just ui-build pass (not applicable)

  • just sdk-lint, just sdk-check, and just sdk-build pass (not applicable)

  • just python-verify passes (not applicable)

  • just compat-check passes (not applicable)

  • just docs-build passes (not applicable)

  • cd docs && zola check passes (not applicable)

  • Packaging scripts were syntax-checked (not applicable)

  • just e2e-build passes with the normal Servo stack (not applicable)

  • just e2e-build-cpu passes when validating the CPU smoke fallback (not applicable)

  • just e2e passes against the Servo stack (not applicable)

  • Tested on real hardware, simulator, or e2e harness (not exercised on a live rig; the sleep transition is covered by the integration test)

🔍 Review focus

Start with should_switch_to_late_sleep_frame in frame_executor.rs and the two call sites that depend on it. The trade-off is one extra rendered frame reaching devices when a release sleep lands mid-frame, versus a device held at the off colour indefinitely. The startup no-op the test helper tolerates is a harmless idempotent call on a source that is not capturing; making the publication worker wait for the first authoritative demand would be a separate daemon change with its own trade-off.

🤖 Generated with Claude Code

https://claude.ai/code/session_013dXbUNEe4yxrGhQLXbzRou

Summary by CodeRabbit

  • Bug Fixes

    • Improved sleep-state handling so release-style sleep no longer incorrectly uses the static late-sleep behavior.
    • Preserved the static late-sleep optimization where applicable.
  • Tests

    • Improved test reliability for audio and screen capture gating scenarios.
    • Tests now tolerate expected startup reconciliation events while still verifying correct capture activation and deactivation.

@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: ad437b31-9bf6-4eb6-a1d6-9674fe21a994

📥 Commits

Reviewing files that changed from the base of the PR and between 6de2f25 and 0692cd6.

📒 Files selected for processing (2)
  • crates/hypercolor-daemon/src/render_thread/frame_executor.rs
  • crates/hypercolor-daemon/tests/render_thread_tests.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The render thread limits the late-sleep shortcut to static output behavior. Capture transition tests now tolerate an optional startup no-op before validating expected activation and deactivation transitions.

Changes

Render thread behavior

Layer / File(s) Summary
Late-sleep selection for static output
crates/hypercolor-daemon/src/render_thread/frame_executor.rs
should_switch_to_late_sleep_frame now requires OffOutputBehavior::Static. A test covers running-to-releasing and releasing-to-releasing transitions.
Capture transition assertions
crates/hypercolor-daemon/tests/render_thread_tests.rs
Adds assert_capture_transitions, which removes an optional leading false transition. Related assertions now validate the remaining expected transitions.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 0692c

The change restricts late sleep to static output and makes capture assertions tolerate only the documented startup ordering; no merge-blocking regression remains evident.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main render change: release sleep landing mid-frame no longer leaves the output uncleared. It is concise and specific.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

Three render-thread tests asserted a demand-gated source sees exactly
[false, true, false]. The leading false is a race, not a contract:
start_all clears the input manager's cached demand, and the input
publication worker reconciles capture demand on its first pass, before
the first frame publishes the scene's authoritative demand. When the
worker wins it applies "inactive" to an already-inactive source; when
the first frame wins, the first application is already true. Fast
Windows runners take the second path and failed with [true, false].

A shared helper now requires exactly one activation followed by the
deactivation and tolerates the optional startup no-op. Twenty back to
back runs of all three tests pass after the change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dXbUNEe4yxrGhQLXbzRou
@hyperb1iss
hyperb1iss force-pushed the nova/capture-demand-transitions branch from e7d29c0 to 6de2f25 Compare September 15, 2026 21:13
When the session goes to sleep after a frame's power snapshot was taken,
the frame executor switches to a static sleep frame late: it writes one
off-color frame to every device, publishes populated zones, and latches
the sleep frame as pushed. That shortcut only satisfies the static off
behavior. Under the release behavior the throttle path is what clears
the published zones and stops driving devices, and it never ran because
the latch told it the sleep frame was already out. Devices stayed held
at the off color instead of being released, and the output-sleep test
waited the full deadline for zones that never emptied, which is how the
Windows lane surfaced it.

The late switch now applies only to static sleep. A release sleep lets
the admitted frame finish and the next frame takes the release path, one
frame later than the static case. The output-sleep test also adopts the
startup-order helper for its transition assertions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dXbUNEe4yxrGhQLXbzRou
@hyperb1iss hyperb1iss changed the title test(daemon): accept either capture-demand startup order in gating tests fix(render): release sleep landing mid-frame never cleared output Sep 15, 2026
@hyperb1iss
hyperb1iss merged commit fff0a27 into main Sep 16, 2026
78 of 79 checks passed
@hyperb1iss
hyperb1iss deleted the nova/capture-demand-transitions branch September 16, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant