提交 8b8d7ec5 编写于 作者: C CyrusNajmabadi

Merge pull request #8675 from CyrusNajmabadi/addUsingPerf2

 Asynchronously produce the SpellChecker used for fuzzy matching.
......@@ -223,7 +223,7 @@ End Namespace
End Function
<Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddImport)>
Public Async Function TestAddProjectReference_CSharpToCSharp() As Task
Public Async Function AddProjectReference_CSharpToCSharp_Test() As Task
Dim input =
<Workspace>
<Project Language='C#' AssemblyName='CSAssembly1' CommonReferences='true'>
......
......@@ -177,7 +177,8 @@ public override SymbolReference CreateReference<T>(SearchResult<T> searchResult)
_metadataReference);
}
protected override async Task<IEnumerable<ISymbol>> FindDeclarationsAsync(string name, SymbolFilter filter, SearchQuery searchQuery)
protected override async Task<IEnumerable<ISymbol>> FindDeclarationsAsync(
string name, SymbolFilter filter, SearchQuery searchQuery)
{
var service = _solution.Workspace.Services.GetService<ISymbolTreeInfoCacheService>();
var info = await service.TryGetSymbolTreeInfoAsync(_solution, _assembly, _metadataReference, CancellationToken).ConfigureAwait(false);
......
......@@ -85,7 +85,14 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
if (allSymbolReferences.Count == 0)
{
// No exact matches found. Fall back to fuzzy searching.
await FindResults(projectToAssembly, referenceToCompilation, project, allSymbolReferences, finder, exact: false, cancellationToken: cancellationToken).ConfigureAwait(false);
// Only bother doing this for host workspaces. We don't want this for
// things like the Interactive workspace as this will cause us to
// create expensive bktrees which we won't even be able to save for
// future use.
if (IsHostOrTestWorkspace(project))
{
await FindResults(projectToAssembly, referenceToCompilation, project, allSymbolReferences, finder, exact: false, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
// Nothing found at all. No need to proceed.
......@@ -112,6 +119,12 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
}
}
private static bool IsHostOrTestWorkspace(Project project)
{
return project.Solution.Workspace.Kind == WorkspaceKind.Host ||
project.Solution.Workspace.Kind == "Test";
}
private async Task FindResults(
ConcurrentDictionary<Project, AsyncLazy<IAssemblySymbol>> projectToAssembly,
ConcurrentDictionary<PortableExecutableReference, Compilation> referenceToCompilation,
......@@ -121,12 +134,19 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
// search string.
await FindResultsInAllProjectSymbolsAsync(project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false);
// Now search unreferenced projects, and see if they have any source symbols that match
// the search string.
await FindResultsInUnreferencedProjectSourceSymbolsAsync(projectToAssembly, project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false);
// Only bother doing this for host workspaces. We don't want this for
// things like the Interactive workspace as we can't even add project
// references to the interactive window. We could consider adding metadata
// references with #r in the future.
if (IsHostOrTestWorkspace(project))
{
// Now search unreferenced projects, and see if they have any source symbols that match
// the search string.
await FindResultsInUnreferencedProjectSourceSymbolsAsync(projectToAssembly, project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false);
// Finally, check and see if we have any metadata symbols that match the search string.
await FindResultsInUnreferencedMetadataSymbolsAsync(referenceToCompilation, project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false);
// Finally, check and see if we have any metadata symbols that match the search string.
await FindResultsInUnreferencedMetadataSymbolsAsync(referenceToCompilation, project, allSymbolReferences, finder, exact, cancellationToken).ConfigureAwait(false);
}
}
private async Task FindResultsInAllProjectSymbolsAsync(
......
......@@ -23,9 +23,16 @@ internal partial class SymbolTreeInfo
private readonly IReadOnlyList<Node> _nodes;
/// <summary>
/// The spell checker we use for fuzzy match queries.
/// The task that produces the spell checker we use for fuzzy match queries.
/// We use a task so that we can generate the <see cref="SymbolTreeInfo"/>
/// without having to wait for the spell checker construction to finish.
///
/// Features that don't need fuzzy matching don't want to incur the cost of
/// the creation of this value. And the only feature which does want fuzzy
/// matching (add-using) doesn't want to block waiting for the value to be
/// created.
/// </summary>
private readonly SpellChecker _spellChecker;
private readonly Task<SpellChecker> _spellCheckerTask;
private static readonly StringComparer s_caseInsensitiveComparer = CaseInsensitiveComparison.Comparer;
......@@ -45,11 +52,11 @@ internal partial class SymbolTreeInfo
: StringComparer.Ordinal.Compare(s1, s2);
};
private SymbolTreeInfo(VersionStamp version, IReadOnlyList<Node> orderedNodes, SpellChecker spellChecker)
private SymbolTreeInfo(VersionStamp version, IReadOnlyList<Node> orderedNodes, Task<SpellChecker> spellCheckerTask)
{
_version = version;
_nodes = orderedNodes;
_spellChecker = spellChecker;
_spellCheckerTask = spellCheckerTask;
}
public int Count => _nodes.Count;
......@@ -59,7 +66,8 @@ public Task<IEnumerable<ISymbol>> FindAsync(SearchQuery query, IAssemblySymbol a
return FindAsync(query, new AsyncLazy<IAssemblySymbol>(assembly), cancellationToken);
}
public Task<IEnumerable<ISymbol>> FindAsync(SearchQuery query, AsyncLazy<IAssemblySymbol> lazyAssembly, CancellationToken cancellationToken)
public Task<IEnumerable<ISymbol>> FindAsync(
SearchQuery query, AsyncLazy<IAssemblySymbol> 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.
......@@ -82,9 +90,17 @@ public Task<IEnumerable<ISymbol>> FindAsync(SearchQuery query, AsyncLazy<IAssemb
/// <summary>
/// Finds symbols in this assembly that match the provided name in a fuzzy manner.
/// </summary>
public async Task<IEnumerable<ISymbol>> FuzzyFindAsync(AsyncLazy<IAssemblySymbol> lazyAssembly, string name, CancellationToken cancellationToken)
public async Task<IEnumerable<ISymbol>> FuzzyFindAsync(
AsyncLazy<IAssemblySymbol> lazyAssembly, string name, CancellationToken cancellationToken)
{
var similarNames = _spellChecker.FindSimilarWords(name);
if (_spellCheckerTask.Status != TaskStatus.RanToCompletion)
{
// Spell checker isn't ready. Just return immediately.
return SpecializedCollections.EmptyEnumerable<ISymbol>();
}
var spellChecker = _spellCheckerTask.Result;
var similarNames = spellChecker.FindSimilarWords(name);
var result = new List<ISymbol>();
foreach (var similarName in similarNames)
......@@ -215,7 +231,7 @@ private int BinarySearch(string name)
return -1;
}
#region Construction
#region Construction
// Cache the symbol tree infos for assembly symbols that share the same underlying metadata.
// Generating symbol trees for metadata can be expensive (in large metadata cases). And it's
......@@ -262,7 +278,7 @@ private int BinarySearch(string name)
return info;
}
info = await LoadOrCreateAsync(solution, assembly, reference.FilePath, loadOnly, cancellationToken).ConfigureAwait(false);
info = await LoadOrCreateSymbolTreeInfoAsync(solution, assembly, reference.FilePath, loadOnly, cancellationToken).ConfigureAwait(false);
if (info == null && loadOnly)
{
return null;
......@@ -277,11 +293,12 @@ private int BinarySearch(string name)
{
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
return await LoadOrCreateAsync(
return await LoadOrCreateSymbolTreeInfoAsync(
project.Solution, compilation.Assembly, project.FilePath, loadOnly: false, cancellationToken: cancellationToken).ConfigureAwait(false);
}
internal static SymbolTreeInfo Create(VersionStamp version, IAssemblySymbol assembly, CancellationToken cancellationToken)
internal static SymbolTreeInfo CreateSymbolTreeInfo(
Solution solution, VersionStamp version, IAssemblySymbol assembly, string filePath, CancellationToken cancellationToken)
{
if (assembly == null)
{
......@@ -291,8 +308,20 @@ internal static SymbolTreeInfo Create(VersionStamp version, IAssemblySymbol asse
var list = new List<Node>();
GenerateNodes(assembly.GlobalNamespace, list);
var spellChecker = new SpellChecker(list.Select(n => n.Name));
return new SymbolTreeInfo(version, SortNodes(list), spellChecker);
var sortedNodes = SortNodes(list);
var createSpellCheckerTask = GetSpellCheckerTask(solution, version, assembly, filePath, sortedNodes);
return new SymbolTreeInfo(version, sortedNodes, createSpellCheckerTask);
}
private static Task<SpellChecker> GetSpellCheckerTask(
Solution solution, VersionStamp version, IAssemblySymbol assembly, string filePath, Node[] nodes)
{
// Create a new task to attempt to load or create the spell checker for this
// SymbolTreeInfo. This way the SymbolTreeInfo will be ready immediately
// for non-fuzzy searches, and soon afterwards it will be able to perform
// fuzzy searches as well.
return Task.Run(() => LoadOrCreateSpellCheckerAsync(solution, assembly, filePath,
v => new SpellChecker(v, nodes.Select(n => n.Name))));
}
private static Node[] SortNodes(List<Node> nodes)
......@@ -399,9 +428,9 @@ private static void GenerateNodes(string name, int parentIndex, IEnumerable<ISym
: SpecializedCollections.EmptyEnumerable<ISymbol>();
};
#endregion
#endregion
#region Binding
#region Binding
// returns all the symbols in the container corresponding to the node
private IEnumerable<ISymbol> Bind(int index, INamespaceOrTypeSymbol rootContainer, CancellationToken cancellationToken)
......@@ -450,7 +479,7 @@ private void Bind(int index, INamespaceOrTypeSymbol rootContainer, List<ISymbol>
}
}
}
#endregion
#endregion
internal bool IsEquivalent(SymbolTreeInfo other)
{
......
......@@ -2,9 +2,11 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Serialization;
using Roslyn.Utilities;
......@@ -13,74 +15,101 @@ namespace Microsoft.CodeAnalysis.FindSymbols
internal partial class SymbolTreeInfo : IObjectWritable
{
private const string PrefixMetadataSymbolTreeInfo = "<MetadataSymbolTreeInfoPersistence>_";
private const string SerializationFormat = "9";
private const string SerializationFormat = "10";
private static bool ShouldCreateFromScratch(
/// <summary>
/// Loads the SymbolTreeInfo for a given assembly symbol (metadata or project). If the
/// info can't be loaded, it will be created (and persisted if possible).
/// </summary>
private static Task<SymbolTreeInfo> LoadOrCreateSymbolTreeInfoAsync(
Solution solution,
IAssemblySymbol assembly,
string filePath,
out string prefix,
out VersionStamp version,
bool loadOnly,
CancellationToken cancellationToken)
{
prefix = null;
version = default(VersionStamp);
var service = solution.Workspace.Services.GetService<IAssemblySerializationInfoService>();
if (service == null)
{
return true;
}
// check whether the assembly that belong to a solution is something we can serialize
if (!service.Serializable(solution, filePath))
{
return true;
}
if (!service.TryGetSerializationPrefixAndVersion(solution, filePath, out prefix, out version))
{
return true;
}
return LoadOrCreateAsync(
solution,
assembly,
filePath,
loadOnly,
create: version => CreateSymbolTreeInfo(solution, version, assembly, filePath, cancellationToken),
keySuffix: "",
getVersion: info => info._version,
readObject: reader => ReadSymbolTreeInfo(reader, (version, nodes) => GetSpellCheckerTask(solution, version, assembly, filePath, nodes)),
writeObject: (w, i) => i.WriteTo(w),
cancellationToken: cancellationToken);
}
return false;
/// <summary>
/// Loads the SpellChecker for a given assembly symbol (metadata or project). If the
/// info can't be loaded, it will be created (and persisted if possible).
/// </summary>
private static Task<SpellChecker> LoadOrCreateSpellCheckerAsync(
Solution solution,
IAssemblySymbol assembly,
string filePath,
Func<VersionStamp, SpellChecker> create)
{
return LoadOrCreateAsync(
solution,
assembly,
filePath,
loadOnly: false,
create: create,
keySuffix: "SpellChecker",
getVersion: s => s.Version,
readObject: SpellChecker.ReadFrom,
writeObject: (w, i) => i.WriteTo(w),
cancellationToken: CancellationToken.None);
}
/// <summary>
/// this is for a metadata reference in a solution
/// Generalized function for loading/creating/persisting data. Used as the common core
/// code for serialization of SymbolTreeInfos and SpellCheckers.
/// </summary>
private static async Task<SymbolTreeInfo> LoadOrCreateAsync(
private static async Task<T> LoadOrCreateAsync<T>(
Solution solution,
IAssemblySymbol assembly,
string filePath,
bool loadOnly,
CancellationToken cancellationToken)
Func<VersionStamp, T> create,
string keySuffix,
Func<T, VersionStamp> getVersion,
Func<ObjectReader, T> readObject,
Action<ObjectWriter, T> writeObject,
CancellationToken cancellationToken) where T : class
{
// See if we can even use serialization. If not, we'll just have to make the value
// from scratch.
string prefix;
VersionStamp version;
if (ShouldCreateFromScratch(solution, assembly, filePath, out prefix, out version, cancellationToken))
{
return loadOnly ? null : Create(VersionStamp.Default, assembly, cancellationToken);
return loadOnly ? null : create(VersionStamp.Default);
}
// Ok, we can use persistence. First try to load from the persistence service.
var persistentStorageService = solution.Workspace.Services.GetService<IPersistentStorageService>();
// okay, see whether we can get one from persistence service.
// attempt to load from persisted state. metadata reference is solution wise information
SymbolTreeInfo info;
T result;
using (var storage = persistentStorageService.GetStorage(solution))
{
var key = PrefixMetadataSymbolTreeInfo + prefix;
// Get the unique key to identify our data.
var key = PrefixMetadataSymbolTreeInfo + prefix + keySuffix;
using (var stream = await storage.ReadStreamAsync(key, cancellationToken).ConfigureAwait(false))
{
if (stream != null)
{
using (var reader = new ObjectReader(stream))
{
info = ReadFrom(reader);
if (info != null && VersionStamp.CanReusePersistedVersion(version, info._version))
// We have some previously persisted data. Attempt to read it back.
// If we're able to, and the version of the persisted data matches
// our version, then we can reuse this instance.
result = readObject(reader);
if (result != null && VersionStamp.CanReusePersistedVersion(version, getVersion(result)))
{
return info;
return result;
}
}
}
......@@ -88,19 +117,22 @@ internal partial class SymbolTreeInfo : IObjectWritable
cancellationToken.ThrowIfCancellationRequested();
// Couldn't read from the persistence service. If we've been asked to only load
// data and not create new instances in their absense, then there's nothing left
// to do at this point.
if (loadOnly)
{
return null;
}
// compute it if we couldn't load it from cache
info = Create(version, assembly, cancellationToken);
if (info != null)
// Now, try to create a new instance and write it to the persistence service.
result = create(version);
if (result != null)
{
using (var stream = SerializableBytes.CreateWritableStream())
using (var writer = new ObjectWriter(stream, cancellationToken: cancellationToken))
{
info.WriteTo(writer);
writeObject(writer, result);
stream.Position = 0;
await storage.WriteStreamAsync(key, stream, cancellationToken).ConfigureAwait(false);
......@@ -108,7 +140,38 @@ internal partial class SymbolTreeInfo : IObjectWritable
}
}
return info;
return result;
}
private static bool ShouldCreateFromScratch(
Solution solution,
IAssemblySymbol assembly,
string filePath,
out string prefix,
out VersionStamp version,
CancellationToken cancellationToken)
{
prefix = null;
version = default(VersionStamp);
var service = solution.Workspace.Services.GetService<IAssemblySerializationInfoService>();
if (service == null)
{
return true;
}
// check whether the assembly that belong to a solution is something we can serialize
if (!service.Serializable(solution, filePath))
{
return true;
}
if (!service.TryGetSerializationPrefixAndVersion(solution, filePath, out prefix, out version))
{
return true;
}
return false;
}
public void WriteTo(ObjectWriter writer)
......@@ -122,41 +185,47 @@ public void WriteTo(ObjectWriter writer)
writer.WriteString(node.Name);
writer.WriteInt32(node.ParentIndex);
}
}
_spellChecker.WriteTo(writer);
internal static SymbolTreeInfo ReadSymbolTreeInfo_ForTestingPurposesOnly(ObjectReader reader)
{
return ReadSymbolTreeInfo(reader,
(version, nodes) => Task.FromResult(new SpellChecker(version, nodes.Select(n => n.Name))));
}
internal static SymbolTreeInfo ReadFrom(ObjectReader reader)
private static SymbolTreeInfo ReadSymbolTreeInfo(
ObjectReader reader, Func<VersionStamp, Node[], Task<SpellChecker>> createSpellCheckerTask)
{
try
{
var formatVersion = reader.ReadString();
if (!string.Equals(formatVersion, SerializationFormat, StringComparison.Ordinal))
if (string.Equals(formatVersion, SerializationFormat, StringComparison.Ordinal))
{
return null;
}
var version = VersionStamp.ReadFrom(reader);
var version = VersionStamp.ReadFrom(reader);
var count = reader.ReadInt32();
if (count == 0)
{
return new SymbolTreeInfo(version, ImmutableArray<Node>.Empty,
Task.FromResult(new SpellChecker(version, BKTree.Empty)));
}
var count = reader.ReadInt32();
if (count == 0)
{
return new SymbolTreeInfo(version, ImmutableArray<Node>.Empty, SpellChecker.Empty);
}
var nodes = new Node[count];
for (var i = 0; i < count; i++)
{
var name = reader.ReadString();
var parentIndex = reader.ReadInt32();
var nodes = new Node[count];
for (var i = 0; i < count; i++)
{
var name = reader.ReadString();
var parentIndex = reader.ReadInt32();
nodes[i] = new Node(name, parentIndex);
}
nodes[i] = new Node(name, parentIndex);
var spellCheckerTask = createSpellCheckerTask(version, nodes);
return new SymbolTreeInfo(version, nodes, spellCheckerTask);
}
return new SymbolTreeInfo(version, nodes, SpellChecker.ReadFrom(reader));
}
catch (Exception)
catch
{
Logger.Log(FunctionId.SymbolTreeInfo_ExceptionInCacheRead);
}
return null;
......
......@@ -317,5 +317,10 @@ internal enum FunctionId
CodefixInfobar_EnableAndIgnoreFutureErrors,
CodefixInfobar_LeaveDisabled,
CodefixInfobar_ErrorIgnored,
// Caches
SymbolTreeInfo_ExceptionInCacheRead,
SpellChecker_ExceptionInCacheRead,
BKTree_ExceptionInCacheRead,
}
}
// 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.Collections.Immutable;
using Microsoft.CodeAnalysis.Internal.Log;
namespace Roslyn.Utilities
{
......@@ -29,27 +30,35 @@ internal void WriteTo(ObjectWriter writer)
internal static BKTree ReadFrom(ObjectReader reader)
{
var concatenatedLowerCaseWords = new char[reader.ReadInt32()];
for (var i = 0; i < concatenatedLowerCaseWords.Length; i++)
try
{
concatenatedLowerCaseWords[i] = reader.ReadChar();
}
var concatenatedLowerCaseWords = new char[reader.ReadInt32()];
for (var i = 0; i < concatenatedLowerCaseWords.Length; i++)
{
concatenatedLowerCaseWords[i] = reader.ReadChar();
}
var nodeCount = reader.ReadInt32();
var nodes = ImmutableArray.CreateBuilder<Node>(nodeCount);
for (var i = 0; i < nodeCount; i++)
{
nodes.Add(Node.ReadFrom(reader));
}
var nodeCount = reader.ReadInt32();
var nodes = ImmutableArray.CreateBuilder<Node>(nodeCount);
for (var i = 0; i < nodeCount; i++)
{
nodes.Add(Node.ReadFrom(reader));
}
var edgeCount = reader.ReadInt32();
var edges = ImmutableArray.CreateBuilder<Edge>(edgeCount);
for (var i = 0; i < edgeCount; i++)
var edgeCount = reader.ReadInt32();
var edges = ImmutableArray.CreateBuilder<Edge>(edgeCount);
for (var i = 0; i < edgeCount; i++)
{
edges.Add(Edge.ReadFrom(reader));
}
return new BKTree(concatenatedLowerCaseWords, nodes.MoveToImmutable(), edges.MoveToImmutable());
}
catch
{
edges.Add(Edge.ReadFrom(reader));
Logger.Log(FunctionId.BKTree_ExceptionInCacheRead);
return null;
}
return new BKTree(concatenatedLowerCaseWords, nodes.MoveToImmutable(), edges.MoveToImmutable());
}
}
}
......@@ -4,21 +4,26 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Internal.Log;
namespace Roslyn.Utilities
{
internal class SpellChecker
{
public static readonly SpellChecker Empty = new SpellChecker(BKTree.Empty);
private const string SerializationFormat = "1";
public VersionStamp Version { get; }
private readonly BKTree _bkTree;
public SpellChecker(BKTree bKTree)
public SpellChecker(VersionStamp version, BKTree bKTree)
{
Version = version;
_bkTree = bKTree;
}
public SpellChecker(IEnumerable<string> corpus) : this(BKTree.Create(corpus))
public SpellChecker(VersionStamp version, IEnumerable<string> corpus)
: this(version, BKTree.Create(corpus))
{
}
......@@ -34,12 +39,32 @@ public IList<string> FindSimilarWords(string value)
internal void WriteTo(ObjectWriter writer)
{
writer.WriteString(SerializationFormat);
Version.WriteTo(writer);
_bkTree.WriteTo(writer);
}
internal static SpellChecker ReadFrom(ObjectReader reader)
{
return new SpellChecker(BKTree.ReadFrom(reader));
try
{
var formatVersion = reader.ReadString();
if (string.Equals(formatVersion, SerializationFormat, StringComparison.Ordinal))
{
var version = VersionStamp.ReadFrom(reader);
var bkTree = BKTree.ReadFrom(reader);
if (bkTree != null)
{
return new SpellChecker(version, bkTree);
}
}
}
catch
{
Logger.Log(FunctionId.SpellChecker_ExceptionInCacheRead);
}
return null;
}
}
......
......@@ -577,7 +577,7 @@ public static async Task TestSymbolTreeInfoSerialization()
// create symbol tree info from assembly
var version = VersionStamp.Create();
var info = SymbolTreeInfo.Create(version, assembly, CancellationToken.None);
var info = SymbolTreeInfo.CreateSymbolTreeInfo(solution, version, assembly, "", CancellationToken.None);
using (var writerStream = new MemoryStream())
{
......@@ -589,7 +589,7 @@ public static async Task TestSymbolTreeInfoSerialization()
using (var readerStream = new MemoryStream(writerStream.ToArray()))
using (var reader = new ObjectReader(readerStream))
{
var readInfo = SymbolTreeInfo.ReadFrom(reader);
var readInfo = SymbolTreeInfo.ReadSymbolTreeInfo_ForTestingPurposesOnly(reader);
Assert.True(info.IsEquivalent(readInfo));
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册