File: FindSymbols\SymbolTree\SymbolTreeInfoCacheService.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.FindSymbols.SymbolTree;
 
internal sealed partial class SymbolTreeInfoCacheServiceFactory
{
    internal sealed partial class SymbolTreeInfoCacheService : ISymbolTreeInfoCacheService, IDisposable
    {
        /// <summary>
        /// Same value as SolutionCrawlerTimeSpan.EntireProjectWorkerBackOff
        /// </summary>
        private static readonly TimeSpan EntireProjectWorkerBackOff = TimeSpan.FromMilliseconds(5000);
 
        private static readonly TaskScheduler s_exclusiveScheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler;
 
        private readonly ConcurrentDictionary<ProjectId, (VersionStamp semanticVersion, SymbolTreeInfo info)> _projectIdToInfo = [];
        private readonly ConcurrentDictionary<PortableExecutableReference, MetadataInfo> _peReferenceToInfo = [];
 
        private readonly CancellationTokenSource _tokenSource = new();
 
        private readonly Workspace _workspace;
        private readonly AsyncBatchingWorkQueue<ProjectId> _workQueue;
 
        /// <summary>
        /// Scheduler to run our tasks on.  If we're in the remote host , we'll run all our tasks concurrently.
        /// Otherwise, we will run them serially using <see cref="s_exclusiveScheduler"/>
        /// </summary>
        private readonly TaskScheduler _scheduler;
 
        public SymbolTreeInfoCacheService(Workspace workspace, IAsynchronousOperationListener listener)
        {
            _workspace = workspace;
            _workQueue = new AsyncBatchingWorkQueue<ProjectId>(
                EntireProjectWorkerBackOff,
                ProcessProjectsAsync,
                EqualityComparer<ProjectId>.Default,
                listener,
                _tokenSource.Token);
 
            _scheduler = workspace.Kind == WorkspaceKind.RemoteWorkspace ? TaskScheduler.Default : s_exclusiveScheduler;
        }
 
        void IDisposable.Dispose()
            => _tokenSource.Cancel();
 
        private Task CreateWorkAsync(Func<Task> createWorkAsync, CancellationToken cancellationToken)
            => Task.Factory.StartNew(createWorkAsync, cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap();
 
        /// <summary>
        /// Gets the latest computed <see cref="SymbolTreeInfo"/> for the requested <paramref name="reference"/>.
        /// This may return an index corresponding to a prior version of the reference if it has since changed.
        /// Another system is responsible for bringing these indices up to date in the background.
        /// </summary>
        public async ValueTask<SymbolTreeInfo?> TryGetPotentiallyStaleMetadataSymbolTreeInfoAsync(
            Project project,
            PortableExecutableReference reference,
            CancellationToken cancellationToken)
        {
            if (!project.SupportsCompilation)
                return null;
 
            // Kick off the work to update the data we have for this project.
            _workQueue.AddWork(project.Id);
 
            // See if the last value produced exactly matches what the caller is asking for.  If so, return that.
            if (_peReferenceToInfo.TryGetValue(reference, out var metadataInfo))
                return metadataInfo.SymbolTreeInfo;
 
            // If we didn't have it in our cache, see if we can load it from disk.
            var solution = project.Solution;
            var info = await SymbolTreeInfo.LoadAnyInfoForMetadataReferenceAsync(solution, reference, cancellationToken).ConfigureAwait(false);
            if (info is null)
                return null;
 
            var referencingProjects = new HashSet<ProjectId>(solution.Projects.Where(p => p.MetadataReferences.Contains(reference)).Select(p => p.Id));
 
            // attempt to add this item to the map.  But defer to whatever is in the map now if something else beat us to this.
            return _peReferenceToInfo.GetOrAdd(reference, new MetadataInfo(info, referencingProjects)).SymbolTreeInfo;
        }
 
        public async ValueTask<SymbolTreeInfo?> TryGetPotentiallyStaleSourceSymbolTreeInfoAsync(
            Project project, CancellationToken cancellationToken)
        {
            if (!project.SupportsCompilation)
                return null;
 
            // Kick off the work to update the data we have for this project.
            _workQueue.AddWork(project.Id);
 
            // See if the last value produced exactly matches what the caller is asking for.  If so, return that.
            if (_projectIdToInfo.TryGetValue(project.Id, out var projectInfo))
                return projectInfo.info;
 
            // If we didn't have it in our cache, see if we can load some version of it from disk.
            var info = await SymbolTreeInfo.LoadAnyInfoForSourceAssemblyAsync(project, cancellationToken).ConfigureAwait(false);
            if (info is null)
                return null;
 
            // attempt to add this item to the map.  But defer to whatever is in the map now if something else beat
            // us to this.  Don't provide a version here so that the next time we update this data it will get
            // overwritten with the latest computed data.
            return _projectIdToInfo.GetOrAdd(project.Id, (semanticVersion: default, info)).info;
        }
 
        private async ValueTask ProcessProjectsAsync(
            ImmutableSegmentedList<ProjectId> projectIds, CancellationToken cancellationToken)
        {
            var solution = _workspace.CurrentSolution;
 
            foreach (var projectId in projectIds)
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                var project = solution.GetProject(projectId);
                if (project is not { SupportsCompilation: true })
                    continue;
 
                // NOTE: currently we process projects serially.  This is because the same metadata reference might be
                // found in multiple projects and we can't currently process that in parallel.
                await AnalyzeProjectAsync(project, cancellationToken).ConfigureAwait(false);
            }
 
            // Now that we've produced all the indices for the projects asked for, also remove any indices for projects
            // no longer in the solution.
            var removedProjectIds = _projectIdToInfo.Keys.Except(solution.ProjectIds).ToArray();
            foreach (var projectId in removedProjectIds)
                this.RemoveProject(projectId);
        }
 
        private async Task AnalyzeProjectAsync(Project project, CancellationToken cancellationToken)
        {
            using var _ = ArrayBuilder<Task>.GetInstance(out var tasks);
 
            // We can compute source-symbols in parallel with the metadata symbols.
            tasks.Add(CreateWorkAsync(() => this.UpdateSourceSymbolTreeInfoAsync(project, cancellationToken), cancellationToken));
 
            // Add tasks to update the symboltree for all metadata references.  As these are all distinct, they can run
            // in parallel as we won't be trying to update the associated data for the same reference at the same time.
            foreach (var reference in project.MetadataReferences.OfType<PortableExecutableReference>().Distinct())
                tasks.Add(CreateWorkAsync(() => UpdateReferenceAsync(project, reference, cancellationToken), cancellationToken));
 
            await Task.WhenAll(tasks).ConfigureAwait(false);
        }
 
        private async Task UpdateSourceSymbolTreeInfoAsync(Project project, CancellationToken cancellationToken)
        {
            // Find the top-level-semantic-version of this project.  We only want to recompute if it has changed. This is
            // because the symboltree contains the names of the types/namespaces in the project and would not change
            // if the semantic-version of the project hasn't changed.  We also do not need to check the 'dependent
            // version'.  As this is just tracking parent/child relationships of namespace/type names for the source
            // types in this project, this cannot change based on what happens in other projects.
            var semanticVersion = await project.GetSemanticVersionAsync(cancellationToken).ConfigureAwait(false);
 
            if (!_projectIdToInfo.TryGetValue(project.Id, out var projectInfo) ||
                projectInfo.semanticVersion != semanticVersion)
            {
                // If the checksum is the same (which can happen if we loaded the previous index from disk), then no
                // need to recompute.
                var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false);
                if (projectInfo.info?.Checksum != checksum)
                {
                    // Otherwise, looks like things changed.  Compute and persist the latest index.
                    var info = await SymbolTreeInfo.GetInfoForSourceAssemblyAsync(
                        project, checksum, cancellationToken).ConfigureAwait(false);
 
                    Contract.ThrowIfNull(info);
                    Contract.ThrowIfTrue(info.Checksum != checksum, "If we computed a SymbolTreeInfo, then its checksum must match our checksum.");
 
                    // Mark that we're up to date with this project.  Future calls with the same semantic-version or
                    // checksum can bail out immediately.
                    _projectIdToInfo[project.Id] = (semanticVersion, info);
                }
            }
        }
 
        private async Task UpdateReferenceAsync(
            Project project,
            PortableExecutableReference reference,
            CancellationToken cancellationToken)
        {
            var checksum = SymbolTreeInfo.GetMetadataChecksum(project.Solution.Services, reference, cancellationToken);
            if (!_peReferenceToInfo.TryGetValue(reference, out var metadataInfo) ||
                metadataInfo.SymbolTreeInfo.Checksum != checksum)
            {
                var info = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync(
                    project.Solution, reference, checksum, cancellationToken).ConfigureAwait(false);
 
                Contract.ThrowIfNull(info);
                Contract.ThrowIfTrue(info.Checksum != checksum, "If we computed a SymbolTreeInfo, then its checksum must match our checksum.");
 
                metadataInfo = new MetadataInfo(info, metadataInfo.ReferencingProjects ?? []);
                _peReferenceToInfo[reference] = metadataInfo;
            }
 
            // Keep track that this dll is referenced by this project.
            lock (metadataInfo.ReferencingProjects)
            {
                metadataInfo.ReferencingProjects.Add(project.Id);
            }
        }
 
        public void RemoveProject(ProjectId projectId)
        {
            _projectIdToInfo.TryRemove(projectId, out _);
            RemoveMetadataReferences(projectId);
        }
 
        private void RemoveMetadataReferences(ProjectId projectId)
        {
            foreach (var (reference, info) in _peReferenceToInfo.ToArray())
            {
                lock (info.ReferencingProjects)
                {
                    info.ReferencingProjects.Remove(projectId);
 
                    // If this metadata dll isn't referenced by any project.  We can just dump it.
                    if (info.ReferencingProjects.Count == 0)
                        _peReferenceToInfo.TryRemove(reference, out _);
                }
            }
        }
 
        public TestAccessor GetTestAccessor()
            => new(this);
 
        public struct TestAccessor(SymbolTreeInfoCacheService service)
        {
            public readonly Task AnalyzeSolutionAsync()
            {
                foreach (var projectId in service._workspace.CurrentSolution.ProjectIds)
                    service._workQueue.AddWork(projectId);
 
                return service._workQueue.WaitUntilCurrentBatchCompletesAsync();
            }
        }
    }
}