File: Host\AssetProvider.cs
Web Access
Project: ..\..\..\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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Serialization;
 
namespace Microsoft.CodeAnalysis.Remote
{
    /// <summary>
    /// This service provide a way to get roslyn objects from checksum
    /// </summary>
    internal sealed class AssetProvider : AbstractAssetProvider
    {
        private readonly Checksum _solutionChecksum;
        private readonly ISerializerService _serializerService;
        private readonly SolutionAssetCache _assetCache;
        private readonly IAssetSource _assetSource;
 
        public AssetProvider(Checksum solutionChecksum, SolutionAssetCache assetCache, IAssetSource assetSource, ISerializerService serializerService)
        {
            _solutionChecksum = solutionChecksum;
            _assetCache = assetCache;
            _assetSource = assetSource;
            _serializerService = serializerService;
        }
 
        public override async ValueTask<T> GetAssetAsync<T>(Checksum checksum, CancellationToken cancellationToken)
        {
            Debug.Assert(checksum != Checksum.Null);
 
            if (_assetCache.TryGetAsset<T>(checksum, out var asset))
            {
                return asset;
            }
 
            using (Logger.LogBlock(FunctionId.AssetService_GetAssetAsync, Checksum.GetChecksumLogInfo, checksum, cancellationToken))
            {
                var value = await RequestAssetAsync(checksum, cancellationToken).ConfigureAwait(false);
 
                // Attempt to add the value to the cache.  If someone else beat us, we'll return their value.
                return (T)_assetCache.GetOrAdd(checksum, value);
            }
        }
 
        public async ValueTask<ImmutableArray<ValueTuple<Checksum, T>>> GetAssetsAsync<T>(HashSet<Checksum> checksums, CancellationToken cancellationToken)
        {
            // this only works when caller wants to get same kind of assets at once
 
            // bulk synchronize checksums first
            var syncer = new ChecksumSynchronizer(this);
            await syncer.SynchronizeAssetsAsync(checksums, cancellationToken).ConfigureAwait(false);
 
            using var _ = ArrayBuilder<ValueTuple<Checksum, T>>.GetInstance(checksums.Count, out var list);
            foreach (var checksum in checksums)
                list.Add(ValueTuple.Create(checksum, await GetAssetAsync<T>(checksum, cancellationToken).ConfigureAwait(false)));
 
            return list.ToImmutableAndClear();
        }
 
        public async ValueTask SynchronizeSolutionAssetsAsync(Checksum solutionChecksum, CancellationToken cancellationToken)
        {
            var timer = new Stopwatch();
            timer.Start();
 
            // this will pull in assets that belong to the given solution checksum to this remote host.
            // this one is not supposed to be used for functionality but only for perf. that is why it doesn't return anything.
            // to get actual data GetAssetAsync should be used. and that will return actual data and if there is any missing data in cache, GetAssetAsync
            // itself will bring that data in from data source (VS)
 
            // one can call this method to make cache hot for all assets that belong to the solution checksum so that GetAssetAsync call will most likely cache hit.
            // it is most likely since we might change cache hueristic in future which make data to live a lot shorter in the cache, and the data might get expired
            // before one actually consume the data. 
            using (Logger.LogBlock(FunctionId.AssetService_SynchronizeSolutionAssetsAsync, Checksum.GetChecksumLogInfo, solutionChecksum, cancellationToken))
            {
                var syncer = new ChecksumSynchronizer(this);
                await syncer.SynchronizeSolutionAssetsAsync(solutionChecksum, cancellationToken).ConfigureAwait(false);
            }
 
            timer.Stop();
 
            // report telemetry to help correlate slow solution sync with UI delays
            if (timer.ElapsedMilliseconds > 1000)
            {
                Logger.Log(FunctionId.AssetService_Perf, KeyValueLogMessage.Create(map => map["SolutionSyncTime"] = timer.ElapsedMilliseconds));
            }
        }
 
        public async ValueTask SynchronizeProjectAssetsAsync(HashSet<Checksum> projectChecksums, CancellationToken cancellationToken)
        {
            // this will pull in assets that belong to the given project checksum to this remote host.
            // this one is not supposed to be used for functionality but only for perf. that is why it doesn't return anything.
            // to get actual data GetAssetAsync should be used. and that will return actual data and if there is any missing data in cache, GetAssetAsync
            // itself will bring that data in from data source (VS)
 
            // one can call this method to make cache hot for all assets that belong to the project checksum so that GetAssetAsync call will most likely cache hit.
            // it is most likely since we might change cache hueristic in future which make data to live a lot shorter in the cache, and the data might get expired
            // before one actually consume the data. 
            using (Logger.LogBlock(FunctionId.AssetService_SynchronizeProjectAssetsAsync, Checksum.GetChecksumsLogInfo, projectChecksums, cancellationToken))
            {
                var syncer = new ChecksumSynchronizer(this);
                await syncer.SynchronizeProjectAssetsAsync(projectChecksums, cancellationToken).ConfigureAwait(false);
            }
        }
 
        public bool EnsureCacheEntryIfExists(Checksum checksum)
        {
            // this will check whether checksum exists in the cache and if it does,
            // it will touch the entry so that it doesn't expire right after we checked it.
            //
            // even if it got expired after this for whatever reason, functionality wise everything will still work, 
            // just perf will be impacted since we will fetch it from data source (VS)
            return _assetCache.TryGetAsset<object>(checksum, out _);
        }
 
        public async ValueTask SynchronizeAssetsAsync(ISet<Checksum> checksums, CancellationToken cancellationToken)
        {
            Debug.Assert(!checksums.Contains(Checksum.Null));
            if (checksums.Count == 0)
                return;
 
            using (Logger.LogBlock(FunctionId.AssetService_SynchronizeAssetsAsync, Checksum.GetChecksumsLogInfo, checksums, cancellationToken))
            {
                var assets = await RequestAssetsAsync(checksums, cancellationToken).ConfigureAwait(false);
 
                foreach (var (checksum, value) in assets)
                    _assetCache.GetOrAdd(checksum, value);
            }
        }
 
        private async Task<object> RequestAssetAsync(Checksum checksum, CancellationToken cancellationToken)
        {
            Debug.Assert(checksum != Checksum.Null);
 
            using var _ = PooledHashSet<Checksum>.GetInstance(out var checksums);
            checksums.Add(checksum);
 
            var assets = await RequestAssetsAsync(checksums, cancellationToken).ConfigureAwait(false);
            return assets.Single().value;
        }
 
        private async Task<ImmutableArray<(Checksum checksum, object value)>> RequestAssetsAsync(ISet<Checksum> checksums, CancellationToken cancellationToken)
        {
            Debug.Assert(!checksums.Contains(Checksum.Null));
 
            if (checksums.Count == 0)
            {
                return ImmutableArray<(Checksum, object)>.Empty;
            }
 
            return await _assetSource.GetAssetsAsync(_solutionChecksum, checksums, _serializerService, cancellationToken).ConfigureAwait(false);
        }
    }
}