mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Resolving one assembly resolves its whole reference closure, and every reference in it asked the same framework directories the same questions. The worst of it was the scan for the closest version folder of a shared framework: a directory listing plus a recursive file search, repeated per reference and per runtime pack - 42 scans for two distinct answers when decompiling ICSharpCode.ILSpyX.dll. The scan result is only safe to keep for a bounded time: a runtime can be installed or removed while ILSpy runs, and reloading an assembly list has to see that. So it is kept for the length of an explicitly opened scope, which the type system opens around the closure it resolves and closes again afterwards; outside a scope the file system is read as before. The scope owns what was read, so two of them on one resolver do not stack - the first to end takes it, and the other reads the file system again. BeginSnapshot is on IAssemblyResolver rather than an interface of its own: it is core functionality of a resolver, and one implementation is not an abstraction. This breaks the interface for implementors outside this repository, who opt out by returning null - which is what the three resolvers here that hold nothing do. The remaining probes cost nothing to fix: the preferred runtime pack was listed among the defaults it already belongs to, so its directory was scanned twice for every reference that is not in it, and one package folder was probed once per assembly the package contains. Measured over 27 references with a fresh resolver each time: 3.3 ms per assembly before, 3.1 ms without a scope, 1.1 ms with one. Assisted-by: Claude:claude-opus-5:Claude Codepull/4106/head
10 changed files with 244 additions and 24 deletions
@ -0,0 +1,135 @@
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.IO; |
||||
|
||||
using ICSharpCode.Decompiler.Metadata; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.Decompiler.Tests |
||||
{ |
||||
[TestFixture] |
||||
public class ResolutionSnapshotTests |
||||
{ |
||||
static UniversalAssemblyResolver Resolver() |
||||
{ |
||||
return new UniversalAssemblyResolver(null, throwOnError: false, targetFramework: null); |
||||
} |
||||
|
||||
[Test] |
||||
public void DirectoryIsScannedOnceWhileASnapshotIsOpen() |
||||
{ |
||||
var resolver = Resolver(); |
||||
int scans = 0; |
||||
|
||||
using (resolver.BeginSnapshot()) |
||||
{ |
||||
for (int i = 0; i < 3; i++) |
||||
{ |
||||
Assert.That(resolver.GetOrAddVersionFolder("/shared/pack", _ => { scans++; return "9.0.0"; }), |
||||
Is.EqualTo("9.0.0")); |
||||
} |
||||
} |
||||
|
||||
Assert.That(scans, Is.EqualTo(1)); |
||||
} |
||||
|
||||
[Test] |
||||
public void DirectoryIsScannedEveryTimeWithoutASnapshot() |
||||
{ |
||||
var resolver = Resolver(); |
||||
int scans = 0; |
||||
|
||||
for (int i = 0; i < 3; i++) |
||||
{ |
||||
resolver.GetOrAddVersionFolder("/shared/pack", _ => { scans++; return "9.0.0"; }); |
||||
} |
||||
|
||||
Assert.That(scans, Is.EqualTo(3)); |
||||
} |
||||
|
||||
[Test] |
||||
public void WhatWasScannedIsForgottenWhenTheSnapshotEnds() |
||||
{ |
||||
// The point of the scope: outside it the file system is read afresh, so an assembly
|
||||
// list reloaded after a runtime was installed or removed sees the new state.
|
||||
var resolver = Resolver(); |
||||
int scans = 0; |
||||
|
||||
using (resolver.BeginSnapshot()) |
||||
{ |
||||
resolver.GetOrAddVersionFolder("/shared/pack", _ => { scans++; return "9.0.0"; }); |
||||
} |
||||
using (resolver.BeginSnapshot()) |
||||
{ |
||||
resolver.GetOrAddVersionFolder("/shared/pack", _ => { scans++; return "10.0.0"; }); |
||||
} |
||||
|
||||
Assert.That(scans, Is.EqualTo(2)); |
||||
} |
||||
|
||||
[Test] |
||||
public void ASecondScopeDoesNotStackWithTheFirst() |
||||
{ |
||||
// Two decompilations can overlap on the resolver a LoadedAssembly owns. Whichever scope
|
||||
// ends first takes the cache with it and the other reads the file system again, which
|
||||
// costs that one its head start and nothing else.
|
||||
var resolver = Resolver(); |
||||
int scans = 0; |
||||
|
||||
var outer = resolver.BeginSnapshot(); |
||||
var inner = resolver.BeginSnapshot(); |
||||
inner.Dispose(); |
||||
resolver.GetOrAddVersionFolder("/shared/pack", _ => { scans++; return "9.0.0"; }); |
||||
outer.Dispose(); |
||||
|
||||
Assert.That(scans, Is.EqualTo(1), "the scan happens, it is simply not held any more"); |
||||
} |
||||
|
||||
[Test] |
||||
public void ResolverFindsTheSameFileInsideAndOutsideASnapshot() |
||||
{ |
||||
string directory = Path.Combine(Path.GetTempPath(), "ILSpySnapshotTest_" + Path.GetRandomFileName()); |
||||
Directory.CreateDirectory(directory); |
||||
try |
||||
{ |
||||
string file = Path.Combine(directory, "SomeLibrary.dll"); |
||||
File.WriteAllBytes(file, new byte[0]); |
||||
|
||||
var resolver = new UniversalAssemblyResolver(null, throwOnError: false, targetFramework: null); |
||||
resolver.AddSearchDirectory(directory); |
||||
var reference = AssemblyNameReference.Parse("SomeLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); |
||||
|
||||
string outside = resolver.FindAssemblyFile(reference); |
||||
string inside; |
||||
using (resolver.BeginSnapshot()) |
||||
{ |
||||
inside = resolver.FindAssemblyFile(reference); |
||||
} |
||||
|
||||
Assert.That(outside, Is.EqualTo(file)); |
||||
Assert.That(inside, Is.EqualTo(file)); |
||||
} |
||||
finally |
||||
{ |
||||
Directory.Delete(directory, recursive: true); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue