diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs index 288d42bfae0b6..81d8161f3cbdd 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs @@ -52,13 +52,13 @@ internal static async Task FindAsync( { foreach (var (_, state) in documentStates.States) { + cancellationToken.ThrowIfCancellationRequested(); + if (searchingChecksumsLeft.Count == 0) + return; + Contract.ThrowIfFalse(state.TryGetStateChecksums(out var stateChecksums)); await stateChecksums.FindAsync(state, searchingChecksumsLeft, result, cancellationToken).ConfigureAwait(false); - if (searchingChecksumsLeft.Count == 0) - { - return; - } } } @@ -74,7 +74,6 @@ internal static void Find( for (var i = 0; i < checksums.Children.Length; i++) { cancellationToken.ThrowIfCancellationRequested(); - if (searchingChecksumsLeft.Count == 0) return; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs index 05bf5865dc5b3..95b57de98df7d 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState_Checksum.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Serialization; using Roslyn.Utilities; @@ -128,13 +129,13 @@ private async Task ComputeChecksumsAsync( // if it's a project that's specifically in the sync'ed cone, include this checksum so that // this project definitely syncs over. if (t.mustCompute) - return await t.state.GetChecksumAsync(cancellationToken).ConfigureAwait(false); + return await t.state.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false); // If it's a project that is not in the cone, still try to get the latest checksum for it if // we have it. That way we don't send over a checksum *without* that project, causing the // OOP side to throw that project away (along with all the compilation info stored with it). if (t.state.TryGetStateChecksums(out var stateChecksums)) - return stateChecksums.Checksum; + return stateChecksums; // We have never computed the checksum for this project. Don't send anything for it. return null; @@ -156,10 +157,22 @@ private async Task ComputeChecksumsAsync( var analyzerReferenceChecksums = ChecksumCache.GetOrCreate(AnalyzerReferences, _ => new ChecksumCollection(AnalyzerReferences.SelectAsArray(r => serializer.CreateChecksum(r, cancellationToken)))); - var projectChecksums = await Task.WhenAll(projectChecksumTasks).ConfigureAwait(false); + var allResults = await Task.WhenAll(projectChecksumTasks).ConfigureAwait(false); + using var _1 = ArrayBuilder.GetInstance(allResults.Length, out var projectIds); + using var _2 = ArrayBuilder.GetInstance(allResults.Length, out var projectChecksums); + foreach (var projectStateChecksums in allResults) + { + if (projectStateChecksums != null) + { + projectIds.Add(projectStateChecksums.ProjectId); + projectChecksums.Add(projectStateChecksums.Checksum); + } + } + return new SolutionStateChecksums( attributesChecksum, - new ChecksumCollection(projectChecksums.WhereNotNull().ToImmutableArray()), + projectIds.ToImmutableAndClear(), + new ChecksumCollection(projectChecksums.ToImmutableAndClear()), analyzerReferenceChecksums, frozenSourceGeneratedDocumentIdentityChecksum, frozenSourceGeneratedDocumentTextChecksum); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs index d00510ee8d76f..26339d4d1360a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -14,27 +15,43 @@ namespace Microsoft.CodeAnalysis.Serialization; -internal sealed class SolutionStateChecksums( - Checksum attributesChecksum, - ChecksumCollection projectChecksums, - ChecksumCollection analyzerReferenceChecksums, - Checksum frozenSourceGeneratedDocumentIdentity, - Checksum frozenSourceGeneratedDocumentText) : IChecksummedObject +internal sealed class SolutionStateChecksums : IChecksummedObject { - public Checksum Checksum { get; } = Checksum.Create(stackalloc[] + public Checksum Checksum { get; } + + public Checksum Attributes { get; } + public ImmutableArray ProjectIds { get; } + public ChecksumCollection Projects { get; } + public ChecksumCollection AnalyzerReferences { get; } + public Checksum FrozenSourceGeneratedDocumentIdentity { get; } + public Checksum FrozenSourceGeneratedDocumentText { get; } + + public SolutionStateChecksums( + Checksum attributes, + ImmutableArray projectIds, + ChecksumCollection projects, + ChecksumCollection analyzerReferences, + Checksum frozenSourceGeneratedDocumentIdentity, + Checksum frozenSourceGeneratedDocumentText) { - attributesChecksum.Hash, - projectChecksums.Checksum.Hash, - analyzerReferenceChecksums.Checksum.Hash, - frozenSourceGeneratedDocumentIdentity.Hash, - frozenSourceGeneratedDocumentText.Hash, - }); + Contract.ThrowIfFalse(projectIds.Length == projects.Children.Length); - public Checksum Attributes => attributesChecksum; - public ChecksumCollection Projects => projectChecksums; - public ChecksumCollection AnalyzerReferences => analyzerReferenceChecksums; - public Checksum FrozenSourceGeneratedDocumentIdentity => frozenSourceGeneratedDocumentIdentity; - public Checksum FrozenSourceGeneratedDocumentText => frozenSourceGeneratedDocumentText; + this.Checksum = Checksum.Create(stackalloc[] + { + attributes.Hash, + projects.Checksum.Hash, + analyzerReferences.Checksum.Hash, + frozenSourceGeneratedDocumentIdentity.Hash, + frozenSourceGeneratedDocumentText.Hash, + }); + + this.Attributes = attributes; + this.ProjectIds = projectIds; + this.Projects = projects; + this.AnalyzerReferences = analyzerReferences; + this.FrozenSourceGeneratedDocumentIdentity = frozenSourceGeneratedDocumentIdentity; + this.FrozenSourceGeneratedDocumentText = frozenSourceGeneratedDocumentText; + } public void AddAllTo(HashSet checksums) { @@ -51,6 +68,7 @@ public void Serialize(ObjectWriter writer) // Writing this is optional, but helps ensure checksums are being computed properly on both the host and oop side. this.Checksum.WriteTo(writer); this.Attributes.WriteTo(writer); + writer.WriteArray(this.ProjectIds, static (writer, value) => value.WriteTo(writer)); this.Projects.WriteTo(writer); this.AnalyzerReferences.WriteTo(writer); this.FrozenSourceGeneratedDocumentIdentity.WriteTo(writer); @@ -61,9 +79,10 @@ public static SolutionStateChecksums Deserialize(ObjectReader reader) { var checksum = Checksum.ReadFrom(reader); var result = new SolutionStateChecksums( - attributesChecksum: Checksum.ReadFrom(reader), - projectChecksums: ChecksumCollection.ReadFrom(reader), - analyzerReferenceChecksums: ChecksumCollection.ReadFrom(reader), + attributes: Checksum.ReadFrom(reader), + projectIds: reader.ReadArray(ProjectId.ReadFrom), + projects: ChecksumCollection.ReadFrom(reader), + analyzerReferences: ChecksumCollection.ReadFrom(reader), frozenSourceGeneratedDocumentIdentity: Checksum.ReadFrom(reader), frozenSourceGeneratedDocumentText: Checksum.ReadFrom(reader)); Contract.ThrowIfFalse(result.Checksum == checksum); @@ -102,39 +121,48 @@ public async Task FindAsync( ChecksumCollection.Find(state.AnalyzerReferences, AnalyzerReferences, searchingChecksumsLeft, result, cancellationToken); - // Before doing a depth-first-search *into* each project, first run across all the project at their top level. - // This ensures that when we are trying to sync the projects referenced by a SolutionStateChecksums' instance - // that we don't unnecessarily walk all documents looking just for those. + if (searchingChecksumsLeft.Count == 0) + return; - foreach (var (projectId, projectState) in state.ProjectStates) + if (hintProject != null) { - if (searchingChecksumsLeft.Count == 0) - break; - - if (hintProject != null && hintProject != projectId) - continue; - - if (projectState.TryGetStateChecksums(out var projectStateChecksums) && - searchingChecksumsLeft.Remove(projectStateChecksums.Checksum)) + var projectState = state.GetProjectState(hintProject); + if (projectState != null && + projectState.TryGetStateChecksums(out var projectStateChecksums)) { - result[projectStateChecksums.Checksum] = projectStateChecksums; + await projectStateChecksums.FindAsync(projectState, searchingChecksumsLeft, result, cancellationToken).ConfigureAwait(false); } } + else + { + // Before doing a depth-first-search *into* each project, first run across all the project at their top level. + // This ensures that when we are trying to sync the projects referenced by a SolutionStateChecksums' instance + // that we don't unnecessarily walk all documents looking just for those. - // Now actually do the depth first search into each project. + foreach (var (_, projectState) in state.ProjectStates) + { + if (searchingChecksumsLeft.Count == 0) + break; + + if (projectState.TryGetStateChecksums(out var projectStateChecksums) && + searchingChecksumsLeft.Remove(projectStateChecksums.Checksum)) + { + result[projectStateChecksums.Checksum] = projectStateChecksums; + } + } - foreach (var (projectId, projectState) in state.ProjectStates) - { - if (searchingChecksumsLeft.Count == 0) - break; + // Now actually do the depth first search into each project. - if (hintProject != null && hintProject != projectId) - continue; + foreach (var (_, projectState) in state.ProjectStates) + { + if (searchingChecksumsLeft.Count == 0) + break; - // It's possible not all all our projects have checksums. Specifically, we may have only been - // asked to compute the checksum tree for a subset of projects that were all that a feature needed. - if (projectState.TryGetStateChecksums(out var projectStateChecksums)) - await projectStateChecksums.FindAsync(projectState, searchingChecksumsLeft, result, cancellationToken).ConfigureAwait(false); + // It's possible not all all our projects have checksums. Specifically, we may have only been + // asked to compute the checksum tree for a subset of projects that were all that a feature needed. + if (projectState.TryGetStateChecksums(out var projectStateChecksums)) + await projectStateChecksums.FindAsync(projectState, searchingChecksumsLeft, result, cancellationToken).ConfigureAwait(false); + } } } } @@ -256,6 +284,11 @@ public async Task FindAsync( result[Checksum] = this; } + // It's normal for callers to just want to sync a single ProjectStateChecksum. So quickly check this, without + // doing all the expensive linear work below if we can bail out early here. + if (searchingChecksumsLeft.Count == 0) + return; + if (searchingChecksumsLeft.Remove(Info)) { result[Info] = state.ProjectInfo.Attributes; diff --git a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs index b7f0cf2486ed0..c033731c44722 100644 --- a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs +++ b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Serialization; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Remote; @@ -27,8 +28,8 @@ public async Task CreateSolutionInfoAsync(Checksum solutionChecksu var solutionAttributes = await GetAssetAsync(hintProject: null, solutionChecksums.Attributes, cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder.GetInstance(solutionChecksums.Projects.Count, out var projects); - foreach (var projectChecksum in solutionChecksums.Projects) - projects.AddIfNotNull(await CreateProjectInfoAsync(projectChecksum, cancellationToken).ConfigureAwait(false)); + for (int i = 0, n = solutionChecksums.ProjectIds.Length; i < n; i++) + projects.AddIfNotNull(await CreateProjectInfoAsync(solutionChecksums.ProjectIds[i], solutionChecksums.Projects[i], cancellationToken).ConfigureAwait(false)); var analyzerReferences = await CreateCollectionAsync(hintProject: null, solutionChecksums.AnalyzerReferences, cancellationToken).ConfigureAwait(false); @@ -36,11 +37,11 @@ public async Task CreateSolutionInfoAsync(Checksum solutionChecksu solutionAttributes.Id, solutionAttributes.Version, solutionAttributes.FilePath, projects.ToImmutableAndClear(), analyzerReferences).WithTelemetryId(solutionAttributes.TelemetryId); } - public async Task CreateProjectInfoAsync(Checksum projectChecksum, CancellationToken cancellationToken) + public async Task CreateProjectInfoAsync(ProjectId projectId, Checksum projectChecksum, CancellationToken cancellationToken) { - var projectChecksums = await GetAssetAsync(hintProject: null, projectChecksum, cancellationToken).ConfigureAwait(false); + var projectChecksums = await GetAssetAsync(hintProject: projectId, projectChecksum, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfFalse(projectId == projectChecksums.ProjectId); - var projectId = projectChecksums.ProjectId; var attributes = await GetAssetAsync(hintProject: projectId, projectChecksums.Info, cancellationToken).ConfigureAwait(false); if (!RemoteSupportedLanguages.IsSupported(attributes.Language)) { diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs index ccbbb3cf7162c..335e988637597 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs @@ -71,7 +71,8 @@ public async Task CreateSolutionAsync(Checksum newSolutionChecksum, Ca if (oldSolutionChecksums.Projects.Checksum != newSolutionChecksums.Projects.Checksum) { - solution = await UpdateProjectsAsync(solution, oldSolutionChecksums.Projects, newSolutionChecksums.Projects, cancellationToken).ConfigureAwait(false); + solution = await UpdateProjectsAsync( + solution, oldSolutionChecksums, newSolutionChecksums, cancellationToken).ConfigureAwait(false); } if (oldSolutionChecksums.AnalyzerReferences.Checksum != newSolutionChecksums.AnalyzerReferences.Checksum) @@ -104,17 +105,18 @@ public async Task CreateSolutionAsync(Checksum newSolutionChecksum, Ca } } - private async Task UpdateProjectsAsync(Solution solution, ChecksumCollection oldChecksums, ChecksumCollection newChecksums, CancellationToken cancellationToken) + private async Task UpdateProjectsAsync( + Solution solution, SolutionStateChecksums oldSolutionChecksums, SolutionStateChecksums newSolutionChecksums, CancellationToken cancellationToken) { using var olds = SharedPools.Default>().GetPooledObject(); using var news = SharedPools.Default>().GetPooledObject(); - olds.Object.UnionWith(oldChecksums); - news.Object.UnionWith(newChecksums); + olds.Object.UnionWith(oldSolutionChecksums.Projects); + news.Object.UnionWith(newSolutionChecksums.Projects); // remove projects that exist in both side - olds.Object.ExceptWith(newChecksums); - news.Object.ExceptWith(oldChecksums); + olds.Object.ExceptWith(newSolutionChecksums.Projects); + news.Object.ExceptWith(oldSolutionChecksums.Projects); using var _1 = PooledDictionary.GetInstance(out var oldMap); using var _2 = PooledDictionary.GetInstance(out var newMap); @@ -130,7 +132,7 @@ private async Task UpdateProjectsAsync(Solution solution, ChecksumColl { if (!oldMap.ContainsKey(projectId)) { - var projectInfo = await _assetProvider.CreateProjectInfoAsync(newProjectChecksums.Checksum, cancellationToken).ConfigureAwait(false); + var projectInfo = await _assetProvider.CreateProjectInfoAsync(projectId, newProjectChecksums.Checksum, cancellationToken).ConfigureAwait(false); if (projectInfo == null) { // this project is not supported in OOP