File: SourceGeneration\SyntaxStore.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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.Immutable;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal sealed class SyntaxStore
    {
        private readonly StateTableStore _tables;
        private readonly Compilation? _compilation;
        internal static readonly SyntaxStore Empty = new SyntaxStore(StateTableStore.Empty, compilation: null);
 
        private SyntaxStore(StateTableStore tables, Compilation? compilation)
        {
            _tables = tables;
            _compilation = compilation;
        }
 
        public Builder ToBuilder(Compilation compilation, ImmutableArray<SyntaxInputNode> syntaxInputNodes, bool enableTracking, CancellationToken cancellationToken) => new Builder(compilation, syntaxInputNodes, enableTracking, this, cancellationToken);
 
        public sealed class Builder
        {
            private readonly ImmutableDictionary<SyntaxInputNode, Exception>.Builder _syntaxExceptions = ImmutableDictionary.CreateBuilder<SyntaxInputNode, Exception>();
            private readonly ImmutableDictionary<SyntaxInputNode, TimeSpan>.Builder _syntaxTimes = ImmutableDictionary.CreateBuilder<SyntaxInputNode, TimeSpan>();
            private readonly StateTableStore.Builder _tableBuilder = new StateTableStore.Builder();
            private readonly Compilation _compilation;
            private readonly ImmutableArray<SyntaxInputNode> _syntaxInputNodes;
            private readonly bool _enableTracking;
            private readonly SyntaxStore _previous;
            private readonly CancellationToken _cancellationToken;
 
            internal Builder(Compilation compilation, ImmutableArray<SyntaxInputNode> syntaxInputNodes, bool enableTracking, SyntaxStore previousStore, CancellationToken cancellationToken)
            {
                _compilation = compilation;
                _syntaxInputNodes = syntaxInputNodes;
                _enableTracking = enableTracking;
                _previous = previousStore;
                _cancellationToken = cancellationToken;
            }
 
            public IStateTable GetSyntaxInputTable(SyntaxInputNode syntaxInputNode, NodeStateTable<SyntaxTree> syntaxTreeTable)
            {
                Debug.Assert(_syntaxInputNodes.Contains(syntaxInputNode));
 
                // when we don't have a value for this node, we update all the syntax inputs at once
                if (!_tableBuilder.Contains(syntaxInputNode))
                {
                    // CONSIDER: when the compilation is the same as previous, the syntax trees must also be the same.
                    // if we have a previous state table for a node, we can just short circuit knowing that it is up to date
                    // This step isn't part of the tree, so we can skip recording.
                    var compilationIsCached = _compilation == _previous._compilation;
 
                    // get a builder for each input node
                    var syntaxInputBuilders = ArrayBuilder<(SyntaxInputNode node, ISyntaxInputBuilder builder)>.GetInstance(_syntaxInputNodes.Length);
                    foreach (var node in _syntaxInputNodes)
                    {
                        // We don't cache the tracked incremental steps in a manner that we can easily rehydrate between runs,
                        // so we disable the cached compilation perf optimization when incremental step tracking is enabled.
                        if (compilationIsCached && !_enableTracking && _previous._tables.TryGetValue(node, out var previousStateTable))
                        {
                            _tableBuilder.SetTable(node, previousStateTable);
                        }
                        else
                        {
                            syntaxInputBuilders.Add((node, node.GetBuilder(_previous._tables, _enableTracking)));
                            _syntaxTimes[node] = TimeSpan.Zero;
                        }
                    }
 
                    if (syntaxInputBuilders.Count > 0)
                    {
                        // Ensure that even if the node that caused the update was cached, we can still adjust it to take into account other nodes that weren't 
                        _syntaxTimes[syntaxInputNode] = TimeSpan.Zero;
 
                        // at this point we need to grab the syntax trees from the new compilation, and optionally diff them against the old ones
                        NodeStateTable<SyntaxTree> syntaxTreeState = syntaxTreeTable;
 
                        // update each tree for the builders, sharing the semantic model
                        foreach (var (tree, state, syntaxTreeIndex, stepInfo) in syntaxTreeState)
                        {
                            var root = new Lazy<SyntaxNode>(() => tree.GetRoot(_cancellationToken));
                            var model = state != EntryState.Removed ? new Lazy<SemanticModel>(() => _compilation.GetSemanticModel(tree)) : null;
                            for (int i = 0; i < syntaxInputBuilders.Count; i++)
                            {
                                var currentNode = syntaxInputBuilders[i].node;
                                try
                                {
                                    var sw = SharedStopwatch.StartNew();
                                    try
                                    {
                                        _cancellationToken.ThrowIfCancellationRequested();
                                        syntaxInputBuilders[i].builder.VisitTree(root, state, model, _cancellationToken);
                                    }
                                    finally
                                    {
                                        var elapsed = sw.Elapsed;
 
                                        // if this node isn't the one that caused the update, ensure we remember it and remove the time it took from the requester
                                        if (currentNode != syntaxInputNode)
                                        {
                                            _syntaxTimes[syntaxInputNode] = _syntaxTimes[syntaxInputNode].Subtract(elapsed);
                                            _syntaxTimes[currentNode] = _syntaxTimes[currentNode].Add(elapsed);
                                        }
                                    }
                                }
                                catch (UserFunctionException ufe)
                                {
                                    // we're evaluating this node ahead of time, so we can't just throw the exception
                                    // instead we'll hold onto it, and throw the exception when a downstream node actually
                                    // attempts to read the value
                                    _syntaxExceptions[currentNode] = ufe;
                                    syntaxInputBuilders.RemoveAt(i);
                                    i--;
                                }
                            }
                        }
 
                        // save the updated inputs
                        foreach ((var node, ISyntaxInputBuilder builder) in syntaxInputBuilders)
                        {
                            builder.SaveStateAndFree(_tableBuilder);
                            Debug.Assert(_tableBuilder.Contains(node));
                        }
                    }
                    syntaxInputBuilders.Free();
                }
 
                // if we don't have an entry for this node, it must have thrown an exception
                if (!_tableBuilder.TryGetTable(syntaxInputNode, out var result))
                {
                    throw _syntaxExceptions[syntaxInputNode];
                }
                return result;
            }
 
            /// <summary>
            /// Gets the adjustment to wall clock time that should be applied for a set of input nodes.
            /// </summary>
            /// <remarks>
            /// The syntax store updates all input nodes in parallel the first time an input node is asked to update,
            /// so that it can share the semantic model between multiple nodes and improve perf. 
            /// 
            /// Unfortunately that means that the first generator to request the results of a syntax node will incorrectly 
            /// have its wall clock time contain the time of all other syntax nodes. And conversely other input nodes will 
            /// not have the true time taken.
            /// 
            /// This method gets the adjustment that should be applied to the wall clock time for a set of input nodes
            /// so that the correct time is attributed to each.
            /// </remarks>
            public TimeSpan GetRuntimeAdjustment(ImmutableArray<SyntaxInputNode> inputNodes)
            {
                TimeSpan result = TimeSpan.Zero;
                foreach (var node in inputNodes)
                {
                    // only add if this node ran at all during this pass
                    if (_syntaxTimes.TryGetValue(node, out var adjustment))
                    {
                        result = result.Add(adjustment);
                    }
                }
                return result;
            }
 
            public SyntaxStore ToImmutable()
            {
                return new SyntaxStore(_tableBuilder.ToImmutable(), _compilation);
            }
        }
    }
}