Skip to content

Commit db38112

Browse files
Make diagnostic checksum computation into an extension method. (#77277)
2 parents 7cee927 + 131fe40 commit db38112

11 files changed

+85
-131
lines changed

src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerExtensions.cs

-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
using System.Linq;
99
using System.Reflection;
1010
using Microsoft.CodeAnalysis.Diagnostics.Telemetry;
11-
using Microsoft.CodeAnalysis.Simplification;
12-
using Roslyn.Utilities;
1311

1412
namespace Microsoft.CodeAnalysis.Diagnostics;
1513

src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using System;
66
using System.Collections.Immutable;
7-
using System.Diagnostics;
87
using System.Linq;
98
using System.Runtime.CompilerServices;
109
using System.Threading;
@@ -37,7 +36,7 @@ internal partial class DiagnosticAnalyzerService
3736
return null;
3837

3938
var projectState = project.State;
40-
var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false);
39+
var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
4140

4241
// Make sure the cached pair was computed with at least the same state sets we're asking about. if not,
4342
// recompute and cache with the new state sets.

src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async Task<ImmutableDictionary<DiagnosticAnalyzer, DiagnosticAnalysisResult>> Ge
121121
if (s_projectToForceAnalysisData.TryGetValue(project.State, out var box) &&
122122
analyzers.IsSubsetOf(box.Value.analyzers))
123123
{
124-
var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false);
124+
var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
125125
if (box.Value.checksum == checksum)
126126
return box.Value.diagnosticAnalysisResults;
127127
}

src/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ private partial class DiagnosticIncrementalAnalyzer
3737
public async Task<ImmutableArray<DiagnosticData>> ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken)
3838
{
3939
var projectState = project.State;
40-
var checksum = await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false);
40+
var checksum = await project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false);
4141

4242
try
4343
{

src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsPullCache.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ internal abstract partial class AbstractPullDiagnosticHandler<TDiagnosticsParams
1818
internal readonly record struct DiagnosticsRequestState(Project Project, int GlobalStateVersion, RequestContext Context, IDiagnosticSource DiagnosticSource);
1919

2020
/// <summary>
21-
/// Cache where we store the data produced by prior requests so that they can be returned if nothing of significance
22-
/// changed. The <see cref="VersionStamp"/> is produced by <see cref="Project.GetDependentVersionAsync(CancellationToken)"/> while the
23-
/// <see cref="Checksum"/> is produced by <see cref="Project.GetDependentChecksumAsync(CancellationToken)"/>. The former is faster
24-
/// and works well for us in the normal case. The latter still allows us to reuse diagnostics when changes happen that
25-
/// update the version stamp but not the content (for example, forking LSP text).
21+
/// Cache where we store the data produced by prior requests so that they can be returned if nothing of significance
22+
/// changed. The <see cref="VersionStamp"/> is produced by <see
23+
/// cref="Project.GetDependentVersionAsync(CancellationToken)"/> while the <see cref="Checksum"/> is produced by
24+
/// <see cref="CodeAnalysis.Diagnostics.Extensions.GetDiagnosticChecksumAsync"/>. The former is faster and works
25+
/// well for us in the normal case. The latter still allows us to reuse diagnostics when changes happen that update
26+
/// the version stamp but not the content (for example, forking LSP text).
2627
/// </summary>
2728
private sealed class DiagnosticsPullCache(string uniqueKey) : VersionedPullCache<(int globalStateVersion, VersionStamp? dependentVersion), (int globalStateVersion, Checksum dependentChecksum), DiagnosticsRequestState, ImmutableArray<DiagnosticData>>(uniqueKey)
2829
{
@@ -33,7 +34,7 @@ private sealed class DiagnosticsPullCache(string uniqueKey) : VersionedPullCache
3334

3435
public override async Task<(int globalStateVersion, Checksum dependentChecksum)> ComputeExpensiveVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken)
3536
{
36-
return (state.GlobalStateVersion, await state.Project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false));
37+
return (state.GlobalStateVersion, await state.Project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false));
3738
}
3839

3940
/// <inheritdoc cref="VersionedPullCache{TCheapVersion, TExpensiveVersion, TState, TComputedData}.ComputeDataAsync(TState, CancellationToken)"/>

src/Workspaces/Core/Portable/Diagnostics/Extensions.cs

+75
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Immutable;
88
using System.Diagnostics;
99
using System.Linq;
10+
using System.Runtime.CompilerServices;
1011
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Microsoft.CodeAnalysis.Collections;
@@ -21,6 +22,8 @@ namespace Microsoft.CodeAnalysis.Diagnostics;
2122

2223
internal static partial class Extensions
2324
{
25+
private static readonly ConditionalWeakTable<Project, AsyncLazy<Checksum>> s_projectToDiagnosticChecksum = new();
26+
2427
public static async Task<ImmutableArray<Diagnostic>> ToDiagnosticsAsync(this IEnumerable<DiagnosticData> diagnostics, Project project, CancellationToken cancellationToken)
2528
{
2629
var result = ArrayBuilder<Diagnostic>.GetInstance();
@@ -429,4 +432,76 @@ await suppressionAnalyzer.AnalyzeAsync(
429432
semanticModel, span, hostCompilationWithAnalyzers, analyzerInfoCache.GetDiagnosticDescriptors, reportDiagnostic, cancellationToken).ConfigureAwait(false);
430433
}
431434
}
435+
436+
/// <summary>
437+
/// Calculates a checksum that contains a project's checksum along with a checksum for each of the project's
438+
/// transitive dependencies.
439+
/// </summary>
440+
/// <remarks>
441+
/// This checksum calculation can be used for cases where a feature needs to know if the semantics in this project
442+
/// changed. For example, for diagnostics or caching computed semantic data. The goal is to ensure that changes to
443+
/// <list type="bullet">
444+
/// <item>Files inside the current project</item>
445+
/// <item>Project properties of the current project</item>
446+
/// <item>Visible files in referenced projects</item>
447+
/// <item>Project properties in referenced projects</item>
448+
/// </list>
449+
/// are reflected in the metadata we keep so that comparing solutions accurately tells us when we need to recompute
450+
/// semantic work.
451+
///
452+
/// <para>This method of checking for changes has a few important properties that differentiate it from other methods of determining project version.
453+
/// <list type="bullet">
454+
/// <item>Changes to methods inside the current project will be reflected to compute updated diagnostics.
455+
/// <see cref="Project.GetDependentSemanticVersionAsync(CancellationToken)"/> does not change as it only returns top level changes.</item>
456+
/// <item>Reloading a project without making any changes will re-use cached diagnostics.
457+
/// <see cref="Project.GetDependentSemanticVersionAsync(CancellationToken)"/> changes as the project is removed, then added resulting in a version change.</item>
458+
/// </list>
459+
/// </para>
460+
/// This checksum is also affected by the <see cref="SourceGeneratorExecutionVersion"/> for this project.
461+
/// As such, it is not usable across different sessions of a particular host.
462+
/// </remarks>
463+
public static Task<Checksum> GetDiagnosticChecksumAsync(this Project? project, CancellationToken cancellationToken)
464+
{
465+
if (project is null)
466+
return SpecializedTasks.Default<Checksum>();
467+
468+
var lazyChecksum = s_projectToDiagnosticChecksum.GetValue(
469+
project,
470+
static project => AsyncLazy.Create(
471+
static (project, cancellationToken) => ComputeDiagnosticChecksumAsync(project, cancellationToken),
472+
project));
473+
474+
return lazyChecksum.GetValueAsync(cancellationToken);
475+
476+
static async Task<Checksum> ComputeDiagnosticChecksumAsync(Project project, CancellationToken cancellationToken)
477+
{
478+
var solution = project.Solution;
479+
480+
using var _ = ArrayBuilder<Checksum>.GetInstance(out var tempChecksumArray);
481+
482+
// Mix in the SG information for this project. That way if it changes, we will have a different
483+
// checksum (since semantics could have changed because of this).
484+
if (solution.CompilationState.SourceGeneratorExecutionVersionMap.Map.TryGetValue(project.Id, out var executionVersion))
485+
tempChecksumArray.Add(executionVersion.Checksum);
486+
487+
// Get the checksum for the project itself. Note: this will normally be cached. As such, even if we
488+
// have a different Project instance (due to a change in an unrelated project), this will be fast to
489+
// compute and return.
490+
var projectChecksum = await project.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
491+
tempChecksumArray.Add(projectChecksum);
492+
493+
// Calculate a checksum this project and for each dependent project that could affect semantics for this
494+
// project. We order the projects guid so that we are resilient to the underlying in-memory graph structure
495+
// changing this arbitrarily.
496+
foreach (var projectRef in project.ProjectReferences.OrderBy(r => r.ProjectId.Id))
497+
{
498+
// Note that these checksums should only actually be calculated once, if the project is unchanged
499+
// the same checksum will be returned.
500+
tempChecksumArray.Add(await GetDiagnosticChecksumAsync(
501+
solution.GetProject(projectRef.ProjectId), cancellationToken).ConfigureAwait(false));
502+
}
503+
504+
return Checksum.Create(tempChecksumArray);
505+
}
506+
}
432507
}

src/Workspaces/Core/Portable/Workspace/Solution/Project.cs

-30
Original file line numberDiff line numberDiff line change
@@ -544,36 +544,6 @@ public Task<VersionStamp> GetDependentSemanticVersionAsync(CancellationToken can
544544
public Task<VersionStamp> GetSemanticVersionAsync(CancellationToken cancellationToken = default)
545545
=> State.GetSemanticVersionAsync(cancellationToken);
546546

547-
/// <summary>
548-
/// Calculates a checksum that contains a project's checksum along with a checksum for each of the project's
549-
/// transitive dependencies.
550-
/// </summary>
551-
/// <remarks>
552-
/// This checksum calculation can be used for cases where a feature needs to know if the semantics in this project
553-
/// changed. For example, for diagnostics or caching computed semantic data. The goal is to ensure that changes to
554-
/// <list type="bullet">
555-
/// <item>Files inside the current project</item>
556-
/// <item>Project properties of the current project</item>
557-
/// <item>Visible files in referenced projects</item>
558-
/// <item>Project properties in referenced projects</item>
559-
/// </list>
560-
/// are reflected in the metadata we keep so that comparing solutions accurately tells us when we need to recompute
561-
/// semantic work.
562-
///
563-
/// <para>This method of checking for changes has a few important properties that differentiate it from other methods of determining project version.
564-
/// <list type="bullet">
565-
/// <item>Changes to methods inside the current project will be reflected to compute updated diagnostics.
566-
/// <see cref="Project.GetDependentSemanticVersionAsync(CancellationToken)"/> does not change as it only returns top level changes.</item>
567-
/// <item>Reloading a project without making any changes will re-use cached diagnostics.
568-
/// <see cref="Project.GetDependentSemanticVersionAsync(CancellationToken)"/> changes as the project is removed, then added resulting in a version change.</item>
569-
/// </list>
570-
/// </para>
571-
/// This checksum is also affected by the <see cref="SourceGeneratorExecutionVersion"/> for this project.
572-
/// As such, it is not usable across different sessions of a particular host.
573-
/// </remarks>
574-
internal Task<Checksum> GetDependentChecksumAsync(CancellationToken cancellationToken)
575-
=> Solution.CompilationState.GetDependentChecksumAsync(this.Id, cancellationToken);
576-
577547
/// <summary>
578548
/// Creates a new instance of this project updated to have the new assembly name.
579549
/// </summary>

src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs

-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ bool ContainsAssemblyOrModuleOrDynamic(
5353

5454
Task<VersionStamp> GetDependentVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken);
5555
Task<VersionStamp> GetDependentSemanticVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken);
56-
Task<Checksum> GetDependentChecksumAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken);
5756

5857
/// <summary>
5958
/// Gets the source generator files generated by this <see cref="ICompilationTracker"/>. <paramref

src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker.cs

-61
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,6 @@ public CompilationTrackerValidationException(string message, Exception inner) :
10081008

10091009
private AsyncLazy<VersionStamp>? _lazyDependentVersion;
10101010
private AsyncLazy<VersionStamp>? _lazyDependentSemanticVersion;
1011-
private AsyncLazy<Checksum>? _lazyDependentChecksum;
10121011

10131012
public Task<VersionStamp> GetDependentVersionAsync(
10141013
SolutionCompilationState compilationState, CancellationToken cancellationToken)
@@ -1087,66 +1086,6 @@ private async Task<VersionStamp> ComputeDependentSemanticVersionAsync(
10871086
return version;
10881087
}
10891088

1090-
public Task<Checksum> GetDependentChecksumAsync(
1091-
SolutionCompilationState compilationState, CancellationToken cancellationToken)
1092-
{
1093-
if (_lazyDependentChecksum == null)
1094-
{
1095-
// note: solution is captured here, but it will go away once GetValueAsync executes.
1096-
Interlocked.CompareExchange(
1097-
ref _lazyDependentChecksum,
1098-
AsyncLazy.Create(static (arg, c) =>
1099-
arg.self.ComputeDependentChecksumAsync(arg.compilationState, c),
1100-
arg: (self: this, compilationState)),
1101-
null);
1102-
}
1103-
1104-
return _lazyDependentChecksum.GetValueAsync(cancellationToken);
1105-
}
1106-
1107-
private async Task<Checksum> ComputeDependentChecksumAsync(
1108-
SolutionCompilationState solution, CancellationToken cancellationToken)
1109-
{
1110-
using var _ = ArrayBuilder<Checksum>.GetInstance(out var tempChecksumArray);
1111-
1112-
// Mix in the SG information for this project. That way if it changes, we will have a different
1113-
// checksum (since semantics could have changed because of this).
1114-
if (solution.SourceGeneratorExecutionVersionMap.Map.TryGetValue(this.ProjectState.Id, out var executionVersion))
1115-
tempChecksumArray.Add(executionVersion.Checksum);
1116-
1117-
// Get the checksum for the project itself.
1118-
var projectChecksum = await this.ProjectState.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
1119-
tempChecksumArray.Add(projectChecksum);
1120-
1121-
// Calculate a checksum this project and for each dependent project that could affect semantics for this
1122-
// project. We order the projects so that we are resilient to the underlying in-memory graph structure
1123-
// changing this arbitrarily. We do not want that to cause us to change our semantic version.. Note: we
1124-
// use the project filepath+name as a unique way to reference a project. This matches the logic in our
1125-
// persistence-service implementation as to how information is associated with a project.
1126-
var transitiveDependencies = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatThisProjectTransitivelyDependsOn(this.ProjectState.Id);
1127-
var orderedProjectIds = transitiveDependencies.OrderBy(id =>
1128-
{
1129-
var depProject = solution.SolutionState.GetRequiredProjectState(id);
1130-
return (depProject.FilePath, depProject.Name);
1131-
});
1132-
1133-
foreach (var projectId in orderedProjectIds)
1134-
{
1135-
// Mix in the SG information for the dependent project. That way if it changes, we will have a
1136-
// different checksum (since semantics could have changed because of this).
1137-
if (solution.SourceGeneratorExecutionVersionMap.Map.TryGetValue(projectId, out executionVersion))
1138-
tempChecksumArray.Add(executionVersion.Checksum);
1139-
1140-
// Note that these checksums should only actually be calculated once, if the project is unchanged
1141-
// the same checksum will be returned.
1142-
var referencedProject = solution.SolutionState.GetRequiredProjectState(projectId);
1143-
var referencedProjectChecksum = await referencedProject.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
1144-
tempChecksumArray.Add(referencedProjectChecksum);
1145-
}
1146-
1147-
return Checksum.Create(tempChecksumArray);
1148-
}
1149-
11501089
#endregion
11511090
}
11521091
}

src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.WithFrozenSourceGeneratedDocumentsCompilationTracker.cs

-24
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ private sealed class WithFrozenSourceGeneratedDocumentsCompilationTracker : ICom
2828
{
2929
private readonly TextDocumentStates<SourceGeneratedDocumentState> _replacementDocumentStates;
3030

31-
private AsyncLazy<Checksum>? _lazyDependentChecksum;
32-
3331
/// <summary>
3432
/// The lazily-produced compilation that has the generated document updated. This is initialized by call to
3533
/// <see cref="GetCompilationAsync"/>.
@@ -145,28 +143,6 @@ public Task<VersionStamp> GetDependentVersionAsync(SolutionCompilationState comp
145143
public Task<VersionStamp> GetDependentSemanticVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken)
146144
=> UnderlyingTracker.GetDependentSemanticVersionAsync(compilationState, cancellationToken);
147145

148-
public Task<Checksum> GetDependentChecksumAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken)
149-
{
150-
if (_lazyDependentChecksum == null)
151-
{
152-
var tmp = compilationState; // temp. local to avoid a closure allocation for the fast path
153-
// note: solution is captured here, but it will go away once GetValueAsync executes.
154-
Interlocked.CompareExchange(
155-
ref _lazyDependentChecksum,
156-
AsyncLazy.Create(static (arg, c) =>
157-
arg.self.ComputeDependentChecksumAsync(arg.tmp, c),
158-
arg: (self: this, tmp)),
159-
null);
160-
}
161-
162-
return _lazyDependentChecksum.GetValueAsync(cancellationToken);
163-
}
164-
165-
private async Task<Checksum> ComputeDependentChecksumAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken)
166-
=> Checksum.Create(
167-
await UnderlyingTracker.GetDependentChecksumAsync(compilationState, cancellationToken).ConfigureAwait(false),
168-
(await _replacementDocumentStates.GetDocumentChecksumsAndIdsAsync(cancellationToken).ConfigureAwait(false)).Checksum);
169-
170146
public async ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSourceGeneratedDocumentStatesAsync(
171147
SolutionCompilationState compilationState, bool withFrozenSourceGeneratedDocuments, CancellationToken cancellationToken)
172148
{

0 commit comments

Comments
 (0)