-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure we sync source-generator versions over properly when doing a cone-sync #73688
Changes from all commits
0e049af
1f26235
213d6f6
72f0d13
78d622b
b9cbac7
5f6d7f2
cb26888
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -140,10 +140,8 @@ public async Task<Checksum> GetChecksumAsync(ProjectId projectId, CancellationTo | |
frozenSourceGeneratedDocumentGenerationDateTimes = FrozenSourceGeneratedDocumentStates.SelectAsArray(d => d.GenerationDateTime); | ||
} | ||
|
||
var versionMapChecksum = ChecksumCache.GetOrCreate( | ||
this.SourceGeneratorExecutionVersionMap, | ||
static (map, @this) => GetVersionMapChecksum(@this), | ||
this); | ||
// Ensure we only send the execution map over for projects in the project cone. | ||
var versionMapChecksum = this.GetFilteredSourceGenerationExecutionMap(projectCone).GetChecksum(); | ||
|
||
var compilationStateChecksums = new SolutionCompilationStateChecksums( | ||
solutionStateChecksum, | ||
|
@@ -158,30 +156,27 @@ public async Task<Checksum> GetChecksumAsync(ProjectId projectId, CancellationTo | |
{ | ||
throw ExceptionUtilities.Unreachable(); | ||
} | ||
} | ||
|
||
static Checksum GetVersionMapChecksum(SolutionCompilationState @this) | ||
{ | ||
// We want the projects in sorted order so we can generate the checksum for the | ||
// source-generation-execution-map consistently. | ||
var sortedProjectIds = SolutionState.GetOrCreateSortedProjectIds(@this.SolutionState.ProjectIds); | ||
var supportedCount = sortedProjectIds.Count( | ||
static (projectId, @this) => RemoteSupportedLanguages.IsSupported(@this.SolutionState.GetRequiredProjectState(projectId).Language), | ||
@this); | ||
|
||
// For each project, we'll add one checksum for the project id and one for the version map. | ||
using var _ = ArrayBuilder<Checksum>.GetInstance(2 * supportedCount, out var checksums); | ||
public SourceGeneratorExecutionVersionMap GetFilteredSourceGenerationExecutionMap(ProjectCone? projectCone) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was very much part of the functional fix. When we're producing the checksum on the host side, we want to filter the map to what's in the cone. Then, on the oop side, we want to be able to ask for this filtered map portion. We then update that part of the OOP side with this filtered set. |
||
{ | ||
var builder = this.SourceGeneratorExecutionVersionMap.Map.ToBuilder(); | ||
|
||
foreach (var projectId in sortedProjectIds) | ||
foreach (var (projectId, projectState) in this.SolutionState.ProjectStates) | ||
{ | ||
if (!RemoteSupportedLanguages.IsSupported(projectState.Language)) | ||
{ | ||
var projectState = @this.SolutionState.GetRequiredProjectState(projectId); | ||
if (!RemoteSupportedLanguages.IsSupported(projectState.Language)) | ||
continue; | ||
|
||
checksums.Add(projectId.Checksum); | ||
checksums.Add(Checksum.Create(@this.SourceGeneratorExecutionVersionMap[projectId], static (v, w) => v.WriteTo(w))); | ||
builder.Remove(projectId); | ||
} | ||
else if (projectCone != null && !projectCone.Contains(projectId)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a project-cone very often spans multiple projects. it represents a project and all the projects that project depends on. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
that would work as well. I don't have strong feelings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was a great explanation, thank you! Can you put a blurb in the code so I don't have that thought again? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do. |
||
{ | ||
builder.Remove(projectId); | ||
} | ||
|
||
return Checksum.Create(checksums); | ||
} | ||
|
||
if (builder.Count == this.SourceGeneratorExecutionVersionMap.Map.Count) | ||
return this.SourceGeneratorExecutionVersionMap; | ||
|
||
return new SourceGeneratorExecutionVersionMap(builder.ToImmutable()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -103,6 +103,19 @@ public static SourceGeneratorExecutionVersionMap Deserialize(ObjectReader reader | |
return new(builder.ToImmutable()); | ||
} | ||
|
||
public Checksum GetChecksum() | ||
{ | ||
using var _ = ArrayBuilder<Checksum>.GetInstance(this.Map.Count * 2, out var checksums); | ||
|
||
foreach (var (projectId, version) in this.Map) | ||
{ | ||
checksums.Add(projectId.Checksum); | ||
checksums.Add(Checksum.Create(version, static (v, w) => v.WriteTo(w))); | ||
} | ||
|
||
return Checksum.Create(checksums); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a bit off topic: is the ReadOnlySpan overload of Checksum.Create more performant than the ImmutableArray/ArrayBuilder versions? If so, and not that it helps here, couldn't the ImmutableArray overload get the underlying array and use the equivalent of the readonlyspan implementation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. the IA version should defer to the ROS version. That said, this is not an IA. This is an ArrayBuilder. |
||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a move. |
||
|
||
public override string ToString() | ||
{ | ||
using var _ = PooledStringBuilder.GetInstance(out var builder); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
@@ -122,11 +123,21 @@ public async Task<Solution> CreateSolutionAsync(Checksum newSolutionChecksum, Ca | |
var newVersions = await _assetProvider.GetAssetAsync<SourceGeneratorExecutionVersionMap>( | ||
AssetPathKind.SolutionSourceGeneratorExecutionVersionMap, newSolutionCompilationChecksums.SourceGeneratorExecutionVersionMap, cancellationToken).ConfigureAwait(false); | ||
|
||
// The execution version map will be for the entire solution on the host side. However, we may | ||
// only be syncing over a partial cone. In that case, filter down the version map we apply to | ||
// the local solution to only be for that cone as well. | ||
newVersions = FilterToProjectCone(newVersions, newSolutionChecksums.ProjectCone); | ||
solution = solution.WithSourceGeneratorExecutionVersions(newVersions, cancellationToken); | ||
#if DEBUG | ||
var projectCone = newSolutionChecksums.ProjectCone; | ||
if (projectCone != null) | ||
{ | ||
Debug.Assert(projectCone.ProjectIds.Count == newVersions.Map.Count); | ||
Debug.Assert(projectCone.ProjectIds.All(id => newVersions.Map.ContainsKey(id))); | ||
Comment on lines
+130
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not have a SetEquals helper to call? This works but would be shorter/clearer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. def can clean that up in followup. |
||
} | ||
else | ||
{ | ||
Debug.Assert(solution.ProjectIds.Count == newVersions.Map.Count); | ||
Debug.Assert(solution.ProjectIds.All(id => newVersions.Map.ContainsKey(id))); | ||
} | ||
#endif | ||
|
||
solution = solution.UpdateSpecificSourceGeneratorExecutionVersions(newVersions, cancellationToken); | ||
} | ||
|
||
#if DEBUG | ||
|
@@ -140,21 +151,6 @@ public async Task<Solution> CreateSolutionAsync(Checksum newSolutionChecksum, Ca | |
{ | ||
throw ExceptionUtilities.Unreachable(); | ||
} | ||
|
||
static SourceGeneratorExecutionVersionMap FilterToProjectCone(SourceGeneratorExecutionVersionMap map, ProjectCone? projectCone) | ||
{ | ||
if (projectCone is null) | ||
return map; | ||
|
||
var builder = map.Map.ToBuilder(); | ||
foreach (var (projectId, _) in map.Map) | ||
{ | ||
if (!projectCone.Contains(projectId)) | ||
builder.Remove(projectId); | ||
} | ||
|
||
return new(builder.ToImmutable()); | ||
} | ||
} | ||
|
||
private async Task<Solution> UpdateProjectsAsync( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
using Microsoft.CodeAnalysis.Serialization; | ||
using Microsoft.VisualStudio.Telemetry; | ||
using Microsoft.VisualStudio.Threading; | ||
using Roslyn.Utilities; | ||
using static Microsoft.VisualStudio.Threading.ThreadingTools; | ||
|
@@ -207,7 +208,7 @@ async ValueTask DecrementInFlightCountAsync(InFlightSolution inFlightSolution) | |
|
||
/// <summary> | ||
/// Create an appropriate <see cref="Solution"/> instance corresponding to the <paramref | ||
/// name="solutionChecksum"/> passed in. Note: this method changes no Workspace state and exists purely to | ||
/// name="newSolutionChecksum"/> passed in. Note: this method changes no Workspace state and exists purely to | ||
/// compute the corresponding solution. Updating of our caches, or storing this solution as the <see | ||
/// cref="Workspace.CurrentSolution"/> of this <see cref="RemoteWorkspace"/> is the responsibility of any | ||
/// callers. | ||
|
@@ -224,26 +225,41 @@ async ValueTask DecrementInFlightCountAsync(InFlightSolution inFlightSolution) | |
/// </summary> | ||
private async Task<Solution> ComputeDisconnectedSolutionAsync( | ||
AssetProvider assetProvider, | ||
Checksum solutionChecksum, | ||
Checksum newSolutionChecksum, | ||
CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
// Try to create the solution snapshot incrementally off of the workspaces CurrentSolution first. | ||
var updater = new SolutionCreator(Services.HostServices, assetProvider, this.CurrentSolution); | ||
if (await updater.IsIncrementalUpdateAsync(solutionChecksum, cancellationToken).ConfigureAwait(false)) | ||
if (await updater.IsIncrementalUpdateAsync(newSolutionChecksum, cancellationToken).ConfigureAwait(false)) | ||
{ | ||
return await updater.CreateSolutionAsync(solutionChecksum, cancellationToken).ConfigureAwait(false); | ||
return await updater.CreateSolutionAsync(newSolutionChecksum, cancellationToken).ConfigureAwait(false); | ||
} | ||
else | ||
{ | ||
// Otherwise, this is a different solution, or the first time we're creating this solution. Bulk | ||
// sync over all assets for it. | ||
await assetProvider.SynchronizeSolutionAssetsAsync(solutionChecksum, cancellationToken).ConfigureAwait(false); | ||
await assetProvider.SynchronizeSolutionAssetsAsync(newSolutionChecksum, cancellationToken).ConfigureAwait(false); | ||
|
||
// get new solution info and options | ||
var solutionInfo = await assetProvider.CreateSolutionInfoAsync(solutionChecksum, cancellationToken).ConfigureAwait(false); | ||
return CreateSolutionFromInfo(solutionInfo); | ||
var solutionInfo = await assetProvider.CreateSolutionInfoAsync(newSolutionChecksum, cancellationToken).ConfigureAwait(false); | ||
var solution = CreateSolutionFromInfo(solutionInfo); | ||
|
||
// ensure that the solution has the correct source generator execution versions. note we should do | ||
// this all in a unified fashion with CreateSolutionAsync above. However, that is blocked in | ||
// https://github.com/dotnet/roslyn/pull/72860 | ||
{ | ||
var newSolutionCompilationChecksums = await assetProvider.GetAssetAsync<SolutionCompilationStateChecksums>( | ||
AssetPathKind.SolutionCompilationStateChecksums, newSolutionChecksum, cancellationToken).ConfigureAwait(false); | ||
|
||
var newVersions = await assetProvider.GetAssetAsync<SourceGeneratorExecutionVersionMap>( | ||
AssetPathKind.SolutionSourceGeneratorExecutionVersionMap, newSolutionCompilationChecksums.SourceGeneratorExecutionVersionMap, cancellationToken).ConfigureAwait(false); | ||
|
||
solution = solution.UpdateSpecificSourceGeneratorExecutionVersions(newVersions, cancellationToken); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: this is something that was missing. but is also a stopgap. the right way to do things is to do teh initial bulk sync, then run the normal 'delta' sync algorithm to ensure that all checksums match. That's what #72860, but it has a problem i'm working with Jason on. |
||
|
||
return solution; | ||
} | ||
} | ||
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this need an extra assert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. The impl actually already asserts that the checksums are the same (and throws if not). (so this failed prior to this fix).