From 0ff063b551264cfa8be695246ebc0cee88919f2f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 22 May 2026 21:26:27 +0200 Subject: [PATCH] Throttle background-load sweep + lengthen cooldown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep fires LoadedAssembly.GetLoadResultAsync on every sibling assembly so users see icons "fill in" rather than each one showing a loading state until first click. As written the sweep was firing 500 ms after TreeReady — which lands inside the first decompile's Dispatcher.InvokeAsync window, triggering Server-GC pauses on the UI thread during apply-text marshal-back. Measured: +460 ms FirstText vs sweep-disabled on a single-type decompile workload. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/AssemblyTree/AssemblyTreeModel.cs | 33 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index 9cb53ad58..853735d98 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -25,6 +25,7 @@ using System.Linq; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Runtime.Serialization; +using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -417,18 +418,34 @@ namespace ILSpy.AssemblyTree try { await TreeReady.ConfigureAwait(false); - // Small grace period after Loaded so the first paint has time to settle - // — kicking off 122 metadata loads the same frame the tree appears would - // steal cycles from the layout pass that just brought it on screen. - await Task.Delay(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false); + // Generous cooldown so the first decompile (selected via path-restore or + // --navigateto) has fully landed in the editor before the sweep fires. + // The earlier 500 ms cooldown overlapped with the decompile's + // Dispatcher.InvokeAsync marshal-back, triggering Server-GC pauses on the + // UI thread mid-apply-text. 5 s puts the sweep solidly past the typical + // foreground-work window without making sibling-icon population feel + // unreasonably delayed to a user scanning the list. + await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false); AppEnv.AppLog.Mark("Background-load sweep starting"); + // Cap concurrent loads so a 200-assembly list doesn't kick off 200 + // simultaneous Task.Run + GetLoadResultAsync chains. Each load reads PE + // headers + metadata tables; mostly IO-bound but not zero CPU/memory. + // Throttling to 4 keeps the peak allocation rate predictable so Server GC + // has fewer reasons to pause the UI thread if the user clicks back into + // the tree mid-sweep. + using var throttle = new SemaphoreSlim(4); + var loadTasks = new List(); foreach (var assembly in list.GetAssemblies()) { - // Calling GetLoadResultAsync triggers the Lazy creation. We don't - // await — fire-and-forget so all 122/200/whatever loads run in parallel - // on the thread pool, just delayed past the active-assembly window. - _ = assembly.GetLoadResultAsync(); + await throttle.WaitAsync().ConfigureAwait(false); + loadTasks.Add(Task.Run(async () => { + try + { await assembly.GetLoadResultAsync().ConfigureAwait(false); } + finally + { throttle.Release(); } + })); } + await Task.WhenAll(loadTasks).ConfigureAwait(false); AppEnv.AppLog.Mark("Background-load sweep dispatched"); } catch (Exception ex)