// 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.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
{
///
/// Features like add-using want to be able to quickly search symbol indices for projects and
/// metadata. However, creating those indices can be expensive. As such, we don't want to
/// construct them during the add-using process itself. Instead, we expose this type as an
/// Incremental-Analyzer to walk our projects/metadata in the background to keep the indices
/// up to date.
///
/// We also then export this type as a service that can give back the index for a project or
/// metadata dll on request. If the index has been produced then it will be returned and
/// can be used by add-using. Otherwise, nothing is returned and no results will be found.
///
/// This means that as the project is being indexed, partial results may be returned. However
/// once it is fully indexed, then total results will be returned.
///
[Shared]
[ExportIncrementalAnalyzerProvider(nameof(SymbolTreeInfoIncrementalAnalyzerProvider), new[] { WorkspaceKind.Host })]
[ExportWorkspaceServiceFactory(typeof(ISymbolTreeInfoCacheService))]
internal class SymbolTreeInfoIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider, IWorkspaceServiceFactory
{
private struct ProjectInfo
{
public readonly VersionStamp VersionStamp;
public readonly SymbolTreeInfo SymbolTreeInfo;
public ProjectInfo(VersionStamp versionStamp, SymbolTreeInfo info)
{
VersionStamp = versionStamp;
SymbolTreeInfo = info;
}
}
private struct MetadataInfo
{
public readonly DateTime TimeStamp;
public readonly SymbolTreeInfo SymbolTreeInfo;
///
/// Note: the Incremental-Analyzer infrastructure guarantees that it will call all the methods
/// on in a serial fashion. As that is the only type that
/// reads/writes these objects, we don't need to lock this.
///
public readonly HashSet ReferencingProjects;
public MetadataInfo(DateTime timeStamp, SymbolTreeInfo info, HashSet referencingProjects)
{
TimeStamp = timeStamp;
SymbolTreeInfo = info;
ReferencingProjects = referencingProjects;
}
}
// 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)
{
var cacheService = workspace.Services.GetService();
if (cacheService != null)
{
cacheService.CacheFlushRequested += OnCacheFlushRequested;
}
return new IncrementalAnalyzer(_projectToInfo, _metadataPathToInfo);
}
private void OnCacheFlushRequested(object sender, EventArgs e)
{
// If we hear about low memory conditions, flush our caches. This will degrade the
// experience a bit (as we will no longer offer to Add-Using for p2p refs/metadata),
// but will be better than OOM'ing. These caches will be regenerated in the future
// when the incremental analyzer reanalyzers the projects in teh workspace.
_projectToInfo.Clear();
_metadataPathToInfo.Clear();
}
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
{
return new SymbolTreeInfoCacheService(_projectToInfo, _metadataPathToInfo);
}
private static string GetReferenceKey(PortableExecutableReference reference)
{
return reference.FilePath ?? reference.Display;
}
private static bool TryGetLastWriteTime(string path, out DateTime time)
{
var succeeded = false;
time = IOUtilities.PerformIO(
() =>
{
var result = File.GetLastWriteTimeUtc(path);
succeeded = true;
return result;
},
default(DateTime));
return succeeded;
}
private class SymbolTreeInfoCacheService : ISymbolTreeInfoCacheService
{
private readonly ConcurrentDictionary _projectToInfo;
private readonly ConcurrentDictionary _metadataPathToInfo;
public SymbolTreeInfoCacheService(
ConcurrentDictionary projectToInfo,
ConcurrentDictionary metadataPathToInfo)
{
_projectToInfo = projectToInfo;
_metadataPathToInfo = metadataPathToInfo;
}
public async Task TryGetSymbolTreeInfoAsync(
Solution solution,
IAssemblySymbol assembly,
PortableExecutableReference reference,
CancellationToken cancellationToken)
{
var key = GetReferenceKey(reference);
if (key != null)
{
MetadataInfo metadataInfo;
if (_metadataPathToInfo.TryGetValue(key, out metadataInfo))
{
DateTime writeTime;
if (TryGetLastWriteTime(key, out writeTime) && writeTime == metadataInfo.TimeStamp)
{
return metadataInfo.SymbolTreeInfo;
}
}
}
// If we didn't have it in our cache, see if we can load it from disk.
// Note: pass 'loadOnly' so we only attempt to load from disk, not to actually
// try to create the metadata.
var info = await SymbolTreeInfo.TryGetInfoForMetadataAssemblyAsync(
solution, assembly, reference, loadOnly: true, cancellationToken: cancellationToken).ConfigureAwait(false);
return info;
}
public async Task TryGetSymbolTreeInfoAsync(
Project project, CancellationToken cancellationToken)
{
ProjectInfo projectInfo;
if (_projectToInfo.TryGetValue(project.Id, out projectInfo))
{
var version = await project.GetSemanticVersionAsync(cancellationToken).ConfigureAwait(false);
if (version == projectInfo.VersionStamp)
{
return projectInfo.SymbolTreeInfo;
}
}
return null;
}
}
private class IncrementalAnalyzer : IncrementalAnalyzerBase
{
private readonly ConcurrentDictionary _projectToInfo;
private readonly ConcurrentDictionary _metadataPathToInfo;
public IncrementalAnalyzer(
ConcurrentDictionary projectToInfo,
ConcurrentDictionary metadataPathToInfo)
{
_projectToInfo = projectToInfo;
_metadataPathToInfo = metadataPathToInfo;
}
public override Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, CancellationToken cancellationToken)
{
if (!document.SupportsSyntaxTree)
{
// Not a language we can produce indices for (i.e. TypeScript). Bail immediately.
return SpecializedTasks.EmptyTask;
}
if (bodyOpt != null)
{
// This was a method level edit. This can't change the symbol tree info
// for this project. Bail immediately.
return SpecializedTasks.EmptyTask;
}
return UpdateSymbolTreeInfoAsync(document.Project, cancellationToken);
}
public override Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken)
{
return UpdateSymbolTreeInfoAsync(project, cancellationToken);
}
private async Task UpdateSymbolTreeInfoAsync(Project project, CancellationToken cancellationToken)
{
if (!project.SupportsCompilation)
{
return;
}
// Check the semantic version of this project. The semantic version will change
// if any of the source files changed, or if the project version itself changed.
// (The latter happens when something happens to the project like metadata
// changing on disk).
var version = await project.GetSemanticVersionAsync(cancellationToken).ConfigureAwait(false);
ProjectInfo projectInfo;
if (!_projectToInfo.TryGetValue(project.Id, out projectInfo) || projectInfo.VersionStamp != version)
{
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
// Update the symbol tree infos for metadata and source in parallel.
var referencesTask = UpdateReferencesAync(project, compilation, cancellationToken);
var projectTask = SymbolTreeInfo.GetInfoForSourceAssemblyAsync(project, cancellationToken);
await Task.WhenAll(referencesTask, projectTask).ConfigureAwait(false);
// Mark that we're up to date with this project. Future calls with the same
// semantic version can bail out immediately.
projectInfo = new ProjectInfo(version, await projectTask.ConfigureAwait(false));
_projectToInfo.AddOrUpdate(project.Id, projectInfo, (_1, _2) => projectInfo);
}
}
private Task UpdateReferencesAync(Project project, Compilation compilation, CancellationToken cancellationToken)
{
// Process all metadata references in parallel.
var tasks = project.MetadataReferences.OfType()
.Select(r => UpdateReferenceAsync(project, compilation, r, cancellationToken))
.ToArray();
return Task.WhenAll(tasks);
}
private async Task UpdateReferenceAsync(
Project project, Compilation compilation, PortableExecutableReference reference, CancellationToken cancellationToken)
{
var key = GetReferenceKey(reference);
if (key == null)
{
return;
}
DateTime lastWriteTime;
if (!TryGetLastWriteTime(key, out lastWriteTime))
{
// Couldn't get the write time. Just ignore this reference.
return;
}
MetadataInfo metadataInfo;
if (!_metadataPathToInfo.TryGetValue(key, out metadataInfo) || metadataInfo.TimeStamp == lastWriteTime)
{
var assembly = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol;
var info = assembly == null
? null
: await SymbolTreeInfo.TryGetInfoForMetadataAssemblyAsync(project.Solution, assembly, reference, loadOnly: false, cancellationToken: cancellationToken).ConfigureAwait(false);
metadataInfo = new MetadataInfo(lastWriteTime, info, metadataInfo.ReferencingProjects ?? new HashSet());
_metadataPathToInfo.AddOrUpdate(key, metadataInfo, (_1, _2) => metadataInfo);
}
// Keep track that this dll is referenced by this project.
metadataInfo.ReferencingProjects.Add(project.Id);
}
public override void RemoveProject(ProjectId projectId)
{
ProjectInfo info;
_projectToInfo.TryRemove(projectId, out info);
RemoveMetadataReferences(projectId);
}
private void RemoveMetadataReferences(ProjectId projectId)
{
foreach (var kvp in _metadataPathToInfo.ToArray())
{
if (kvp.Value.ReferencingProjects.Remove(projectId))
{
if (kvp.Value.ReferencingProjects.Count == 0)
{
// This metadata dll isn't referenced by any project. We can just dump it.
MetadataInfo unneeded;
_metadataPathToInfo.TryRemove(kvp.Key, out unneeded);
}
}
}
}
}
}
}