From 3b37eff5d3cf87d1d2bce4f4b165358d09d38e11 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sat, 15 Aug 2026 19:29:03 +0200 Subject: [PATCH 1/7] Close every window a headless UI test showed, and let go of shared flyouts The headless test host runs the app without an application lifetime, so the window-closing step in ResetAppState never had a list to work from and every MainWindow the suite showed stayed open - and reachable from the compositor, together with its view-models, assembly tree and loaded assemblies. Measured at about 13 MB per test, 15 GB over the suite, enough to page out the CI runner and stall the tests that scan process module lists. Closing is not sufficient on its own: Avalonia's Button subscribes to its flyout's Opened/Closed and only unsubscribes when the Flyout property changes, and Dock's ToolChromeControl theme hands every tool pane's chrome button one shared MenuFlyout resource, which therefore pinned every closed window's visual tree. The flyouts are detached before the window closes. Assisted-by: Claude:claude-fable-5:Claude Code --- ILSpy.Tests/ResetAppStateAttribute.cs | 47 ++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/ILSpy.Tests/ResetAppStateAttribute.cs b/ILSpy.Tests/ResetAppStateAttribute.cs index 8aea605b91..c4d3ebc73f 100644 --- a/ILSpy.Tests/ResetAppStateAttribute.cs +++ b/ILSpy.Tests/ResetAppStateAttribute.cs @@ -17,14 +17,16 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls; using Avalonia.Threading; +using Avalonia.VisualTree; using ICSharpCode.ILSpyX.Settings; @@ -100,15 +102,50 @@ public void AfterTest(ITest test) DrainPendingWork(); // Close any windows the test showed so their view-models (alive and weakly subscribed to - // MessageBus) can't react to events raised by later tests, then drain once more. - if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + // MessageBus) can't react to events raised by later tests, then drain once more. A window + // left open also outlives its container: the compositor keeps every open top level + // reachable, and with it the view-models, the assembly tree and the loaded assemblies - + // about 13 MB per test, which over the suite is what pushed the CI runner into paging. + foreach (var window in openWindows.ToArray()) { - foreach (var window in desktop.Windows.ToArray()) - window.Close(); + DetachFlyouts(window); + window.Close(); } Dispatcher.UIThread.RunJobs(); } + // Avalonia's Button subscribes to its flyout's Opened/Closed events when its template is + // applied and unsubscribes only when the Flyout property changes, not when the button leaves + // the tree. Dock's ToolChromeControl theme gives every tool pane's chrome button the same + // MenuFlyout resource, so that one shared flyout would keep the visual tree of every window + // this suite ever showed alive. Clearing the property before the window closes is what + // makes the button let go. + static void DetachFlyouts(Window window) + { + foreach (var button in window.GetVisualDescendants().OfType