From 6f8c1126854d16f18e2effcd672e0cc1c3c9769f Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 7 Jan 2016 11:28:16 -0800 Subject: [PATCH] Produce SymbolTreeInfo indices for projects and metadata in the background. Now, Add-Using will use the indices if they have been built. Or not search those projects/metadata if they're not available. Note: this doesn't change any of the searching of the current project. That works the same way it always has. This is only for searching unreleated projects and metadata dlls. --- src/EditorFeatures/Core/EditorFeatures.csproj | 1 - .../FileSystem/FileSystemCompletionHelper.cs | 1 + .../CSharpAddImportCodeFixProvider.cs | 5 + ...actAddImportCodeFixProvider.SearchScope.cs | 82 +++++-- .../AbstractAddImportCodeFixProvider.cs | 115 +++++----- src/Features/Core/Portable/Features.csproj | 1 + ...mbolTreeInfoIncrementalAnalyzerProvider.cs | 205 ++++++++++++++---- .../Portable/Shared/Utilities}/IOUtilities.cs | 2 +- .../VisualBasicAddImportCodeFixProvider.vb | 4 + .../GlobalAssemblyCacheCompletionHelper.cs | 1 + .../FindSymbols/SymbolFinder_Declarations.cs | 64 ++---- .../SymbolTree/ISymbolTreeInfoCacheService.cs | 13 ++ .../FindSymbols/SymbolTree/SymbolTreeInfo.cs | 77 +++++-- .../Core/Portable/Workspaces.csproj | 1 + 14 files changed, 400 insertions(+), 172 deletions(-) rename src/{EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem => Features/Core/Portable/Shared/Utilities}/IOUtilities.cs (90%) create mode 100644 src/Workspaces/Core/Portable/FindSymbols/SymbolTree/ISymbolTreeInfoCacheService.cs diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj index 206de118ccd..eea48f82675 100644 --- a/src/EditorFeatures/Core/EditorFeatures.csproj +++ b/src/EditorFeatures/Core/EditorFeatures.csproj @@ -407,7 +407,6 @@ - diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/FileSystemCompletionHelper.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/FileSystemCompletionHelper.cs index a523b88c5a8..8448a8805cb 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/FileSystemCompletionHelper.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/FileSystemCompletionHelper.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; diff --git a/src/Features/CSharp/Portable/CodeFixes/AddImport/CSharpAddImportCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/AddImport/CSharpAddImportCodeFixProvider.cs index 566f34fe4b0..18c98a4e9a9 100644 --- a/src/Features/CSharp/Portable/CodeFixes/AddImport/CSharpAddImportCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/AddImport/CSharpAddImportCodeFixProvider.cs @@ -143,6 +143,11 @@ public override ImmutableArray FixableDiagnosticIds } } + protected override Compilation CreateCompilation(PortableExecutableReference reference) + { + return CSharpCompilation.Create("TempAssembly", references: SpecializedCollections.SingletonEnumerable(reference)); + } + protected override bool CanAddImport(SyntaxNode node, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) diff --git a/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.SearchScope.cs b/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.SearchScope.cs index 71945a4099c..dcdd7101423 100644 --- a/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.SearchScope.cs +++ b/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.SearchScope.cs @@ -1,12 +1,15 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.FindSymbols.SymbolTree; +using Microsoft.CodeAnalysis.Host; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CodeFixes.AddImport @@ -61,28 +64,73 @@ public async Task>> FindDeclarationsAsync(stri } } - private class ProjectSearchScope : SearchScope + private abstract class ProjectSearchScope : SearchScope { - private readonly bool _includeDirectReferences; - private readonly Project _project; + protected readonly Project _project; - public ProjectSearchScope(Project project, bool includeDirectReferences, bool ignoreCase, CancellationToken cancellationToken) + public ProjectSearchScope(Project project, bool ignoreCase, CancellationToken cancellationToken) : base(ignoreCase, cancellationToken) { _project = project; - _includeDirectReferences = includeDirectReferences; + } + + public override SymbolReference CreateReference(SearchResult searchResult) + { + return new ProjectSymbolReference( + searchResult.WithSymbol(searchResult.Symbol), _project.Id); + } + } + + private class ProjectAndDirectReferencesSearchScope : ProjectSearchScope + { + public ProjectAndDirectReferencesSearchScope(Project project, bool ignoreCase, CancellationToken cancellationToken) + : base(project, ignoreCase, cancellationToken) + { } protected override Task> FindDeclarationsAsync(string name, SymbolFilter filter, SearchQuery searchQuery) { - return SymbolFinder.FindDeclarationsAsync( - _project, searchQuery, filter, _includeDirectReferences, cancellationToken); + return SymbolFinder.FindDeclarationsAsync(_project, searchQuery, filter, cancellationToken); } + } - public override SymbolReference CreateReference(SearchResult searchResult) + private class ProjectSourceOnlySearchScope : ProjectSearchScope + { + private readonly ConcurrentDictionary> _projectToAssembly; + + public ProjectSourceOnlySearchScope( + ConcurrentDictionary> projectToAssembly, + Project project, bool ignoreCase, CancellationToken cancellationToken) + : base(project, ignoreCase, cancellationToken) { - return new ProjectSymbolReference( - searchResult.WithSymbol(searchResult.Symbol), _project.Id); + _projectToAssembly = projectToAssembly; + } + + protected override async Task> FindDeclarationsAsync(string name, SymbolFilter filter, SearchQuery searchQuery) + { + var service = _project.Solution.Workspace.Services.GetService(); + var result = await service.TryGetSymbolTreeInfoAsync(_project, cancellationToken).ConfigureAwait(false); + if (!result.Item1) + { + return SpecializedCollections.EmptyEnumerable(); + } + + // Don't create the assembly until it is actually needed by the SymbolTreeInfo.FindAsync + // code. Creating the assembly can be costly and we want to avoid it until it is actually + // needed. + var lazyAssembly = _projectToAssembly.GetOrAdd(_project, CreateLazyAssembly); + + return await result.Item2.FindAsync(searchQuery, lazyAssembly, cancellationToken).ConfigureAwait(false); + } + + private static AsyncLazy CreateLazyAssembly(Project project) + { + return new AsyncLazy( + async c => + { + var compilation = await project.GetCompilationAsync(c).ConfigureAwait(false); + return compilation.Assembly; + }, cacheResult: true); } } @@ -112,11 +160,17 @@ public override SymbolReference CreateReference(SearchResult searchResult) _metadataReference); } - protected override Task> FindDeclarationsAsync(string name, SymbolFilter filter, SearchQuery searchQuery) + protected override async Task> FindDeclarationsAsync(string name, SymbolFilter filter, SearchQuery searchQuery) { - return SymbolFinder.FindDeclarationsAsync( - _solution, _assembly, _metadataReference, searchQuery, filter, cancellationToken); + var service = _solution.Workspace.Services.GetService(); + var result = await service.TryGetSymbolTreeInfoAsync(_metadataReference, cancellationToken).ConfigureAwait(false); + if (!result.Item1) + { + return SpecializedCollections.EmptyEnumerable(); + } + + return await result.Item2.FindAsync(searchQuery, _assembly, cancellationToken).ConfigureAwait(false); } } } -} +} \ No newline at end of file diff --git a/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.cs b/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.cs index fa12068ca19..f6555b411f3 100644 --- a/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/AddImport/AbstractAddImportCodeFixProvider.cs @@ -1,20 +1,18 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Options; using Roslyn.Utilities; -using static Roslyn.Utilities.PortableShim; namespace Microsoft.CodeAnalysis.CodeFixes.AddImport { @@ -38,6 +36,8 @@ internal abstract partial class AbstractAddImportCodeFixProvider>(); + var referenceToCompilation = new ConcurrentDictionary(); + // Look for exact matches first: - await FindResults(project, allSymbolReferences, finder, exact: true, cancellationToken: cancellationToken).ConfigureAwait(false); + await FindResults(projectToAssembly, referenceToCompilation, project, allSymbolReferences, finder, exact: true, cancellationToken: cancellationToken).ConfigureAwait(false); if (allSymbolReferences.Count == 0) { // No exact matches found. Fall back to fuzzy searching. - await FindResults(project, allSymbolReferences, finder, exact: false, cancellationToken: cancellationToken).ConfigureAwait(false); + await FindResults(projectToAssembly, referenceToCompilation, project, allSymbolReferences, finder, exact: false, cancellationToken: cancellationToken).ConfigureAwait(false); } // Nothing found at all. No need to proceed. @@ -106,17 +111,21 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } } - private async Task FindResults(Project project, List allSymbolReferences, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) + private async Task FindResults( + ConcurrentDictionary> projectToAssembly, + ConcurrentDictionary referenceToCompilation, + Project project, List allSymbolReferences, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) { await FindResultsInCurrentProject(project, allSymbolReferences, finder, exact).ConfigureAwait(false); - await FindResultsInUnreferencedProjects(project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false); - await FindResultsInUnreferencedMetadataReferences(project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false); + await FindResultsInUnreferencedProjects(projectToAssembly, project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false); + await FindResultsInUnreferencedMetadataReferences(referenceToCompilation, project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false); } private async Task FindResultsInCurrentProject( Project project, List allSymbolReferences, SymbolReferenceFinder finder, bool exact) { - AddRange(allSymbolReferences, await finder.FindInProjectAsync(project, includeDirectReferences: true, exact: exact).ConfigureAwait(false)); + var references = await finder.FindInProjectAndDirectReferencesAsync(project, exact).ConfigureAwait(false); + AddRange(allSymbolReferences, references); } private async Task AddImportAndReferenceAsync( @@ -160,6 +169,7 @@ private async Task FindResults(Project project, List allSymbolR } private async Task FindResultsInUnreferencedProjects( + ConcurrentDictionary> projectToAssembly, Project project, List allSymbolReferences, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) { // If we didn't find enough hits searching just in the project, then check @@ -176,7 +186,7 @@ private async Task FindResults(Project project, List allSymbolR // direct references. i.e. we don't want to search in its metadata references // or in the projects it references itself. We'll be searching those entities // individually. - AddRange(allSymbolReferences, await finder.FindInProjectAsync(unreferencedProject, includeDirectReferences: false, exact: exact).ConfigureAwait(false)); + AddRange(allSymbolReferences, await finder.FindInProjectSourceOnlyAsync(projectToAssembly, unreferencedProject, exact: exact).ConfigureAwait(false)); if (allSymbolReferences.Count >= MaxResults) { return; @@ -185,7 +195,9 @@ private async Task FindResults(Project project, List allSymbolR } private async Task FindResultsInUnreferencedMetadataReferences( - Project project, List allSymbolReferences, SymbolReferenceFinder finder, bool exact, CancellationToken cancellationToken) + ConcurrentDictionary referenceToCompilation, + Project project, List allSymbolReferences, SymbolReferenceFinder finder, bool exact, + CancellationToken cancellationToken) { if (allSymbolReferences.Count > 0) { @@ -201,54 +213,44 @@ private async Task FindResults(Project project, List allSymbolR var seenReferences = new HashSet(comparer: this); seenReferences.AddAll(project.MetadataReferences.OfType()); - // Check all the other projects in the system so see if they have a metadata reference - // with a potential result. - foreach (var otherProject in project.Solution.Projects) + var newReferences = + project.Solution.Projects.Where(p => p != project) + .SelectMany(p => p.MetadataReferences.OfType()) + .Distinct(comparer: this) + .Where(r => !seenReferences.Contains(r)); + + // Search all metadata references in parallel. + var findTasks = new HashSet>>(); + + foreach (var reference in newReferences) { - if (otherProject == project) - { - continue; - } + var compilation = referenceToCompilation.GetOrAdd(reference, CreateCompilation); - await FindResultsInMetadataReferences( - otherProject, allSymbolReferences, finder, seenReferences, exact, cancellationToken).ConfigureAwait(false); - if (allSymbolReferences.Count >= MaxResults) + // Ignore netmodules. First, they're incredibly esoteric and barely used. + // Second, the SymbolFinder api doesn't even support searching them. + var assembly = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; + if (assembly != null) { - break; + findTasks.Add(finder.FindInMetadataAsync(project.Solution, assembly, reference, exact)); } } - } - private async Task FindResultsInMetadataReferences( - Project otherProject, - List allSymbolReferences, - SymbolReferenceFinder finder, - HashSet seenReferences, - bool exact, - CancellationToken cancellationToken) - { - // See if this project has a metadata reference we haven't already looked at. - var newMetadataReferences = otherProject.MetadataReferences.OfType(); - - Compilation compilation = null; - foreach (var reference in newMetadataReferences) + while (findTasks.Count > 0) { - // Make sure we don't check the same metadata reference multiple times from - // different projects. - if (seenReferences.Add(reference)) - { - // Defer making the compilation until necessary. - compilation = compilation ?? await otherProject.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + // Keep on looping through the 'find' tasks, processing each when they finish. + cancellationToken.ThrowIfCancellationRequested(); + var doneTask = await Task.WhenAny(findTasks).ConfigureAwait(false); - // Ignore netmodules. First, they're incredibly esoteric and barely used. - // Second, the SymbolFinder api doesn't even support searching them. - var assembly = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; - if (assembly != null) - { - AddRange(allSymbolReferences, await finder.FindInMetadataAsync(otherProject.Solution, assembly, reference, exact).ConfigureAwait(false)); - } - } + // One of the tasks finished. Remove it from the list we're waiting on. + findTasks.Remove(doneTask); + + // Add its results to the final result set we're keeping. + AddRange(allSymbolReferences, await doneTask.ConfigureAwait(false)); + // If we've got enough, no need to keep searching. + // Note: Should we cancel the existing work? IMO, no. These tasks will + // cause our indices to be created if necessary. And that's good for future searches. + // If the indices are already created, then searching them should be quick. if (allSymbolReferences.Count >= MaxResults) { break; @@ -378,9 +380,18 @@ private class SymbolReferenceFinder _syntaxFacts = document.Project.LanguageServices.GetService(); } - internal Task> FindInProjectAsync(Project project, bool includeDirectReferences, bool exact) + internal Task> FindInProjectAndDirectReferencesAsync( + Project project, bool exact) + { + var searchScope = new ProjectAndDirectReferencesSearchScope(project, exact, _cancellationToken); + return DoAsync(searchScope); + } + + internal Task> FindInProjectSourceOnlyAsync( + ConcurrentDictionary> projectToAssembly, + Project project, bool exact) { - var searchScope = new ProjectSearchScope(project, includeDirectReferences, exact, _cancellationToken); + var searchScope = new ProjectSourceOnlySearchScope(projectToAssembly, project, exact, _cancellationToken); return DoAsync(searchScope); } diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 3cfcc45b0a9..f9c810a26a8 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -255,6 +255,7 @@ + diff --git a/src/Features/Core/Portable/IncrementalCaches/SymbolTreeInfoIncrementalAnalyzerProvider.cs b/src/Features/Core/Portable/IncrementalCaches/SymbolTreeInfoIncrementalAnalyzerProvider.cs index 43d6a1acf53..a2236bd8885 100644 --- a/src/Features/Core/Portable/IncrementalCaches/SymbolTreeInfoIncrementalAnalyzerProvider.cs +++ b/src/Features/Core/Portable/IncrementalCaches/SymbolTreeInfoIncrementalAnalyzerProvider.cs @@ -1,100 +1,215 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.FindSymbols.SymbolTree; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.SolutionCrawler; using Roslyn.Utilities; +using static Roslyn.Utilities.PortableShim; namespace Microsoft.CodeAnalysis.IncrementalCaches { - [ExportIncrementalAnalyzerProvider(WorkspaceKind.Host), Shared] - internal class SymbolTreeInfoIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider + [Shared] + [ExportIncrementalAnalyzerProvider(WorkspaceKind.Host)] + [ExportWorkspaceServiceFactory(typeof(ISymbolTreeInfoCacheService))] + internal class SymbolTreeInfoIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider, IWorkspaceServiceFactory { + // Concurrent dictionaries so they can be read from the SymbolTreeInfoCacheService while + // they are being populated/updated by the IncrementalAnalyzer. + private readonly ConcurrentDictionary> _projectToInfo = + new ConcurrentDictionary>(); + private readonly ConcurrentDictionary>> _metadataPathToInfo = + new ConcurrentDictionary>>(); + public IIncrementalAnalyzer CreateIncrementalAnalyzer(Workspace workspace) { - return new IncrementalAnalyzer(); + return new IncrementalAnalyzer(_projectToInfo, _metadataPathToInfo); } - private class IncrementalAnalyzer : IncrementalAnalyzerBase + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) { - private SolutionId _solutionId; - private Dictionary _symbolCountByProjectMap = new Dictionary(); + return new SymbolTreeInfoCacheService(_projectToInfo, _metadataPathToInfo); + } - public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken) + private static string GetReferenceKey(PortableExecutableReference reference) + { + return reference.FilePath ?? reference.Display; + } + + private static ValueTuple GetLastWriteTime(string path) + { + return IOUtilities.PerformIO( + () => ValueTuple.Create(true, File.GetLastWriteTimeUtc(path)), + ValueTuple.Create(false, default(DateTime))); + } + + private class SymbolTreeInfoCacheService : ISymbolTreeInfoCacheService + { + private readonly ConcurrentDictionary> _projectToInfo; + private readonly ConcurrentDictionary>> _metadataPathToInfo; + + public SymbolTreeInfoCacheService( + ConcurrentDictionary> projectToInfo, + ConcurrentDictionary>> metadataPathToInfo) + { + _projectToInfo = projectToInfo; + _metadataPathToInfo = metadataPathToInfo; + } + + public Task> TryGetSymbolTreeInfoAsync(PortableExecutableReference reference, CancellationToken cancellationToken) { - if (_symbolCountByProjectMap == null || !project.SupportsCompilation || !semanticsChanged) + var key = GetReferenceKey(reference); + if (key != null) { - return; + Tuple> tuple; + if (_metadataPathToInfo.TryGetValue(key, out tuple)) + { + var version = GetLastWriteTime(key); + if (version.Item1 && version.Item2 == tuple.Item1) + { + return Task.FromResult(ValueTuple.Create(true, tuple.Item2)); + } + } } - // we do this just to report total symbol numbers - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + return Task.FromResult(default(ValueTuple)); + } - var info = SymbolTreeInfo.Create(VersionStamp.Default, compilation.Assembly, cancellationToken); - if (info != null) + public async Task> TryGetSymbolTreeInfoAsync(Project project, CancellationToken cancellationToken) + { + Tuple tuple; + if (_projectToInfo.TryGetValue(project.Id, out tuple)) { - RecordCount(project.Id, info.Count); + var version = await project.GetSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + if (version == tuple.Item1) + { + return ValueTuple.Create(true, tuple.Item2); + } } + + return default(ValueTuple); + } + } + + private class IncrementalAnalyzer : IncrementalAnalyzerBase + { + private readonly ConcurrentDictionary> _projectToInfo; + + // Note: the Incremental-Analyzer infrastructure guarantees that it will call all the methods + // on this type in a serial fashion. As such, we don't need explicit locking, or threadsafe + // collections (if they're only used by this type). So, for example, the map we populate + // needs to be a ConcurrentDictionary as it will be read and written from multiple types. + // However, the HashSet is ok as it will only be used by this type and there is + // no concurrency in this type on its own. + private readonly ConcurrentDictionary>> _metadataPathToInfo; + + public IncrementalAnalyzer( + ConcurrentDictionary> projectToInfo, + ConcurrentDictionary>> metadataPathToInfo) + { + _projectToInfo = projectToInfo; + _metadataPathToInfo = metadataPathToInfo; } - public override Task NewSolutionSnapshotAsync(Solution solution, CancellationToken cancellationToken) + public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken) { - // check whether we are good to report total symbol numbers - if (_symbolCountByProjectMap == null || _symbolCountByProjectMap.Count < solution.ProjectIds.Count || string.IsNullOrEmpty(solution.FilePath)) + if (!project.SupportsCompilation || project.FilePath == null) { - return SpecializedTasks.EmptyTask; + return; } - if (_solutionId != null && _solutionId != solution.Id) + await UpdateReferencesAync(project, cancellationToken).ConfigureAwait(false); + + var version = await project.GetSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + Tuple tuple; + if (_projectToInfo.TryGetValue(project.Id, out tuple) && tuple.Item1 == version) { - ReportCount(); - return SpecializedTasks.EmptyTask; + return; } - _solutionId = solution.Id; - foreach (var projectId in solution.ProjectIds) + var info = await SymbolTreeInfo.GetInfoForSourceAssemblyAsync(project, cancellationToken).ConfigureAwait(false); + tuple = Tuple.Create(version, info); + _projectToInfo.AddOrUpdate(project.Id, tuple, (_1, _2) => tuple); + } + + private async Task UpdateReferencesAync(Project project, CancellationToken cancellationToken) + { + Compilation compilation = null; + foreach (var reference in project.MetadataReferences.OfType()) { - if (!_symbolCountByProjectMap.ContainsKey(projectId)) - { - return SpecializedTasks.EmptyTask; - } + compilation = await UpdateReferenceAsync(project, reference, compilation, cancellationToken).ConfigureAwait(false); } - - ReportCount(); - return SpecializedTasks.EmptyTask; } - public override void RemoveProject(ProjectId projectId) + private async Task UpdateReferenceAsync( + Project project, PortableExecutableReference reference, Compilation compilation, CancellationToken cancellationToken) { - if (_symbolCountByProjectMap != null) + var key = GetReferenceKey(reference); + if (key != null) { - _symbolCountByProjectMap.Remove(projectId); + var lastWriteTime = GetLastWriteTime(key); + if (!lastWriteTime.Item1) + { + // Couldn't get the write time. Just ignore this reference. + return compilation; + } + + Tuple> tuple; + if (_metadataPathToInfo.TryGetValue(key, out tuple) && tuple.Item1 == lastWriteTime.Item2) + { + // We've already computed and cached the info for this reference. + return compilation; + } + + compilation = compilation ?? await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var assembly = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; + if (assembly != null) + { + var info = await SymbolTreeInfo.TryGetInfoForMetadataAssemblyAsync(project.Solution, assembly, reference, cancellationToken).ConfigureAwait(false); + tuple = tuple ?? Tuple.Create(lastWriteTime.Item2, info, new HashSet()); + + // Keep track that this dll is referenced by this project. + tuple.Item3.Add(project.Id); + + _metadataPathToInfo.AddOrUpdate(key, tuple, (_1, _2) => tuple); + } } + + return compilation; } - private void ReportCount() + public override void RemoveProject(ProjectId projectId) { - var sourceSymbolCount = _symbolCountByProjectMap.Sum(kv => kv.Value); - Logger.Log(FunctionId.Run_Environment, KeyValueLogMessage.Create(m => m["SourceSymbolCount"] = sourceSymbolCount)); + Tuple tuple; + _projectToInfo.TryRemove(projectId, out tuple); - // we only report it once - _symbolCountByProjectMap = null; - _solutionId = null; + RemoveMetadataReferences(projectId); } - private void RecordCount(ProjectId id, int count) + private void RemoveMetadataReferences(ProjectId projectId) { - if (_symbolCountByProjectMap == null) + foreach (var kvp in _metadataPathToInfo.ToArray()) { - return; + var tuple = kvp.Value; + if (kvp.Value.Item3.Remove(projectId)) + { + if (kvp.Value.Item3.Count == 0) + { + // This metadata dll isn't referenced by any project. We can just dump it. + Tuple> unneeded; + _metadataPathToInfo.TryRemove(kvp.Key, out unneeded); + } + } } - - _symbolCountByProjectMap[id] = count; } } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/IOUtilities.cs b/src/Features/Core/Portable/Shared/Utilities/IOUtilities.cs similarity index 90% rename from src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/IOUtilities.cs rename to src/Features/Core/Portable/Shared/Utilities/IOUtilities.cs index dbc2ce06d75..edf636808c4 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/Completion/FileSystem/IOUtilities.cs +++ b/src/Features/Core/Portable/Shared/Utilities/IOUtilities.cs @@ -4,7 +4,7 @@ using System.IO; using System.Security; -namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion.FileSystem +namespace Microsoft.CodeAnalysis.Shared.Utilities { internal static class IOUtilities { diff --git a/src/Features/VisualBasic/Portable/CodeFixes/AddImport/VisualBasicAddImportCodeFixProvider.vb b/src/Features/VisualBasic/Portable/CodeFixes/AddImport/VisualBasicAddImportCodeFixProvider.vb index bf937ccce44..80ade99da42 100644 --- a/src/Features/VisualBasic/Portable/CodeFixes/AddImport/VisualBasicAddImportCodeFixProvider.vb +++ b/src/Features/VisualBasic/Portable/CodeFixes/AddImport/VisualBasicAddImportCodeFixProvider.vb @@ -97,6 +97,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.CodeFixes.AddImport End Get End Property + Protected Overrides Function CreateCompilation(reference As PortableExecutableReference) As Compilation + Return VisualBasicCompilation.Create("TempAssembly", references:=SpecializedCollections.SingletonEnumerable(reference)) + End Function + Protected Overrides Function CanAddImport(node As SyntaxNode, cancellationToken As CancellationToken) As Boolean If node.GetAncestor(Of ImportsStatementSyntax)() IsNot Nothing Then Return False diff --git a/src/Interactive/EditorFeatures/Core/Completion/GlobalAssemblyCacheCompletionHelper.cs b/src/Interactive/EditorFeatures/Core/Completion/GlobalAssemblyCacheCompletionHelper.cs index f6bc1362a47..57c8c1f73b8 100644 --- a/src/Interactive/EditorFeatures/Core/Completion/GlobalAssemblyCacheCompletionHelper.cs +++ b/src/Interactive/EditorFeatures/Core/Completion/GlobalAssemblyCacheCompletionHelper.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion.FileSystem; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Declarations.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Declarations.cs index d3061379a77..138666d96e4 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Declarations.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Declarations.cs @@ -126,13 +126,13 @@ public static Task> FindDeclarationsAsync(Project project, return SpecializedTasks.EmptyEnumerable(); } - return FindDeclarationsAsync(project, SearchQuery.Create(name, ignoreCase), includeDirectReferences: true, cancellationToken: cancellationToken); + return FindDeclarationsAsync(project, SearchQuery.Create(name, ignoreCase), cancellationToken: cancellationToken); } internal static Task> FindDeclarationsAsync( - Project project, SearchQuery query, bool includeDirectReferences, CancellationToken cancellationToken) + Project project, SearchQuery query, CancellationToken cancellationToken) { - return FindDeclarationsAsync(project, query, SymbolFilter.All, includeDirectReferences, cancellationToken); + return FindDeclarationsAsync(project, query, SymbolFilter.All, cancellationToken); } /// @@ -151,11 +151,11 @@ public static Task> FindDeclarationsAsync(Project project, return SpecializedTasks.EmptyEnumerable(); } - return FindDeclarationsAsync(project, SearchQuery.Create(name, ignoreCase), filter, includeDirectReferences: true, cancellationToken: cancellationToken); + return FindDeclarationsAsync(project, SearchQuery.Create(name, ignoreCase), filter, cancellationToken: cancellationToken); } internal static Task> FindDeclarationsAsync( - Project project, SearchQuery query, SymbolFilter filter, bool includeDirectReferences, CancellationToken cancellationToken) + Project project, SearchQuery query, SymbolFilter filter, CancellationToken cancellationToken) { if (project == null) { @@ -169,12 +169,12 @@ public static Task> FindDeclarationsAsync(Project project, using (Logger.LogBlock(FunctionId.SymbolFinder_FindDeclarationsAsync, cancellationToken)) { - return FindDeclarationsAsyncImpl(project, query, filter, includeDirectReferences, cancellationToken); + return FindDeclarationsAsyncImpl(project, query, filter, cancellationToken); } } private static async Task> FindDeclarationsAsyncImpl( - Project project, SearchQuery query, SymbolFilter criteria, bool includeDirectReferences, CancellationToken cancellationToken) + Project project, SearchQuery query, SymbolFilter criteria, CancellationToken cancellationToken) { var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); @@ -184,21 +184,18 @@ public static Task> FindDeclarationsAsync(Project project, await AddDeclarationsAsync(project, query, criteria, list, cancellationToken).ConfigureAwait(false); // get declarations from directly referenced projects and metadata - if (includeDirectReferences) + foreach (var assembly in compilation.GetReferencedAssemblySymbols()) { - foreach (var assembly in compilation.GetReferencedAssemblySymbols()) + var assemblyProject = project.Solution.GetProject(assembly, cancellationToken); + if (assemblyProject != null) { - var assemblyProject = project.Solution.GetProject(assembly, cancellationToken); - if (assemblyProject != null) - { - await AddDeclarationsAsync(assemblyProject, query, criteria, list, compilation, assembly, cancellationToken).ConfigureAwait(false); - } - else - { - await AddDeclarationsAsync( - project.Solution, assembly, compilation.GetMetadataReference(assembly) as PortableExecutableReference, - query, criteria, list, cancellationToken).ConfigureAwait(false); - } + await AddDeclarationsAsync(assemblyProject, query, criteria, list, compilation, assembly, cancellationToken).ConfigureAwait(false); + } + else + { + await AddDeclarationsAsync( + project.Solution, assembly, compilation.GetMetadataReference(assembly) as PortableExecutableReference, + query, criteria, list, cancellationToken).ConfigureAwait(false); } } @@ -291,35 +288,16 @@ private static IEnumerable TranslateNamespaces(List symbols, C { if (referenceOpt != null) { - var info = await SymbolTreeInfo.TryGetInfoForAssemblyAsync(solution, assembly, referenceOpt, cancellationToken).ConfigureAwait(false); + var info = await SymbolTreeInfo.TryGetInfoForMetadataAssemblyAsync(solution, assembly, referenceOpt, cancellationToken).ConfigureAwait(false); if (info != null) { - list.AddRange(FilterByCriteria(Find(query, info, assembly, cancellationToken), filter)); + var symbols = await info.FindAsync(query, assembly, cancellationToken).ConfigureAwait(false); + list.AddRange(FilterByCriteria(symbols, filter)); } } } } - private static IEnumerable Find(SearchQuery query, SymbolTreeInfo info, IAssemblySymbol assembly, CancellationToken cancellationToken) - { - // If the query has a specific string provided, then call into the SymbolTreeInfo - // helpers optimized for lookup based on an exact name. - switch (query.Kind) - { - case SearchKind.Exact: - return info.Find(assembly, query.Name, ignoreCase: false, cancellationToken: cancellationToken); - case SearchKind.ExactIgnoreCase: - return info.Find(assembly, query.Name, ignoreCase: true, cancellationToken: cancellationToken); - case SearchKind.Fuzzy: - return info.FuzzyFind(assembly, query.Name, cancellationToken); - case SearchKind.Custom: - // Otherwise, we'll have to do a slow linear search over all possible symbols. - return info.Find(assembly, query.GetPredicate(), cancellationToken); - } - - throw new InvalidOperationException(); - } - /// /// Find the symbols for declarations made in source with the specified name. /// @@ -500,7 +478,7 @@ internal static async Task> FindSourceDeclarationsAsync(Pro } } - private static IEnumerable FilterByCriteria(IEnumerable symbols, SymbolFilter criteria) + internal static IEnumerable FilterByCriteria(IEnumerable symbols, SymbolFilter criteria) { foreach (var symbol in symbols) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/ISymbolTreeInfoCacheService.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/ISymbolTreeInfoCacheService.cs new file mode 100644 index 00000000000..707670ee3e8 --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/ISymbolTreeInfoCacheService.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.FindSymbols.SymbolTree +{ + internal interface ISymbolTreeInfoCacheService : IWorkspaceService + { + Task> TryGetSymbolTreeInfoAsync(Project project, CancellationToken cancellationToken); + Task> TryGetSymbolTreeInfoAsync(PortableExecutableReference reference, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs index 96c93e3e20a..cb85ed1ded0 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo.cs @@ -54,55 +54,92 @@ private SymbolTreeInfo(VersionStamp version, IReadOnlyList orderedNodes, S public int Count => _nodes.Count; + public Task> FindAsync(SearchQuery query, IAssemblySymbol assembly, CancellationToken cancellationToken) + { + return FindAsync(query, new AsyncLazy(assembly), cancellationToken); + } + + public Task> FindAsync(SearchQuery query, AsyncLazy lazyAssembly, CancellationToken cancellationToken) + { + // If the query has a specific string provided, then call into the SymbolTreeInfo + // helpers optimized for lookup based on an exact name. + switch (query.Kind) + { + case SearchKind.Exact: + return this.FindAsync(lazyAssembly, query.Name, ignoreCase: false, cancellationToken: cancellationToken); + case SearchKind.ExactIgnoreCase: + return this.FindAsync(lazyAssembly, query.Name, ignoreCase: true, cancellationToken: cancellationToken); + case SearchKind.Fuzzy: + return this.FuzzyFindAsync(lazyAssembly, query.Name, cancellationToken); + case SearchKind.Custom: + // Otherwise, we'll have to do a slow linear search over all possible symbols. + return this.FindAsync(lazyAssembly, query.GetPredicate(), cancellationToken); + } + + throw new InvalidOperationException(); + } + /// /// Finds symbols in this assembly that match the provided name in a fuzzy manner. /// - public IEnumerable FuzzyFind(IAssemblySymbol assembly, string name, CancellationToken cancellationToken) + public async Task> FuzzyFindAsync(AsyncLazy lazyAssembly, string name, CancellationToken cancellationToken) { var similarNames = _spellChecker.FindSimilarWords(name); - return similarNames.SelectMany(n => Find(assembly, n, ignoreCase: true, cancellationToken: cancellationToken)); + var result = new List(); + + foreach (var similarName in similarNames) + { + var symbols = await FindAsync(lazyAssembly, similarName, ignoreCase: true, cancellationToken: cancellationToken).ConfigureAwait(false); + result.AddRange(symbols); + } + + return result; } /// /// Get all symbols that have a name matching the specified name. /// - public IEnumerable Find( - IAssemblySymbol assembly, + public async Task> FindAsync( + AsyncLazy lazyAssembly, string name, bool ignoreCase, CancellationToken cancellationToken) { var comparer = GetComparer(ignoreCase); + var result = new List(); + IAssemblySymbol assemblySymbol = null; foreach (var node in FindNodes(name, comparer)) { cancellationToken.ThrowIfCancellationRequested(); - foreach (var symbol in Bind(node, assembly.GlobalNamespace, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return symbol; - } + assemblySymbol = assemblySymbol ?? await lazyAssembly.GetValueAsync(cancellationToken).ConfigureAwait(false); + + result.AddRange(Bind(node, assemblySymbol.GlobalNamespace, cancellationToken)); } + + return result; } /// /// Slow, linear scan of all the symbols in this assembly to look for matches. /// - public IEnumerable Find(IAssemblySymbol assembly, Func predicate, CancellationToken cancellationToken) + public async Task> FindAsync(AsyncLazy lazyAssembly, Func predicate, CancellationToken cancellationToken) { + var result = new List(); + IAssemblySymbol assembly = null; for (int i = 0, n = _nodes.Count; i < n; i++) { cancellationToken.ThrowIfCancellationRequested(); var node = _nodes[i]; if (predicate(node.Name)) { - foreach (var symbol in Bind(i, assembly.GlobalNamespace, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return symbol; - } + assembly = assembly ?? await lazyAssembly.GetValueAsync(cancellationToken).ConfigureAwait(false); + + result.AddRange(Bind(i, assembly.GlobalNamespace, cancellationToken)); } } + + return result; } private static StringComparer GetComparer(bool ignoreCase) @@ -199,7 +236,7 @@ private int BinarySearch(string name) /// /// this gives you SymbolTreeInfo for a metadata /// - public static async Task TryGetInfoForAssemblyAsync(Solution solution, IAssemblySymbol assembly, PortableExecutableReference reference, CancellationToken cancellationToken) + public static async Task TryGetInfoForMetadataAssemblyAsync(Solution solution, IAssemblySymbol assembly, PortableExecutableReference reference, CancellationToken cancellationToken) { var metadata = assembly.GetMetadata(); if (metadata == null) @@ -225,6 +262,14 @@ public static async Task TryGetInfoForAssemblyAsync(Solution sol } } + public static async Task GetInfoForSourceAssemblyAsync( + Project project, CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + return await LoadOrCreateAsync(project.Solution, compilation.Assembly, project.FilePath, cancellationToken).ConfigureAwait(false); + } + internal static SymbolTreeInfo Create(VersionStamp version, IAssemblySymbol assembly, CancellationToken cancellationToken) { if (assembly == null) diff --git a/src/Workspaces/Core/Portable/Workspaces.csproj b/src/Workspaces/Core/Portable/Workspaces.csproj index 561f10c4f30..c0f8d4e23e9 100644 --- a/src/Workspaces/Core/Portable/Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Workspaces.csproj @@ -383,6 +383,7 @@ + -- GitLab