File: Diagnostics\DiagnosticProgressReporter.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.TaskStatusCenter;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics
{
    [Export(typeof(TaskCenterSolutionAnalysisProgressReporter))]
    internal sealed class TaskCenterSolutionAnalysisProgressReporter
    {
        private static readonly TimeSpan s_minimumInterval = TimeSpan.FromMilliseconds(200);
        private static readonly TaskHandlerOptions _options = new()
        {
            Title = ServicesVSResources.Running_low_priority_background_processes,
            ActionsAfterCompletion = CompletionActions.None
        };
 
        private IVsTaskStatusCenterService? _taskCenterService;
 
        #region Fields protected by _lock
 
        /// <summary>
        /// Gate access to reporting sln crawler events so we cannot
        /// report UI changes concurrently.
        /// </summary>
        private readonly object _lock = new();
        private readonly IThreadingContext _threadingContext;
        private readonly VisualStudioWorkspace _workspace;
 
        /// <summary>
        /// Task used to trigger throttled UI updates in an interval
        /// defined by <see cref="s_minimumInterval"/>
        /// Protected from concurrent access by the <see cref="_lock"/>
        /// </summary>
        private Task? _intervalTask;
 
        /// <summary>
        /// Stores the last shown <see cref="ProgressData"/>
        /// Protected from concurrent access by the <see cref="_lock"/>
        /// </summary>
        private ProgressData _lastProgressData;
 
        /// <summary>
        /// Task used to ensure serialization of UI updates.
        /// Protected from concurrent access by the <see cref="_lock"/>
        /// </summary>
        private Task _updateUITask = Task.CompletedTask;
 
        #endregion
 
        #region Fields protected by _updateUITask running serially
 
        /// <summary>
        /// Task handler to provide a task to the <see cref="_taskCenterService"/>
        /// Protected from concurrent access due to serialization from <see cref="_updateUITask"/>
        /// </summary>
        private ITaskHandler? _taskHandler;
 
        /// <summary>
        /// Stores the currently running task center task.
        /// This is manually started and completed based on receiving start / stop events
        /// from the <see cref="ISolutionCrawlerProgressReporter"/>
        /// Protected from concurrent access due to serialization from <see cref="_updateUITask"/>
        /// </summary>
        private TaskCompletionSource<VoidResult>? _taskCenterTask;
 
        /// <summary>
        /// Unfortunately, <see cref="ProgressData.PendingItemCount"/> is only reported
        /// when the <see cref="ProgressData.Status"/> is <see cref="ProgressStatus.PendingItemCountUpdated"/>
        /// So we have to store the count separately for the UI so that we do not overwrite the last reported count with 0.
        /// Protected from concurrent access due to serialization from <see cref="_updateUITask"/>
        /// </summary>
        private int _lastPendingItemCount;
 
        #endregion
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public TaskCenterSolutionAnalysisProgressReporter(
            IThreadingContext threadingContext,
            VisualStudioWorkspace workspace)
        {
            _threadingContext = threadingContext;
            _workspace = workspace;
        }
 
        public async Task InitializeAsync(IAsyncServiceProvider serviceProvider)
        {
            _taskCenterService = await serviceProvider.GetServiceAsync<SVsTaskStatusCenterService, IVsTaskStatusCenterService>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
 
            var crawlerService = _workspace.Services.GetRequiredService<ISolutionCrawlerService>();
            var reporter = crawlerService.GetProgressReporter(_workspace);
 
            if (reporter.InProgress)
            {
                // The reporter was already sending events before we were able to subscribe, so trigger an update to the task center.
                OnSolutionCrawlerProgressChanged(this, new ProgressData(ProgressStatus.Started, pendingItemCount: null));
            }
 
            reporter.ProgressChanged += OnSolutionCrawlerProgressChanged;
        }
 
        /// <summary>
        /// Retrieve and throttle solution crawler events to be sent to the progress reporter UI.
        /// 
        /// there is no concurrent call to this method since ISolutionCrawlerProgressReporter will serialize all
        /// events to preserve event ordering
        /// </summary>
        /// <param name="progressData"></param>
        public void OnSolutionCrawlerProgressChanged(object sender, ProgressData progressData)
        {
            lock (_lock)
            {
                _lastProgressData = progressData;
 
                // The task is running which will update the progress.
                if (_intervalTask != null)
                {
                    return;
                }
 
                // Kick off task to update the UI after a delay to pick up any new events.
                _intervalTask = Task.Delay(s_minimumInterval).ContinueWith(_ => ReportProgress(),
                    CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
            }
        }
 
        private void ReportProgress()
        {
            lock (_lock)
            {
                var data = _lastProgressData;
                _intervalTask = null;
 
                _updateUITask = _updateUITask.ContinueWith(_ => UpdateUI(data),
                    CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
            }
        }
 
        private void UpdateUI(ProgressData progressData)
        {
            if (progressData.Status == ProgressStatus.Stopped)
            {
                StopTaskCenter();
                return;
            }
 
            // Update the pending item count if the progress data specifies a value.
            if (progressData.PendingItemCount.HasValue)
            {
                _lastPendingItemCount = progressData.PendingItemCount.Value;
            }
 
            // Start the task center task if not already running.
            if (_taskHandler == null)
            {
                Contract.ThrowIfNull(_taskCenterService);
 
                // Register a new task handler to handle a new task center task.
                // Each task handler can only register one task, so we must create a new one each time we start.
                _taskHandler = _taskCenterService.PreRegister(_options, data: default);
 
                // Create a new non-completed task to be tracked by the task handler.
                _taskCenterTask = new TaskCompletionSource<VoidResult>();
                _taskHandler.RegisterTask(_taskCenterTask.Task);
            }
 
            var statusMessage = progressData.Status == ProgressStatus.Paused
                ? ServicesVSResources.Paused_0_tasks_in_queue
                : ServicesVSResources.Evaluating_0_tasks_in_queue;
 
            _taskHandler.Progress.Report(new TaskProgressData
            {
                ProgressText = string.Format(statusMessage, _lastPendingItemCount),
                CanBeCanceled = false,
                PercentComplete = null,
            });
        }
 
        private void StopTaskCenter()
        {
            // Mark the progress task as completed so it shows complete in the task center.
            _taskCenterTask?.TrySetResult(default);
 
            // Clear tasks and data.
            _taskCenterTask = null;
            _taskHandler = null;
            _lastProgressData = default;
            _lastPendingItemCount = 0;
        }
    }
}