diff --git a/ICSharpCode.ILSpyX/AssemblyList.cs b/ICSharpCode.ILSpyX/AssemblyList.cs
index 86b0e1cc8..d53f91a19 100644
--- a/ICSharpCode.ILSpyX/AssemblyList.cs
+++ b/ICSharpCode.ILSpyX/AssemblyList.cs
@@ -161,6 +161,15 @@ namespace ICSharpCode.ILSpyX
return GetSnapshot().GetAllAssembliesAsync();
}
+ ///
+ /// Streaming variant of , for consumers that can act on each
+ /// assembly as it loads instead of waiting for the whole list.
+ ///
+ public IAsyncEnumerable EnumerateAllAssemblies(CancellationToken cancellationToken = default)
+ {
+ return GetSnapshot().EnumerateAllAssembliesAsync(cancellationToken);
+ }
+
public int Count {
get {
lock (lockObj)
diff --git a/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs b/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs
index 5a1b6edcf..5c520fc83 100644
--- a/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs
+++ b/ICSharpCode.ILSpyX/AssemblyListSnapshot.cs
@@ -22,6 +22,8 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using ICSharpCode.Decompiler.Metadata;
@@ -171,48 +173,80 @@ namespace ICSharpCode.ILSpyX
public async Task> GetAllAssembliesAsync()
{
var results = new List(assemblies.Length);
+ await foreach (var asm in EnumerateAllAssembliesAsync().ConfigureAwait(false))
+ {
+ results.Add(asm);
+ }
+ return results;
+ }
+ ///
+ /// Streaming variant of : yields each assembly as soon
+ /// as it is known. Awaiting the load result is what triggers the lazy load, so a consumer
+ /// that materializes the whole sequence first waits for every assembly on the list to be
+ /// read off disk before it can do any work.
+ ///
+ public async IAsyncEnumerable EnumerateAllAssembliesAsync(
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
foreach (var asm in assemblies)
{
- LoadResult result;
+ cancellationToken.ThrowIfCancellationRequested();
+ LoadResult? result = null;
try
{
result = await asm.GetLoadResultAsync().ConfigureAwait(false);
}
catch
{
- results.Add(asm);
- continue;
+ // Load failure: still yield the assembly so the consumer can surface it.
+ }
+ if (result == null)
+ {
+ yield return asm;
}
- if (result.Package != null)
+ else if (result.Package != null)
{
- AddDescendants(result.Package.RootFolder);
+ foreach (var descendant in EnumerateDescendants(result.Package.RootFolder))
+ {
+ yield return descendant;
+ }
}
else if (result.MetadataFile != null)
{
- results.Add(asm);
+ yield return asm;
}
}
- void AddDescendants(PackageFolder folder)
+ static IEnumerable EnumerateDescendants(PackageFolder folder)
{
foreach (var subFolder in folder.Folders)
{
- AddDescendants(subFolder);
+ foreach (var descendant in EnumerateDescendants(subFolder))
+ {
+ yield return descendant;
+ }
}
foreach (var entry in folder.Entries)
{
if (!entry.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !entry.Name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
continue;
- var asm = folder.ResolveFileName(entry.Name);
+ LoadedAssembly? asm;
+ try
+ {
+ asm = folder.ResolveFileName(entry.Name);
+ }
+ catch
+ {
+ // One unreadable entry must not abandon the rest of the package.
+ continue;
+ }
if (asm == null)
continue;
- results.Add(asm);
+ yield return asm;
}
}
-
- return results;
}
}
}
diff --git a/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs b/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs
index 119e4aa96..87c488623 100644
--- a/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs
+++ b/ILSpy.Tests/TreeNodes/TypeSystemSharingTests.cs
@@ -24,6 +24,7 @@ using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpy.Languages;
using ICSharpCode.ILSpy.Search;
using ICSharpCode.ILSpy.TreeNodes;
@@ -75,7 +76,8 @@ public class TypeSystemSharingTests
var language = AppComposition.Current.GetExport().CurrentLanguage;
var search = new RunningSearch(
- new[] { fixture }, FixtureAssembly.TypeName, SearchMode.TypeAndMember, language,
+ AppComposition.Current.GetExport().AssemblyList!,
+ FixtureAssembly.TypeName, SearchMode.TypeAndMember, language,
ApiVisibility.PublicAndInternal, new AvaloniaSearchResultFactory(language),
new ObservableCollection(), SearchResult.ComparerByName);
var request = search.BuildRequest();
diff --git a/ILSpy/Controls/Omnibar/OmnibarViewModel.cs b/ILSpy/Controls/Omnibar/OmnibarViewModel.cs
index 1f9099978..3486429f0 100644
--- a/ILSpy/Controls/Omnibar/OmnibarViewModel.cs
+++ b/ILSpy/Controls/Omnibar/OmnibarViewModel.cs
@@ -213,7 +213,7 @@ namespace ICSharpCode.ILSpy.Controls.Omnibar
?? ApiVisibility.PublicOnly;
var run = new RunningSearch(
- assemblyList.GetAssemblies(),
+ assemblyList,
term,
SearchMode.TypeAndMember,
language,
diff --git a/ILSpy/Search/RunningSearch.cs b/ILSpy/Search/RunningSearch.cs
index 9b73dd479..2801571fd 100644
--- a/ILSpy/Search/RunningSearch.cs
+++ b/ILSpy/Search/RunningSearch.cs
@@ -53,7 +53,7 @@ namespace ICSharpCode.ILSpy.Search
// when the queue is jammed with thousands of late-arriving hits.
const int RefreshTimeBudgetMs = 10;
- readonly IReadOnlyList assemblies;
+ readonly AssemblyList assemblyList;
readonly SearchMode mode;
readonly string searchTerm;
readonly Language language;
@@ -70,7 +70,7 @@ namespace ICSharpCode.ILSpy.Search
bool completedRaised;
public RunningSearch(
- IReadOnlyList assemblies,
+ AssemblyList assemblyList,
string searchTerm,
SearchMode mode,
Language language,
@@ -79,7 +79,7 @@ namespace ICSharpCode.ILSpy.Search
ObservableCollection sink,
IComparer sortComparer)
{
- this.assemblies = assemblies;
+ this.assemblyList = assemblyList;
this.searchTerm = searchTerm;
this.mode = mode;
this.language = language;
@@ -123,7 +123,7 @@ namespace ICSharpCode.ILSpy.Search
RaiseCompletedIfFirst();
}
- void RunSearch(CancellationToken ct)
+ async Task RunSearch(CancellationToken ct)
{
try
{
@@ -131,20 +131,23 @@ namespace ICSharpCode.ILSpy.Search
var strategy = GetStrategy(request);
if (strategy == null)
return;
+ // Streaming enumeration: expanding bundles/packages into their contained
+ // assemblies triggers the lazy load of every entry on the list, so awaiting
+ // the whole set up front would mean no results at all until the last
+ // assembly is off disk. Enumerating starts the walk here on the worker
+ // thread, not at construction time on the UI thread.
+ //
// Serial walk: per-assembly metadata walk is allocation-dominated, and 4
// parallel producers fighting for the ConcurrentQueue + the resulting UI
// batching jitter end up slower than serial in practice.
- foreach (var assembly in assemblies)
+ await foreach (var assembly in assemblyList.EnumerateAllAssemblies(ct).ConfigureAwait(false))
{
if (ct.IsCancellationRequested)
break;
MetadataFile? module;
try
{
- // Block here — we're already on a worker thread (Task.Run) and
- // this matches the WPF call shape. ConfigureAwait in an async
- // state machine would just add overhead for the same effect.
- module = assembly.GetMetadataFileAsync().GetAwaiter().GetResult();
+ module = await assembly.GetMetadataFileAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
diff --git a/ILSpy/Search/SearchPaneModel.cs b/ILSpy/Search/SearchPaneModel.cs
index 08094f1e3..56aca2131 100644
--- a/ILSpy/Search/SearchPaneModel.cs
+++ b/ILSpy/Search/SearchPaneModel.cs
@@ -251,7 +251,7 @@ namespace ICSharpCode.ILSpy.Search
? SearchResult.ComparerByFitness
: SearchResult.ComparerByName;
var run = new RunningSearch(
- assemblyList.GetAssemblies(),
+ assemblyList,
term,
SelectedSearchMode.Mode,
language,