| File: SourceGeneration\CompilationCache.cs | Web Access |
| Project: src\roslyn\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.Linq; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis { /// <summary> /// Caches the compilation produced by the pre-compilation phase, keyed on the inputs that /// determine its content. When a subsequent run produces the same inputs the cached /// <see cref="Compilation"/> reference is reused, which preserves reference-equality on /// <c>SharedInputNodes.Compilation</c> and therefore keeps every generator's /// <c>CompilationProvider</c>-derived caching valid across runs. /// </summary> /// <remarks> /// Always present on <see cref="GeneratorDriverState"/> (initially <see cref="Empty"/>). /// Driver code accumulates the run's inputs into a <see cref="Builder"/> obtained via /// <see cref="ToBuilder"/>, then calls <see cref="Builder.ToImmutableAndFree"/> to obtain /// either this same instance (cache hit) or a new one (cache miss / no caching needed). /// The result's <see cref="Compilation"/> is always the compilation the driver should pass /// to standard-phase generators. /// </remarks> internal sealed class CompilationCache { /// <summary> /// Initial sentinel used as the seed before any driver run has populated the cache. /// Its <see cref="Compilation"/> is <c>null</c> and is never read by the driver, since /// every run replaces the seed via <see cref="Builder.ToImmutableAndFree"/> before reading. /// </summary> public static readonly CompilationCache Empty = new(); private readonly Compilation? _compilation; private readonly Compilation? _inputCompilation; private readonly ImmutableArray<SyntaxTree> _postInitTrees; private readonly ImmutableArray<PreCompCacheKey> _preCompKeys; private CompilationCache() { } private CompilationCache( Compilation compilation, Compilation inputCompilation, ImmutableArray<SyntaxTree> postInitTrees, ImmutableArray<PreCompCacheKey> preCompKeys) { _compilation = compilation; _inputCompilation = inputCompilation; _postInitTrees = postInitTrees; _preCompKeys = preCompKeys; } /// <summary> /// The compilation the driver should pass to standard-phase generators. Only valid to /// read after a <see cref="Builder.ToImmutableAndFree"/> call has produced this cache; /// reading it on the <see cref="Empty"/> sentinel will assert. /// </summary> public Compilation Compilation { get { Debug.Assert(_compilation is not null, $"{nameof(Compilation)} read before any builder produced this cache."); return _compilation; } } /// <summary> /// Starts accumulating inputs for the current run. The builder takes the input /// compilation reference (used for cache identity) and the compilation after post-init /// trees have been added (used as the base for any pre-compilation augmentation). /// </summary> public Builder ToBuilder(Compilation inputCompilation, Compilation compilationWithPostInit) => new Builder(this, inputCompilation, compilationWithPostInit); internal sealed class Builder { private readonly CompilationCache _previous; private readonly Compilation _inputCompilation; private readonly Compilation _compilationWithPostInit; private readonly ArrayBuilder<SyntaxTree> _postInitTrees = ArrayBuilder<SyntaxTree>.GetInstance(); private readonly ArrayBuilder<PreCompCacheKey> _preCompKeys = ArrayBuilder<PreCompCacheKey>.GetInstance(); private readonly ArrayBuilder<SyntaxTree> _preCompTreesToAdd = ArrayBuilder<SyntaxTree>.GetInstance(); internal Builder(CompilationCache previous, Compilation inputCompilation, Compilation compilationWithPostInit) { _previous = previous; _inputCompilation = inputCompilation; _compilationWithPostInit = compilationWithPostInit; } /// <summary> /// Records a post-init <see cref="SyntaxTree"/> in driver order. The flat list of /// post-init trees participates in the cache key. /// </summary> public void AddPostInitTree(SyntaxTree tree) => _postInitTrees.Add(tree); /// <summary> /// Records a pre-compilation tree: contributes to the cache key (via generator /// index, hint name, text reference, and parse options) and to the set of trees /// appended to <c>compilationWithPostInit</c> on a cache miss. /// </summary> public void AddPreCompTree(int generatorIndex, GeneratedSyntaxTree tree) { _preCompKeys.Add(new PreCompCacheKey(generatorIndex, tree.HintName, tree.Text, tree.Tree.Options)); _preCompTreesToAdd.Add(tree.Tree); } /// <summary> /// Decides between reusing the previous run's cache (when its inputs match) or /// producing a new entry. Always returns a cache whose <see cref="CompilationCache.Compilation"/> /// is the compilation the driver should use; reference equality with the source /// <see cref="CompilationCache"/> indicates the cache state is unchanged. /// </summary> public CompilationCache ToImmutableAndFree() { var preCompKeys = _preCompKeys.ToImmutableAndFree(); var postInitTrees = _postInitTrees.ToImmutableAndFree(); var preCompTreesToAdd = _preCompTreesToAdd.ToImmutableAndFree(); if (preCompKeys.IsEmpty) { // No pre-compilation contributions this run. Return a fresh cache holding // just compilationWithPostInit; we deliberately don't attempt to reuse // _previous here because that compilation reference is regenerated every // run (post-init AddSyntaxTrees) and standard-phase consumers expect to // see that fresh reference to preserve their own re-execution semantics. return new CompilationCache(_compilationWithPostInit, _inputCompilation, ImmutableArray<SyntaxTree>.Empty, ImmutableArray<PreCompCacheKey>.Empty); } if (_previous._compilation is not null && ReferenceEquals(_previous._inputCompilation, _inputCompilation) && ReferenceSequenceEqual(_previous._postInitTrees, postInitTrees) && _previous._preCompKeys.AsSpan().SequenceEqual(preCompKeys.AsSpan())) { // Cache hit -- reuse the previous run's compilation reference. This makes // SharedInputNodes.Compilation report Cached for every generator, which is // the whole point of this cache. return _previous; } var newCompilation = preCompTreesToAdd.IsEmpty ? _compilationWithPostInit : _compilationWithPostInit.AddSyntaxTrees(preCompTreesToAdd); return new CompilationCache(newCompilation, _inputCompilation, postInitTrees, preCompKeys); } // Reference-equality SequenceEqual for ImmutableArray<T> where T is a class. // ReferenceEqualityComparer is IEqualityComparer<object?>, so it can't be passed to // ImmutableArray<T>.SequenceEqual's IEqualityComparer<T> parameter directly; this // walks the underlying buffers via spans for an allocation-free comparison. private static bool ReferenceSequenceEqual<T>(ImmutableArray<T> a, ImmutableArray<T> b) where T : class { if (a.Length != b.Length) { return false; } var aSpan = a.AsSpan(); var bSpan = b.AsSpan(); for (int i = 0; i < aSpan.Length; i++) { if (!ReferenceEquals(aSpan[i], bSpan[i])) { return false; } } return true; } } } /// <summary> /// Identity for a single pre-compilation source as fed into the compilation cache. /// Equality is reference-based on <see cref="Text"/> and <see cref="Options"/>; this matches /// what <c>AbstractSourceOutputNode</c> already does — when the pre-compilation output's /// inputs are unchanged it returns the previous <see cref="GeneratedSourceText"/> with the /// same <see cref="SourceText"/> reference, so reference equality is the right level here. /// </summary> internal readonly struct PreCompCacheKey : IEquatable<PreCompCacheKey> { public PreCompCacheKey(int generatorIndex, string hintName, SourceText text, ParseOptions options) { GeneratorIndex = generatorIndex; HintName = hintName; Text = text; Options = options; } public int GeneratorIndex { get; } public string HintName { get; } public SourceText Text { get; } public ParseOptions Options { get; } public bool Equals(PreCompCacheKey other) => GeneratorIndex == other.GeneratorIndex && string.Equals(HintName, other.HintName, StringComparison.OrdinalIgnoreCase) && ReferenceEquals(Text, other.Text) && ReferenceEquals(Options, other.Options); public override bool Equals(object? obj) => obj is PreCompCacheKey k && Equals(k); public override int GetHashCode() => Roslyn.Utilities.Hash.Combine( GeneratorIndex, Roslyn.Utilities.Hash.Combine( StringComparer.OrdinalIgnoreCase.GetHashCode(HintName), Roslyn.Utilities.Hash.Combine( System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(Text), System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(Options)))); } }