File: ExternalAccess\UnitTesting\SolutionCrawler\UnitTestingSolutionCrawlerProgressReporter.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;
 
namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.SolutionCrawler;
 
internal sealed partial class UnitTestingSolutionCrawlerRegistrationService : IUnitTestingSolutionCrawlerRegistrationService
{
    /// <summary>
    /// Progress reporter
    /// 
    /// this progress reporter is a best effort implementation. it doesn't stop world to find out accurate data
    /// 
    /// what this reporter care is we show start/stop background work and show things are moving or paused
    /// without too much cost.
    /// 
    /// due to how solution cralwer calls Start/Stop (see caller of those 2), those 2 can't have a race
    /// and that is all we care for this reporter
    /// </summary>
    internal sealed class UnitTestingSolutionCrawlerProgressReporter : IUnitTestingSolutionCrawlerProgressReporter
    {
        // we use ref count here since solution crawler has multiple queues per priority
        // where an item can be enqueued and dequeued independently. 
        // first item added in any of those queues will cause the "start" event to be sent
        // and the very last item processed from those queues will cause "stop" event to be sent
        // evaluating and paused is also ref counted since work in the lower priority queue can
        // be canceled due to new higher priority work item enqueued to higher queue.
        // but before lower priority work actually exit due to cancellation, higher work could
        // start processing. causing an overlap. the ref count make sure that exiting lower
        // work doesn't flip evaluating state to paused state.
        private int _progressStartCount = 0;
        private int _progressEvaluateCount = 0;
 
        public event EventHandler<UnitTestingProgressData>? ProgressChanged;
 
        public bool InProgress => _progressStartCount > 0;
 
        public void Start() => ChangeProgressStatus(ref _progressStartCount, UnitTestingProgressStatus.Started);
        public void Stop() => ChangeProgressStatus(ref _progressStartCount, UnitTestingProgressStatus.Stopped);
 
        private void Evaluate() => ChangeProgressStatus(ref _progressEvaluateCount, UnitTestingProgressStatus.Evaluating);
        private void Pause() => ChangeProgressStatus(ref _progressEvaluateCount, UnitTestingProgressStatus.Paused);
 
        public void UpdatePendingItemCount(int pendingItemCount)
        {
            if (_progressStartCount > 0)
            {
                var progressData = new UnitTestingProgressData(UnitTestingProgressStatus.PendingItemCountUpdated, pendingItemCount);
                OnProgressChanged(progressData);
            }
        }
 
        /// <summary>
        /// Allows the solution crawler to start evaluating work enqueued to it. 
        /// Returns an IDisposable that the caller must dispose of to indicate that it no longer needs the crawler to continue evaluating. 
        /// Multiple callers can call into this simultaneously. 
        /// Only when the last one actually disposes the scope-object will the crawler 
        /// actually revert back to the paused state where no work proceeds.
        /// </summary>
        public IDisposable GetEvaluatingScope()
            => new UnitTestingProgressStatusRAII(this);
 
        private void ChangeProgressStatus(ref int referenceCount, UnitTestingProgressStatus status)
        {
            var start = status is UnitTestingProgressStatus.Started or UnitTestingProgressStatus.Evaluating;
            if (start ? (Interlocked.Increment(ref referenceCount) == 1) : (Interlocked.Decrement(ref referenceCount) == 0))
            {
                var progressData = new UnitTestingProgressData(status, pendingItemCount: null);
                OnProgressChanged(progressData);
            }
        }
 
        private void OnProgressChanged(UnitTestingProgressData progressData)
            => ProgressChanged?.Invoke(this, progressData);
 
        private readonly struct UnitTestingProgressStatusRAII : IDisposable
        {
            private readonly UnitTestingSolutionCrawlerProgressReporter _owner;
 
            public UnitTestingProgressStatusRAII(UnitTestingSolutionCrawlerProgressReporter owner)
            {
                _owner = owner;
                _owner.Evaluate();
            }
 
            public void Dispose()
                => _owner.Pause();
        }
    }
 
    /// <summary>
    /// reporter that doesn't do anything
    /// </summary>
    private sealed class UnitTestingNullReporter : IUnitTestingSolutionCrawlerProgressReporter
    {
        public static readonly UnitTestingNullReporter Instance = new();
 
        public bool InProgress => false;
 
        public event EventHandler<UnitTestingProgressData> ProgressChanged
        {
            add { }
            remove { }
        }
    }
}