From f39b104474c10edce62b1c5194d4394f5276f359 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 11:22:16 +1100 Subject: [PATCH 1/8] Remove duplicate code and optimize common case These item lists are often empty. Optimize the common case here. --- .../Snapshots/RestoreBuilder.cs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs index 40c2c9ce07d..6d321637e2f 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs @@ -18,41 +18,38 @@ internal static class RestoreBuilder public static ProjectRestoreInfo ToProjectRestoreInfo(IImmutableDictionary update) { IImmutableDictionary properties = update.GetSnapshotOrEmpty(NuGetRestore.SchemaName).Properties; - IProjectRuleSnapshot frameworkReferences = update.GetSnapshotOrEmpty(CollectedFrameworkReference.SchemaName); - IProjectRuleSnapshot packageDownloads = update.GetSnapshotOrEmpty(CollectedPackageDownload.SchemaName); - IProjectRuleSnapshot projectReferences = update.GetSnapshotOrEmpty(EvaluatedProjectReference.SchemaName); - IProjectRuleSnapshot packageReferences = update.GetSnapshotOrEmpty(CollectedPackageReference.SchemaName); - IProjectRuleSnapshot packageVersions = update.GetSnapshotOrEmpty(CollectedPackageVersion.SchemaName); - IProjectRuleSnapshot nuGetAuditSuppress = update.GetSnapshotOrEmpty(CollectedNuGetAuditSuppressions.SchemaName); - IProjectRuleSnapshot prunePackageReferences = update.GetSnapshotOrEmpty(CollectedPrunePackageReference.SchemaName); - IProjectRuleSnapshot toolReferences = update.GetSnapshotOrEmpty(DotNetCliToolReference.SchemaName); // For certain project types such as UWP, "TargetFrameworkMoniker" != the moniker that restore uses string targetMoniker = properties.GetPropertyOrEmpty(NuGetRestore.NuGetTargetMonikerProperty); if (targetMoniker.Length == 0) targetMoniker = properties.GetPropertyOrEmpty(NuGetRestore.TargetFrameworkMonikerProperty); - TargetFrameworkInfo frameworkInfo = new TargetFrameworkInfo( + TargetFrameworkInfo frameworkInfo = new( targetMoniker, - ToReferenceItems(frameworkReferences.Items), - ToReferenceItems(packageDownloads.Items), - ToReferenceItems(projectReferences.Items), - ToReferenceItems(packageReferences.Items), - ToReferenceItems(packageVersions.Items), - ToReferenceItems(nuGetAuditSuppress.Items), - ToReferenceItems(prunePackageReferences.Items), - properties); + frameworkReferences: GetReferenceItems(CollectedFrameworkReference.SchemaName), + packageDownloads: GetReferenceItems(CollectedPackageDownload.SchemaName), + projectReferences: GetReferenceItems(EvaluatedProjectReference.SchemaName), + packageReferences: GetReferenceItems(CollectedPackageReference.SchemaName), + centralPackageVersions: GetReferenceItems(CollectedPackageVersion.SchemaName), + nuGetAuditSuppress: GetReferenceItems(CollectedNuGetAuditSuppressions.SchemaName), + prunePackageReferences: GetReferenceItems(CollectedPrunePackageReference.SchemaName), + properties: properties); return new ProjectRestoreInfo( - properties.GetPropertyOrEmpty(NuGetRestore.MSBuildProjectExtensionsPathProperty), - properties.GetPropertyOrEmpty(NuGetRestore.ProjectAssetsFileProperty), - properties.GetPropertyOrEmpty(NuGetRestore.TargetFrameworksProperty), - EmptyTargetFrameworks.Add(frameworkInfo), - ToReferenceItems(toolReferences.Items)); + msbuildProjectExtensionsPath: properties.GetPropertyOrEmpty(NuGetRestore.MSBuildProjectExtensionsPathProperty), + projectAssetsFilePath: properties.GetPropertyOrEmpty(NuGetRestore.ProjectAssetsFileProperty), + originalTargetFrameworks: properties.GetPropertyOrEmpty(NuGetRestore.TargetFrameworksProperty), + targetFrameworks: [frameworkInfo], + toolReferences: GetReferenceItems(DotNetCliToolReference.SchemaName)); - static ImmutableArray ToReferenceItems(IImmutableDictionary> items) + ImmutableArray GetReferenceItems(string schemaName) { - return items.ToImmutableArray(static (name, metadata) => new ReferenceItem(name, metadata)); + if (!update.TryGetValue(schemaName, out IProjectRuleSnapshot? result)) + { + return []; + } + + return result.Items.ToImmutableArray(static (name, metadata) => new ReferenceItem(name, metadata)); } } } From f857ee6102034c18c4722338b6ed17cce21e23d6 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 11:22:37 +1100 Subject: [PATCH 2/8] Consistent use of extension methods in class --- .../PackageRestore/Snapshots/RestoreHasher.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs index c20df0ae62f..a350b98c6ff 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs @@ -28,7 +28,7 @@ public static Hash CalculateHash(ProjectRestoreInfo restoreInfo) hasher.AppendReferences(framework.NuGetAuditSuppress); } - AppendReferences(hasher, restoreInfo.ToolReferences); + hasher.AppendReferences(restoreInfo.ToolReferences); return hasher.GetHashAndReset(); } @@ -37,7 +37,7 @@ private static void AppendFrameworkProperties(this IncrementalHasher hasher, Tar { foreach ((string key, string value) in framework.Properties) { - AppendProperty(hasher, key, value); + hasher.AppendProperty(key, value); } } @@ -45,8 +45,8 @@ private static void AppendReferences(this IncrementalHasher hasher, ImmutableArr { foreach (ReferenceItem reference in references) { - AppendProperty(hasher, nameof(reference.Name), reference.Name); - AppendReferenceProperties(hasher, reference); + hasher.AppendProperty(nameof(reference.Name), reference.Name); + hasher.AppendReferenceProperties(reference); } } @@ -54,7 +54,7 @@ private static void AppendReferenceProperties(this IncrementalHasher hasher, Ref { foreach ((string key, string value) in reference.Properties) { - AppendProperty(hasher, key, value); + hasher.AppendProperty(key, value); } } From 94ff4678715e8b503b1a6f880ce4070336e0a93f Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 11:23:34 +1100 Subject: [PATCH 3/8] Remove singleton empty collections They were only used in test code, and they don't add any value over `[]`. --- .../PackageRestore/Snapshots/RestoreBuilder.cs | 3 --- .../Mocks/ProjectRestoreInfoFactory.cs | 9 ++++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs index 6d321637e2f..20258851154 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreBuilder.cs @@ -9,9 +9,6 @@ namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// internal static class RestoreBuilder { - public static readonly ImmutableArray EmptyTargetFrameworks = []; - public static readonly ImmutableArray EmptyReferences = []; - /// /// Converts an immutable dictionary of rule snapshot data into an instance. /// diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ProjectRestoreInfoFactory.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ProjectRestoreInfoFactory.cs index fe7d6320465..1eb5443036b 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ProjectRestoreInfoFactory.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ProjectRestoreInfoFactory.cs @@ -6,8 +6,11 @@ internal static class ProjectRestoreInfoFactory { public static ProjectRestoreInfo Create(string? msbuildProjectExtensionsPath = null) { - return new ProjectRestoreInfo(msbuildProjectExtensionsPath ?? string.Empty, string.Empty, string.Empty, - RestoreBuilder.EmptyTargetFrameworks, - RestoreBuilder.EmptyReferences); + return new ProjectRestoreInfo( + msbuildProjectExtensionsPath: msbuildProjectExtensionsPath ?? "", + projectAssetsFilePath: "", + originalTargetFrameworks: "", + targetFrameworks: [], + toolReferences: []); } } From a82ad0ffab14af43dc9bc27f76c8eb9d066319e8 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 11:23:53 +1100 Subject: [PATCH 4/8] Documentation updates --- .../PackageRestore/Snapshots/ReferenceItem.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs index 73702161f38..7650a20d4a0 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs @@ -5,12 +5,12 @@ namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// -/// Represents a single package, tool or project reference. +/// Represents a reference item involved in package restore, with its associated metadata. /// [DebuggerDisplay("Name = {Name}")] internal class ReferenceItem { - // If additional fields/properties are added to this class, please update RestoreHasher + // If additional state is added to this class, please update RestoreHasher public ReferenceItem(string name, IImmutableDictionary properties) { @@ -20,7 +20,13 @@ public ReferenceItem(string name, IImmutableDictionary propertie Properties = properties; } + /// + /// Gets the name (item spec) of the reference. + /// public string Name { get; } + /// + /// Gets the name/value pair metadata associated with the reference. + /// public IImmutableDictionary Properties { get; } } From fd40c34c373cf2479707c7519f87cfbfbd2b65ae Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 11:24:39 +1100 Subject: [PATCH 5/8] Seal class --- .../ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs index 7650a20d4a0..f3494407d49 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs @@ -8,7 +8,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// Represents a reference item involved in package restore, with its associated metadata. /// [DebuggerDisplay("Name = {Name}")] -internal class ReferenceItem +internal sealed class ReferenceItem { // If additional state is added to this class, please update RestoreHasher From adbaaf83cfa6a99dedd446fa78a2f83b42537235 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 11:26:18 +1100 Subject: [PATCH 6/8] Rename "properties" to "metadata" We refer to name/value pairs on items as "metadata" to differentiate them from project-level properties. --- .../Snapshots/VsReferenceItem.cs | 2 +- .../PackageRestore/Snapshots/ReferenceItem.cs | 6 +-- ...eComparer.ReferenceItemEqualityComparer.cs | 2 +- .../PackageRestore/Snapshots/RestoreHasher.cs | 2 +- .../PackageRestore/Snapshots/RestoreLogger.cs | 2 +- .../Snapshots/RestoreBuilderTests.cs | 40 +++++++++---------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/PackageRestore/Snapshots/VsReferenceItem.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/PackageRestore/Snapshots/VsReferenceItem.cs index b33668af604..bb6c66641ee 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/PackageRestore/Snapshots/VsReferenceItem.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed.VS/ProjectSystem/VS/PackageRestore/Snapshots/VsReferenceItem.cs @@ -15,5 +15,5 @@ internal class VsReferenceItem(ReferenceItem referenceItem) : IVsReferenceItem2 { public string Name => referenceItem.Name; - public IReadOnlyDictionary? Metadata => referenceItem.Properties; + public IReadOnlyDictionary? Metadata => referenceItem.Metadata; } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs index f3494407d49..5a6fdeae4a0 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs @@ -12,12 +12,12 @@ internal sealed class ReferenceItem { // If additional state is added to this class, please update RestoreHasher - public ReferenceItem(string name, IImmutableDictionary properties) + public ReferenceItem(string name, IImmutableDictionary metadata) { Requires.NotNullOrEmpty(name); Name = name; - Properties = properties; + Metadata = metadata; } /// @@ -28,5 +28,5 @@ public ReferenceItem(string name, IImmutableDictionary propertie /// /// Gets the name/value pair metadata associated with the reference. /// - public IImmutableDictionary Properties { get; } + public IImmutableDictionary Metadata { get; } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreComparer.ReferenceItemEqualityComparer.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreComparer.ReferenceItemEqualityComparer.cs index 269a3a81f0e..ba64d48d49c 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreComparer.ReferenceItemEqualityComparer.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreComparer.ReferenceItemEqualityComparer.cs @@ -14,7 +14,7 @@ public override bool Equals(ReferenceItem? x, ReferenceItem? y) if (!StringComparers.ItemNames.Equals(x.Name, y.Name)) return false; - if (!PropertiesAreEqual(x.Properties, y.Properties)) + if (!PropertiesAreEqual(x.Metadata, y.Metadata)) return false; return true; diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs index a350b98c6ff..50220cc094e 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs @@ -52,7 +52,7 @@ private static void AppendReferences(this IncrementalHasher hasher, ImmutableArr private static void AppendReferenceProperties(this IncrementalHasher hasher, ReferenceItem reference) { - foreach ((string key, string value) in reference.Properties) + foreach ((string key, string value) in reference.Metadata) { hasher.AppendProperty(key, value); } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreLogger.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreLogger.cs index 7169f3d0544..199516faa8b 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreLogger.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreLogger.cs @@ -86,7 +86,7 @@ private static void LogReferenceItems(BatchLogger logger, string heading, Immuta foreach (ReferenceItem reference in references) { - IEnumerable properties = reference.Properties.Select(prop => $"{prop.Key}:{prop.Value}"); + IEnumerable properties = reference.Metadata.Select(prop => $"{prop.Key}:{prop.Value}"); logger.WriteLine($"{reference.Name} -- ({string.Join(" | ", properties)})"); } diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/PackageRestore/Snapshots/RestoreBuilderTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/PackageRestore/Snapshots/RestoreBuilderTests.cs index 045288d81d1..41703877c96 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/PackageRestore/Snapshots/RestoreBuilderTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/ProjectSystem/PackageRestore/Snapshots/RestoreBuilderTests.cs @@ -192,15 +192,15 @@ public void ToProjectRestoreInfo_SetsToolReferences() var toolReference1 = references.FirstOrDefault(r => r.Name == "ToolReference1"); Assert.NotNull(toolReference1); - AssertContainsProperty("Version", "1.0.0.0", toolReference1.Properties); + AssertContainsProperty("Version", "1.0.0.0", toolReference1.Metadata); var toolReference2 = references.FirstOrDefault(r => r.Name == "ToolReference2"); Assert.NotNull(toolReference2); - AssertContainsProperty("Version", "2.0.0.0", toolReference2.Properties); + AssertContainsProperty("Version", "2.0.0.0", toolReference2.Metadata); var toolReference3 = references.FirstOrDefault(r => r.Name == "ToolReference3"); Assert.NotNull(toolReference3); - AssertContainsProperty("Name", "Value", toolReference3.Properties); + AssertContainsProperty("Name", "Value", toolReference3.Metadata); } [Fact] @@ -234,15 +234,15 @@ public void ToProjectRestoreInfo_SetsPackageReferences() var packageReference1 = references.FirstOrDefault(r => r.Name == "PackageReference1"); Assert.NotNull(packageReference1); - AssertContainsProperty("Version", "1.0.0.0", packageReference1.Properties); + AssertContainsProperty("Version", "1.0.0.0", packageReference1.Metadata); var packageReference2 = references.FirstOrDefault(r => r.Name == "PackageReference2"); Assert.NotNull(packageReference2); - AssertContainsProperty("Version", "2.0.0.0", packageReference2.Properties); + AssertContainsProperty("Version", "2.0.0.0", packageReference2.Metadata); var packageReference3 = references.FirstOrDefault(r => r.Name == "PackageReference3"); Assert.NotNull(packageReference3); - AssertContainsProperty("Name", "Value", packageReference3.Properties); + AssertContainsProperty("Name", "Value", packageReference3.Metadata); } [Fact] @@ -276,17 +276,17 @@ public void ToProjectRestoreInfo_SetsCentralPackageVersions() var reference1 = versions.FirstOrDefault(r => r.Name == "Newtonsoft.Json"); Assert.NotNull(reference1); - AssertContainsProperty("Version", "1.0", reference1.Properties); + AssertContainsProperty("Version", "1.0", reference1.Metadata); var reference2 = versions.FirstOrDefault(r => r.Name == "System.IO"); Assert.NotNull(reference2); - AssertContainsProperty("Version", "2.0", reference2.Properties); + AssertContainsProperty("Version", "2.0", reference2.Metadata); var reference3 = versions.FirstOrDefault(r => r.Name == "Microsoft.Extensions"); Assert.NotNull(reference3); Assert.Equal("Microsoft.Extensions", reference3.Name); - AssertContainsProperty("Version", "3.0", reference3.Properties); + AssertContainsProperty("Version", "3.0", reference3.Metadata); } [Fact] @@ -344,17 +344,17 @@ public void ToProjectRestoreInfo_SetsPrunePackageReferences() var reference1 = prunePackageReferences.FirstOrDefault(r => r.Name == "Newtonsoft.Json"); Assert.NotNull(reference1); - AssertContainsProperty("Version", "1.0", reference1.Properties); + AssertContainsProperty("Version", "1.0", reference1.Metadata); var reference2 = prunePackageReferences.FirstOrDefault(r => r.Name == "System.IO"); Assert.NotNull(reference2); - AssertContainsProperty("Version", "2.0", reference2.Properties); + AssertContainsProperty("Version", "2.0", reference2.Metadata); var reference3 = prunePackageReferences.FirstOrDefault(r => r.Name == "Microsoft.Extensions"); Assert.NotNull(reference3); Assert.Equal("Microsoft.Extensions", reference3.Name); - AssertContainsProperty("Version", "3.0", reference3.Properties); + AssertContainsProperty("Version", "3.0", reference3.Metadata); } [Fact] @@ -389,16 +389,16 @@ public void ToProjectRestoreInfo_SetsProjectReferences() var reference1 = references.FirstOrDefault(p => p.Name == "..\\Project\\Project1.csproj"); Assert.NotNull(reference1); - AssertContainsProperty("ProjectFileFullPath", "C:\\Solution\\Project\\Project1.csproj", reference1.Properties); + AssertContainsProperty("ProjectFileFullPath", "C:\\Solution\\Project\\Project1.csproj", reference1.Metadata); var reference2 = references.FirstOrDefault(p => p.Name == "..\\Project\\Project2.csproj"); Assert.NotNull(reference2); - AssertContainsProperty("ProjectFileFullPath", "C:\\Solution\\Project\\Project2.csproj", reference2.Properties); + AssertContainsProperty("ProjectFileFullPath", "C:\\Solution\\Project\\Project2.csproj", reference2.Metadata); var reference3 = references.FirstOrDefault(p => p.Name == "..\\Project\\Project3.csproj"); Assert.NotNull(reference3); - AssertContainsProperty("ProjectFileFullPath", "C:\\Solution\\Project\\Project3.csproj", reference3.Properties); - AssertContainsProperty("MetadataName", "MetadataValue", reference3.Properties); + AssertContainsProperty("ProjectFileFullPath", "C:\\Solution\\Project\\Project3.csproj", reference3.Metadata); + AssertContainsProperty("MetadataName", "MetadataValue", reference3.Metadata); } [Fact] @@ -428,11 +428,11 @@ public void ToProjectRestoreInfo_SetsFrameworkReferences() var reference1 = references.FirstOrDefault(r => r.Name == "WindowsForms"); Assert.NotNull(reference1); - Assert.Empty(reference1.Properties); + Assert.Empty(reference1.Metadata); var reference2 = references.FirstOrDefault(r => r.Name == "WPF"); Assert.NotNull(reference2); - AssertContainsProperty("PrivateAssets", "all", reference2.Properties); + AssertContainsProperty("PrivateAssets", "all", reference2.Metadata); } [Fact] @@ -463,11 +463,11 @@ public void ToProjectRestoreInfo_SetsPackageDownloads() var download1 = downloads.FirstOrDefault(d => d.Name == "NuGet.Common"); Assert.NotNull(download1); - AssertContainsProperty("Version", "[4.0.0];[5.0.0]", download1.Properties); + AssertContainsProperty("Version", "[4.0.0];[5.0.0]", download1.Metadata); var download2 = downloads.FirstOrDefault(d => d.Name == "NuGet.Frameworks"); Assert.NotNull(download2); - AssertContainsProperty("Version", "[4.9.4]", download2.Properties); + AssertContainsProperty("Version", "[4.9.4]", download2.Metadata); } private static void AssertContainsProperty(string name, string value, IImmutableDictionary properties) From 755c7cd0336c1dfa542b7fd22af33f3eba6861e5 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Wed, 8 Oct 2025 14:59:34 +1100 Subject: [PATCH 7/8] Publish ETW events for package restore nominations In automated performance regression tests, it's helpful to validate that package restore nominations are not happening more often than required. However there are no telemetry events for this operation, so we do not have a direct way to measure this. This change adds a new `TraceSource` to the .NET Project System, that ETW clients can opt into. When enabled, any package restore nominations are logged, including information about the reason for the nomination. For example, if a package version is changed: ``` TargetFrameworks .NETCoreApp,Version=v8.0 PackageReferences MetadataExtractor Version Before: 2.8.1 After: 2.9.0 ``` These ETW events are off by default. We'll enable them in the lab for performance regressions, and they will allow catching regressions in the number of nominations, as well as helping to quickly identify what changes led to that nomination. There's no impact in regular use. --- .../PackageRestoreDataSource.cs | 37 ++++- .../PackageRestore/Snapshots/IRestoreState.cs | 23 +++ .../Snapshots/IncrementalHasherExtensions.cs | 23 +++ .../Snapshots/ProjectRestoreInfo.cs | 51 ++++--- .../PackageRestore/Snapshots/ReferenceItem.cs | 24 +++- .../PackageRestore/Snapshots/RestoreHasher.cs | 67 --------- .../RestoreStateComparisonBuilder.cs | 134 ++++++++++++++++++ .../Snapshots/TargetFrameworkInfo.cs | 86 ++++++----- .../DotNetProjectSystemEventSource.cs | 19 +++ .../Utilities/SetDiff.cs | 40 ++---- 10 files changed, 355 insertions(+), 149 deletions(-) create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IRestoreState.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IncrementalHasherExtensions.cs delete mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreStateComparisonBuilder.cs create mode 100644 src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Telemetry/DotNetProjectSystemEventSource.cs diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/PackageRestoreDataSource.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/PackageRestoreDataSource.cs index 12f4f633d86..91fbd139911 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/PackageRestoreDataSource.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/PackageRestoreDataSource.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks.Dataflow; using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.IO; +using Microsoft.VisualStudio.ProjectSystem.Telemetry; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Threading; @@ -58,6 +59,7 @@ internal partial class PackageRestoreDataSource : ChainedProjectValueDataSourceB private readonly IManagedProjectDiagnosticOutputService _logger; private readonly INuGetRestoreService _nuGetRestoreService; private Hash? _lastHash; + private ProjectRestoreInfo? _lastRestoreInfo; private bool _enabled; private bool _wasSourceBlockContinuationSet; @@ -109,10 +111,7 @@ internal async Task>> RestoreAsy RestoreData restoreData = CreateRestoreData(e.Value.RestoreInfo, succeeded); - return new[] - { - new ProjectVersionedValue(restoreData, e.DataSourceVersions) - }; + return [new ProjectVersionedValue(restoreData, e.DataSourceVersions)]; } private async Task RestoreCoreAsync(PackageRestoreUnconfiguredInput value) @@ -125,22 +124,43 @@ private async Task RestoreCoreAsync(PackageRestoreUnconfiguredInput value) // Restore service always does work regardless of whether the value we pass // them to actually contains changes, only nominate if there are any. - Hash hash = RestoreHasher.CalculateHash(restoreInfo); + Hash hash = CalculateHash(); if (await _cycleDetector.IsCycleDetectedAsync(hash, value.ActiveConfiguration, token)) { _lastHash = hash; + _lastRestoreInfo = restoreInfo; return false; } if (_lastHash?.Equals(hash) == true) { await _nuGetRestoreService.UpdateWithoutNominationAsync(value.ConfiguredInputs); + _lastRestoreInfo = restoreInfo; return true; } _lastHash = hash; + if (DotNetProjectSystemEventSource.Instance.IsEnabled()) + { + string changes; + if (_lastRestoreInfo is null) + { + changes = "Initial restore."; + } + else + { + RestoreStateComparisonBuilder builder = new(); + _lastRestoreInfo.DescribeChanges(builder, restoreInfo); + changes = builder.ToString(); + } + + DotNetProjectSystemEventSource.Instance.NominateForRestore(projectFilePath: _project.FullPath, changes: changes); + } + + _lastRestoreInfo = restoreInfo; + JoinableTask joinableTask = JoinableFactory.RunAsync(() => { return NominateForRestoreAsync(restoreInfo, value.ConfiguredInputs, token); @@ -152,6 +172,13 @@ private async Task RestoreCoreAsync(PackageRestoreUnconfiguredInput value) registerFaultHandler: true); return await joinableTask; + + Hash CalculateHash() + { + using var hasher = new IncrementalHasher(); + restoreInfo.AddToHash(hasher); + return hasher.GetHashAndReset(); + } } private async Task NominateForRestoreAsync(ProjectRestoreInfo restoreInfo, IReadOnlyCollection versions, CancellationToken cancellationToken) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IRestoreState.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IRestoreState.cs new file mode 100644 index 00000000000..70d5411ce69 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IRestoreState.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; + +/// +/// A data object at some level within the restore state snapshot, . +/// +/// +internal interface IRestoreState where T : class +{ + /// + /// Adds state from this object to . + /// + /// + void AddToHash(IncrementalHasher hasher); + + /// + /// Compares all state between this instance and , and logs details of any changes. + /// + void DescribeChanges(RestoreStateComparisonBuilder builder, T after); +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IncrementalHasherExtensions.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IncrementalHasherExtensions.cs new file mode 100644 index 00000000000..3e1d231284c --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/IncrementalHasherExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; + +internal static class IncrementalHasherExtensions +{ + public static void AppendProperty(this IncrementalHasher hasher, string name, string value) + { + hasher.Append(name); + hasher.Append("|"); + hasher.Append(value); + } + + public static void AppendArray(this IncrementalHasher hasher, ImmutableArray items) where T : class, IRestoreState + { + foreach (T item in items) + { + item.AddToHash(hasher); + } + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ProjectRestoreInfo.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ProjectRestoreInfo.cs index 6bcfe1378bc..67bd2dd5fa5 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ProjectRestoreInfo.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ProjectRestoreInfo.cs @@ -1,30 +1,49 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. +using Microsoft.VisualStudio.Text; + namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// -/// A complete set of restore data for a project. +/// A complete set of restore data for a project. /// -internal class ProjectRestoreInfo +internal sealed class ProjectRestoreInfo( + string msbuildProjectExtensionsPath, + string projectAssetsFilePath, + string originalTargetFrameworks, + ImmutableArray targetFrameworks, + ImmutableArray toolReferences) + : IRestoreState { - // If additional fields/properties are added to this class, please update RestoreHasher + // IMPORTANT: If additional state is added, update AddToHash and DescribeChanges below. - public ProjectRestoreInfo(string msbuildProjectExtensionsPath, string projectAssetsFilePath, string originalTargetFrameworks, ImmutableArray targetFrameworks, ImmutableArray toolReferences) - { - MSBuildProjectExtensionsPath = msbuildProjectExtensionsPath; - ProjectAssetsFilePath = projectAssetsFilePath; - OriginalTargetFrameworks = originalTargetFrameworks; - TargetFrameworks = targetFrameworks; - ToolReferences = toolReferences; - } + public string MSBuildProjectExtensionsPath { get; } = msbuildProjectExtensionsPath; + + public string ProjectAssetsFilePath { get; } = projectAssetsFilePath; + + public string OriginalTargetFrameworks { get; } = originalTargetFrameworks; - public string MSBuildProjectExtensionsPath { get; } + public ImmutableArray TargetFrameworks { get; } = targetFrameworks; - public string ProjectAssetsFilePath { get; } + public ImmutableArray ToolReferences { get; } = toolReferences; - public string OriginalTargetFrameworks { get; } + public void AddToHash(IncrementalHasher hasher) + { + hasher.AppendProperty(nameof(ProjectAssetsFilePath), ProjectAssetsFilePath); + hasher.AppendProperty(nameof(MSBuildProjectExtensionsPath), MSBuildProjectExtensionsPath); + hasher.AppendProperty(nameof(OriginalTargetFrameworks), OriginalTargetFrameworks); - public ImmutableArray TargetFrameworks { get; } + hasher.AppendArray(TargetFrameworks); + hasher.AppendArray(ToolReferences); + } - public ImmutableArray ToolReferences { get; } + public void DescribeChanges(RestoreStateComparisonBuilder builder, ProjectRestoreInfo after) + { + builder.CompareString(MSBuildProjectExtensionsPath, after.MSBuildProjectExtensionsPath, nameof(MSBuildProjectExtensionsPath)); + builder.CompareString(ProjectAssetsFilePath, after.ProjectAssetsFilePath, nameof(ProjectAssetsFilePath)); + builder.CompareString(OriginalTargetFrameworks, after.OriginalTargetFrameworks, nameof(OriginalTargetFrameworks)); + + builder.CompareArray(TargetFrameworks, after.TargetFrameworks, nameof(TargetFrameworks)); + builder.CompareArray(ToolReferences, after.ToolReferences, nameof(ToolReferences)); + } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs index 5a6fdeae4a0..8b768e83752 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. using System.Diagnostics; +using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; @@ -8,7 +9,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// Represents a reference item involved in package restore, with its associated metadata. /// [DebuggerDisplay("Name = {Name}")] -internal sealed class ReferenceItem +internal sealed class ReferenceItem : IRestoreState { // If additional state is added to this class, please update RestoreHasher @@ -29,4 +30,25 @@ public ReferenceItem(string name, IImmutableDictionary metadata) /// Gets the name/value pair metadata associated with the reference. /// public IImmutableDictionary Metadata { get; } + + public void AddToHash(IncrementalHasher hasher) + { + hasher.AppendProperty(nameof(Name), Name); + + foreach ((string key, string value) in Metadata) + { + hasher.AppendProperty(key, value); + } + } + + public void DescribeChanges(RestoreStateComparisonBuilder builder, ReferenceItem after) + { + builder.PushScope(Name); + + builder.CompareString(Name, after.Name, name: "%(Identity)"); + + builder.CompareDictionary(Metadata, after.Metadata); + + builder.PopScope(); + } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs deleted file mode 100644 index 50220cc094e..00000000000 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. - -using Microsoft.VisualStudio.Text; - -namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; - -internal static class RestoreHasher -{ - public static Hash CalculateHash(ProjectRestoreInfo restoreInfo) - { - Requires.NotNull(restoreInfo); - - using var hasher = new IncrementalHasher(); - - hasher.AppendProperty(nameof(restoreInfo.ProjectAssetsFilePath), restoreInfo.ProjectAssetsFilePath); - hasher.AppendProperty(nameof(restoreInfo.MSBuildProjectExtensionsPath), restoreInfo.MSBuildProjectExtensionsPath); - hasher.AppendProperty(nameof(restoreInfo.OriginalTargetFrameworks), restoreInfo.OriginalTargetFrameworks); - - foreach (TargetFrameworkInfo framework in restoreInfo.TargetFrameworks) - { - hasher.AppendProperty(nameof(framework.TargetFrameworkMoniker), framework.TargetFrameworkMoniker); - hasher.AppendFrameworkProperties(framework); - hasher.AppendReferences(framework.ProjectReferences); - hasher.AppendReferences(framework.PackageReferences); - hasher.AppendReferences(framework.FrameworkReferences); - hasher.AppendReferences(framework.PackageDownloads); - hasher.AppendReferences(framework.CentralPackageVersions); - hasher.AppendReferences(framework.NuGetAuditSuppress); - } - - hasher.AppendReferences(restoreInfo.ToolReferences); - - return hasher.GetHashAndReset(); - } - - private static void AppendFrameworkProperties(this IncrementalHasher hasher, TargetFrameworkInfo framework) - { - foreach ((string key, string value) in framework.Properties) - { - hasher.AppendProperty(key, value); - } - } - - private static void AppendReferences(this IncrementalHasher hasher, ImmutableArray references) - { - foreach (ReferenceItem reference in references) - { - hasher.AppendProperty(nameof(reference.Name), reference.Name); - hasher.AppendReferenceProperties(reference); - } - } - - private static void AppendReferenceProperties(this IncrementalHasher hasher, ReferenceItem reference) - { - foreach ((string key, string value) in reference.Metadata) - { - hasher.AppendProperty(key, value); - } - } - - private static void AppendProperty(this IncrementalHasher hasher, string name, string value) - { - hasher.Append(name); - hasher.Append("|"); - hasher.Append(value); - } -} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreStateComparisonBuilder.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreStateComparisonBuilder.cs new file mode 100644 index 00000000000..8838495c17c --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreStateComparisonBuilder.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Text; + +namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; + +internal sealed class RestoreStateComparisonBuilder +{ + private StringBuilder? _sb; + + /// + /// Tracks the names of hierarchical scopes within which log messages should be written. + /// + private readonly List _scopes = []; + + private int _loggedScopeDepth = 0; + + public void PushScope(string title) + { + _scopes.Add(title); + } + + public void PopScope() + { + _scopes.RemoveAt(_scopes.Count - 1); + if (_scopes.Count < _loggedScopeDepth) + { + _loggedScopeDepth = _scopes.Count; + } + } + + public void CompareString(string before, string after, string? name = null) + { + // Use the same comparison approach as RestoreHasher. + // All strings use ordinal comparison in the hash (via UTF8 bytes). + + if (!StringComparer.Ordinal.Equals(before, after)) + { + if (name is not null) + PushScope(name); + Log($"Before: {before}"); + Log($"After: {after}"); + if (name is not null) + PopScope(); + } + } + + public void CompareArray(ImmutableArray before, ImmutableArray after, string? name = null) where T : class, IRestoreState + { + if (name is not null) + PushScope(name); + + if (before.Length != after.Length) + { + Log($"The number of items changed from {before.Length} to {after.Length}."); + } + else + { + foreach ((T a, T b) in before.Zip(after, static (a, b) => (a, b))) + { + a.DescribeChanges(this, b); + } + } + + if (name is not null) + PopScope(); + } + + internal void CompareDictionary(IImmutableDictionary before, IImmutableDictionary after, string? name = null) + { + if (name is not null) + PushScope(name); + + SetDiff diff = new(before.Keys, after.Keys, StringComparer.Ordinal); + + if (diff.HasChange) + { + foreach (string added in diff.Added) + { + Log($"{added} added"); + } + + foreach (string removed in diff.Removed) + { + Log($"{removed} removed"); + } + } + + foreach ((string beforeKey, string beforeValue) in before) + { + if (!after.TryGetValue(beforeKey, out string afterValue)) + { + continue; + } + + CompareString(beforeValue, afterValue, beforeKey); + } + + if (name is not null) + PopScope(); + } + + private void Log(string line) + { + _sb ??= new(); + + // Ensure scope logged + if (_loggedScopeDepth < _scopes.Count) + { + // Need to log at least one scope + for (int i = _loggedScopeDepth; i < _scopes.Count; i++) + { + string scope = _scopes[i]; + + Indent(_sb, i); + _sb.AppendLine(scope); + } + + _loggedScopeDepth = _scopes.Count; + } + + Indent(_sb, _scopes.Count); + + _sb.AppendLine(line); + + static void Indent(StringBuilder sb, int indent) + { + for (int i = 0; i < indent; i++) + sb.Append(" "); + } + } + + public override string ToString() => _sb?.ToString() ?? ""; +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/TargetFrameworkInfo.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/TargetFrameworkInfo.cs index 98b1869815a..69ad4d0e7b6 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/TargetFrameworkInfo.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/TargetFrameworkInfo.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. using System.Diagnostics; +using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; @@ -8,46 +9,67 @@ namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// Represents the restore data for a single target framework in . /// [DebuggerDisplay("TargetFrameworkMoniker = {TargetFrameworkMoniker}")] -internal class TargetFrameworkInfo +internal sealed class TargetFrameworkInfo( + string targetFrameworkMoniker, + ImmutableArray frameworkReferences, + ImmutableArray packageDownloads, + ImmutableArray projectReferences, + ImmutableArray packageReferences, + ImmutableArray centralPackageVersions, + ImmutableArray nuGetAuditSuppress, + ImmutableArray prunePackageReferences, + IImmutableDictionary properties) : IRestoreState { - // If additional fields/properties are added to this class, please update RestoreHasher - public TargetFrameworkInfo( - string targetFrameworkMoniker, - ImmutableArray frameworkReferences, - ImmutableArray packageDownloads, - ImmutableArray projectReferences, - ImmutableArray packageReferences, - ImmutableArray centralPackageVersions, - ImmutableArray nuGetAuditSuppress, - ImmutableArray prunePackageReferences, - IImmutableDictionary properties) - { - TargetFrameworkMoniker = targetFrameworkMoniker; - FrameworkReferences = frameworkReferences; - PackageDownloads = packageDownloads; - ProjectReferences = projectReferences; - PackageReferences = packageReferences; - CentralPackageVersions = centralPackageVersions; - NuGetAuditSuppress = nuGetAuditSuppress; - PrunePackageReferences = prunePackageReferences; - Properties = properties; - } + // IMPORTANT: If additional state is added, update AddToHash and DescribeChanges below. - public string TargetFrameworkMoniker { get; } + public string TargetFrameworkMoniker { get; } = targetFrameworkMoniker; - public ImmutableArray FrameworkReferences { get; } + public ImmutableArray FrameworkReferences { get; } = frameworkReferences; - public ImmutableArray PackageDownloads { get; } + public ImmutableArray PackageDownloads { get; } = packageDownloads; - public ImmutableArray PackageReferences { get; } + public ImmutableArray PackageReferences { get; } = packageReferences; - public ImmutableArray ProjectReferences { get; } + public ImmutableArray ProjectReferences { get; } = projectReferences; - public ImmutableArray CentralPackageVersions { get; } + public ImmutableArray CentralPackageVersions { get; } = centralPackageVersions; - public ImmutableArray NuGetAuditSuppress { get; } + public ImmutableArray NuGetAuditSuppress { get; } = nuGetAuditSuppress; - public ImmutableArray PrunePackageReferences { get; } + public ImmutableArray PrunePackageReferences { get; } = prunePackageReferences; - public IImmutableDictionary Properties { get; } + public IImmutableDictionary Properties { get; } = properties; + + public void AddToHash(IncrementalHasher hasher) + { + hasher.AppendProperty(nameof(TargetFrameworkMoniker), TargetFrameworkMoniker); + + foreach ((string key, string value) in Properties) + { + hasher.AppendProperty(key, value); + } + + hasher.AppendArray(ProjectReferences); + hasher.AppendArray(PackageReferences); + hasher.AppendArray(FrameworkReferences); + hasher.AppendArray(PackageDownloads); + hasher.AppendArray(CentralPackageVersions); + hasher.AppendArray(NuGetAuditSuppress); + } + + public void DescribeChanges(RestoreStateComparisonBuilder builder, TargetFrameworkInfo after) + { + builder.PushScope(TargetFrameworkMoniker); + + builder.CompareDictionary(Properties, after.Properties, nameof(Properties)); + + builder.CompareArray(ProjectReferences, after.ProjectReferences, nameof(ProjectReferences)); + builder.CompareArray(PackageReferences, after.PackageReferences, nameof(PackageReferences)); + builder.CompareArray(FrameworkReferences, after.FrameworkReferences, nameof(FrameworkReferences)); + builder.CompareArray(PackageDownloads, after.PackageDownloads, nameof(PackageDownloads)); + builder.CompareArray(CentralPackageVersions, after.CentralPackageVersions, nameof(CentralPackageVersions)); + builder.CompareArray(NuGetAuditSuppress, after.NuGetAuditSuppress, nameof(NuGetAuditSuppress)); + + builder.PopScope(); + } } diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Telemetry/DotNetProjectSystemEventSource.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Telemetry/DotNetProjectSystemEventSource.cs new file mode 100644 index 00000000000..487bed33606 --- /dev/null +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Telemetry/DotNetProjectSystemEventSource.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics.Tracing; + +namespace Microsoft.VisualStudio.ProjectSystem.Telemetry; + +[EventSource(Name = "Microsoft-VisualStudio-DotNetProjectSystem")] +internal sealed class DotNetProjectSystemEventSource : EventSource +{ + public static readonly DotNetProjectSystemEventSource Instance = new(); + + private DotNetProjectSystemEventSource() { } + + [Event(eventId: 1, Level = EventLevel.Informational)] + public void NominateForRestore(string projectFilePath, string changes) + { + WriteEvent(eventId: 1, projectFilePath, changes); + } +} diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Utilities/SetDiff.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Utilities/SetDiff.cs index c693eecb635..3074fe5680b 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/Utilities/SetDiff.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/Utilities/SetDiff.cs @@ -15,6 +15,8 @@ internal sealed class SetDiff where T : notnull public Part Added => new(_dic, FlagAfter); + public bool HasChange => _dic.Count is not 0; + public SetDiff(IEnumerable before, IEnumerable after, IEqualityComparer? equalityComparer = null) { Requires.NotNull(before); @@ -22,7 +24,7 @@ public SetDiff(IEnumerable before, IEnumerable after, IEqualityComparer equalityComparer ??= EqualityComparer.Default; - var dic = new Dictionary(equalityComparer); + Dictionary dic = new(equalityComparer); foreach (T item in before) { @@ -40,42 +42,24 @@ public SetDiff(IEnumerable before, IEnumerable after, IEqualityComparer _dic = dic; } - public readonly struct Part : IEnumerable + public readonly struct Part(Dictionary dic, byte flag) : IEnumerable { - private readonly Dictionary _dic; - private readonly byte _flag; - - public Part(Dictionary dic, byte flag) - { - _dic = dic; - _flag = flag; - } - - public PartEnumerator GetEnumerator() => new(_dic, _flag); + public PartEnumerator GetEnumerator() => new(dic, flag); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public struct PartEnumerator : IEnumerator + public struct PartEnumerator(Dictionary dic, byte flag) : IEnumerator { - private readonly byte _flag; - // IMPORTANT cannot be readonly - private Dictionary.Enumerator _enumerator; - - public PartEnumerator(Dictionary dic, byte flag) - { - _flag = flag; - _enumerator = dic.GetEnumerator(); - Current = default!; - } + private Dictionary.Enumerator _enumerator = dic.GetEnumerator(); public bool MoveNext() { while (_enumerator.MoveNext()) { - if (_enumerator.Current.Value == _flag) + if (_enumerator.Current.Value == flag) { Current = _enumerator.Current.Key; return true; @@ -85,13 +69,13 @@ public bool MoveNext() return false; } - public T Current { get; private set; } + public T Current { get; private set; } = default!; - object IEnumerator.Current => Current!; + readonly object IEnumerator.Current => Current!; - void IEnumerator.Reset() => throw new NotSupportedException(); + readonly void IEnumerator.Reset() => throw new NotSupportedException(); - void IDisposable.Dispose() { } + readonly void IDisposable.Dispose() { } } } } From 70cb7dfa064dc14504109c40f1fad2e34905eb55 Mon Sep 17 00:00:00 2001 From: Drew Noakes Date: Thu, 9 Oct 2025 09:42:07 +1100 Subject: [PATCH 8/8] Make ReferenceItem consistent with other snapshot types --- .../PackageRestore/Snapshots/ReferenceItem.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs index 8b768e83752..7231f26f54c 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs @@ -9,27 +9,22 @@ namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore; /// Represents a reference item involved in package restore, with its associated metadata. /// [DebuggerDisplay("Name = {Name}")] -internal sealed class ReferenceItem : IRestoreState +internal sealed class ReferenceItem( + string name, + IImmutableDictionary metadata) + : IRestoreState { - // If additional state is added to this class, please update RestoreHasher - - public ReferenceItem(string name, IImmutableDictionary metadata) - { - Requires.NotNullOrEmpty(name); - - Name = name; - Metadata = metadata; - } + // IMPORTANT: If additional state is added, update AddToHash and DescribeChanges below. /// /// Gets the name (item spec) of the reference. /// - public string Name { get; } + public string Name { get; } = name; /// /// Gets the name/value pair metadata associated with the reference. /// - public IImmutableDictionary Metadata { get; } + public IImmutableDictionary Metadata { get; } = metadata; public void AddToHash(IncrementalHasher hasher) {