Skip to content

Commit 7f1f516

Browse files
fix(selftest): drain dispatcher after UpdateLayout in Harness.Render
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>
1 parent 836c301 commit 7f1f516

1 file changed

Lines changed: 24 additions & 9 deletions

File tree

tests/Reactor.AppTests.Host/SelfTest/Harness.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,31 @@ public static async Task Render(int ms = 0)
229229
{
230230
await host.WaitForIdleAsync();
231231
}
232-
else
233-
{
234-
// No active host — yield once at Low priority as a fallback
235-
var dq = DispatcherQueue.GetForCurrentThread();
236-
var tcs = new TaskCompletionSource();
237-
dq.TryEnqueue(DispatcherQueuePriority.Low, () => tcs.SetResult());
238-
await tcs.Task;
239-
}
240232

241-
// Force synchronous layout so ActualWidth/ActualHeight are ready
233+
var dq = DispatcherQueue.GetForCurrentThread();
234+
235+
// Force synchronous layout so ActualWidth/ActualHeight are ready.
236+
// This is also what triggers TabView's selected-tab content presenter
237+
// to schedule its content-realization work onto the dispatcher.
238+
(_currentWindow?.Content as UIElement)?.UpdateLayout();
239+
240+
// Yield once at Low priority AFTER UpdateLayout. WaitForIdleAsync
241+
// short-circuits when Reactor reports idle; that left callers racing
242+
// the WinUI side because TabView lazy-realizes the selected pane's
243+
// body via Normal-priority dispatcher messages SCHEDULED BY the
244+
// layout pass we just forced. A Low-priority yield here guarantees
245+
// those messages have drained — without it, a 16ms wall-clock delay
246+
// is enough on CI but flakes on contended local machines (visible
247+
// in NativeDocking_* fixtures where the pane Memo subtree probes
248+
// returned null ~30–60% of the time on local 10x sweeps).
249+
var yieldTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
250+
if (!dq.TryEnqueue(DispatcherQueuePriority.Low, () => yieldTcs.SetResult()))
251+
yieldTcs.SetResult();
252+
await yieldTcs.Task;
253+
254+
// Re-run layout in case the just-realized content needs an arrangement
255+
// pass (e.g. a Memo body that mounted during the yield needs to size
256+
// its TextBlocks before FindText can match by exact-text).
242257
(_currentWindow?.Content as UIElement)?.UpdateLayout();
243258

244259
// Small breathing room for the compositor to finish processing

0 commit comments

Comments
 (0)