|
// 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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Shared.TestHooks;
namespace Microsoft.CodeAnalysis.SolutionCrawler
{
internal sealed partial class SolutionCrawlerRegistrationService
{
internal sealed partial class WorkCoordinator
{
private sealed partial class IncrementalAnalyzerProcessor
{
private abstract class AbstractPriorityProcessor : GlobalOperationAwareIdleProcessor
{
protected readonly IncrementalAnalyzerProcessor Processor;
private readonly object _gate = new();
private Lazy<ImmutableArray<IIncrementalAnalyzer>> _lazyAnalyzers;
public AbstractPriorityProcessor(
IAsynchronousOperationListener listener,
IncrementalAnalyzerProcessor processor,
Lazy<ImmutableArray<IIncrementalAnalyzer>> lazyAnalyzers,
IGlobalOperationNotificationService? globalOperationNotificationService,
TimeSpan backOffTimeSpan,
CancellationToken shutdownToken)
: base(listener, globalOperationNotificationService, backOffTimeSpan, shutdownToken)
{
_lazyAnalyzers = lazyAnalyzers;
Processor = processor;
Processor._documentTracker.NonRoslynBufferTextChanged += OnNonRoslynBufferTextChanged;
}
public ImmutableArray<IIncrementalAnalyzer> Analyzers
{
get
{
lock (_gate)
{
return _lazyAnalyzers.Value;
}
}
}
public void AddAnalyzer(IIncrementalAnalyzer analyzer)
{
lock (_gate)
{
var analyzers = _lazyAnalyzers.Value;
_lazyAnalyzers = new Lazy<ImmutableArray<IIncrementalAnalyzer>>(() => analyzers.Add(analyzer));
}
}
protected override void OnPaused()
=> SolutionCrawlerLogger.LogGlobalOperation(Processor._logAggregator);
protected abstract Task HigherQueueOperationTask { get; }
protected abstract bool HigherQueueHasWorkItem { get; }
protected async Task WaitForHigherPriorityOperationsAsync()
{
using (Logger.LogBlock(FunctionId.WorkCoordinator_WaitForHigherPriorityOperationsAsync, CancellationToken))
{
while (true)
{
CancellationToken.ThrowIfCancellationRequested();
// we wait for global operation and higher queue operation if there is anything going on
await HigherQueueOperationTask.ConfigureAwait(false);
if (HigherQueueHasWorkItem)
{
// There was still something more important in another queue. Back off again (e.g.
// call UpdateLastAccessTime) then wait that amount of time and check again to see
// if that queue is clear.
UpdateLastAccessTime();
await WaitForIdleAsync(Listener).ConfigureAwait(false);
continue;
}
if (GetIsPaused())
{
// if we're paused, we still want to keep waiting until we become unpaused. After we
// become unpaused though, loop around those to see if there is still high pri work
// to do.
await WaitForIdleAsync(Listener).ConfigureAwait(false);
continue;
}
// There was no higher queue work item and we're not paused. However, we may not have
// waited long enough to actually satisfy our own backoff-delay. If so, wait until
// we're actually idle.
if (ShouldContinueToBackOff())
{
// Do the wait. If it returns 'true' then we did the full wait. Loop around again
// to see if there is higher priority work, or if we got paused.
// However, if it returns 'false' then that means the delay completed quickly
// because some unit/integration test is asking us to expedite our work. In that
// case, just return out immediately so we can process what is in our queue.
if (await WaitForIdleAsync(Listener).ConfigureAwait(false))
continue;
// intentional fall-through.
}
return;
}
}
}
public override void Shutdown()
{
base.Shutdown();
Processor._documentTracker.NonRoslynBufferTextChanged -= OnNonRoslynBufferTextChanged;
foreach (var analyzer in Analyzers)
{
analyzer.Shutdown();
}
}
private void OnNonRoslynBufferTextChanged(object? sender, EventArgs e)
{
// There are 2 things incremental processor takes care of
//
// #1 is making sure we delay processing any work until there is enough idle (ex, typing) in host.
// #2 is managing cancellation and pending works.
//
// we used to do #1 and #2 only for Roslyn files. and that is usually fine since most of time solution contains only roslyn files.
//
// but for mixed solution (ex, Roslyn files + HTML + JS + CSS), #2 still makes sense but #1 doesn't. We want
// to pause any work while something is going on in other project types as well.
//
// we need to make sure we play nice with neighbors as well.
//
// now, we don't care where changes are coming from. if there is any change in host, we pause ourselves for a while.
UpdateLastAccessTime();
}
}
}
}
}
}
|