fix(selftest): drain dispatcher after UpdateLayout to fix local TabView race#442
Merged
Merged
Conversation
Local 10x sweeps of the SelfTests suite flake at ~60% (6/10 runs failed),
clustered in NativeDocking_* fixtures whose pane bodies are hosted in a
TabView. The same suite is stable on CI.
Root cause: Harness.Render() did `WaitForIdleAsync() → UpdateLayout() →
Task.Delay(16)`. When Reactor reports idle, WaitForIdleAsync short-circuits
without pumping the dispatcher (the comment at ReactorHost.cs:910 already
flags this: "Returning early here is the classic flake source"). That left
16ms wall-clock as the only safety net for WinUI work that UpdateLayout()
itself scheduled — notably TabView's lazy ContentPresenter realization for
the selected tab, posted at Normal priority.
CI dispatchers run with steady load → 16ms always covered it. Local boxes
(IDE, file watchers, AV) introduced enough jitter that the post-Render
visual-tree probe occasionally lost the race. Failure signature was the
selected pane body / Memo subtree returning null from FindText, which then
cascaded across every assertion in the affected fixture.
Fix: yield once at Low priority between two UpdateLayout() calls. The
first layout schedules content-realization onto the dispatcher; the Low
yield drains it (guaranteed, not timing-dependent); the second layout
arranges the just-realized content. The trailing 16ms Task.Delay is now
just compositor breathing room rather than the entire safety margin.
Same pattern WaitForIdleAsync uses internally and what the no-host
fallback already did — pulled up so the host-present path benefits too.
Verified with 3 local 10x sweeps:
before: 4/10 clean, 5 distinct flaky fixtures, top offender 3/10
v1 fix: 7/10 clean (yield before UpdateLayout — wrong order, didn't
drain layout-spawned work)
final: 10/10 clean, 0 flakes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a local-only flake in NativeDocking_* selftest fixtures by restructuring Harness.Render's settle sequence. Previously, after WaitForIdleAsync() (which short-circuits when Reactor reports idle), a single UpdateLayout() plus Task.Delay(16) was the only safety net for WinUI work scheduled by layout itself (notably TabView's lazy content-presenter realization). The new sequence runs UpdateLayout → Low-priority dispatcher yield → UpdateLayout again, so dispatcher-scheduled realization work is deterministically drained before assertions probe the visual tree.
Changes:
- Replaced the no-host
elsefallback with an unconditional Low-priority yield placed between twoUpdateLayout()calls. - Added the second
UpdateLayout()to arrange content realized during the yield. - Added an explanatory comment block documenting the race and the ordering rationale.
Show a summary per file
| File | Description |
|---|---|
| tests/Reactor.AppTests.Host/SelfTest/Harness.cs | Restructures Render() to drain dispatcher work scheduled by UpdateLayout via a Low-priority yield, then re-runs layout. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 1/1 changed files
- Comments generated: 0
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.
Summary
Reactor.SelfTestsflaked at ~60% (top offender 3/10), all clustered inNativeDocking_*fixtures whose pane bodies render through a WinUITabView. CI was stable.Harness.Render()didWaitForIdleAsync() → UpdateLayout() → Task.Delay(16).WaitForIdleAsyncshort-circuits when Reactor reports idle (a known footgun — flagged in the comment atReactorHost.cs:910), leaving 16 ms wall-clock as the only safety net for WinUI work thatUpdateLayout()itself schedules (notably TabView's lazyContentPresenterrealization for the selected tab). CI dispatchers had steady enough load that 16 ms always covered it; local boxes (IDE, file watchers, AV) lost the race ~30–60% of the time.UpdateLayout()calls inHarness.Render. The first layout schedules content-realization onto the dispatcher; the Low yield drains it (guaranteed, not timing-dependent); the second layout arranges the just-realized content. The 16 msTask.Delayis now just compositor breathing room rather than the entire safety margin.This is the same pattern
WaitForIdleAsyncuses internally and what theelse(no-host) fallback already did — pulled up so the host-present path benefits too.Affected flakes observed in the original 10x sweep (all eliminated):
NativeDocking_TabGroupRendersToTabView(3/10)NativeDocking_Composition_ContentMutationFlowsToActivePane(2/10)NativeDocking_DockContextHooksResolveOnRealMount(1/10)NativeDocking_Reliability_UseEffectCleanup_BodyRemovedOnPaneClose(1/10)NativeDocking_DynamicallyDockedComponentPage_WithOuterShellState(1/10)Verification
Three 10x sweeps on local (
tools/flake-loop.ps1againstReactor.SelfTests, x64 Release):UpdateLayout(wrong order)UpdateLayout, second layout to arrange)Test plan
🤖 Generated with Claude Code