|
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis
{
/// <summary>
/// A recoverable TextAndVersion source that saves its text to temporary storage.
/// </summary>
internal sealed class RecoverableTextAndVersion : ITextVersionable, ITextAndVersionSource
{
private readonly SolutionServices _services;
// Starts as ITextAndVersionSource and is replaced with RecoverableText when the TextAndVersion value is requested.
// At that point the initial source is no longer referenced and can be garbage collected.
private object _initialSourceOrRecoverableText;
public bool CanReloadText { get; }
public RecoverableTextAndVersion(ITextAndVersionSource initialSource, SolutionServices services)
{
_initialSourceOrRecoverableText = initialSource;
_services = services;
CanReloadText = initialSource.CanReloadText;
}
/// <returns>
/// True if the <paramref name="source"/> is available, false if <paramref name="text"/> is returned.
/// </returns>
private bool TryGetInitialSourceOrRecoverableText([NotNullWhen(true)] out ITextAndVersionSource? source, [NotNullWhen(false)] out RecoverableText? text)
{
// store to local to avoid race:
var sourceOrRecoverableText = _initialSourceOrRecoverableText;
source = sourceOrRecoverableText as ITextAndVersionSource;
if (source != null)
{
text = null;
return true;
}
text = (RecoverableText)sourceOrRecoverableText;
return false;
}
public ITemporaryTextStorageInternal? Storage
=> (_initialSourceOrRecoverableText as RecoverableText)?.Storage;
public bool TryGetValue(LoadTextOptions options, [MaybeNullWhen(false)] out TextAndVersion value)
{
if (TryGetInitialSourceOrRecoverableText(out var source, out var recoverableText))
{
return source.TryGetValue(options, out value);
}
if (recoverableText.TryGetValue(out var text) && recoverableText.LoadTextOptions == options)
{
value = TextAndVersion.Create(text, recoverableText.Version, recoverableText.LoadDiagnostic);
return true;
}
value = null;
return false;
}
public bool TryGetTextVersion(LoadTextOptions options, out VersionStamp version)
{
if (_initialSourceOrRecoverableText is ITextVersionable textVersionable)
{
return textVersionable.TryGetTextVersion(options, out version);
}
if (TryGetValue(options, out var textAndVersion))
{
version = textAndVersion.Version;
return true;
}
version = default;
return false;
}
public TextAndVersion GetValue(LoadTextOptions options, CancellationToken cancellationToken)
{
if (_initialSourceOrRecoverableText is ITextAndVersionSource source)
{
// replace initial source with recoverable text if it hasn't been replaced already:
Interlocked.CompareExchange(
ref _initialSourceOrRecoverableText,
value: new RecoverableText(source, source.GetValue(options, cancellationToken), options, _services),
comparand: source);
}
// If we have a recoverable text but the options it was created for do not match the current options
// and the initial source supports reloading, reload and replace the recoverable text.
var recoverableText = (RecoverableText)_initialSourceOrRecoverableText;
if (recoverableText.LoadTextOptions != options && recoverableText.InitialSource != null)
{
Interlocked.Exchange(
ref _initialSourceOrRecoverableText,
new RecoverableText(recoverableText.InitialSource, recoverableText.InitialSource.GetValue(options, cancellationToken), options, _services));
}
recoverableText = (RecoverableText)_initialSourceOrRecoverableText;
return recoverableText.ToTextAndVersion(recoverableText.GetValue(cancellationToken));
}
public async Task<TextAndVersion> GetValueAsync(LoadTextOptions options, CancellationToken cancellationToken)
{
if (_initialSourceOrRecoverableText is ITextAndVersionSource source)
{
// replace initial source with recoverable text if it hasn't been replaced already:
Interlocked.CompareExchange(
ref _initialSourceOrRecoverableText,
value: new RecoverableText(source, await source.GetValueAsync(options, cancellationToken).ConfigureAwait(false), options, _services),
comparand: source);
}
// If we have a recoverable text but the options it was created for do not match the current options
// and the initial source supports reloading, reload and replace the recoverable text.
var recoverableText = (RecoverableText)_initialSourceOrRecoverableText;
if (recoverableText.LoadTextOptions != options && recoverableText.InitialSource != null)
{
Interlocked.Exchange(
ref _initialSourceOrRecoverableText,
new RecoverableText(recoverableText.InitialSource, await recoverableText.InitialSource.GetValueAsync(options, cancellationToken).ConfigureAwait(false), options, _services));
}
recoverableText = (RecoverableText)_initialSourceOrRecoverableText;
return recoverableText.ToTextAndVersion(await recoverableText.GetValueAsync(cancellationToken).ConfigureAwait(false));
}
private sealed class RecoverableText : WeaklyCachedRecoverableValueSource<SourceText>, ITextVersionable
{
private readonly ITemporaryStorageServiceInternal _storageService;
public readonly VersionStamp Version;
public readonly Diagnostic? LoadDiagnostic;
public readonly ITextAndVersionSource? InitialSource;
public readonly LoadTextOptions LoadTextOptions;
public ITemporaryTextStorageInternal? _storage;
public RecoverableText(ITextAndVersionSource source, TextAndVersion textAndVersion, LoadTextOptions options, SolutionServices services)
: base(textAndVersion.Text)
{
_storageService = services.GetRequiredService<ITemporaryStorageServiceInternal>();
Version = textAndVersion.Version;
LoadDiagnostic = textAndVersion.LoadDiagnostic;
LoadTextOptions = options;
if (source.CanReloadText)
{
// reloadable source must not cache results
Contract.ThrowIfTrue(source is LoadableTextAndVersionSource { CacheResult: true });
InitialSource = source;
}
}
public TextAndVersion ToTextAndVersion(SourceText text)
=> TextAndVersion.Create(text, Version, LoadDiagnostic);
public ITemporaryTextStorageInternal? Storage => _storage;
protected override async Task<SourceText> RecoverAsync(CancellationToken cancellationToken)
{
Contract.ThrowIfNull(_storage);
using (Logger.LogBlock(FunctionId.Workspace_Recoverable_RecoverTextAsync, cancellationToken))
{
return await _storage.ReadTextAsync(cancellationToken).ConfigureAwait(false);
}
}
protected override SourceText Recover(CancellationToken cancellationToken)
{
Contract.ThrowIfNull(_storage);
using (Logger.LogBlock(FunctionId.Workspace_Recoverable_RecoverText, cancellationToken))
{
return _storage.ReadText(cancellationToken);
}
}
protected override async Task SaveAsync(SourceText text, CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(_storage == null); // Cannot save more than once
var storage = _storageService.CreateTemporaryTextStorage();
await storage.WriteTextAsync(text, cancellationToken).ConfigureAwait(false);
// make sure write is done before setting _storage field
Interlocked.CompareExchange(ref _storage, storage, null);
}
public bool TryGetTextVersion(LoadTextOptions options, out VersionStamp version)
{
version = Version;
return options == LoadTextOptions;
}
}
}
}
|