// Copyright (c) 2026 Christoph Wille // // 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.Diagnostics; using System.Diagnostics.Tracing; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.ILSpyX.Search; namespace ICSharpCode.ILSpyX.Instrumentation { /// /// Outcome reported in the AssemblyResolveStop event. /// public enum AssemblyResolveOutcome { NotFound = 0, FoundInList = 1, LoadedFromDisk = 2, SimilarNameMatch = 3, ProvidedByParentResolver = 4, } /// /// Performance tracing for assembly loading, reference resolution, search, analyzers and /// package extraction. /// /// The provider is consumable via ETW (PerfView) on Windows and via EventPipe /// (dotnet-trace) on all platforms. Start/Stop event pairs let trace viewers compute /// durations and nesting from the event timestamps; call sites therefore do not measure /// elapsed time themselves except for the high-volume single-shot events (per-entry /// extraction), which carry an explicit elapsedMs payload. /// /// Where payload extraction would allocate on a hot path, call sites use the /// strongly-typed [NonEvent] overloads below, which check IsEnabled() before computing /// anything. The remaining [Event] methods are cheap enough to call unconditionally: /// WriteEvent no-ops internally when the provider is disabled. /// [EventSource(Name = "ICSharpCode.ILSpyX")] public sealed class ILSpyXEventSource : EventSource { public static class Keywords { /// Loading assembly files from disk (file loaders, PE parsing). public const EventKeywords AssemblyLoad = (EventKeywords)0x1; /// Assembly reference resolution and assembly-list lookup construction. public const EventKeywords Resolver = (EventKeywords)0x2; /// Per-module search strategy execution. public const EventKeywords Search = (EventKeywords)0x4; /// Analyzer scope determination (referencing-modules scans). public const EventKeywords Analyzers = (EventKeywords)0x8; /// Bundle/zip package opening and per-entry extraction (extraction is Verbose). public const EventKeywords Packages = (EventKeywords)0x10; /// Debug symbol (PDB) loading. public const EventKeywords DebugInfo = (EventKeywords)0x20; } [Event(1, Level = EventLevel.Informational, Keywords = Keywords.AssemblyLoad)] public void AssemblyLoadStart(string fileName) { WriteEvent(1, fileName); } [Event(2, Level = EventLevel.Informational, Keywords = Keywords.AssemblyLoad)] public void AssemblyLoadStop(string fileName, string loaderName, bool success) { WriteEvent(2, fileName, loaderName, success); } [Event(3, Level = EventLevel.Informational, Keywords = Keywords.Resolver)] public void AssemblyResolveStart(string referenceName) { WriteEvent(3, referenceName); } /// Full name of the assembly reference. /// One of the values. [Event(4, Level = EventLevel.Informational, Keywords = Keywords.Resolver)] public void AssemblyResolveStop(string referenceName, int outcome) { WriteEvent(4, referenceName, outcome); } [Event(5, Level = EventLevel.Informational, Keywords = Keywords.Resolver)] public void SnapshotLookupBuildStart(int assemblyCount) { WriteEvent(5, assemblyCount); } [Event(6, Level = EventLevel.Informational, Keywords = Keywords.Resolver)] public void SnapshotLookupBuildStop(int assemblyCount) { WriteEvent(6, assemblyCount); } [Event(7, Level = EventLevel.Informational, Keywords = Keywords.DebugInfo)] public void DebugInfoLoadStart(string assemblyFileName) { WriteEvent(7, assemblyFileName); } [Event(8, Level = EventLevel.Informational, Keywords = Keywords.DebugInfo)] public void DebugInfoLoadStop(string assemblyFileName, string providerKind) { WriteEvent(8, assemblyFileName, providerKind); } [Event(9, Level = EventLevel.Informational, Keywords = Keywords.Search)] public void SearchModuleStart(string moduleName, string strategyName) { WriteEvent(9, moduleName, strategyName); } [Event(10, Level = EventLevel.Informational, Keywords = Keywords.Search)] public void SearchModuleStop(string moduleName, string strategyName) { WriteEvent(10, moduleName, strategyName); } [Event(11, Level = EventLevel.Informational, Keywords = Keywords.Analyzers)] public void AnalyzerScopeStart(string analyzedEntityName) { WriteEvent(11, analyzedEntityName); } [Event(12, Level = EventLevel.Informational, Keywords = Keywords.Analyzers)] public void AnalyzerScopeStop(string analyzedEntityName, int modulesInScope) { WriteEvent(12, analyzedEntityName, modulesInScope); } [Event(13, Level = EventLevel.Informational, Keywords = Keywords.Packages)] public void PackageOpened(string fileName, string packageKind, int entryCount) { WriteEvent(13, fileName, packageKind, entryCount); } [Event(14, Level = EventLevel.Verbose, Keywords = Keywords.Packages)] public void PackageEntryExtracted(string entryName, long bytes, double elapsedMs) { WriteEvent(14, entryName, bytes, elapsedMs); } // Strongly-typed entry points for hot paths: they check IsEnabled() before // extracting payloads, so a disabled provider costs a single branch and zero // allocations. [NonEvent] public void AssemblyResolveStart(IAssemblyReference reference) { if (IsEnabled(EventLevel.Informational, Keywords.Resolver)) AssemblyResolveStart(reference.FullName); } [NonEvent] public void AssemblyResolveStop(IAssemblyReference reference, AssemblyResolveOutcome outcome) { if (IsEnabled(EventLevel.Informational, Keywords.Resolver)) AssemblyResolveStop(reference.FullName, (int)outcome); } [NonEvent] public void SearchModuleStart(MetadataFile module, AbstractSearchStrategy strategy) { if (IsEnabled(EventLevel.Informational, Keywords.Search)) SearchModuleStart(module.Name, strategy.GetType().Name); } [NonEvent] public void SearchModuleStop(MetadataFile module, AbstractSearchStrategy strategy) { if (IsEnabled(EventLevel.Informational, Keywords.Search)) SearchModuleStop(module.Name, strategy.GetType().Name); } /// /// Fractional milliseconds elapsed since a value. /// [NonEvent] public static double ElapsedMilliseconds(long startTimestamp) { return (Stopwatch.GetTimestamp() - startTimestamp) * 1000.0 / Stopwatch.Frequency; } /// /// Gate for the analyzer scope span: AnalyzerScope only wraps its lazily-evaluated /// module enumeration in the tracing iterator when a session is attached. /// [NonEvent] public bool IsAnalyzerTracingEnabled() { return IsEnabled(EventLevel.Informational, Keywords.Analyzers); } /// /// Gate for the per-entry extraction events: extraction sites capture this once so /// they skip the Stopwatch.GetTimestamp() calls when no Verbose session is attached. /// [NonEvent] public bool IsPackageExtractionTracingEnabled() { return IsEnabled(EventLevel.Verbose, Keywords.Packages); } public static readonly ILSpyXEventSource Log = new ILSpyXEventSource(); } }