Skip to content

Commit 755c7cd

Browse files
committed
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.
1 parent adbaaf8 commit 755c7cd

File tree

10 files changed

+355
-149
lines changed

10 files changed

+355
-149
lines changed

src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/PackageRestoreDataSource.cs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks.Dataflow;
44
using Microsoft.VisualStudio.Composition;
55
using Microsoft.VisualStudio.IO;
6+
using Microsoft.VisualStudio.ProjectSystem.Telemetry;
67
using Microsoft.VisualStudio.Text;
78
using Microsoft.VisualStudio.Threading;
89

@@ -58,6 +59,7 @@ internal partial class PackageRestoreDataSource : ChainedProjectValueDataSourceB
5859
private readonly IManagedProjectDiagnosticOutputService _logger;
5960
private readonly INuGetRestoreService _nuGetRestoreService;
6061
private Hash? _lastHash;
62+
private ProjectRestoreInfo? _lastRestoreInfo;
6163
private bool _enabled;
6264
private bool _wasSourceBlockContinuationSet;
6365

@@ -109,10 +111,7 @@ internal async Task<IEnumerable<IProjectVersionedValue<RestoreData>>> RestoreAsy
109111

110112
RestoreData restoreData = CreateRestoreData(e.Value.RestoreInfo, succeeded);
111113

112-
return new[]
113-
{
114-
new ProjectVersionedValue<RestoreData>(restoreData, e.DataSourceVersions)
115-
};
114+
return [new ProjectVersionedValue<RestoreData>(restoreData, e.DataSourceVersions)];
116115
}
117116

118117
private async Task<bool> RestoreCoreAsync(PackageRestoreUnconfiguredInput value)
@@ -125,22 +124,43 @@ private async Task<bool> RestoreCoreAsync(PackageRestoreUnconfiguredInput value)
125124

126125
// Restore service always does work regardless of whether the value we pass
127126
// them to actually contains changes, only nominate if there are any.
128-
Hash hash = RestoreHasher.CalculateHash(restoreInfo);
127+
Hash hash = CalculateHash();
129128

130129
if (await _cycleDetector.IsCycleDetectedAsync(hash, value.ActiveConfiguration, token))
131130
{
132131
_lastHash = hash;
132+
_lastRestoreInfo = restoreInfo;
133133
return false;
134134
}
135135

136136
if (_lastHash?.Equals(hash) == true)
137137
{
138138
await _nuGetRestoreService.UpdateWithoutNominationAsync(value.ConfiguredInputs);
139+
_lastRestoreInfo = restoreInfo;
139140
return true;
140141
}
141142

142143
_lastHash = hash;
143144

145+
if (DotNetProjectSystemEventSource.Instance.IsEnabled())
146+
{
147+
string changes;
148+
if (_lastRestoreInfo is null)
149+
{
150+
changes = "Initial restore.";
151+
}
152+
else
153+
{
154+
RestoreStateComparisonBuilder builder = new();
155+
_lastRestoreInfo.DescribeChanges(builder, restoreInfo);
156+
changes = builder.ToString();
157+
}
158+
159+
DotNetProjectSystemEventSource.Instance.NominateForRestore(projectFilePath: _project.FullPath, changes: changes);
160+
}
161+
162+
_lastRestoreInfo = restoreInfo;
163+
144164
JoinableTask<bool> joinableTask = JoinableFactory.RunAsync(() =>
145165
{
146166
return NominateForRestoreAsync(restoreInfo, value.ConfiguredInputs, token);
@@ -152,6 +172,13 @@ private async Task<bool> RestoreCoreAsync(PackageRestoreUnconfiguredInput value)
152172
registerFaultHandler: true);
153173

154174
return await joinableTask;
175+
176+
Hash CalculateHash()
177+
{
178+
using var hasher = new IncrementalHasher();
179+
restoreInfo.AddToHash(hasher);
180+
return hasher.GetHashAndReset();
181+
}
155182
}
156183

157184
private async Task<bool> NominateForRestoreAsync(ProjectRestoreInfo restoreInfo, IReadOnlyCollection<PackageRestoreConfiguredInput> versions, CancellationToken cancellationToken)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// 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.
2+
3+
using Microsoft.VisualStudio.Text;
4+
5+
namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore;
6+
7+
/// <summary>
8+
/// A data object at some level within the restore state snapshot, <see cref="ProjectRestoreInfo"/>.
9+
/// </summary>
10+
/// <typeparam name="T"></typeparam>
11+
internal interface IRestoreState<T> where T : class
12+
{
13+
/// <summary>
14+
/// Adds state from this object to <paramref name="hasher"/>.
15+
/// </summary>
16+
/// <param name="hasher"></param>
17+
void AddToHash(IncrementalHasher hasher);
18+
19+
/// <summary>
20+
/// Compares all state between this instance and <paramref name="after"/>, and logs details of any changes.
21+
/// </summary>
22+
void DescribeChanges(RestoreStateComparisonBuilder builder, T after);
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// 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.
2+
3+
using Microsoft.VisualStudio.Text;
4+
5+
namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore;
6+
7+
internal static class IncrementalHasherExtensions
8+
{
9+
public static void AppendProperty(this IncrementalHasher hasher, string name, string value)
10+
{
11+
hasher.Append(name);
12+
hasher.Append("|");
13+
hasher.Append(value);
14+
}
15+
16+
public static void AppendArray<T>(this IncrementalHasher hasher, ImmutableArray<T> items) where T : class, IRestoreState<T>
17+
{
18+
foreach (T item in items)
19+
{
20+
item.AddToHash(hasher);
21+
}
22+
}
23+
}
Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,49 @@
11
// 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.
22

3+
using Microsoft.VisualStudio.Text;
4+
35
namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore;
46

57
/// <summary>
6-
/// A complete set of restore data for a project.
8+
/// A complete set of restore data for a project.
79
/// </summary>
8-
internal class ProjectRestoreInfo
10+
internal sealed class ProjectRestoreInfo(
11+
string msbuildProjectExtensionsPath,
12+
string projectAssetsFilePath,
13+
string originalTargetFrameworks,
14+
ImmutableArray<TargetFrameworkInfo> targetFrameworks,
15+
ImmutableArray<ReferenceItem> toolReferences)
16+
: IRestoreState<ProjectRestoreInfo>
917
{
10-
// If additional fields/properties are added to this class, please update RestoreHasher
18+
// IMPORTANT: If additional state is added, update AddToHash and DescribeChanges below.
1119

12-
public ProjectRestoreInfo(string msbuildProjectExtensionsPath, string projectAssetsFilePath, string originalTargetFrameworks, ImmutableArray<TargetFrameworkInfo> targetFrameworks, ImmutableArray<ReferenceItem> toolReferences)
13-
{
14-
MSBuildProjectExtensionsPath = msbuildProjectExtensionsPath;
15-
ProjectAssetsFilePath = projectAssetsFilePath;
16-
OriginalTargetFrameworks = originalTargetFrameworks;
17-
TargetFrameworks = targetFrameworks;
18-
ToolReferences = toolReferences;
19-
}
20+
public string MSBuildProjectExtensionsPath { get; } = msbuildProjectExtensionsPath;
21+
22+
public string ProjectAssetsFilePath { get; } = projectAssetsFilePath;
23+
24+
public string OriginalTargetFrameworks { get; } = originalTargetFrameworks;
2025

21-
public string MSBuildProjectExtensionsPath { get; }
26+
public ImmutableArray<TargetFrameworkInfo> TargetFrameworks { get; } = targetFrameworks;
2227

23-
public string ProjectAssetsFilePath { get; }
28+
public ImmutableArray<ReferenceItem> ToolReferences { get; } = toolReferences;
2429

25-
public string OriginalTargetFrameworks { get; }
30+
public void AddToHash(IncrementalHasher hasher)
31+
{
32+
hasher.AppendProperty(nameof(ProjectAssetsFilePath), ProjectAssetsFilePath);
33+
hasher.AppendProperty(nameof(MSBuildProjectExtensionsPath), MSBuildProjectExtensionsPath);
34+
hasher.AppendProperty(nameof(OriginalTargetFrameworks), OriginalTargetFrameworks);
2635

27-
public ImmutableArray<TargetFrameworkInfo> TargetFrameworks { get; }
36+
hasher.AppendArray(TargetFrameworks);
37+
hasher.AppendArray(ToolReferences);
38+
}
2839

29-
public ImmutableArray<ReferenceItem> ToolReferences { get; }
40+
public void DescribeChanges(RestoreStateComparisonBuilder builder, ProjectRestoreInfo after)
41+
{
42+
builder.CompareString(MSBuildProjectExtensionsPath, after.MSBuildProjectExtensionsPath, nameof(MSBuildProjectExtensionsPath));
43+
builder.CompareString(ProjectAssetsFilePath, after.ProjectAssetsFilePath, nameof(ProjectAssetsFilePath));
44+
builder.CompareString(OriginalTargetFrameworks, after.OriginalTargetFrameworks, nameof(OriginalTargetFrameworks));
45+
46+
builder.CompareArray(TargetFrameworks, after.TargetFrameworks, nameof(TargetFrameworks));
47+
builder.CompareArray(ToolReferences, after.ToolReferences, nameof(ToolReferences));
48+
}
3049
}

src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/ReferenceItem.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// 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.
22

33
using System.Diagnostics;
4+
using Microsoft.VisualStudio.Text;
45

56
namespace Microsoft.VisualStudio.ProjectSystem.PackageRestore;
67

78
/// <summary>
89
/// Represents a reference item involved in package restore, with its associated metadata.
910
/// </summary>
1011
[DebuggerDisplay("Name = {Name}")]
11-
internal sealed class ReferenceItem
12+
internal sealed class ReferenceItem : IRestoreState<ReferenceItem>
1213
{
1314
// If additional state is added to this class, please update RestoreHasher
1415

@@ -29,4 +30,25 @@ public ReferenceItem(string name, IImmutableDictionary<string, string> metadata)
2930
/// Gets the name/value pair metadata associated with the reference.
3031
/// </summary>
3132
public IImmutableDictionary<string, string> Metadata { get; }
33+
34+
public void AddToHash(IncrementalHasher hasher)
35+
{
36+
hasher.AppendProperty(nameof(Name), Name);
37+
38+
foreach ((string key, string value) in Metadata)
39+
{
40+
hasher.AppendProperty(key, value);
41+
}
42+
}
43+
44+
public void DescribeChanges(RestoreStateComparisonBuilder builder, ReferenceItem after)
45+
{
46+
builder.PushScope(Name);
47+
48+
builder.CompareString(Name, after.Name, name: "%(Identity)");
49+
50+
builder.CompareDictionary(Metadata, after.Metadata);
51+
52+
builder.PopScope();
53+
}
3254
}

src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/PackageRestore/Snapshots/RestoreHasher.cs

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)