File: Host\RemoteSolutionCache.cs
Web Access
Project: src\src\Workspaces\Remote\ServiceHub\Microsoft.CodeAnalysis.Remote.ServiceHub.csproj (Microsoft.CodeAnalysis.Remote.ServiceHub)
// 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.Collections.Generic;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Remote;
 
/// <summary>
/// LRU cache of checksum+solution pairs.  Used to keep track of the last few solutions the remote server knows about,
/// helping to avoid unnecessary syncs/recreations of those solutions while many requests are coming into the server.
/// Not threadsafe.  Only use while under a lock.
/// </summary>
internal sealed class RemoteSolutionCache<TChecksum, TSolution>
    where TChecksum : struct
    where TSolution : class
{
    /// <summary>
    /// The max number of solution instances we'll hold onto at a time.
    /// </summary>
    private readonly int _maxCapacity;
 
    /// <summary>
    /// The total history kept.  Used to record telemetry about how useful it would be to increase the max capacity.
    /// </summary>
    private readonly int _totalHistory;
 
    /// <summary>
    /// Keep track of what index we found find a checksum at in the history.  This will help us tell both if the cache
    /// is too large, or if it's too small.
    /// </summary>
    private readonly HistogramLogAggregator<int>.HistogramCounter _cacheHitIndexHistogram;
 
    /// <summary>
    /// The number of times we successfully found a solution.  When this happens we'll increment <see
    /// cref="_cacheHitIndexHistogram"/>.
    /// </summary>
    private int _cacheHits;
 
    /// <summary>
    /// The number of times we failed to find a solution, but could have found it if we cached more items (up to
    /// TotalHistory).  When this happens we'll increment <see cref="_cacheHitIndexHistogram"/>.
    /// </summary>
    private int _cacheMissesInHistory;
 
    /// <summary>
    /// The number of times we failed to find a solution, and would not have found it even if we didn't cache more items.
    /// </summary>
    private int _cacheMissesNotInHistory;
 
    /// <summary>
    /// The list of checksum+solution pairs.  Note: only the first <see cref="_maxCapacity"/> items will actually point
    /// at a non-null solution.  The ones after that will point at <see langword="null"/>.  We store both so that we can
    /// collect useful telemetry on how much benefit we would get by having a larger history.
    /// </summary>
    private readonly LinkedList<CacheNode> _cacheNodes = new();
 
    public RemoteSolutionCache(int maxCapacity = 4, int totalHistory = 16)
    {
        _maxCapacity = maxCapacity;
        _totalHistory = totalHistory;
        _cacheHitIndexHistogram = new(bucketSize: 1, maxBucketValue: int.MaxValue, bucketCount: _totalHistory + 1);
    }
 
    private void FindAndMoveNodeToFront(TChecksum checksum)
    {
        var index = 0;
        for (var current = _cacheNodes.First; current != null; current = current.Next, index++)
        {
            if (current.Value.Checksum.Equals(checksum))
            {
                // Found the item.  
                if (index > 0)
                {
                    // If it's not already at the front, move it there.
                    _cacheNodes.Remove(current);
                    _cacheNodes.AddFirst(current);
                }
 
                return;
            }
        }
 
        // Didn't find the item at all.  Just add to the front.
        _cacheNodes.AddFirst(new CacheNode(checksum));
    }
 
    public void Add(TChecksum checksum, TSolution solution)
    {
        Contract.ThrowIfTrue(_cacheNodes.Count > _totalHistory);
 
        FindAndMoveNodeToFront(checksum);
 
        Contract.ThrowIfTrue(_cacheNodes.Count > _totalHistory + 1);
        Contract.ThrowIfNull(_cacheNodes.First);
        Contract.ThrowIfFalse(_cacheNodes.First.Value.Checksum.Equals(checksum));
 
        // Ensure we're holding onto the solution.
        _cacheNodes.First.Value.Solution = solution;
 
        // Now, if our history is too long, remove the last item.
        if (_cacheNodes.Count == _totalHistory + 1)
            _cacheNodes.RemoveLast();
 
        // Finally, ensure that only the first `MaxCapacity` are pointing at solutions and the rest are not.
        var index = 0;
        for (var current = _cacheNodes.First; current != null; current = current.Next, index++)
        {
            if (index >= _maxCapacity)
            {
                current.Value.Solution = null;
                break;
            }
        }
    }
 
    public TSolution? Find(TChecksum checksum)
    {
        // Note: we intentionally do not move an item when we find it.  That's because our caller will always call 'Add'
        // with the found solution afterwards.  This will ensure that that solution makes it to the front of the line.
        var index = 0;
        for (var current = _cacheNodes.First; current != null; current = current.Next, index++)
        {
            if (current.Value.Checksum.Equals(checksum))
            {
                // Found it!
                if (current.Value.Solution is null)
                {
                    // Track that we would have been able to return this if our history was longer.
                    _cacheMissesInHistory++;
                }
                else
                {
                    // Success!
                    _cacheHits++;
                }
 
                _cacheHitIndexHistogram.IncreaseCount(index);
                return current.Value.Solution;
            }
        }
 
        // Couldn't find it at all, even in the history.
        _cacheMissesNotInHistory++;
        return null;
    }
 
    public void ReportTelemetry()
    {
        Logger.Log(FunctionId.RemoteWorkspace_SolutionCachingStatistics, KeyValueLogMessage.Create(m =>
        {
            m.Add(nameof(_cacheHits), _cacheHits);
            m.Add(nameof(_cacheMissesInHistory), _cacheMissesInHistory);
            m.Add(nameof(_cacheMissesNotInHistory), _cacheMissesNotInHistory);
            _cacheHitIndexHistogram.WriteTelemetryPropertiesTo(m, prefix: nameof(_cacheHitIndexHistogram));
        }));
    }
 
    public void AddAllTo(HashSet<TSolution> solutions)
    {
        foreach (var node in _cacheNodes)
            solutions.AddIfNotNull(node.Solution);
    }
 
    private sealed class CacheNode(TChecksum checksum)
    {
        public readonly TChecksum Checksum = checksum;
        public TSolution? Solution;
    }
}