From 8abd8741926fedeb8b7b2a9fe28bc6c1628820e6 Mon Sep 17 00:00:00 2001 From: tmat Date: Tue, 23 Feb 2021 11:40:47 -0800 Subject: [PATCH 1/2] Document state storage refactoring --- .../CSharpTest/Workspaces/WorkspaceTests.cs | 6 +- .../SolutionCrawler/WorkCoordinator.cs | 2 +- .../ProjectSystem/VisualStudioProject.cs | 2 +- ...pl.RemoveAnalyzerConfigDocumentUndoUnit.cs | 2 +- .../SymbolTree/SymbolTreeInfo_Source.cs | 4 +- .../Workspace/Solution/ChecksumCollection.cs | 8 +- .../Portable/Workspace/Solution/Project.cs | 116 +++---- .../Workspace/Solution/ProjectChanges.cs | 126 ++------ .../Workspace/Solution/ProjectState.cs | 295 ++++++------------ .../Solution/ProjectState_Checksum.cs | 8 +- .../Portable/Workspace/Solution/Solution.cs | 24 +- ...eneratorDriverTranslationAction_Actions.cs | 4 +- .../SolutionState.CompilationTracker.State.cs | 18 +- .../SolutionState.CompilationTracker.cs | 49 ++- .../Workspace/Solution/SolutionState.cs | 53 ++-- .../Workspace/Solution/StateChecksums.cs | 7 +- .../Workspace/Solution/TextDocumentStates.cs | 226 ++++++++++++++ .../Core/Portable/Workspace/Workspace.cs | 4 +- .../CoreTest/SolutionTests/SolutionTests.cs | 6 +- .../Host/RemoteWorkspace.SolutionCreator.cs | 6 +- .../ImmutableHashMapExtensions.cs | 15 +- 21 files changed, 483 insertions(+), 498 deletions(-) create mode 100644 src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs diff --git a/src/EditorFeatures/CSharpTest/Workspaces/WorkspaceTests.cs b/src/EditorFeatures/CSharpTest/Workspaces/WorkspaceTests.cs index 444c639e74df7..4e4c96a53dab1 100644 --- a/src/EditorFeatures/CSharpTest/Workspaces/WorkspaceTests.cs +++ b/src/EditorFeatures/CSharpTest/Workspaces/WorkspaceTests.cs @@ -220,14 +220,14 @@ public async Task TestAddedSubmissionParseTreeHasEmptyFilePath() // Check that a parse tree for a submission has an empty file path. var tree1 = await workspace.CurrentSolution .GetProjectState(project1.Id) - .GetDocumentState(document1.Id) + .DocumentStates.GetState(document1.Id) .GetSyntaxTreeAsync(CancellationToken.None); Assert.Equal("", tree1.FilePath); // Check that a parse tree for a script does not have an empty file path. var tree2 = await workspace.CurrentSolution .GetProjectState(project2.Id) - .GetDocumentState(document2.Id) + .DocumentStates.GetState(document2.Id) .GetSyntaxTreeAsync(CancellationToken.None); Assert.Equal("a.csx", tree2.FilePath); } @@ -821,7 +821,7 @@ public async Task TestAnalyzerConfigFile_Properties() Assert.Equal(1, project.Documents.Count()); Assert.Equal(1, project.AnalyzerConfigDocuments.Count()); - Assert.Equal(1, project.State.AnalyzerConfigDocumentIds.Count()); + Assert.Equal(1, project.State.AnalyzerConfigDocumentStates.Count); var doc = project.GetDocument(analyzerConfigDoc.Id); Assert.Null(doc); diff --git a/src/Features/Core/Portable/SolutionCrawler/WorkCoordinator.cs b/src/Features/Core/Portable/SolutionCrawler/WorkCoordinator.cs index 464a2840dff8a..670561dfe1c0c 100644 --- a/src/Features/Core/Portable/SolutionCrawler/WorkCoordinator.cs +++ b/src/Features/Core/Portable/SolutionCrawler/WorkCoordinator.cs @@ -792,7 +792,7 @@ public int GetDocumentCount(Solution solution) { foreach (var projectState in solution.State.ProjectStates) { - count += projectState.Value.DocumentIds.Count; + count += projectState.Value.DocumentStates.Count; } return count; diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs index 092fd7707797e..e485f58db778c 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs @@ -234,7 +234,7 @@ private static void TryReportCompilationThrownAway(SolutionState solutionState, // We log the number of syntax trees that have been parsed even if there was no compilation created yet var projectState = solutionState.GetRequiredProjectState(projectId); var parsedTrees = 0; - foreach (var documentState in projectState.DocumentStates.Values) + foreach (var documentState in projectState.DocumentStates.States) { if (documentState.TryGetSyntaxTree(out _)) { diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.RemoveAnalyzerConfigDocumentUndoUnit.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.RemoveAnalyzerConfigDocumentUndoUnit.cs index 7031144c360eb..9823be9c71390 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.RemoveAnalyzerConfigDocumentUndoUnit.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.RemoveAnalyzerConfigDocumentUndoUnit.cs @@ -19,7 +19,7 @@ public RemoveAnalyzerConfigDocumentUndoUnit( } protected override IReadOnlyList GetDocumentIds(Project fromProject) - => fromProject.State.AnalyzerConfigDocumentIds.AsImmutable(); + => fromProject.State.AnalyzerConfigDocumentStates.Ids; protected override TextDocument? GetDocument(Solution currentSolution) => currentSolution.GetAnalyzerConfigDocument(this.DocumentId); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs index 110b077247103..b27e3aba6aaa8 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Source.cs @@ -74,9 +74,9 @@ private static async Task ComputeSourceSymbolsChecksumAsync(ProjectSta // Order the documents by FilePath. Default ordering in the RemoteWorkspace is // to be ordered by Guid (which is not consistent across VS sessions). - var textChecksumsTasks = projectState.DocumentStates.OrderBy(d => d.Value.FilePath, StringComparer.Ordinal).Select(async d => + var textChecksumsTasks = projectState.DocumentStates.States.OrderBy(state => state.FilePath, StringComparer.Ordinal).Select(async state => { - var documentStateChecksum = await d.Value.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false); + var documentStateChecksum = await state.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false); return documentStateChecksum.Text; }); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs index d9959c4777b6d..7675cf7d47687 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ChecksumCollection.cs @@ -36,13 +36,13 @@ public IEnumerator GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - internal static async Task FindAsync( - ImmutableSortedDictionary documentStates, + internal static async Task FindAsync( + TextDocumentStates documentStates, HashSet searchingChecksumsLeft, Dictionary result, - CancellationToken cancellationToken) where TValue : TextDocumentState + CancellationToken cancellationToken) where TState : TextDocumentState { - foreach (var (_, state) in documentStates) + foreach (var state in documentStates.States) { Contract.ThrowIfFalse(state.TryGetStateChecksums(out var stateChecksums)); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index dd8288b741a0a..17ae094d0c3c4 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -160,51 +160,56 @@ internal Project(Solution solution, ProjectState projectState) /// /// True if the project has any documents. /// - public bool HasDocuments => _projectState.HasDocuments; + public bool HasDocuments => !_projectState.DocumentStates.IsEmpty; /// /// All the document IDs associated with this project. /// - public IReadOnlyList DocumentIds => _projectState.DocumentIds; + public IReadOnlyList DocumentIds => _projectState.DocumentStates.Ids; /// /// All the additional document IDs associated with this project. /// - public IReadOnlyList AdditionalDocumentIds => _projectState.AdditionalDocumentIds; + public IReadOnlyList AdditionalDocumentIds => _projectState.AdditionalDocumentStates.Ids; + + /// + /// All the additional document IDs associated with this project. + /// + internal IReadOnlyList AnalyzerConfigDocumentIds => _projectState.AnalyzerConfigDocumentStates.Ids; /// /// All the regular documents associated with this project. Documents produced from source generators are returned by /// . /// - public IEnumerable Documents => _projectState.DocumentIds.Select(GetDocument)!; + public IEnumerable Documents => DocumentIds.Select(GetDocument)!; /// /// All the additional documents associated with this project. /// - public IEnumerable AdditionalDocuments => _projectState.AdditionalDocumentIds.Select(GetAdditionalDocument)!; + public IEnumerable AdditionalDocuments => AdditionalDocumentIds.Select(GetAdditionalDocument)!; /// /// All the s associated with this project. /// - public IEnumerable AnalyzerConfigDocuments => _projectState.AnalyzerConfigDocumentIds.Select(GetAnalyzerConfigDocument)!; + public IEnumerable AnalyzerConfigDocuments => AnalyzerConfigDocumentIds.Select(GetAnalyzerConfigDocument)!; /// /// True if the project contains a document with the specified ID. /// public bool ContainsDocument(DocumentId documentId) - => _projectState.ContainsDocument(documentId); + => _projectState.DocumentStates.Contains(documentId); /// /// True if the project contains an additional document with the specified ID. /// public bool ContainsAdditionalDocument(DocumentId documentId) - => _projectState.ContainsAdditionalDocument(documentId); + => _projectState.AdditionalDocumentStates.Contains(documentId); /// /// True if the project contains an with the specified ID. /// public bool ContainsAnalyzerConfigDocument(DocumentId documentId) - => _projectState.ContainsAnalyzerConfigDocument(documentId); + => _projectState.AnalyzerConfigDocumentStates.Contains(documentId); /// /// Get the documentId in this project with the specified syntax tree. @@ -222,51 +227,30 @@ public bool ContainsAnalyzerConfigDocument(DocumentId documentId) /// Get the document in this project with the specified document Id. /// public Document? GetDocument(DocumentId documentId) - { - if (!ContainsDocument(documentId)) - { - return null; - } - - return ImmutableHashMapExtensions.GetOrAdd(ref _idToDocumentMap, documentId, s_createDocumentFunction, this); - } + => ImmutableHashMapExtensions.GetOrAdd(ref _idToDocumentMap, documentId, s_tryCreateDocumentFunction, this); /// /// Get the additional document in this project with the specified document Id. /// public TextDocument? GetAdditionalDocument(DocumentId documentId) - { - if (!ContainsAdditionalDocument(documentId)) - { - return null; - } - - return ImmutableHashMapExtensions.GetOrAdd(ref _idToAdditionalDocumentMap, documentId, s_createAdditionalDocumentFunction, this); - } + => ImmutableHashMapExtensions.GetOrAdd(ref _idToAdditionalDocumentMap, documentId, s_tryCreateAdditionalDocumentFunction, this); /// /// Get the analyzer config document in this project with the specified document Id. /// public AnalyzerConfigDocument? GetAnalyzerConfigDocument(DocumentId documentId) - { - if (!ContainsAnalyzerConfigDocument(documentId)) - { - return null; - } + => ImmutableHashMapExtensions.GetOrAdd(ref _idToAnalyzerConfigDocumentMap, documentId, s_tryCreateAnalyzerConfigDocumentFunction, this); - return ImmutableHashMapExtensions.GetOrAdd(ref _idToAnalyzerConfigDocumentMap, documentId, s_createAnalyzerConfigDocumentFunction, this); - } + /// + /// Gets all source generated documents in this project. + /// public async ValueTask> GetSourceGeneratedDocumentsAsync(CancellationToken cancellationToken = default) { var generatedDocumentStates = await _solution.State.GetSourceGeneratedDocumentStatesAsync(this.State, cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(generatedDocumentStates.Length, out var builder); - - foreach (var generatedDocumentState in generatedDocumentStates) - { - builder.Add(ImmutableHashMapExtensions.GetOrAdd(ref _idToSourceGeneratedDocumentMap, generatedDocumentState.Id, s_createSourceGeneratedDocumentFunction, (generatedDocumentState, this))); - } - return builder.ToImmutable(); + // return an interator to avoid eagerly allocating all the document instances + return generatedDocumentStates.States.Select(state => + ImmutableHashMapExtensions.GetOrAdd(ref _idToSourceGeneratedDocumentMap, state.Id, s_createSourceGeneratedDocumentFunction, (state, this)))!; } internal async ValueTask> GetAllRegularAndSourceGeneratedDocumentsAsync(CancellationToken cancellationToken = default) @@ -283,14 +267,11 @@ internal async ValueTask> GetAllRegularAndSourceGeneratedD } // We'll have to run generators if we haven't already and now try to find it. - var generatedDocumentStates = await _solution.State.GetSourceGeneratedDocumentStatesAsync(this.State, cancellationToken).ConfigureAwait(false); - - foreach (var generatedDocumentState in generatedDocumentStates) + var generatedDocumentStates = await _solution.State.GetSourceGeneratedDocumentStatesAsync(State, cancellationToken).ConfigureAwait(false); + var generatedDocumentState = generatedDocumentStates.GetState(documentId); + if (generatedDocumentState != null) { - if (generatedDocumentState.Id == documentId) - { - return ImmutableHashMapExtensions.GetOrAdd(ref _idToSourceGeneratedDocumentMap, generatedDocumentState.Id, s_createSourceGeneratedDocumentFunction, (generatedDocumentState, this)); - } + return ImmutableHashMapExtensions.GetOrAdd(ref _idToSourceGeneratedDocumentMap, generatedDocumentState.Id, s_createSourceGeneratedDocumentFunction, (generatedDocumentState, this)); } return null; @@ -324,15 +305,6 @@ internal async ValueTask> GetAllRegularAndSourceGeneratedD return ImmutableHashMapExtensions.GetOrAdd(ref _idToSourceGeneratedDocumentMap, documentId, s_createSourceGeneratedDocumentFunction, (documentState, this)); } - internal DocumentState? GetDocumentState(DocumentId documentId) - => _projectState.GetDocumentState(documentId); - - internal TextDocumentState? GetAdditionalDocumentState(DocumentId documentId) - => _projectState.GetAdditionalDocumentState(documentId); - - internal AnalyzerConfigDocumentState? GetAnalyzerConfigDocumentState(DocumentId documentId) - => _projectState.GetAnalyzerConfigDocumentState(documentId); - internal async Task ContainsSymbolsWithNameAsync(string name, SymbolFilter filter, CancellationToken cancellationToken) { return this.SupportsCompilation && @@ -348,35 +320,17 @@ internal async Task ContainsSymbolsWithNameAsync(Func predic internal async Task> GetDocumentsWithNameAsync(Func predicate, SymbolFilter filter, CancellationToken cancellationToken) => (await _solution.State.GetDocumentsWithNameAsync(Id, predicate, filter, cancellationToken).ConfigureAwait(false)).Select(s => _solution.GetDocument(s.Id)!); - private static readonly Func s_createDocumentFunction = CreateDocument; - private static Document CreateDocument(DocumentId documentId, Project project) - { - var state = project._projectState.GetDocumentState(documentId); - Contract.ThrowIfNull(state); - return new Document(project, state); - } + private static readonly Func s_tryCreateDocumentFunction = + (documentId, project) => project._projectState.DocumentStates.TryGetState(documentId, out var state) ? new Document(project, state) : null; - private static readonly Func s_createAdditionalDocumentFunction = CreateAdditionalDocument; - private static AdditionalDocument CreateAdditionalDocument(DocumentId documentId, Project project) - { - var state = project._projectState.GetAdditionalDocumentState(documentId); - Contract.ThrowIfNull(state); - return new AdditionalDocument(project, state); - } + private static readonly Func s_tryCreateAdditionalDocumentFunction = + (documentId, project) => project._projectState.AdditionalDocumentStates.TryGetState(documentId, out var state) ? new AdditionalDocument(project, state) : null; - private static readonly Func s_createAnalyzerConfigDocumentFunction = CreateAnalyzerConfigDocument; - private static AnalyzerConfigDocument CreateAnalyzerConfigDocument(DocumentId documentId, Project project) - { - var state = project._projectState.GetAnalyzerConfigDocumentState(documentId); - Contract.ThrowIfNull(state); - return new AnalyzerConfigDocument(project, state); - } + private static readonly Func s_tryCreateAnalyzerConfigDocumentFunction = + (documentId, project) => project._projectState.AnalyzerConfigDocumentStates.TryGetState(documentId, out var state) ? new AnalyzerConfigDocument(project, state) : null; - private static readonly Func s_createSourceGeneratedDocumentFunction = CreateSourceGeneratedDocument; - private static SourceGeneratedDocument CreateSourceGeneratedDocument(DocumentId documentId, (SourceGeneratedDocumentState, Project) stateAndProject) - { - return new SourceGeneratedDocument(stateAndProject.Item2, stateAndProject.Item1); - } + private static readonly Func s_createSourceGeneratedDocumentFunction = + (documentId, stateAndProject) => new SourceGeneratedDocument(stateAndProject.project, stateAndProject.state); /// /// Tries to get the cached for this project if it has already been created and is still cached. In almost all diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs index 901a1f1913c6e..d330a91db9e57 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { @@ -97,138 +99,66 @@ public IEnumerable GetRemovedAnalyzerReferences() } public IEnumerable GetAddedDocuments() - { - foreach (var id in _newProject.DocumentIds) - { - if (!_oldProject.ContainsDocument(id)) - { - yield return id; - } - } - } + => _newProject.State.DocumentStates.GetAddedStateIds(_oldProject.State.DocumentStates); public IEnumerable GetAddedAdditionalDocuments() - { - foreach (var id in _newProject.AdditionalDocumentIds) - { - if (!_oldProject.ContainsAdditionalDocument(id)) - { - yield return id; - } - } - } + => _newProject.State.AdditionalDocumentStates.GetAddedStateIds(_oldProject.State.AdditionalDocumentStates); public IEnumerable GetAddedAnalyzerConfigDocuments() - { - foreach (var doc in _newProject.AnalyzerConfigDocuments) - { - if (!_oldProject.ContainsAnalyzerConfigDocument(doc.Id)) - { - yield return doc.Id; - } - } - } + => _newProject.State.AnalyzerConfigDocumentStates.GetAddedStateIds(_oldProject.State.AnalyzerConfigDocumentStates); /// /// Get Documents with any changes, including textual and non-textual changes /// - /// public IEnumerable GetChangedDocuments() => GetChangedDocuments(onlyGetDocumentsWithTextChanges: false, ignoreUnchangeableDocuments: false); /// - /// Get Changed Documents: - /// When onlyGetDocumentsWithTextChanges is true, only get documents with text changes (we only check text source, not actual content); - /// otherwise get documents with any changes i.e. DocumentState changes: - /// , , + /// Get changed documents. + /// When is true, only get documents with text changes (we only check text source, not actual content); + /// otherwise get documents with any changes i.e. , and file path. /// - /// - /// public IEnumerable GetChangedDocuments(bool onlyGetDocumentsWithTextChanges) => GetChangedDocuments(onlyGetDocumentsWithTextChanges, ignoreUnchangeableDocuments: false); internal IEnumerable GetChangedDocuments(bool onlyGetDocumentsWithTextChanges, bool ignoreUnchangeableDocuments) { - foreach (var id in _newProject.DocumentIds) + foreach (var newState in _newProject.State.DocumentStates.States) { - var newState = _newProject.GetDocumentState(id)!; - var oldState = _oldProject.GetDocumentState(id); - - if (oldState != null) + var oldState = _oldProject.State.DocumentStates.GetState(newState.Id); + if (oldState == null) { - if (onlyGetDocumentsWithTextChanges) - { - if (newState.HasTextChanged(oldState, ignoreUnchangeableDocuments)) - yield return id; - } - else - { - if (newState != oldState) - yield return id; - } + // document was removed + continue; } - } - } - public IEnumerable GetChangedAdditionalDocuments() - { - // if the document states are different then there is a change. - foreach (var id in _newProject.AdditionalDocumentIds) - { - var newState = _newProject.GetAdditionalDocumentState(id); - var oldState = _oldProject.GetAdditionalDocumentState(id); - if (oldState != null && newState != oldState) + if (newState == oldState) { - yield return id; + continue; } - } - } - public IEnumerable GetChangedAnalyzerConfigDocuments() - { - // if the document states are different then there is a change. - foreach (var doc in _newProject.AnalyzerConfigDocuments) - { - var newState = _newProject.GetAnalyzerConfigDocumentState(doc.Id); - var oldState = _oldProject.GetAnalyzerConfigDocumentState(doc.Id); - if (oldState != null && newState != oldState) + if (onlyGetDocumentsWithTextChanges && !newState.HasTextChanged(oldState, ignoreUnchangeableDocuments)) { - yield return doc.Id; + continue; } + + yield return newState.Id; } } + public IEnumerable GetChangedAdditionalDocuments() + => _newProject.State.AdditionalDocumentStates.GetChangedStateIds(_oldProject.State.AdditionalDocumentStates); + + public IEnumerable GetChangedAnalyzerConfigDocuments() + => _newProject.State.AnalyzerConfigDocumentStates.GetChangedStateIds(_oldProject.State.AnalyzerConfigDocumentStates); + public IEnumerable GetRemovedDocuments() - { - foreach (var id in _oldProject.DocumentIds) - { - if (!_newProject.ContainsDocument(id)) - { - yield return id; - } - } - } + => _newProject.State.DocumentStates.GetRemovedStateIds(_oldProject.State.DocumentStates); public IEnumerable GetRemovedAdditionalDocuments() - { - foreach (var id in _oldProject.AdditionalDocumentIds) - { - if (!_newProject.ContainsAdditionalDocument(id)) - { - yield return id; - } - } - } + => _newProject.State.AdditionalDocumentStates.GetRemovedStateIds(_oldProject.State.AdditionalDocumentStates); public IEnumerable GetRemovedAnalyzerConfigDocuments() - { - foreach (var doc in _oldProject.AnalyzerConfigDocuments) - { - if (!_newProject.ContainsAnalyzerConfigDocument(doc.Id)) - { - yield return doc.Id; - } - } - } + => _newProject.State.AnalyzerConfigDocumentStates.GetRemovedStateIds(_oldProject.State.AnalyzerConfigDocumentStates); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index bff7a92d7e884..6c1f3ea019d21 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -29,22 +29,20 @@ internal partial class ProjectState /// The documents in this project. They are sorted by to provide a stable sort for /// . /// - private readonly ImmutableSortedDictionary _documentStates; + public readonly TextDocumentStates DocumentStates; /// /// The additional documents in this project. They are sorted by to provide a stable sort for /// . /// - private readonly ImmutableSortedDictionary _additionalDocumentStates; + public readonly TextDocumentStates AdditionalDocumentStates; /// /// The analyzer config documents in this project. They are sorted by to provide a stable sort for /// . /// - private readonly ImmutableSortedDictionary _analyzerConfigDocumentStates; + public readonly TextDocumentStates AnalyzerConfigDocumentStates; - private readonly ImmutableList _documentIds; - private readonly ImmutableList _additionalDocumentIds; private readonly AsyncLazy _lazyLatestDocumentVersion; private readonly AsyncLazy _lazyLatestDocumentTopLevelChangeVersion; @@ -62,22 +60,18 @@ private ProjectState( ProjectInfo projectInfo, HostLanguageServices languageServices, SolutionServices solutionServices, - ImmutableList documentIds, - ImmutableList additionalDocumentIds, - ImmutableSortedDictionary documentStates, - ImmutableSortedDictionary additionalDocumentStates, - ImmutableSortedDictionary analyzerConfigDocumentStates, + TextDocumentStates documentStates, + TextDocumentStates additionalDocumentStates, + TextDocumentStates analyzerConfigDocumentStates, AsyncLazy lazyLatestDocumentVersion, AsyncLazy lazyLatestDocumentTopLevelChangeVersion, ValueSource lazyAnalyzerConfigSet) { _solutionServices = solutionServices; _languageServices = languageServices; - _documentIds = documentIds; - _additionalDocumentIds = additionalDocumentIds; - _documentStates = documentStates; - _additionalDocumentStates = additionalDocumentStates; - _analyzerConfigDocumentStates = analyzerConfigDocumentStates; + DocumentStates = documentStates; + AdditionalDocumentStates = additionalDocumentStates; + AnalyzerConfigDocumentStates = analyzerConfigDocumentStates; _lazyLatestDocumentVersion = lazyLatestDocumentVersion; _lazyLatestDocumentTopLevelChangeVersion = lazyLatestDocumentTopLevelChangeVersion; _lazyAnalyzerConfigSet = lazyAnalyzerConfigSet; @@ -102,10 +96,9 @@ public ProjectState(ProjectInfo projectInfo, HostLanguageServices languageServic var projectInfoFixed = FixProjectInfo(projectInfo); // We need to compute our AnalyerConfigDocumentStates first, since we use those to produce our DocumentStates - _analyzerConfigDocumentStates = ImmutableSortedDictionary.CreateRange(DocumentIdComparer.Instance, - projectInfoFixed.AnalyzerConfigDocuments.Select(d => - KeyValuePairUtil.Create(d.Id, new AnalyzerConfigDocumentState(d, solutionServices)))); - _lazyAnalyzerConfigSet = ComputeAnalyzerConfigSetValueSource(_analyzerConfigDocumentStates.Values); + AnalyzerConfigDocumentStates = new TextDocumentStates(projectInfoFixed.AnalyzerConfigDocuments, info => new AnalyzerConfigDocumentState(info, solutionServices)); + + _lazyAnalyzerConfigSet = ComputeAnalyzerConfigSetValueSource(AnalyzerConfigDocumentStates); // Add analyzer config information to the compilation options if (projectInfoFixed.CompilationOptions != null) @@ -115,24 +108,13 @@ public ProjectState(ProjectInfo projectInfo, HostLanguageServices languageServic new WorkspaceSyntaxTreeOptionsProvider(_lazyAnalyzerConfigSet))); } - _documentIds = projectInfoFixed.Documents.Select(d => d.Id).ToImmutableList(); - _additionalDocumentIds = projectInfoFixed.AdditionalDocuments.Select(d => d.Id).ToImmutableList(); - var parseOptions = projectInfoFixed.ParseOptions; - var docStates = ImmutableSortedDictionary.CreateRange(DocumentIdComparer.Instance, - projectInfoFixed.Documents.Select(d => - new KeyValuePair(d.Id, - CreateDocument(d, parseOptions)))); - - _documentStates = docStates; - var additionalDocStates = ImmutableSortedDictionary.CreateRange(DocumentIdComparer.Instance, - projectInfoFixed.AdditionalDocuments.Select(d => - new KeyValuePair(d.Id, new TextDocumentState(d, solutionServices)))); + DocumentStates = new TextDocumentStates(projectInfoFixed.Documents, info => CreateDocument(info, parseOptions)); + AdditionalDocumentStates = new TextDocumentStates(projectInfoFixed.AdditionalDocuments, info => new TextDocumentState(info, solutionServices)); - _additionalDocumentStates = additionalDocStates; - _lazyLatestDocumentVersion = new AsyncLazy(c => ComputeLatestDocumentVersionAsync(docStates, additionalDocStates, c), cacheResult: true); - _lazyLatestDocumentTopLevelChangeVersion = new AsyncLazy(c => ComputeLatestDocumentTopLevelChangeVersionAsync(docStates, additionalDocStates, c), cacheResult: true); + _lazyLatestDocumentVersion = new AsyncLazy(c => ComputeLatestDocumentVersionAsync(DocumentStates, AdditionalDocumentStates, c), cacheResult: true); + _lazyLatestDocumentTopLevelChangeVersion = new AsyncLazy(c => ComputeLatestDocumentTopLevelChangeVersionAsync(DocumentStates, AdditionalDocumentStates, c), cacheResult: true); // ownership of information on document has moved to project state. clear out documentInfo the state is // holding on. otherwise, these information will be held onto unnecessarily by projectInfo even after @@ -174,26 +156,26 @@ private ProjectInfo FixProjectInfo(ProjectInfo projectInfo) return projectInfo; } - private static async Task ComputeLatestDocumentVersionAsync(IImmutableDictionary documentStates, IImmutableDictionary additionalDocumentStates, CancellationToken cancellationToken) + private static async Task ComputeLatestDocumentVersionAsync(TextDocumentStates documentStates, TextDocumentStates additionalDocumentStates, CancellationToken cancellationToken) { // this may produce a version that is out of sync with the actual Document versions. var latestVersion = VersionStamp.Default; - foreach (var (_, doc) in documentStates) + foreach (var state in documentStates.States) { cancellationToken.ThrowIfCancellationRequested(); - if (!doc.IsGenerated) + if (!state.IsGenerated) { - var version = await doc.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); + var version = await state.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); latestVersion = version.GetNewerVersion(latestVersion); } } - foreach (var (_, additionalDoc) in additionalDocumentStates) + foreach (var state in additionalDocumentStates.States) { cancellationToken.ThrowIfCancellationRequested(); - var version = await additionalDoc.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); + var version = await state.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); latestVersion = version.GetNewerVersion(latestVersion); } @@ -202,8 +184,8 @@ private static async Task ComputeLatestDocumentVersionAsync(IImmut private AsyncLazy CreateLazyLatestDocumentTopLevelChangeVersion( TextDocumentState newDocument, - IImmutableDictionary newDocumentStates, - IImmutableDictionary newAdditionalDocumentStates) + TextDocumentStates newDocumentStates, + TextDocumentStates newAdditionalDocumentStates) { if (_lazyLatestDocumentTopLevelChangeVersion.TryGetValue(out var oldVersion)) { @@ -221,23 +203,23 @@ private static async Task ComputeTopLevelChangeTextVersionAsync(Ve return newVersion.GetNewerVersion(oldVersion); } - private static async Task ComputeLatestDocumentTopLevelChangeVersionAsync(IImmutableDictionary documentStates, IImmutableDictionary additionalDocumentStates, CancellationToken cancellationToken) + private static async Task ComputeLatestDocumentTopLevelChangeVersionAsync(TextDocumentStates documentStates, TextDocumentStates additionalDocumentStates, CancellationToken cancellationToken) { // this may produce a version that is out of sync with the actual Document versions. var latestVersion = VersionStamp.Default; - foreach (var (_, doc) in documentStates) + foreach (var state in documentStates.States) { cancellationToken.ThrowIfCancellationRequested(); - var version = await doc.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false); + var version = await state.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false); latestVersion = version.GetNewerVersion(latestVersion); } - foreach (var (_, additionalDoc) in additionalDocumentStates) + foreach (var state in additionalDocumentStates.States) { cancellationToken.ThrowIfCancellationRequested(); - var version = await additionalDoc.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false); + var version = await state.GetTopLevelChangeTextVersionAsync(cancellationToken).ConfigureAwait(false); latestVersion = version.GetNewerVersion(latestVersion); } @@ -258,7 +240,7 @@ internal DocumentState CreateDocument(DocumentInfo documentInfo, ParseOptions? p public AnalyzerOptions AnalyzerOptions => _lazyAnalyzerOptions ??= new AnalyzerOptions( - additionalFiles: _additionalDocumentStates.Values.Select(d => new AdditionalTextWithState(d)).ToImmutableArray(), + additionalFiles: AdditionalDocumentStates.SelectAsArray(static state => new AdditionalTextWithState(state)), optionsProvider: new WorkspaceAnalyzerConfigOptionsProvider(this)); public async Task> GetAnalyzerOptionsForPathAsync( @@ -369,12 +351,12 @@ public override bool Equals(object? obj) public override int GetHashCode() => _lazyAnalyzerConfigSet.GetHashCode(); } - private static ValueSource ComputeAnalyzerConfigSetValueSource(IEnumerable analyzerConfigDocumentStates) + private static ValueSource ComputeAnalyzerConfigSetValueSource(TextDocumentStates analyzerConfigDocumentStates) { return new AsyncLazy( asynchronousComputeFunction: async cancellationToken => { - var tasks = analyzerConfigDocumentStates.Select(a => a.GetAnalyzerConfigAsync(cancellationToken)); + var tasks = analyzerConfigDocumentStates.States.Select(a => a.GetAnalyzerConfigAsync(cancellationToken)); var analyzerConfigs = await Task.WhenAll(tasks).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -468,66 +450,11 @@ public async Task GetSemanticVersionAsync(CancellationToken cancel [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] public bool RunAnalyzers => this.ProjectInfo.RunAnalyzers; - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public bool HasDocuments => _documentIds.Count > 0; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public IEnumerable OrderedDocumentStates => this.DocumentIds.Select(GetDocumentState)!; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public IReadOnlyList DocumentIds => _documentIds; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public IReadOnlyList AdditionalDocumentIds => _additionalDocumentIds; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - // Regular documents and additionald documents have an ordering, and so we maintain lists of the IDs in order; in the case of analyzerconfig documents, - // we don't define a workspace ordering because they are ordered via fancier algorithms in the compiler based on directory depth. - public IEnumerable AnalyzerConfigDocumentIds => _analyzerConfigDocumentStates.Keys; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public ImmutableSortedDictionary DocumentStates => _documentStates; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public ImmutableSortedDictionary AdditionalDocumentStates => _additionalDocumentStates; - - [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] - public ImmutableSortedDictionary AnalyzerConfigDocumentStates => _analyzerConfigDocumentStates; - - public bool ContainsDocument(DocumentId documentId) - => _documentStates.ContainsKey(documentId); - - public bool ContainsAdditionalDocument(DocumentId documentId) - => _additionalDocumentStates.ContainsKey(documentId); - - public bool ContainsAnalyzerConfigDocument(DocumentId documentId) - => _analyzerConfigDocumentStates.ContainsKey(documentId); - - public DocumentState? GetDocumentState(DocumentId documentId) - { - _documentStates.TryGetValue(documentId, out var state); - return state; - } - - public TextDocumentState? GetAdditionalDocumentState(DocumentId documentId) - { - _additionalDocumentStates.TryGetValue(documentId, out var state); - return state; - } - - public AnalyzerConfigDocumentState? GetAnalyzerConfigDocumentState(DocumentId documentId) - { - _analyzerConfigDocumentStates.TryGetValue(documentId, out var state); - return state; - } - private ProjectState With( ProjectInfo? projectInfo = null, - ImmutableList? documentIds = null, - ImmutableList? additionalDocumentIds = null, - ImmutableSortedDictionary? documentStates = null, - ImmutableSortedDictionary? additionalDocumentStates = null, - ImmutableSortedDictionary? analyzerConfigDocumentStates = null, + TextDocumentStates? documentStates = null, + TextDocumentStates? additionalDocumentStates = null, + TextDocumentStates? analyzerConfigDocumentStates = null, AsyncLazy? latestDocumentVersion = null, AsyncLazy? latestDocumentTopLevelChangeVersion = null, ValueSource? analyzerConfigSet = null) @@ -536,11 +463,9 @@ private ProjectState With( projectInfo ?? _projectInfo, _languageServices, _solutionServices, - documentIds ?? _documentIds, - additionalDocumentIds ?? _additionalDocumentIds, - documentStates ?? _documentStates, - additionalDocumentStates ?? _additionalDocumentStates, - analyzerConfigDocumentStates ?? _analyzerConfigDocumentStates, + documentStates ?? DocumentStates, + additionalDocumentStates ?? AdditionalDocumentStates, + analyzerConfigDocumentStates ?? AnalyzerConfigDocumentStates, latestDocumentVersion ?? _lazyLatestDocumentVersion, latestDocumentTopLevelChangeVersion ?? _lazyLatestDocumentTopLevelChangeVersion, analyzerConfigSet ?? _lazyAnalyzerConfigSet); @@ -599,18 +524,9 @@ public ProjectState WithParseOptions(ParseOptions options) return this; } - // update parse options for all documents too - var docMap = _documentStates; - - foreach (var (docId, oldDocState) in _documentStates) - { - var newDocState = oldDocState.UpdateParseOptions(options); - docMap = docMap.SetItem(docId, newDocState); - } - return With( projectInfo: ProjectInfo.WithParseOptions(options).WithVersion(Version.GetNewerVersion()), - documentStates: docMap); + documentStates: DocumentStates.UpdateStates(static (state, options) => state.UpdateParseOptions(options), options)); } public static bool IsSameLanguage(ProjectState project1, ProjectState project2) @@ -664,37 +580,35 @@ public ProjectState WithAnalyzerReferences(IEnumerable analyz public ProjectState AddDocuments(ImmutableArray documents) { - Debug.Assert(!documents.Any(d => this.DocumentStates.ContainsKey(d.Id))); + Debug.Assert(!documents.Any(d => DocumentStates.Contains(d.Id))); - return this.With( - projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - documentIds: _documentIds.AddRange(documents.Select(d => d.Id)), - documentStates: _documentStates.AddRange(documents.Select(d => KeyValuePairUtil.Create(d.Id, d)))); + return With( + projectInfo: ProjectInfo.WithVersion(Version.GetNewerVersion()), + documentStates: DocumentStates.AddRange(documents)); } public ProjectState AddAdditionalDocuments(ImmutableArray documents) { - Debug.Assert(!documents.Any(d => this.AdditionalDocumentStates.ContainsKey(d.Id))); + Debug.Assert(!documents.Any(d => AdditionalDocumentStates.Contains(d.Id))); - return this.With( - projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - additionalDocumentIds: _additionalDocumentIds.AddRange(documents.Select(d => d.Id)), - additionalDocumentStates: _additionalDocumentStates.AddRange(documents.Select(d => KeyValuePairUtil.Create(d.Id, d)))); + return With( + projectInfo: ProjectInfo.WithVersion(Version.GetNewerVersion()), + additionalDocumentStates: AdditionalDocumentStates.AddRange(documents)); } public ProjectState AddAnalyzerConfigDocuments(ImmutableArray documents) { - Debug.Assert(!documents.Any(d => this._analyzerConfigDocumentStates.ContainsKey(d.Id))); + Debug.Assert(!documents.Any(d => AnalyzerConfigDocumentStates.Contains(d.Id))); - var newAnalyzerConfigDocumentStates = _analyzerConfigDocumentStates.AddRange(documents.Select(d => KeyValuePairUtil.Create(d.Id, d))); + var newAnalyzerConfigDocumentStates = AnalyzerConfigDocumentStates.AddRange(documents); return CreateNewStateForChangedAnalyzerConfigDocuments(newAnalyzerConfigDocumentStates); } - private ProjectState CreateNewStateForChangedAnalyzerConfigDocuments(ImmutableSortedDictionary newAnalyzerConfigDocumentStates) + private ProjectState CreateNewStateForChangedAnalyzerConfigDocuments(TextDocumentStates newAnalyzerConfigDocumentStates) { - var newAnalyzerConfigSet = ComputeAnalyzerConfigSetValueSource(newAnalyzerConfigDocumentStates.Values); - var projectInfo = ProjectInfo.WithVersion(this.Version.GetNewerVersion()); + var newAnalyzerConfigSet = ComputeAnalyzerConfigSetValueSource(newAnalyzerConfigDocumentStates); + var projectInfo = ProjectInfo.WithVersion(Version.GetNewerVersion()); // Changing analyzer configs changes compilation options if (CompilationOptions != null) @@ -704,7 +618,7 @@ private ProjectState CreateNewStateForChangedAnalyzerConfigDocuments(ImmutableSo .WithCompilationOptions(CompilationOptions.WithSyntaxTreeOptionsProvider(newProvider)); } - return this.With( + return With( projectInfo: projectInfo, analyzerConfigDocumentStates: newAnalyzerConfigDocumentStates, analyzerConfigSet: newAnalyzerConfigSet); @@ -714,24 +628,22 @@ public ProjectState RemoveDocuments(ImmutableArray documentIds) { // We create a new CachingAnalyzerConfigSet for the new snapshot to avoid holding onto cached information // for removed documents. - return this.With( - projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - documentIds: _documentIds.RemoveRange(documentIds), - documentStates: _documentStates.RemoveRange(documentIds), - analyzerConfigSet: ComputeAnalyzerConfigSetValueSource(AnalyzerConfigDocumentStates.Values)); + return With( + projectInfo: ProjectInfo.WithVersion(Version.GetNewerVersion()), + documentStates: DocumentStates.RemoveRange(documentIds), + analyzerConfigSet: ComputeAnalyzerConfigSetValueSource(AnalyzerConfigDocumentStates)); } public ProjectState RemoveAdditionalDocuments(ImmutableArray documentIds) { - return this.With( - projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - additionalDocumentIds: _additionalDocumentIds.RemoveRange(documentIds), - additionalDocumentStates: _additionalDocumentStates.RemoveRange(documentIds)); + return With( + projectInfo: ProjectInfo.WithVersion(Version.GetNewerVersion()), + additionalDocumentStates: AdditionalDocumentStates.RemoveRange(documentIds)); } public ProjectState RemoveAnalyzerConfigDocuments(ImmutableArray documentIds) { - var newAnalyzerConfigDocumentStates = _analyzerConfigDocumentStates.RemoveRange(documentIds); + var newAnalyzerConfigDocumentStates = AnalyzerConfigDocumentStates.RemoveRange(documentIds); return CreateNewStateForChangedAnalyzerConfigDocuments(newAnalyzerConfigDocumentStates); } @@ -740,29 +652,26 @@ public ProjectState RemoveAllDocuments() { // We create a new CachingAnalyzerConfigSet for the new snapshot to avoid holding onto cached information // for removed documents. - return this.With( - projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()).WithDocuments(SpecializedCollections.EmptyEnumerable()), - documentIds: ImmutableList.Empty, - documentStates: ImmutableSortedDictionary.Create(DocumentIdComparer.Instance), - analyzerConfigSet: ComputeAnalyzerConfigSetValueSource(AnalyzerConfigDocumentStates.Values)); + return With( + projectInfo: ProjectInfo.WithVersion(Version.GetNewerVersion()), + documentStates: TextDocumentStates.Empty, + analyzerConfigSet: ComputeAnalyzerConfigSetValueSource(AnalyzerConfigDocumentStates)); } public ProjectState UpdateDocument(DocumentState newDocument, bool textChanged, bool recalculateDependentVersions) { - Debug.Assert(this.ContainsDocument(newDocument.Id)); - - var oldDocument = this.GetDocumentState(newDocument.Id)!; + var oldDocument = DocumentStates.GetRequiredState(newDocument.Id); if (oldDocument == newDocument) { return this; } - var newDocumentStates = _documentStates.SetItem(newDocument.Id, newDocument); + var newDocumentStates = DocumentStates.SetState(newDocument.Id, newDocument); GetLatestDependentVersions( - newDocumentStates, _additionalDocumentStates, oldDocument, newDocument, recalculateDependentVersions, textChanged, + newDocumentStates, AdditionalDocumentStates, oldDocument, newDocument, recalculateDependentVersions, textChanged, out var dependentDocumentVersion, out var dependentSemanticVersion); - return this.With( + return With( documentStates: newDocumentStates, latestDocumentVersion: dependentDocumentVersion, latestDocumentTopLevelChangeVersion: dependentSemanticVersion); @@ -770,17 +679,15 @@ public ProjectState UpdateDocument(DocumentState newDocument, bool textChanged, public ProjectState UpdateAdditionalDocument(TextDocumentState newDocument, bool textChanged, bool recalculateDependentVersions) { - Debug.Assert(this.ContainsAdditionalDocument(newDocument.Id)); - - var oldDocument = this.GetAdditionalDocumentState(newDocument.Id)!; + var oldDocument = AdditionalDocumentStates.GetRequiredState(newDocument.Id); if (oldDocument == newDocument) { return this; } - var newDocumentStates = _additionalDocumentStates.SetItem(newDocument.Id, newDocument); + var newDocumentStates = AdditionalDocumentStates.SetState(newDocument.Id, newDocument); GetLatestDependentVersions( - _documentStates, newDocumentStates, oldDocument, newDocument, recalculateDependentVersions, textChanged, + DocumentStates, newDocumentStates, oldDocument, newDocument, recalculateDependentVersions, textChanged, out var dependentDocumentVersion, out var dependentSemanticVersion); return this.With( @@ -791,45 +698,37 @@ public ProjectState UpdateAdditionalDocument(TextDocumentState newDocument, bool public ProjectState UpdateAnalyzerConfigDocument(AnalyzerConfigDocumentState newDocument) { - Debug.Assert(this.ContainsAnalyzerConfigDocument(newDocument.Id)); - - var oldDocument = this.GetAnalyzerConfigDocumentState(newDocument.Id); + var oldDocument = AnalyzerConfigDocumentStates.GetRequiredState(newDocument.Id); if (oldDocument == newDocument) { return this; } - var newDocumentStates = _analyzerConfigDocumentStates.SetItem(newDocument.Id, newDocument); + var newDocumentStates = AnalyzerConfigDocumentStates.SetState(newDocument.Id, newDocument); return CreateNewStateForChangedAnalyzerConfigDocuments(newDocumentStates); } - public ProjectState UpdateDocumentsOrder(ImmutableList documentIds) + public ProjectState UpdateDocumentsOrder(ImmutableArray documentIds) { if (documentIds.IsEmpty) { throw new ArgumentOutOfRangeException("The specified documents are empty.", nameof(documentIds)); } - if (documentIds.Count != _documentIds.Count) + if (documentIds.Length != DocumentStates.Count) { throw new ArgumentException($"The specified documents do not equal the project document count.", nameof(documentIds)); } var hasOrderChanged = false; - for (var i = 0; i < documentIds.Count; ++i) + for (var i = 0; i < documentIds.Length; i++) { - var documentId = documentIds[i]; - - if (!ContainsDocument(documentId)) - { - throw new InvalidOperationException($"The document '{documentId}' does not exist in the project."); - } - - if (DocumentIds[i] != documentId) + if (DocumentStates.Ids[i] != documentIds[i]) { hasOrderChanged = true; + break; } } @@ -838,14 +737,17 @@ public ProjectState UpdateDocumentsOrder(ImmutableList documentIds) return this; } - return this.With( - projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - documentIds: documentIds); + var reorderedStates = documentIds.Select(id => + DocumentStates.GetState(id) ?? throw new InvalidOperationException($"The document '{id}' does not exist in the project.")); + + return With( + projectInfo: ProjectInfo.WithVersion(Version.GetNewerVersion()), + documentStates: new(reorderedStates)); } private void GetLatestDependentVersions( - IImmutableDictionary newDocumentStates, - IImmutableDictionary newAdditionalDocumentStates, + TextDocumentStates newDocumentStates, + TextDocumentStates newAdditionalDocumentStates, TextDocumentState oldDocument, TextDocumentState newDocument, bool recalculateDependentVersions, bool textChanged, out AsyncLazy dependentDocumentVersion, out AsyncLazy dependentSemanticVersion) @@ -881,28 +783,5 @@ private void GetLatestDependentVersions( CreateLazyLatestDocumentTopLevelChangeVersion(newDocument, newDocumentStates, newAdditionalDocumentStates) : _lazyLatestDocumentTopLevelChangeVersion; } - - private sealed class DocumentIdComparer : IComparer - { - public static readonly IComparer Instance = new DocumentIdComparer(); - - private DocumentIdComparer() - { - } - - public int Compare(DocumentId? x, DocumentId? y) - { - if (x is null) - { - return y is null ? 0 : -1; - } - else if (y is null) - { - return 1; - } - - return x.Id.CompareTo(y.Id); - } - } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs index cf1beb0ded187..18c50562daef9 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs @@ -43,11 +43,9 @@ private async Task ComputeChecksumsAsync(CancellationToke { using (Logger.LogBlock(FunctionId.ProjectState_ComputeChecksumsAsync, FilePath, cancellationToken)) { - // Here, we use the _documentStates and _additionalDocumentStates and visit them in order; we ensure that those are - // sorted by ID so we have a consistent sort. - var documentChecksumsTasks = _documentStates.Select(pair => pair.Value.GetChecksumAsync(cancellationToken)); - var additionalDocumentChecksumTasks = _additionalDocumentStates.Select(pair => pair.Value.GetChecksumAsync(cancellationToken)); - var analyzerConfigDocumentChecksumTasks = _analyzerConfigDocumentStates.Select(pair => pair.Value.GetChecksumAsync(cancellationToken)); + var documentChecksumsTasks = DocumentStates.SelectAsArray(static (state, token) => state.GetChecksumAsync(token), cancellationToken); + var additionalDocumentChecksumTasks = AdditionalDocumentStates.SelectAsArray(static (state, token) => state.GetChecksumAsync(token), cancellationToken); + var analyzerConfigDocumentChecksumTasks = AnalyzerConfigDocumentStates.SelectAsArray(static (state, token) => state.GetChecksumAsync(token), cancellationToken); var serializer = _solutionServices.Workspace.Services.GetService(); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index b8cff263c5f91..028caf13e3209 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -186,13 +186,27 @@ private static Project CreateProject(ProjectId projectId, Solution solution) /// Gets the document in this solution with the specified document ID. /// public Document? GetDocument(DocumentId? documentId) + => GetProject(documentId?.ProjectId)?.GetDocument(documentId!); + + /// + /// Gets a document or a source generated document in this solution with the specified document ID. + /// + internal async ValueTask GetDocumentAsync(DocumentId? documentId, bool includeSourceGenerated = false, CancellationToken cancellationToken = default) { - if (this.ContainsDocument(documentId)) + var project = GetProject(documentId?.ProjectId); + if (project == null) { - return this.GetProject(documentId.ProjectId)!.GetDocument(documentId); + return null; } - return null; + Contract.ThrowIfNull(documentId); + var document = project.GetDocument(documentId); + if (document != null || !includeSourceGenerated) + { + return document; + } + + return await project.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); } /// @@ -518,7 +532,7 @@ internal Solution WithRunAnalyzers(ProjectId projectId, bool runAnalyzers) /// public Solution WithProjectDocumentsOrder(ProjectId projectId, ImmutableList documentIds) { - var newState = _state.WithProjectDocumentsOrder(projectId, documentIds); + var newState = _state.WithProjectDocumentsOrder(projectId, documentIds.ToImmutableArray()); if (newState == _state) { return this; @@ -1638,7 +1652,7 @@ internal ImmutableArray GetRelatedDocumentIds(DocumentId documentId) return ImmutableArray.Empty; } - var documentState = projectState.GetDocumentState(documentId); + var documentState = projectState.DocumentStates.GetState(documentId); if (documentState == null) { // this document no longer exist diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs index ed80422e70d42..44c59dc629cd8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationAndGeneratorDriverTranslationAction_Actions.cs @@ -103,9 +103,9 @@ public ReplaceAllSyntaxTreesAction(ProjectState state) public override async Task TransformCompilationAsync(Compilation oldCompilation, CancellationToken cancellationToken) { - var syntaxTrees = new List(capacity: _state.DocumentIds.Count); + var syntaxTrees = new List(capacity: _state.DocumentStates.Count); - foreach (var documentState in _state.OrderedDocumentStates) + foreach (var documentState in _state.DocumentStates.GetStatesInCompilationOrder()) { cancellationToken.ThrowIfCancellationRequested(); syntaxTrees.Add(await documentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false)); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs index dd0f0c4e05cf3..ca62af4dfa339 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.State.cs @@ -30,7 +30,7 @@ private class State public static readonly State Empty = new( compilationWithoutGeneratedDocuments: null, declarationOnlyCompilation: null, - generatedDocuments: ImmutableArray.Empty, + generatedDocuments: TextDocumentStates.Empty, generatedDocumentsAreFinal: false); /// @@ -51,7 +51,7 @@ private class State /// The best generated documents we have for the current state. specifies whether the /// documents are to be considered final and can be reused, or whether they're from a prior snapshot which needs to be recomputed. /// - public ImmutableArray GeneratedDocuments { get; } + public TextDocumentStates GeneratedDocuments { get; } /// /// Whether the generated documents in are final and should not be regenerated. It's important @@ -77,7 +77,7 @@ private class State protected State( ValueSource>? compilationWithoutGeneratedDocuments, Compilation? declarationOnlyCompilation, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, bool generatedDocumentsAreFinal) { // Declaration-only compilations should never have any references @@ -91,7 +91,7 @@ protected State( public static State Create( Compilation compilation, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, ImmutableArray> intermediateProjects) { Contract.ThrowIfTrue(intermediateProjects.IsDefault); @@ -124,7 +124,7 @@ private sealed class InProgressState : State public InProgressState( Compilation inProgressCompilation, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, ImmutableArray<(ProjectState state, CompilationAndGeneratorDriverTranslationAction action)> intermediateProjects) : base(compilationWithoutGeneratedDocuments: new ConstantValueSource>(inProgressCompilation), declarationOnlyCompilation: null, @@ -144,7 +144,7 @@ public InProgressState( private sealed class LightDeclarationState : State { public LightDeclarationState(Compilation declarationOnlyCompilation, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, bool generatedDocumentsAreFinal) : base(compilationWithoutGeneratedDocuments: null, declarationOnlyCompilation, @@ -161,7 +161,7 @@ public LightDeclarationState(Compilation declarationOnlyCompilation, private sealed class FullDeclarationState : State { public FullDeclarationState(Compilation declarationCompilation, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, bool generatedDocumentsAreFinal) : base(new WeakValueSource(declarationCompilation), declarationCompilation.Clone().RemoveAllReferences(), @@ -208,7 +208,7 @@ private FinalState( ValueSource> compilationWithoutGeneratedFilesSource, Compilation compilationWithoutGeneratedFiles, bool hasSuccessfullyLoaded, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, UnrootedSymbolSet unrootedSymbolSet) : base(compilationWithoutGeneratedFilesSource, compilationWithoutGeneratedFiles.Clone().RemoveAllReferences(), @@ -236,7 +236,7 @@ public static FinalState Create( ValueSource> compilationWithoutGeneratedFilesSource, Compilation compilationWithoutGeneratedFiles, bool hasSuccessfullyLoaded, - ImmutableArray generatedDocuments, + TextDocumentStates generatedDocuments, Compilation finalCompilation, ProjectId projectId, Dictionary? metadataReferenceToProjectId) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs index ab2a6abcf0067..39fbc394c999f 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTracker.cs @@ -32,7 +32,7 @@ internal partial class SolutionState private partial class CompilationTracker { private static readonly Func s_logBuildCompilationAsync = - state => string.Join(",", state.AssemblyName, state.DocumentIds.Count); + state => string.Join(",", state.AssemblyName, state.DocumentStates.Count); public ProjectState ProjectState { get; } @@ -252,7 +252,7 @@ public CompilationTracker FreezePartialStateWithTree(SolutionState solution, Doc else { inProgressCompilation = inProgressCompilation.AddSyntaxTrees(tree); - Debug.Assert(!inProgressProject.DocumentIds.Contains(docState.Id)); + Debug.Assert(!inProgressProject.DocumentStates.Contains(docState.Id)); inProgressProject = inProgressProject.AddDocuments(ImmutableArray.Create(docState)); } } @@ -288,7 +288,7 @@ private void GetPartialCompilationState( DocumentId id, out ProjectState inProgressProject, out Compilation inProgressCompilation, - out ImmutableArray sourceGeneratedDocuments, + out TextDocumentStates sourceGeneratedDocuments, out Dictionary? metadataReferenceToProjectId, CancellationToken cancellationToken) { @@ -310,7 +310,7 @@ private void GetPartialCompilationState( // We'll add in whatever generated documents we do have; these may be from a prior run prior to some changes // being made to the project, but it's the best we have so we'll use it. - inProgressCompilation = compilationWithoutGeneratedDocuments.AddSyntaxTrees(sourceGeneratedDocuments.Select(d => d.SyntaxTree)); + inProgressCompilation = compilationWithoutGeneratedDocuments.AddSyntaxTrees(sourceGeneratedDocuments.States.Select(state => state.SyntaxTree)); // This is likely a bug. It seems possible to pass out a partial compilation state that we don't // properly record assembly symbols for. @@ -353,7 +353,7 @@ private void GetPartialCompilationState( inProgressCompilation = compilationWithoutGeneratedDocuments; } - inProgressCompilation = inProgressCompilation.AddSyntaxTrees(sourceGeneratedDocuments.Select(d => d.SyntaxTree)); + inProgressCompilation = inProgressCompilation.AddSyntaxTrees(sourceGeneratedDocuments.States.Select(state => state.SyntaxTree)); // Now add in back a consistent set of project references. For project references // try to get either a CompilationReference or a SkeletonReference. This ensures @@ -575,8 +575,7 @@ private Task BuildCompilationInfoAsync( // If we have already reached FinalState in the past but the compilation was garbage collected, we still have the generated documents // so we can pass those to FinalizeCompilationAsync to avoid the recomputation. This is necessary for correctness as otherwise // we'd be reparsing trees which could result in generated documents changing identity. - ImmutableArray? authoritativeGeneratedDocuments = - state.GeneratedDocumentsAreFinal ? state.GeneratedDocuments : null; + var authoritativeGeneratedDocuments = state.GeneratedDocumentsAreFinal ? state.GeneratedDocuments : (TextDocumentStates?)null; if (compilation == null) { @@ -629,18 +628,18 @@ private async Task BuildDeclarationCompilationFromScratchAsync( { var compilation = CreateEmptyCompilation(); - var trees = ArrayBuilder.GetInstance(ProjectState.DocumentIds.Count); - foreach (var document in ProjectState.OrderedDocumentStates) + var trees = ArrayBuilder.GetInstance(ProjectState.DocumentStates.Count); + foreach (var documentState in ProjectState.DocumentStates.GetStatesInCompilationOrder()) { cancellationToken.ThrowIfCancellationRequested(); // Include the tree even if the content of the document failed to load. - trees.Add(await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false)); + trees.Add(await documentState.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false)); } compilation = compilation.AddSyntaxTrees(trees); trees.Free(); - WriteState(new FullDeclarationState(compilation, ImmutableArray.Empty, generatedDocumentsAreFinal: false), solutionServices); + WriteState(new FullDeclarationState(compilation, TextDocumentStates.Empty, generatedDocumentsAreFinal: false), solutionServices); return compilation; } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e)) @@ -708,13 +707,13 @@ private async Task BuildDeclarationCompilationFromInProgressAsync( } } - private struct CompilationInfo + private readonly struct CompilationInfo { public Compilation Compilation { get; } public bool HasSuccessfullyLoaded { get; } - public ImmutableArray GeneratedDocuments { get; } + public TextDocumentStates GeneratedDocuments { get; } - public CompilationInfo(Compilation compilation, bool hasSuccessfullyLoaded, ImmutableArray generatedDocuments) + public CompilationInfo(Compilation compilation, bool hasSuccessfullyLoaded, TextDocumentStates generatedDocuments) { Compilation = compilation; HasSuccessfullyLoaded = hasSuccessfullyLoaded; @@ -733,7 +732,7 @@ public CompilationInfo(Compilation compilation, bool hasSuccessfullyLoaded, Immu private async Task FinalizeCompilationAsync( SolutionState solution, Compilation compilation, - ImmutableArray? authoritativeGeneratedDocuments, + TextDocumentStates? authoritativeGeneratedDocuments, CancellationToken cancellationToken) { try @@ -797,9 +796,8 @@ private async Task FinalizeCompilationAsync( // https://github.com/dotnet/roslyn/issues/46418 var compilationWithoutGeneratedFiles = compilation; - ImmutableArray generatedDocuments; - - if (authoritativeGeneratedDocuments != null) + TextDocumentStates generatedDocuments; + if (authoritativeGeneratedDocuments.HasValue) { generatedDocuments = authoritativeGeneratedDocuments.Value; } @@ -809,7 +807,7 @@ private async Task FinalizeCompilationAsync( if (generators.Any()) { - var additionalTexts = this.ProjectState.AdditionalDocumentStates.Values.SelectAsArray(a => (AdditionalText)new AdditionalTextWithState(a)); + var additionalTexts = this.ProjectState.AdditionalDocumentStates.SelectAsArray(state => new AdditionalTextWithState(state)); var compilationFactory = this.ProjectState.LanguageServices.GetRequiredService(); var generatorDriver = compilationFactory.CreateGeneratorDriver( @@ -838,10 +836,10 @@ private async Task FinalizeCompilationAsync( } } - generatedDocuments = generatedDocumentsBuilder.ToImmutable(); + generatedDocuments = new TextDocumentStates(generatedDocumentsBuilder); } - compilation = compilation.AddSyntaxTrees(generatedDocuments.Select(d => d.SyntaxTree)); + compilation = compilation.AddSyntaxTrees(generatedDocuments.States.Select(state => state.SyntaxTree)); var finalState = FinalState.Create( State.CreateValueSource(compilation, solution.Services), @@ -1060,7 +1058,7 @@ private async Task HasSuccessfullyLoadedSlowAsync(SolutionState solution, return compilationInfo.HasSuccessfullyLoaded; } - public async Task> GetSourceGeneratedDocumentStatesAsync(SolutionState solution, CancellationToken cancellationToken) + public async ValueTask> GetSourceGeneratedDocumentStatesAsync(SolutionState solution, CancellationToken cancellationToken) { var compilationInfo = await GetOrBuildCompilationInfoAsync(solution, lockGate: true, cancellationToken: cancellationToken).ConfigureAwait(false); return compilationInfo.GeneratedDocuments; @@ -1073,12 +1071,7 @@ public async Task> GetSourceGenerat // If we are in FinalState, then we have correctly ran generators and then know the final contents of the // Compilation. The GeneratedDocuments can be filled for intermediate states, but those aren't guaranteed to be // correct and can be re-ran later. - if (state is FinalState finalState) - { - return finalState.GeneratedDocuments.SingleOrDefault(d => d.Id == documentId); - } - - return null; + return state is FinalState finalState ? finalState.GeneratedDocuments.GetState(documentId) : null; } #region Versions diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index fead82ed1a54a..c889832cb1ade 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -313,7 +313,7 @@ public bool ContainsDocument([NotNullWhen(returnValue: true)] DocumentId? docume return documentId != null && this.ContainsProject(documentId.ProjectId) && - this.GetProjectState(documentId.ProjectId)!.ContainsDocument(documentId); + this.GetProjectState(documentId.ProjectId)!.DocumentStates.Contains(documentId); } /// @@ -324,7 +324,7 @@ public bool ContainsAdditionalDocument([NotNullWhen(returnValue: true)] Document return documentId != null && this.ContainsProject(documentId.ProjectId) && - this.GetProjectState(documentId.ProjectId)!.ContainsAdditionalDocument(documentId); + this.GetProjectState(documentId.ProjectId)!.AdditionalDocumentStates.Contains(documentId); } /// @@ -335,29 +335,17 @@ public bool ContainsAnalyzerConfigDocument([NotNullWhen(returnValue: true)] Docu return documentId != null && this.ContainsProject(documentId.ProjectId) && - this.GetProjectState(documentId.ProjectId)!.ContainsAnalyzerConfigDocument(documentId); + this.GetProjectState(documentId.ProjectId)!.AnalyzerConfigDocumentStates.Contains(documentId); } private DocumentState GetRequiredDocumentState(DocumentId documentId) - { - var state = GetProjectState(documentId.ProjectId)!.GetDocumentState(documentId); - Contract.ThrowIfNull(state); - return state; - } + => GetRequiredProjectState(documentId.ProjectId).DocumentStates.GetRequiredState(documentId); private TextDocumentState GetRequiredAdditionalDocumentState(DocumentId documentId) - { - var state = GetProjectState(documentId.ProjectId)!.GetAdditionalDocumentState(documentId); - Contract.ThrowIfNull(state); - return state; - } + => GetRequiredProjectState(documentId.ProjectId).AdditionalDocumentStates.GetRequiredState(documentId); private AnalyzerConfigDocumentState GetRequiredAnalyzerConfigDocumentState(DocumentId documentId) - { - var state = GetProjectState(documentId.ProjectId)!.GetAnalyzerConfigDocumentState(documentId); - Contract.ThrowIfNull(state); - return state; - } + => GetRequiredProjectState(documentId.ProjectId).AnalyzerConfigDocumentStates.GetRequiredState(documentId); internal DocumentState? GetDocumentState(SyntaxTree? syntaxTree, ProjectId? projectId) { @@ -371,7 +359,7 @@ private AnalyzerConfigDocumentState GetRequiredAnalyzerConfigDocumentState(Docum var projectState = GetProjectState(documentId.ProjectId); if (projectState != null) { - var document = projectState.GetDocumentState(documentId); + var document = projectState.DocumentStates.GetState(documentId); if (document != null) { // does this document really have the syntax tree? @@ -551,9 +539,9 @@ private ImmutableDictionary> CreateFilePathTo } private static IEnumerable GetDocumentStates(ProjectState projectState) - => projectState.DocumentStates.Values - .Concat(projectState.AdditionalDocumentStates.Values) - .Concat(projectState.AnalyzerConfigDocumentStates.Values); + => projectState.DocumentStates.States + .Concat(projectState.AdditionalDocumentStates.States) + .Concat(projectState.AnalyzerConfigDocumentStates.States); /// /// Create a new solution instance without the project specified. @@ -905,7 +893,7 @@ public SolutionState WithProjectReferences(ProjectId projectId, IReadOnlyList - public SolutionState WithProjectDocumentsOrder(ProjectId projectId, ImmutableList documentIds) + public SolutionState WithProjectDocumentsOrder(ProjectId projectId, ImmutableArray documentIds) { if (projectId == null) { @@ -1121,7 +1109,7 @@ public SolutionState AddAnalyzerConfigDocuments(ImmutableArray doc public SolutionState RemoveAnalyzerConfigDocuments(ImmutableArray documentIds) { return RemoveDocumentsFromMultipleProjects(documentIds, - (projectState, documentId) => projectState.GetAnalyzerConfigDocumentState(documentId)!, + (projectState, documentId) => projectState.AnalyzerConfigDocumentStates.GetRequiredState(documentId), (oldProject, documentIds, _) => { var newProject = oldProject.RemoveAnalyzerConfigDocuments(documentIds); @@ -1135,7 +1123,7 @@ public SolutionState RemoveAnalyzerConfigDocuments(ImmutableArray do public SolutionState RemoveDocuments(ImmutableArray documentIds) { return RemoveDocumentsFromMultipleProjects(documentIds, - (projectState, documentId) => projectState.GetDocumentState(documentId)!, + (projectState, documentId) => projectState.DocumentStates.GetRequiredState(documentId), (projectState, documentIds, documentStates) => (projectState.RemoveDocuments(documentIds), new CompilationAndGeneratorDriverTranslationAction.RemoveDocumentsAction(documentStates))); } @@ -1190,7 +1178,7 @@ private SolutionState RemoveDocumentsFromMultipleProjects( public SolutionState RemoveAdditionalDocuments(ImmutableArray documentIds) { return RemoveDocumentsFromMultipleProjects(documentIds, - (projectState, documentId) => projectState.GetAdditionalDocumentState(documentId)!, + (projectState, documentId) => projectState.AdditionalDocumentStates.GetRequiredState(documentId), (projectState, documentIds, documentStates) => (projectState.RemoveAdditionalDocuments(documentIds), new CompilationAndGeneratorDriverTranslationAction.RemoveAdditionalDocumentsAction(documentStates))); } @@ -1413,9 +1401,7 @@ private SolutionState UpdateDocumentState(DocumentState newDocument, bool textCh // This method shouldn't have been called if the document has not changed. Debug.Assert(oldProject != newProject); - var oldDocument = oldProject.GetDocumentState(newDocument.Id); - Contract.ThrowIfNull(oldDocument); - + var oldDocument = oldProject.DocumentStates.GetRequiredState(newDocument.Id); var newFilePathToDocumentIdsMap = CreateFilePathToDocumentIdsMapWithFilePath(newDocument.Id, oldDocument.FilePath, newDocument.FilePath); return ForkProject( @@ -1432,8 +1418,7 @@ private SolutionState UpdateAdditionalDocumentState(TextDocumentState newDocumen // This method shouldn't have been called if the document has not changed. Debug.Assert(oldProject != newProject); - var oldDocument = oldProject.GetAdditionalDocumentState(newDocument.Id); - Contract.ThrowIfNull(oldDocument); + var oldDocument = oldProject.AdditionalDocumentStates.GetRequiredState(newDocument.Id); return ForkProject( newProject, @@ -1706,7 +1691,7 @@ public SolutionState WithDocumentText(IEnumerable documentIds, Sour continue; } - var doc = GetProjectState(documentId.ProjectId)?.GetDocumentState(documentId); + var doc = GetProjectState(documentId.ProjectId)?.DocumentStates.GetState(documentId); if (doc != null) { if (!doc.TryGetText(out var existingText) || existingText != text) @@ -1770,11 +1755,11 @@ public Task HasSuccessfullyLoadedAsync(ProjectState project, CancellationT /// /// Returns the generated document states for source generated documents. /// - public Task> GetSourceGeneratedDocumentStatesAsync(ProjectState project, CancellationToken cancellationToken) + public ValueTask> GetSourceGeneratedDocumentStatesAsync(ProjectState project, CancellationToken cancellationToken) { return project.SupportsCompilation ? GetCompilationTracker(project.Id).GetSourceGeneratedDocumentStatesAsync(this, cancellationToken) - : SpecializedTasks.EmptyImmutableArray(); + : new(TextDocumentStates.Empty); } /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs index 16c86326ce671..cff827be27a0d 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs @@ -5,6 +5,7 @@ #nullable disable using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -220,11 +221,9 @@ public async Task FindAsync( Dictionary result, CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(state.TryGetStateChecksums(out var stateChecksum) && this == stateChecksum); - // verify input - Contract.ThrowIfFalse(state.TryGetStateChecksums(out var stateChecksum)); - Contract.ThrowIfFalse(this == stateChecksum); + cancellationToken.ThrowIfCancellationRequested(); if (searchingChecksumsLeft.Remove(Checksum)) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs new file mode 100644 index 0000000000000..71f2de0ce3488 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs @@ -0,0 +1,226 @@ +// 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 file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis +{ + /// + /// Holds on a to map and an ordering. + /// + internal readonly struct TextDocumentStates + where TState : TextDocumentState + { + public static readonly TextDocumentStates Empty = new(ImmutableList.Empty, ImmutableSortedDictionary.Empty); + + private readonly ImmutableList _ids; + + /// + /// The entires in the map are sorted by , which yields locally deterministic order but not the order that + /// matches the order in which documents were added. Therefore this ordering can't be used when creating compilations and it can't be + /// used when persisting document lists that do not preserve the GUIDs. + /// + private readonly ImmutableSortedDictionary _map; + + private TextDocumentStates(ImmutableList ids, ImmutableSortedDictionary map) + { + _ids = ids; + _map = map; + } + + public TextDocumentStates(IEnumerable states) + : this(states.Select(s => s.Id).ToImmutableList(), + states.ToImmutableSortedDictionary(state => state.Id, state => state, DocumentIdComparer.Instance)) + { + } + + public TextDocumentStates(IEnumerable infos, Func stateConstructor) + : this(infos.Select(info => info.Id).ToImmutableList(), + infos.ToImmutableSortedDictionary(info => info.Id, stateConstructor, DocumentIdComparer.Instance)) + { + } + + public int Count + => _map.Count; + + public bool IsEmpty + => Count == 0; + + public bool Contains(DocumentId id) + => _map.ContainsKey(id); + + public bool TryGetState(DocumentId documentId, [NotNullWhen(true)] out TState? state) + => _map.TryGetValue(documentId, out state); + + public TState? GetState(DocumentId documentId) + => _map.TryGetValue(documentId, out var state) ? state : null; + + public TState GetRequiredState(DocumentId documentId) + => _map.TryGetValue(documentId, out var state) ? state : throw ExceptionUtilities.Unreachable; + + /// + /// s in the order in which they were added to the project (the compilation order). + /// + public readonly IReadOnlyList Ids => _ids; + + /// + /// States ordered by . + /// + public IEnumerable States + => _map.Values; + + /// + /// Get states ordered in compilation order. + /// + /// + public IEnumerable GetStatesInCompilationOrder() + { + foreach (var id in Ids) + { + yield return _map[id]; + } + } + + public ImmutableArray SelectAsArray(Func selector) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + foreach (var (_, state) in _map) + { + builder.Add(selector(state)); + } + + return builder.ToImmutable(); + } + + public ImmutableArray SelectAsArray(Func selector, TArg arg) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + foreach (var (_, state) in _map) + { + builder.Add(selector(state, arg)); + } + + return builder.ToImmutable(); + } + + public async ValueTask> SelectAsArrayAsync(Func> selector, TArg arg, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + foreach (var (_, state) in _map) + { + builder.Add(await selector(state, arg, cancellationToken).ConfigureAwait(true)); + } + + return builder.ToImmutable(); + } + + public TextDocumentStates AddRange(ImmutableArray states) + => new(_ids.AddRange(states.Select(state => state.Id)), + _map.AddRange(states.Select(state => KeyValuePairUtil.Create(state.Id, state)))); + + public TextDocumentStates RemoveRange(ImmutableArray ids) + { + IEnumerable enumerableIds = ids; + return new(_ids.RemoveRange(enumerableIds), _map.RemoveRange(enumerableIds)); + } + + internal TextDocumentStates SetState(DocumentId id, TState state) + => new(_ids, _map.SetItem(id, state)); + + public TextDocumentStates UpdateStates(Func transformation, TArg arg) + { + var builder = _map.ToBuilder(); + + foreach (var (id, state) in _map) + { + builder[id] = transformation(state, arg); + } + + return new(_ids, builder.ToImmutable()); + } + + /// + /// Returns a s of documents whose state changed when compared to older states. + /// + public IEnumerable GetChangedStateIds(TextDocumentStates oldStates) + { + foreach (var (id, _) in _map) + { + if (oldStates._map.TryGetValue(id, out var oldState) && oldState != _map[id]) + { + yield return id; + } + } + } + + /// + /// Returns a s of added documents. + /// + public IEnumerable GetAddedStateIds(TextDocumentStates oldStates) + { + foreach (var (id, _) in _map) + { + if (!oldStates._map.ContainsKey(id)) + { + yield return id; + } + } + } + + /// + /// Returns a s of removed documents. + /// + public IEnumerable GetRemovedStateIds(TextDocumentStates oldStates) + { + foreach (var (id, _) in oldStates._map) + { + if (!_map.ContainsKey(id)) + { + yield return id; + } + } + } + + public bool HasAnyStateChanges(TextDocumentStates oldStates) + => !_map.Values.SequenceEqual(oldStates._map.Values); + + private sealed class DocumentIdComparer : IComparer + { + public static readonly IComparer Instance = new DocumentIdComparer(); + + private DocumentIdComparer() + { + } + + public int Compare(DocumentId? x, DocumentId? y) + { + if (x is null) + { + return y is null ? 0 : -1; + } + else if (y is null) + { + return 1; + } + + return x.Id.CompareTo(y.Id); + } + } + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Workspace.cs b/src/Workspaces/Core/Portable/Workspace/Workspace.cs index 623a380dfc742..98afa750bd81a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Workspace.cs +++ b/src/Workspaces/Core/Portable/Workspace/Workspace.cs @@ -1349,7 +1349,9 @@ private void CheckAllowedProjectChanges(ProjectChanges projectChanges) // Checking for unchangeable documents will only be done if we were asked not to ignore them. foreach (var documentId in changedDocumentIds) { - var document = projectChanges.OldProject.GetDocumentState(documentId) ?? projectChanges.NewProject.GetDocumentState(documentId)!; + var document = projectChanges.OldProject.State.DocumentStates.GetState(documentId) ?? + projectChanges.NewProject.State.DocumentStates.GetState(documentId)!; + if (!document.CanApplyChange()) { throw new NotSupportedException(string.Format(WorkspacesResources.Changing_document_0_is_not_supported, document.FilePath ?? document.Name)); diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index eec1376871995..5f4086951ad06 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -409,7 +409,7 @@ public void WithAnalyzerConfigDocumentText_SourceText() { using var workspace = CreateWorkspaceWithProjectAndDocuments(); var solution = workspace.CurrentSolution; - var documentId = solution.Projects.Single().State.AnalyzerConfigDocumentStates.Single().Key; + var documentId = solution.Projects.Single().AnalyzerConfigDocumentIds.Single(); var text = SourceText.From("new text"); var newSolution1 = solution.WithAnalyzerConfigDocumentText(documentId, text, PreservationMode.PreserveIdentity); @@ -431,7 +431,7 @@ public void WithAnalyzerConfigDocumentText_TextAndVersion() { using var workspace = CreateWorkspaceWithProjectAndDocuments(); var solution = workspace.CurrentSolution; - var documentId = solution.Projects.Single().State.AnalyzerConfigDocumentStates.Single().Key; + var documentId = solution.Projects.Single().AnalyzerConfigDocumentIds.Single(); var textAndVersion = TextAndVersion.Create(SourceText.From("new text"), VersionStamp.Default); var newSolution1 = solution.WithAnalyzerConfigDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity); @@ -499,7 +499,7 @@ public void WithAnalyzerConfigDocumentTextLoader() { using var workspace = CreateWorkspaceWithProjectAndDocuments(); var solution = workspace.CurrentSolution; - var documentId = solution.Projects.Single().State.AnalyzerConfigDocumentStates.Single().Key; + var documentId = solution.Projects.Single().AnalyzerConfigDocumentIds.Single(); var loader = new TestTextLoader("new text"); var newSolution1 = solution.WithAnalyzerConfigDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity); diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs index f833ce352917c..7489bf7c39b02 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs @@ -230,7 +230,7 @@ await _assetProvider.GetAssetAsync( { project = await UpdateDocumentsAsync( project, - project.State.DocumentStates.Values, + project.State.DocumentStates.States, oldProjectChecksums.Documents, newProjectChecksums.Documents, (solution, documents) => solution.AddDocuments(documents), @@ -242,7 +242,7 @@ await _assetProvider.GetAssetAsync( { project = await UpdateDocumentsAsync( project, - project.State.AdditionalDocumentStates.Values, + project.State.AdditionalDocumentStates.States, oldProjectChecksums.AdditionalDocuments, newProjectChecksums.AdditionalDocuments, (solution, documents) => solution.AddAdditionalDocuments(documents), @@ -254,7 +254,7 @@ await _assetProvider.GetAssetAsync( { project = await UpdateDocumentsAsync( project, - project.State.AnalyzerConfigDocumentStates.Values, + project.State.AnalyzerConfigDocumentStates.States, oldProjectChecksums.AnalyzerConfigDocuments, newProjectChecksums.AnalyzerConfigDocuments, (solution, documents) => solution.AddAnalyzerConfigDocuments(documents), diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/CompilerUtilities/ImmutableHashMapExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/CompilerUtilities/ImmutableHashMapExtensions.cs index f918dc5c83116..ce43083519e6e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/CompilerUtilities/ImmutableHashMapExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/CompilerUtilities/ImmutableHashMapExtensions.cs @@ -18,13 +18,14 @@ internal static class ImmutableHashMapExtensions /// The type of argument supplied to the value factory. /// The variable or field to atomically update if the specified is not in the dictionary. /// The key for the value to retrieve or add. - /// The function to execute to obtain the value to insert into the dictionary if the key is not found. + /// The function to execute to obtain the value to insert into the dictionary if the key is not found. Returns null if the value can't be obtained. /// The argument to pass to the value factory. - /// The value obtained from the dictionary or if it was not present. - public static TValue GetOrAdd(ref ImmutableHashMap location, TKey key, Func valueFactory, TArg factoryArgument) + /// The value obtained from the dictionary or if it was not present. + public static TValue? GetOrAdd(ref ImmutableHashMap location, TKey key, Func valueProvider, TArg factoryArgument) where TKey : notnull + where TValue : class { - Contract.ThrowIfNull(valueFactory); + Contract.ThrowIfNull(valueProvider); var map = Volatile.Read(ref location); Contract.ThrowIfNull(map); @@ -33,7 +34,11 @@ public static TValue GetOrAdd(ref ImmutableHashMap Date: Tue, 23 Feb 2021 17:57:33 -0800 Subject: [PATCH 2/2] Fix ordering --- ...nchmarks.IterateDocuments-report-github.md | 20 ++++++++++ ...tionBenchmarks.IterateDocuments-report.csv | 10 +++++ ...ionBenchmarks.IterateDocuments-report.html | 37 +++++++++++++++++++ .../Workspace/Solution/ProjectChanges.cs | 36 ++++++++++++++++-- .../Workspace/Solution/TextDocumentStates.cs | 2 +- 5 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report-github.md create mode 100644 BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.csv create mode 100644 BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.html diff --git a/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report-github.md b/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report-github.md new file mode 100644 index 0000000000000..e52041f78300c --- /dev/null +++ b/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report-github.md @@ -0,0 +1,20 @@ +``` ini + +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 +AMD Ryzen Threadripper 3960X, 1 CPU, 48 logical and 24 physical cores + [Host] : .NET Framework 4.8 (4.8.4300.0), X64 RyuJIT + DefaultJob : .NET Framework 4.8 (4.8.4300.0), X64 RyuJIT + + +``` +| Method | DocumentCount | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------------- |-------------- |----------------:|--------------:|--------------:|----------------:|-------:|-------:|------:|----------:| +| **Project.DocumentIds** | **0** | **23.34 ns** | **0.494 ns** | **0.891 ns** | **22.91 ns** | **-** | **-** | **-** | **-** | +| Project.Documents | 0 | 77.22 ns | 0.537 ns | 0.502 ns | 77.31 ns | 0.0772 | - | - | 128 B | +| Solution.WithDocumentText | 0 | 14.60 ns | 0.324 ns | 0.690 ns | 14.57 ns | - | - | - | - | +| **Project.DocumentIds** | **100** | **8,159.41 ns** | **10.113 ns** | **8.965 ns** | **8,157.79 ns** | **0.0305** | **-** | **-** | **72 B** | +| Project.Documents | 100 | 16,356.60 ns | 97.365 ns | 91.076 ns | 16,382.64 ns | 0.0916 | - | - | 201 B | +| Solution.WithDocumentText | 100 | 14,436.90 ns | 355.846 ns | 1,015.248 ns | 14,392.68 ns | 0.8240 | 0.2136 | - | 5285 B | +| **Project.DocumentIds** | **10000** | **863,988.25 ns** | **1,297.511 ns** | **1,213.693 ns** | **863,625.88 ns** | **-** | **-** | **-** | **80 B** | +| Project.Documents | 10000 | 1,861,933.16 ns | 11,538.506 ns | 10,793.125 ns | 1,859,064.65 ns | - | - | - | 208 B | +| Solution.WithDocumentText | 10000 | 14,088.55 ns | 281.041 ns | 453.828 ns | 14,003.49 ns | 0.8698 | 0.2289 | - | 5539 B | diff --git a/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.csv b/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.csv new file mode 100644 index 0000000000000..ea47e1cefa272 --- /dev/null +++ b/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.csv @@ -0,0 +1,10 @@ +Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,DocumentCount,Mean,Error,StdDev,Median,Gen 0,Gen 1,Gen 2,Allocated +Project.DocumentIds,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,0,23.34 ns,0.494 ns,0.891 ns,22.91 ns,0.0000,0.0000,0.0000,0 B +Project.Documents,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,0,77.22 ns,0.537 ns,0.502 ns,77.31 ns,0.0772,0.0000,0.0000,128 B +Solution.WithDocumentText,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,0,14.60 ns,0.324 ns,0.690 ns,14.57 ns,0.0000,0.0000,0.0000,0 B +Project.DocumentIds,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,100,"8,159.41 ns",10.113 ns,8.965 ns,"8,157.79 ns",0.0305,0.0000,0.0000,72 B +Project.Documents,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,100,"16,356.60 ns",97.365 ns,91.076 ns,"16,382.64 ns",0.0916,0.0000,0.0000,201 B +Solution.WithDocumentText,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,100,"14,436.90 ns",355.846 ns,"1,015.248 ns","14,392.68 ns",0.8240,0.2136,0.0000,5285 B +Project.DocumentIds,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10000,"863,988.25 ns","1,297.511 ns","1,213.693 ns","863,625.88 ns",0.0000,0.0000,0.0000,80 B +Project.Documents,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10000,"1,861,933.16 ns","11,538.506 ns","10,793.125 ns","1,859,064.65 ns",0.0000,0.0000,0.0000,208 B +Solution.WithDocumentText,DefaultJob,False,Default,Default,Default,Default,Default,Default,111111111111111111111111111111111111111111111111,Empty,RyuJit,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 4.8,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,1,Default,Default,Default,Default,Default,Default,Default,Default,16,Default,10000,"14,088.55 ns",281.041 ns,453.828 ns,"14,003.49 ns",0.8698,0.2289,0.0000,5539 B diff --git a/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.html b/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.html new file mode 100644 index 0000000000000..f30fbcc870850 --- /dev/null +++ b/BenchmarkDotNet.Artifacts/results/IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-report.html @@ -0,0 +1,37 @@ + + + + +IdeCoreBenchmarks.ProjectOperationBenchmarks.IterateDocuments-20210223-114823 + + + + +

+BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
+AMD Ryzen Threadripper 3960X, 1 CPU, 48 logical and 24 physical cores
+  [Host]     : .NET Framework 4.8 (4.8.4300.0), X64 RyuJIT
+  DefaultJob : .NET Framework 4.8 (4.8.4300.0), X64 RyuJIT
+
+
+ + + + + + + + + + + + + +
MethodDocumentCount Mean Error StdDev MedianGen 0Gen 1Gen 2Allocated
Project.DocumentIds023.34 ns0.494 ns0.891 ns22.91 ns----
Project.Documents077.22 ns0.537 ns0.502 ns77.31 ns0.0772--128 B
Solution.WithDocumentText014.60 ns0.324 ns0.690 ns14.57 ns----
Project.DocumentIds1008,159.41 ns10.113 ns8.965 ns8,157.79 ns0.0305--72 B
Project.Documents10016,356.60 ns97.365 ns91.076 ns16,382.64 ns0.0916--201 B
Solution.WithDocumentText10014,436.90 ns355.846 ns1,015.248 ns14,392.68 ns0.82400.2136-5285 B
Project.DocumentIds10000863,988.25 ns1,297.511 ns1,213.693 ns863,625.88 ns---80 B
Project.Documents100001,861,933.16 ns11,538.506 ns10,793.125 ns1,859,064.65 ns---208 B
Solution.WithDocumentText1000014,088.55 ns281.041 ns453.828 ns14,003.49 ns0.86980.2289-5539 B
+ + diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs index d330a91db9e57..ada357ed658d5 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs @@ -98,23 +98,33 @@ public IEnumerable GetRemovedAnalyzerReferences() } } + /// + /// Get s of added documents in the order they appear in of the . + /// public IEnumerable GetAddedDocuments() => _newProject.State.DocumentStates.GetAddedStateIds(_oldProject.State.DocumentStates); + /// + /// Get s of added dditional documents in the order they appear in of . + /// public IEnumerable GetAddedAdditionalDocuments() => _newProject.State.AdditionalDocumentStates.GetAddedStateIds(_oldProject.State.AdditionalDocumentStates); + /// + /// Get s of added analyzer config documents in the order they appear in of . + /// public IEnumerable GetAddedAnalyzerConfigDocuments() => _newProject.State.AnalyzerConfigDocumentStates.GetAddedStateIds(_oldProject.State.AnalyzerConfigDocumentStates); /// - /// Get Documents with any changes, including textual and non-textual changes + /// Get s of documents with any changes (textual and non-textual) + /// in the order they appear in of . /// public IEnumerable GetChangedDocuments() => GetChangedDocuments(onlyGetDocumentsWithTextChanges: false, ignoreUnchangeableDocuments: false); /// - /// Get changed documents. + /// Get changed documents in the order they appear in of . /// When is true, only get documents with text changes (we only check text source, not actual content); /// otherwise get documents with any changes i.e. , and file path. /// @@ -123,15 +133,16 @@ public IEnumerable GetChangedDocuments(bool onlyGetDocumentsWithText internal IEnumerable GetChangedDocuments(bool onlyGetDocumentsWithTextChanges, bool ignoreUnchangeableDocuments) { - foreach (var newState in _newProject.State.DocumentStates.States) + foreach (var id in _newProject.State.DocumentStates.Ids) { - var oldState = _oldProject.State.DocumentStates.GetState(newState.Id); + var oldState = _oldProject.State.DocumentStates.GetState(id); if (oldState == null) { // document was removed continue; } + var newState = _newProject.State.DocumentStates.GetRequiredState(id); if (newState == oldState) { continue; @@ -146,18 +157,35 @@ internal IEnumerable GetChangedDocuments(bool onlyGetDocumentsWithTe } } + /// + /// Get s of additional documents with any changes (textual and non-textual) + /// in the order they appear in of . + /// public IEnumerable GetChangedAdditionalDocuments() => _newProject.State.AdditionalDocumentStates.GetChangedStateIds(_oldProject.State.AdditionalDocumentStates); + /// + /// Get s of analyzer config documents with any changes (textual and non-textual) + /// in the order they appear in of . + /// public IEnumerable GetChangedAnalyzerConfigDocuments() => _newProject.State.AnalyzerConfigDocumentStates.GetChangedStateIds(_oldProject.State.AnalyzerConfigDocumentStates); + /// + /// Get s of removed documents in the order they appear in of . + /// public IEnumerable GetRemovedDocuments() => _newProject.State.DocumentStates.GetRemovedStateIds(_oldProject.State.DocumentStates); + /// + /// Get s of removed additional documents in the order they appear in of . + /// public IEnumerable GetRemovedAdditionalDocuments() => _newProject.State.AdditionalDocumentStates.GetRemovedStateIds(_oldProject.State.AdditionalDocumentStates); + /// + /// Get s of removed analyzer config documents in the order they appear in of . + /// public IEnumerable GetRemovedAnalyzerConfigDocuments() => _newProject.State.AnalyzerConfigDocumentStates.GetRemovedStateIds(_oldProject.State.AnalyzerConfigDocumentStates); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs index 71f2de0ce3488..94942fbbc5ac4 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs @@ -160,7 +160,7 @@ public TextDocumentStates UpdateStates(Func ///
public IEnumerable GetChangedStateIds(TextDocumentStates oldStates) { - foreach (var (id, _) in _map) + foreach (var id in _ids) { if (oldStates._map.TryGetValue(id, out var oldState) && oldState != _map[id]) {