|
// 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.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis;
internal sealed partial class RecoverableTextAndVersion
{
/// <summary>
/// This class holds onto a <see cref="SourceText"/> value weakly, but can save its value and recover it on demand
/// if needed. The value is initially strongly held, until the first time that <see cref="GetValue"/> or <see
/// cref="GetValueAsync"/> is called. At that point, it will be dumped to secondary storage, and retrieved and
/// weakly held from that point on in the future.
/// </summary>
private sealed partial class RecoverableText
{
// enforce saving in a queue so save's don't overload the thread pool.
private static readonly AsyncBatchingWorkQueue<(RecoverableText recoverableText, SourceText sourceText)> s_saveQueue =
new(TimeSpan.Zero,
SaveAllAsync,
AsynchronousOperationListenerProvider.NullListener,
CancellationToken.None);
/// <summary>
/// Lazily created. Access via the <see cref="Gate"/> property.
/// </summary>
private SemaphoreSlim? _lazyGate;
/// <summary>
/// Whether or not we've saved our value to secondary storage. Used so we only do that once.
/// </summary>
private bool _saved;
/// <summary>
/// Initial strong reference to the SourceText this is initialized with. Will be used to respond to the first
/// request to get the value, at which point it will be dumped into secondary storage.
/// </summary>
private SourceText? _initialValue;
/// <summary>
/// Weak reference to the value last returned from this value source. Will thus return the same value as long
/// as something external is holding onto it.
/// </summary>
private WeakReference<SourceText>? _weakReference;
private SemaphoreSlim Gate => InterlockedOperations.Initialize(ref _lazyGate, SemaphoreSlimFactory.Instance);
/// <summary>
/// Attempts to get the value, but only through the weak reference. This will only succeed *after* the value
/// has been retrieved at least once, and has thus then been save to secondary storage.
/// </summary>
private bool TryGetWeakValue([NotNullWhen(true)] out SourceText? value)
{
value = null;
var weakReference = _weakReference;
return weakReference != null && weakReference.TryGetTarget(out value) && value != null;
}
/// <summary>
/// Attempts to get the value, either through our strong or weak reference.
/// </summary>
private bool TryGetStrongOrWeakValue([NotNullWhen(true)] out SourceText? value)
{
// See if we still have the constant value stored. If so, we can trivially return that.
value = _initialValue;
if (value != null)
return true;
// If not, see if it's something someone else is holding into, and is available through the weak-ref.
return TryGetWeakValue(out value);
}
public bool TryGetValue([MaybeNullWhen(false)] out SourceText value)
=> TryGetStrongOrWeakValue(out value);
public SourceText GetValue(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// if the value is currently being held weakly, then we can return that immediately as we know we will have
// kicked off the work to save the value to secondary storage.
if (TryGetWeakValue(out var instance))
return instance;
// Otherwise, we're either holding the value strongly, or we need to recover it from secondary storage.
using (Gate.DisposableWait(cancellationToken))
{
if (!TryGetStrongOrWeakValue(out instance))
instance = Recover(cancellationToken);
// If the value was strongly held, kick off the work to write it to secondary storage and release the
// strong reference to it.
UpdateWeakReferenceAndEnqueueSaveTask_NoLock(instance);
return instance;
}
}
public async Task<SourceText> GetValueAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// if the value is currently being held weakly, then we can return that immediately as we know we will have
// kicked off the work to save the value to secondary storage.
if (TryGetWeakValue(out var instance))
return instance;
using (await Gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
if (!TryGetStrongOrWeakValue(out instance))
instance = await RecoverAsync(cancellationToken).ConfigureAwait(false);
// If the value was strongly held, kick off the work to write it to secondary storage and release the
// strong reference to it.
UpdateWeakReferenceAndEnqueueSaveTask_NoLock(instance);
return instance;
}
}
/// <summary>
/// Kicks off the work to save this instance to secondary storage at some point in the future. Once that save
/// occurs successfully, we will drop our cached data and return values from that storage instead.
/// </summary>
private void UpdateWeakReferenceAndEnqueueSaveTask_NoLock(SourceText instance)
{
Contract.ThrowIfTrue(Gate.CurrentCount != 0);
_weakReference ??= new WeakReference<SourceText>(instance);
_weakReference.SetTarget(instance);
// Ensure we only save once.
if (!_saved)
{
_saved = true;
s_saveQueue.AddWork((this, instance));
}
}
private static async ValueTask SaveAllAsync(
ImmutableSegmentedList<(RecoverableText recoverableText, SourceText sourceText)> list, CancellationToken cancellationToken)
{
foreach (var (recoverableText, sourceText) in list)
await recoverableText.SaveAsync(sourceText, cancellationToken).ConfigureAwait(false);
}
}
}
|