From 28bf0ed2a3abda9279b1b647d158ddbd4b028d84 Mon Sep 17 00:00:00 2001 From: David Federman Date: Thu, 12 Feb 2026 14:40:20 -0800 Subject: [PATCH] Performance optimization --- src/Analyzer/ReferenceTrimmerAnalyzer.cs | 116 +++++++++++++++++++-- src/Shared/DeclaredReferences.cs | 77 +------------- src/Tasks/CollectDeclaredReferencesTask.cs | 40 ++++++- 3 files changed, 147 insertions(+), 86 deletions(-) diff --git a/src/Analyzer/ReferenceTrimmerAnalyzer.cs b/src/Analyzer/ReferenceTrimmerAnalyzer.cs index 52ff60d..0fa619f 100644 --- a/src/Analyzer/ReferenceTrimmerAnalyzer.cs +++ b/src/Analyzer/ReferenceTrimmerAnalyzer.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; using ReferenceTrimmer.Shared; namespace ReferenceTrimmer.Analyzer; @@ -44,6 +45,14 @@ public class ReferenceTrimmerAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true); + private static readonly DiagnosticDescriptor RT9999Descriptor = new( + "RT9999", + "ReferenceTrimmer internal error", + "ReferenceTrimmer encountered an unexpected error: {0}. Please file a bug at https://github.com/dfederm/ReferenceTrimmer/issues", + "ReferenceTrimmer", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + /// /// The supported diagnostics. /// @@ -52,7 +61,8 @@ public override ImmutableArray SupportedDiagnostics RT0000Descriptor, RT0001Descriptor, RT0002Descriptor, - RT0003Descriptor); + RT0003Descriptor, + RT9999Descriptor); /// public override void Initialize(AnalysisContext context) @@ -64,14 +74,35 @@ public override void Initialize(AnalysisContext context) private static void DumpUsedReferences(CompilationAnalysisContext context) { - string? declaredReferencesPath = GetDeclaredReferencesPath(context); - if (declaredReferencesPath == null) + try + { + DumpUsedReferencesCore(context); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create(RT9999Descriptor, Location.None, ex.Message)); + } + } + + private static void DumpUsedReferencesCore(CompilationAnalysisContext context) + { + AdditionalText? declaredReferencesFile = GetDeclaredReferencesFile(context); + if (declaredReferencesFile == null) { // Reference Trimmer is disabled return; } - DeclaredReferences declaredReferences = DeclaredReferences.ReadFromFile(declaredReferencesPath); + SourceText? sourceText = declaredReferencesFile.GetText(context.CancellationToken); + if (sourceText == null) + { + return; + } + Compilation compilation = context.Compilation; if (compilation.SyntaxTrees.FirstOrDefault()?.Options.DocumentationMode == DocumentationMode.None) { @@ -105,11 +136,11 @@ private static void DumpUsedReferences(CompilationAnalysisContext context) } } - DumpReferencesInfo(usedReferences, unusedReferences, declaredReferencesPath); + DumpReferencesInfo(usedReferences, unusedReferences, declaredReferencesFile.Path); } Dictionary> packageAssembliesDict = new(StringComparer.OrdinalIgnoreCase); - foreach (DeclaredReference declaredReference in declaredReferences.References) + foreach (DeclaredReference declaredReference in ReadDeclaredReferences(sourceText)) { switch (declaredReference.Kind) { @@ -150,7 +181,7 @@ private static void DumpUsedReferences(CompilationAnalysisContext context) { string packageName = kvp.Key; List packageAssemblies = kvp.Value; - if (!packageAssemblies.Any(usedReferences.Contains)) + if (!usedReferences.Overlaps(packageAssemblies)) { context.ReportDiagnostic(Diagnostic.Create(RT0003Descriptor, Location.None, packageName)); } @@ -188,16 +219,83 @@ private static void WriteFile(string filePath, string text) } } - private static string? GetDeclaredReferencesPath(CompilationAnalysisContext context) + private static AdditionalText? GetDeclaredReferencesFile(CompilationAnalysisContext context) { foreach (AdditionalText additionalText in context.Options.AdditionalFiles) { if (Path.GetFileName(additionalText.Path).Equals(DeclaredReferencesFileName, StringComparison.Ordinal)) { - return additionalText.Path; + return additionalText; } } return null; } + + // File format: tab-separated fields (AssemblyPath, Kind, Spec), one reference per line. + // Keep in sync with SaveDeclaredReferences in CollectDeclaredReferencesTask.cs. + private static IEnumerable ReadDeclaredReferences(SourceText sourceText) + { + foreach (TextLine textLine in sourceText.Lines) + { + TextSpan lineSpan = textLine.Span; + if (lineSpan.Length == 0) + { + continue; + } + + // Find tab delimiters within the line span to avoid full-line ToString + Split. + int start = lineSpan.Start; + int end = lineSpan.End; + + int firstTab = -1; + int secondTab = -1; + for (int i = start; i < end; i++) + { + if (sourceText[i] == '\t') + { + if (firstTab == -1) + { + firstTab = i; + } + else + { + secondTab = i; + break; + } + } + } + + if (firstTab == -1 || secondTab == -1) + { + yield break; + } + + string assemblyPath = sourceText.ToString(TextSpan.FromBounds(start, firstTab)); + string spec = sourceText.ToString(TextSpan.FromBounds(secondTab + 1, end)); + + // Determine kind without allocating a string. The three possible values are + // "Reference" (len 9), "ProjectReference" (len 16), "PackageReference" (len 16). + int kindLength = secondTab - firstTab - 1; + DeclaredReferenceKind kind; + if (kindLength == 9) + { + kind = DeclaredReferenceKind.Reference; + } + else if (kindLength == 16 && sourceText[firstTab + 1] == 'P' && sourceText[firstTab + 2] == 'r') + { + kind = DeclaredReferenceKind.ProjectReference; + } + else if (kindLength == 16 && sourceText[firstTab + 1] == 'P' && sourceText[firstTab + 2] == 'a') + { + kind = DeclaredReferenceKind.PackageReference; + } + else + { + continue; + } + + yield return new DeclaredReference(assemblyPath, kind, spec); + } + } } diff --git a/src/Shared/DeclaredReferences.cs b/src/Shared/DeclaredReferences.cs index 12128b8..fa9aa05 100644 --- a/src/Shared/DeclaredReferences.cs +++ b/src/Shared/DeclaredReferences.cs @@ -1,80 +1,5 @@ -using System.Text; - namespace ReferenceTrimmer.Shared; -internal record DeclaredReferences(IReadOnlyList References) -{ - private const char FieldDelimiter = '\t'; - - private static readonly char[] FieldDelimiters = new[] { FieldDelimiter }; - - private static readonly Dictionary KindEnumToString = new() - { - { DeclaredReferenceKind.Reference, nameof(DeclaredReferenceKind.Reference) }, - { DeclaredReferenceKind.ProjectReference, nameof(DeclaredReferenceKind.ProjectReference) }, - { DeclaredReferenceKind.PackageReference, nameof(DeclaredReferenceKind.PackageReference) }, - }; - - private static readonly Dictionary KindStringToEnum = new() - { - { nameof(DeclaredReferenceKind.Reference), DeclaredReferenceKind.Reference }, - { nameof(DeclaredReferenceKind.ProjectReference), DeclaredReferenceKind.ProjectReference }, - { nameof(DeclaredReferenceKind.PackageReference), DeclaredReferenceKind.PackageReference }, - }; - - public void SaveToFile(string filePath) - { - StringBuilder writer = new(); - foreach (DeclaredReference reference in References) - { - writer.Append(reference.AssemblyPath); - writer.Append(FieldDelimiter); - writer.Append(KindEnumToString[reference.Kind]); - writer.Append(FieldDelimiter); - writer.Append(reference.Spec); - writer.AppendLine(); - } - - string newContent = writer.ToString(); - if (File.Exists(filePath)) - { - string existing = File.ReadAllText(filePath); - if (string.Equals(existing, newContent, StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - - File.WriteAllText(filePath, newContent); - } - - public static DeclaredReferences ReadFromFile(string filePath) - { - List references = new(); - - using FileStream stream = File.OpenRead(filePath); - using StreamReader reader = new(stream); - - string? line; - while ((line = reader.ReadLine()) != null) - { - string[] parts = line.Split(FieldDelimiters, 3); - if (parts.Length != 3) - { - throw new InvalidDataException($"File '{filePath}' is invalid. Line: {references.Count + 1}"); - } - - string assemblyName = parts[0]; - DeclaredReferenceKind kind = KindStringToEnum[parts[1]]; - string spec = parts[2]; - DeclaredReference reference = new(assemblyName, kind, spec); - references.Add(reference); - } - - return new DeclaredReferences(references); - } -} - -internal record DeclaredReference(string AssemblyPath, DeclaredReferenceKind Kind, string Spec); +internal readonly record struct DeclaredReference(string AssemblyPath, DeclaredReferenceKind Kind, string Spec); internal enum DeclaredReferenceKind { Reference, ProjectReference, PackageReference } \ No newline at end of file diff --git a/src/Tasks/CollectDeclaredReferencesTask.cs b/src/Tasks/CollectDeclaredReferencesTask.cs index 425d624..00e3c6a 100644 --- a/src/Tasks/CollectDeclaredReferencesTask.cs +++ b/src/Tasks/CollectDeclaredReferencesTask.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Text; using System.Xml.Linq; using Microsoft.Build.Framework; using NuGet.Common; @@ -211,7 +212,7 @@ public override bool Execute() if (OutputFile is not null) { - new DeclaredReferences(declaredReferences).SaveToFile(OutputFile); + SaveDeclaredReferences(declaredReferences, OutputFile); Log.LogMessage(MessageImportance.Low, "Saved {0} declared references to '{1}'", declaredReferences.Count, OutputFile); } } @@ -475,6 +476,43 @@ private static bool IsSuppressed(ITaskItem item, string warningId) return false; } + // File format: tab-separated fields (AssemblyPath, Kind, Spec), one reference per line. + // Keep in sync with ReadDeclaredReferences in ReferenceTrimmerAnalyzer.cs. + private static void SaveDeclaredReferences(IReadOnlyList declaredReferences, string filePath) + { + const char fieldDelimiter = '\t'; + + StringBuilder writer = new(); + foreach (DeclaredReference reference in declaredReferences) + { + writer.Append(reference.AssemblyPath); + writer.Append(fieldDelimiter); + string kindString = reference.Kind switch + { + DeclaredReferenceKind.Reference => nameof(DeclaredReferenceKind.Reference), + DeclaredReferenceKind.ProjectReference => nameof(DeclaredReferenceKind.ProjectReference), + DeclaredReferenceKind.PackageReference => nameof(DeclaredReferenceKind.PackageReference), + _ => throw new InvalidDataException($"Unknown reference kind '{reference.Kind}'."), + }; + writer.Append(kindString); + writer.Append(fieldDelimiter); + writer.Append(reference.Spec); + writer.AppendLine(); + } + + string newContent = writer.ToString(); + if (File.Exists(filePath)) + { + string existing = File.ReadAllText(filePath); + if (string.Equals(existing, newContent, StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + + File.WriteAllText(filePath, newContent); + } + private sealed class PackageInfoBuilder { private List? _compileTimeAssemblies;