|
// 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.IO;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Storage;
using Microsoft.VisualStudio.RpcContracts.Caching;
using Nerdbank.Streams;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Storage
{
/// <summary>
/// Implementation of Roslyn's <see cref="IPersistentStorage"/> sitting on top of the platform's cloud storage
/// system.
/// </summary>
internal class CloudCachePersistentStorage : AbstractPersistentStorage
{
private static readonly ObjectPool<byte[]> s_byteArrayPool = new(() => new byte[Checksum.HashSize]);
/// <remarks>
/// We do not need to store anything specific about the solution in this key as the platform cloud cache is
/// already keyed to the current solution. So this just allows us to store values considering that as the root.
/// </remarks>
private static readonly CacheContainerKey s_solutionKey = new("Roslyn.Solution");
/// <summary>
/// Cache from project green nodes to the container keys we've computed for it (and the documents inside of it).
/// We can avoid computing these container keys when called repeatedly for the same projects/documents.
/// </summary>
private static readonly ConditionalWeakTable<ProjectState, ProjectContainerKeyCache> s_projectToContainerKeyCache = new();
private readonly ConditionalWeakTable<ProjectState, ProjectContainerKeyCache>.CreateValueCallback _projectToContainerKeyCacheCallback;
/// <summary>
/// Underlying cache service (owned by platform team) responsible for actual storage and retrieval of data.
/// </summary>
private readonly ICacheService _cacheService;
public CloudCachePersistentStorage(
ICacheService cacheService,
SolutionKey solutionKey,
string workingFolderPath,
string relativePathBase,
string databaseFilePath)
: base(workingFolderPath, relativePathBase, databaseFilePath)
{
_cacheService = cacheService;
_projectToContainerKeyCacheCallback = ps => new ProjectContainerKeyCache(relativePathBase, ProjectKey.ToProjectKey(solutionKey, ps));
}
public sealed override void Dispose()
=> (_cacheService as IDisposable)?.Dispose();
public sealed override ValueTask DisposeAsync()
{
Dispose();
return ValueTaskFactory.CompletedTask;
}
/// <summary>
/// Maps our own roslyn key to the appropriate key to use for the cloud cache system. To avoid lots of
/// allocations we cache these (weakly) so if the same keys are used we can use the same platform keys.
/// </summary>
private CacheContainerKey? GetContainerKey(ProjectKey projectKey, Project? project)
{
return project != null
? s_projectToContainerKeyCache.GetValue(project.State, _projectToContainerKeyCacheCallback).ProjectContainerKey
: ProjectContainerKeyCache.CreateProjectContainerKey(this.SolutionFilePath, projectKey);
}
/// <summary>
/// Maps our own roslyn key to the appropriate key to use for the cloud cache system. To avoid lots of
/// allocations we cache these (weakly) so if the same keys are used we can use the same platform keys.
/// </summary>
private CacheContainerKey? GetContainerKey(
DocumentKey documentKey, Document? document)
{
return document != null
? s_projectToContainerKeyCache.GetValue(document.Project.State, _projectToContainerKeyCacheCallback).GetDocumentContainerKey(document.State)
: ProjectContainerKeyCache.CreateDocumentContainerKey(this.SolutionFilePath, documentKey);
}
public sealed override Task<bool> ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken)
=> ChecksumMatchesAsync(name, checksum, s_solutionKey, cancellationToken);
protected sealed override Task<bool> ChecksumMatchesAsync(ProjectKey projectKey, Project? project, string name, Checksum checksum, CancellationToken cancellationToken)
=> ChecksumMatchesAsync(name, checksum, GetContainerKey(projectKey, project), cancellationToken);
protected sealed override Task<bool> ChecksumMatchesAsync(DocumentKey documentKey, Document? document, string name, Checksum checksum, CancellationToken cancellationToken)
=> ChecksumMatchesAsync(name, checksum, GetContainerKey(documentKey, document), cancellationToken);
private async Task<bool> ChecksumMatchesAsync(string name, Checksum checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken)
{
// If we failed to get a container key (for example, because the client is referencing a file not under the
// solution folder) then we can't proceed.
if (containerKey == null)
return false;
using var bytes = s_byteArrayPool.GetPooledObject();
checksum.WriteTo(bytes.Object);
return await _cacheService.CheckExistsAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, cancellationToken).ConfigureAwait(false);
}
public sealed override Task<Stream?> ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken)
=> ReadStreamAsync(name, checksum, s_solutionKey, cancellationToken);
protected sealed override Task<Stream?> ReadStreamAsync(ProjectKey projectKey, Project? project, string name, Checksum? checksum, CancellationToken cancellationToken)
=> ReadStreamAsync(name, checksum, GetContainerKey(projectKey, project), cancellationToken);
protected sealed override Task<Stream?> ReadStreamAsync(DocumentKey documentKey, Document? document, string name, Checksum? checksum, CancellationToken cancellationToken)
=> ReadStreamAsync(name, checksum, GetContainerKey(documentKey, document), cancellationToken);
private async Task<Stream?> ReadStreamAsync(string name, Checksum? checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken)
{
// If we failed to get a container key (for example, because the client is referencing a file not under the
// solution folder) then we can't proceed.
if (containerKey == null)
return null;
if (checksum == null)
{
return await ReadStreamAsync(new CacheItemKey(containerKey.Value, name), cancellationToken).ConfigureAwait(false);
}
else
{
using var bytes = s_byteArrayPool.GetPooledObject();
checksum.WriteTo(bytes.Object);
return await ReadStreamAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, cancellationToken).ConfigureAwait(false);
}
}
private async Task<Stream?> ReadStreamAsync(CacheItemKey key, CancellationToken cancellationToken)
{
var pipe = new Pipe();
var result = await _cacheService.TryGetItemAsync(key, pipe.Writer, cancellationToken).ConfigureAwait(false);
if (!result)
return null;
// Clients will end up doing blocking reads on the synchronous stream we return from this. This can
// negatively impact our calls as that will cause sync blocking on the async work to fill the pipe. To
// alleviate that issue, we actually asynchronously read in the entire stream into memory inside the reader
// and then pass that out. This should not be a problem in practice as PipeReader internally intelligently
// uses and pools reasonable sized buffers, preventing us from exacerbating the GC or causing LOH
// allocations.
return await pipe.Reader.AsPrebufferedStreamAsync(cancellationToken).ConfigureAwait(false);
}
public sealed override Task<bool> WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
=> WriteStreamAsync(name, stream, checksum, s_solutionKey, cancellationToken);
protected sealed override Task<bool> WriteStreamAsync(ProjectKey projectKey, Project? project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
=> WriteStreamAsync(name, stream, checksum, GetContainerKey(projectKey, project), cancellationToken);
protected sealed override Task<bool> WriteStreamAsync(DocumentKey documentKey, Document? document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken)
=> WriteStreamAsync(name, stream, checksum, GetContainerKey(documentKey, document), cancellationToken);
private async Task<bool> WriteStreamAsync(string name, Stream stream, Checksum? checksum, CacheContainerKey? containerKey, CancellationToken cancellationToken)
{
// If we failed to get a container key (for example, because the client is referencing a file not under the
// solution folder) then we can't proceed.
if (containerKey == null)
return false;
if (checksum == null)
{
return await WriteStreamAsync(new CacheItemKey(containerKey.Value, name), stream, cancellationToken).ConfigureAwait(false);
}
else
{
using var bytes = s_byteArrayPool.GetPooledObject();
checksum.WriteTo(bytes.Object);
return await WriteStreamAsync(new CacheItemKey(containerKey.Value, name) { Version = bytes.Object }, stream, cancellationToken).ConfigureAwait(false);
}
}
private async Task<bool> WriteStreamAsync(CacheItemKey key, Stream stream, CancellationToken cancellationToken)
{
await _cacheService.SetItemAsync(key, PipeReader.Create(stream), shareable: false, cancellationToken).ConfigureAwait(false);
return true;
}
}
}
|