// 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();
}
}