Skip to content

Commit

Permalink
Merge pull request #68382 from CyrusNajmabadi/noThrowDelay
Browse files Browse the repository at this point in the history
Update our 'delay until visible' logic to not throw cancellation exceptions
  • Loading branch information
CyrusNajmabadi authored Jun 9, 2023
2 parents 0993895 + 3bc0a16 commit 55e3953
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ internal partial class NavigationBarController
// If these are navbars for a file that isn't even visible, then avoid doing any unnecessary computation
// work until far in the future (or if visibility changes). This ensures our non-visible docs do settle
// once enough time has passed, while greatly reducing their impact on the system.
//
// Use NoThrow as this is a high source of cancellation exceptions. This avoids the exception and instead
// bails gracefully by checking below.
await _visibilityTracker.DelayWhileNonVisibleAsync(
_threadingContext, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).ConfigureAwait(false);
_threadingContext, _asyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(false);

if (cancellationToken.IsCancellationRequested)
return null;

using (Logger.LogBlock(FunctionId.NavigationBar_ComputeModelAsync, cancellationToken))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,13 @@ private async Task RecomputeTagsAsync(bool highPriority, CancellationToken cance
// possible.
if (!highPriority)
{
// Use NoThrow as this is a high source of cancellation exceptions. This avoids the exception and instead
// bails gracefully by checking below.
await _visibilityTracker.DelayWhileNonVisibleAsync(
_dataSource.ThreadingContext, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).ConfigureAwait(true);
_dataSource.ThreadingContext, _dataSource.AsyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(captureContext: true);

if (cancellationToken.IsCancellationRequested)
return;
}

await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
Expand Down
72 changes: 51 additions & 21 deletions src/EditorFeatures/Core/Workspaces/ITextBufferVisibilityTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;

namespace Microsoft.CodeAnalysis.Workspaces
{
Expand Down Expand Up @@ -37,40 +39,68 @@ internal interface ITextBufferVisibilityTracker
internal static class ITextBufferVisibilityTrackerExtensions
{
/// <summary>
/// Waits the specified amount of time while the specified <paramref name="subjectBuffer"/> is not visible. If any
/// document visibility changes happen, the delay will cancel.
/// Waits the specified amount of time while the specified <paramref name="subjectBuffer"/> is not visible. If
/// any document visibility changes happen, the delay will cancel.
/// </summary>
public static async Task DelayWhileNonVisibleAsync(
public static Task DelayWhileNonVisibleAsync(
this ITextBufferVisibilityTracker? service,
IThreadingContext threadingContext,
IAsynchronousOperationListener listener,
ITextBuffer subjectBuffer,
TimeSpan timeSpan,
CancellationToken cancellationToken)
{
// Only add a delay if we have access to a service that will tell us when the buffer become visible or not.
if (service is null)
return;
return Task.CompletedTask;

await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
if (service.IsVisible(subjectBuffer))
return;
// Because cancellation is both expensive, and a super common thing to occur while we're delaying the caller
// until visibility, we special case the implementation here and transition to the canceled state
// explicitly, rather than throwing a cancellation exception.

// ensure we listen for visibility changes before checking. That way we don't have a race where we check
// something see it is not visible, but then do not hear about its visibility change because we've hooked up
// our event after that happens.
var visibilityChangedTaskSource = new TaskCompletionSource<bool>();
var callback = void () => visibilityChangedTaskSource.TrySetResult(true);
service.RegisterForVisibilityChanges(subjectBuffer, callback);
var delayTask = DelayWhileNonVisibleWorkerAsync();

try
{
// Listen to when the active document changed so that we startup work on a document once it becomes visible.
var delayTask = Task.Delay(timeSpan, cancellationToken);
await Task.WhenAny(delayTask, visibilityChangedTaskSource.Task).ConfigureAwait(true);
}
finally
// it's very reasonable for the delay-task to complete synchronously (we've already been canceled, or the
// buffer is already visible. So fast path that out.
if (delayTask.IsCompleted)
return delayTask;

var taskOfTask = delayTask.ContinueWith(
// Convert a successfully completed task when we were canceled to a canceled task. Otherwise, return
// the faulted or non-canceled task as is.
task => task.Status == TaskStatus.RanToCompletion && cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : task,
CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return taskOfTask.Unwrap();

// Normal delay logic, except that this does not throw in the event of cancellation, but instead returns
// gracefully. The above task continuation logic then ensures we return a canceled task without needing
// exceptions.
async Task DelayWhileNonVisibleWorkerAsync()
{
service.UnregisterForVisibilityChanges(subjectBuffer, callback);
if (cancellationToken.IsCancellationRequested)
return;

await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
if (service.IsVisible(subjectBuffer))
return;

// ensure we listen for visibility changes before checking. That way we don't have a race where we check
// something see it is not visible, but then do not hear about its visibility change because we've hooked up
// our event after that happens.
var visibilityChangedTaskSource = new TaskCompletionSource<bool>();
var callback = void () => visibilityChangedTaskSource.TrySetResult(true);
service.RegisterForVisibilityChanges(subjectBuffer, callback);

try
{
// Listen to when the active document changed so that we startup work on a document once it becomes visible.
var delayTask = listener.Delay(timeSpan, cancellationToken);
await Task.WhenAny(delayTask, visibilityChangedTaskSource.Task).NoThrowAwaitable(captureContext: true);
}
finally
{
service.UnregisterForVisibilityChanges(subjectBuffer, callback);
}
}
}
}
Expand Down

0 comments on commit 55e3953

Please sign in to comment.