File: ExternalAccess\UnitTesting\SolutionCrawler\UnitTestingIdleProcessor.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.SolutionCrawler;
 
internal abstract class UnitTestingIdleProcessor(
    IAsynchronousOperationListener listener,
    TimeSpan backOffTimeSpan,
    CancellationToken cancellationToken)
{
    private static readonly TimeSpan s_minimumDelay = TimeSpan.FromMilliseconds(50);
 
    private readonly object _gate = new();
 
    protected readonly IAsynchronousOperationListener Listener = listener;
    protected readonly CancellationToken CancellationToken = cancellationToken;
    protected readonly TimeSpan BackOffTimeSpan = backOffTimeSpan;
 
    // points to processor task
    private Task? _processorTask;
 
    // there is one thread that writes to it and one thread reads from it
    private SharedStopwatch _timeSinceLastAccess = SharedStopwatch.StartNew();
 
    /// <summary>
    /// Whether or not this processor is paused.  As long as it is paused, it will not start executing new work,
    /// even if <see cref="BackOffTimeSpan"/> has been met.
    /// </summary>
    private bool _isPaused_doNotAccessDirectly;
 
    protected abstract Task WaitAsync(CancellationToken cancellationToken);
    protected abstract Task ExecuteAsync();
 
    /// <summary>
    /// Will be called in a serialized fashion (i.e. never concurrently).
    /// </summary>
    protected abstract void OnPaused();
 
    protected void Start()
    {
        Contract.ThrowIfFalse(_processorTask == null);
        _processorTask = Task.Factory.SafeStartNewFromAsync(ProcessAsync, CancellationToken, TaskScheduler.Default);
    }
 
    protected void UpdateLastAccessTime()
        => _timeSinceLastAccess = SharedStopwatch.StartNew();
 
    /// <summary>
    /// Whether or not we are paused due to a global operation being in effect.
    /// </summary>
    protected bool GetIsPaused()
    {
        lock (_gate)
            return _isPaused_doNotAccessDirectly;
    }
 
    /// <summary>
    /// Whether or not enough time has passed since the last time we were asked to back off.
    /// </summary>
    protected bool ShouldContinueToBackOff()
        => _timeSinceLastAccess.Elapsed < BackOffTimeSpan;
 
    protected void SetIsPaused(bool isPaused)
    {
        lock (_gate)
        {
            // We should never try to transition from paused state to paused state.  That would indicate we
            // missed some resume call, or that the pause-notification are not serialized.  Note: we cannot make
            // the opposite assertion.  We start in the resumed state, and we might then get a call to resume if
            // we were started while in the *middle* of some global operation.
            if (isPaused)
                Contract.ThrowIfTrue(_isPaused_doNotAccessDirectly);
 
            _isPaused_doNotAccessDirectly = isPaused;
        }
 
        // Let subclasses know we're paused so they can change what they're doing accordingly.
        if (isPaused)
            OnPaused();
    }
 
    /// <returns><see langword="true"/> if the delay compeleted normally; otherwise, <see langword="false"/> if the
    /// delay completed due to a request to expedite the delay.</returns>
    protected async Task<bool> WaitForIdleAsync(IExpeditableDelaySource expeditableDelaySource)
    {
        while (true)
        {
            this.CancellationToken.ThrowIfCancellationRequested();
 
            // If we're not paused, and enough time has elapsed, then we're done.  Otherwise, ensure we wait at
            // least s_minimumDelay and check again in the future.
            if (!GetIsPaused() && !ShouldContinueToBackOff())
                return true;
 
            var timeLeft = BackOffTimeSpan - _timeSinceLastAccess.Elapsed;
            var delayTimeSpan = TimeSpan.FromMilliseconds(Math.Max(s_minimumDelay.TotalMilliseconds, timeLeft.TotalMilliseconds));
            if (!await expeditableDelaySource.Delay(delayTimeSpan, CancellationToken).ConfigureAwait(false))
            {
                // The delay terminated early to accommodate a blocking operation. Make sure to yield so low
                // priority (on idle) operations get a chance to be triggered.
                //
                // 📝 At the time this was discovered, it was not clear exactly why the yield (previously delay)
                // was needed in order to avoid live-lock scenarios.
                await Task.Yield().ConfigureAwait(false);
                return false;
            }
        }
    }
 
    private async Task ProcessAsync()
    {
        while (!CancellationToken.IsCancellationRequested)
        {
            try
            {
                // wait for next item available
                await WaitAsync(CancellationToken).ConfigureAwait(false);
                CancellationToken.ThrowIfCancellationRequested();
 
                using (Listener.BeginAsyncOperation("ProcessAsync"))
                {
                    // we have items but workspace is busy. wait for idle.
                    await WaitForIdleAsync(Listener).ConfigureAwait(false);
                    CancellationToken.ThrowIfCancellationRequested();
 
                    await ExecuteAsync().ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException)
            {
                // ignore cancellation exception
            }
            catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e))
            {
                // In case any error happen during the execution, don't exit the loop and continue to work on the next item.
            }
        }
    }
 
    public virtual Task AsyncProcessorTask
        => _processorTask ?? Task.CompletedTask;
}