diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs index 45620952b4f9b..857c35f47cbcb 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/AbstractEditorNavigationBarItemService.cs @@ -30,12 +30,12 @@ protected AbstractEditorNavigationBarItemService(IThreadingContext threadingCont public async Task> GetItemsAsync( Document document, bool workspaceSupportsDocumentChanges, - bool forceFrozenPartialSemanticsForCrossProcessOperations, + bool frozenPartialSemantics, ITextVersion textVersion, CancellationToken cancellationToken) { var service = document.GetRequiredLanguageService(); - var items = await service.GetItemsAsync(document, workspaceSupportsDocumentChanges, forceFrozenPartialSemanticsForCrossProcessOperations, cancellationToken).ConfigureAwait(false); + var items = await service.GetItemsAsync(document, workspaceSupportsDocumentChanges, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); return items.SelectAsArray(v => (NavigationBarItem)new WrappedNavigationBarItem(textVersion, v)); } diff --git a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs index 650a61dea5e00..775c5e83602f9 100644 --- a/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs +++ b/src/EditorFeatures/Core/Extensibility/NavigationBar/INavigationBarItemService.cs @@ -16,7 +16,7 @@ internal interface INavigationBarItemService : ILanguageService Task> GetItemsAsync( Document document, bool workspaceSupportsDocumentChanges, - bool forceFrozenPartialSemanticsForCrossProcessOperations, + bool frozenPartialSemantics, ITextVersion textVersion, CancellationToken cancellationToken); bool ShowItemGrayedIfNear(NavigationBarItem item); diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs index 567a49f8f0f0e..8437277de74c9 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptNavigationBarItemService.cs @@ -33,7 +33,7 @@ public Task> GetItemsAsync( Document document, ITextVersion textVersion, CancellationToken cancellationToken) { return ((INavigationBarItemService)this).GetItemsAsync( - document, workspaceSupportsDocumentChanges: true, forceFrozenPartialSemanticsForCrossProcessOperations: false, textVersion, cancellationToken); + document, workspaceSupportsDocumentChanges: true, frozenPartialSemantics: false, textVersion, cancellationToken); } async Task> INavigationBarItemService.GetItemsAsync( diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs index 74386145ba77e..f8824504bd507 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -64,7 +65,15 @@ internal partial class NavigationBarController : IDisposable /// Queue to batch up work to do to compute the current model. Used so we can batch up a lot of events and only /// compute the model once for every batch. /// - private readonly AsyncBatchingWorkQueue _computeModelQueue; + private readonly AsyncBatchingWorkQueue _computeModelQueue; + + /// + /// This cancellation series controls the non-frozen nav-bar computation pass. We want this to be separately + /// cancellable so that if new events come in that we cancel the expensive non-frozen nav-bar pass (which might be + /// computing skeletons, SG docs, etc.), do the next cheap frozen-nav-bar-pass, and then push the + /// expensive-nonfrozen-nav-bar-pass to the end again. + /// + private readonly CancellationSeries _nonFrozenComputationCancellationSeries; /// /// Queue to batch up work to do to determine the selected item. Used so we can batch up a lot of events and only @@ -92,11 +101,12 @@ public NavigationBarController( _visibilityTracker = visibilityTracker; _uiThreadOperationExecutor = uiThreadOperationExecutor; _asyncListener = asyncListener; + _nonFrozenComputationCancellationSeries = new(_cancellationTokenSource.Token); - _computeModelQueue = new AsyncBatchingWorkQueue( - DelayTimeSpan.Short, + _computeModelQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.Medium, ComputeModelAndSelectItemAsync, - EqualityComparer.Default, + EqualityComparer.Default, asyncListener, _cancellationTokenSource.Token); @@ -196,7 +206,13 @@ private void StartModelUpdateAndSelectedItemUpdateTasks() if (_disconnected) return; - _computeModelQueue.AddWork(default(VoidResult)); + // Cancel any expensive, in-flight, nav-bar work as there's now a request to perform lightweight tagging. Note: + // intentionally ignoring the return value here. We're enqueuing normal work here, so it has no associated + // token with it. + _ = _nonFrozenComputationCancellationSeries.CreateNext(); + _computeModelQueue.AddWork( + new NavigationBarQueueItem(FrozenPartialSemantics: true, NonFrozenComputationToken: null), + cancelExistingWork: true); } private void OnCaretMovedOrActiveViewChanged(object? sender, EventArgs e) diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs index b9dd43d278ced..7f8f55fee5d26 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs @@ -8,12 +8,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Workspaces; -using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; @@ -24,7 +22,58 @@ internal partial class NavigationBarController /// /// Starts a new task to compute the model based on the current text. /// - private async ValueTask ComputeModelAndSelectItemAsync(ImmutableSegmentedList _, CancellationToken cancellationToken) + private async ValueTask ComputeModelAndSelectItemAsync( + ImmutableSegmentedList queueItems, CancellationToken cancellationToken) + { + // If any of the requests are for frozen partial, then we do compute with frozen partial semantics. We + // always want these "fast but inaccurate" passes to happen first. That pass will then enqueue the work + // to do the slow-but-accurate pass. + var frozenPartialSemantics = queueItems.Any(t => t.FrozenPartialSemantics); + + if (!frozenPartialSemantics) + { + // We're asking for the expensive nav-bar-pass, Kick off the work to do that, but attach ourselves to the + // requested cancellation token so this expensive work can be canceled if new requests for frozen partial + // work come in. + + // Since we're not frozen-partial, all requests must have an associated cancellation token. And all but + // the last *must* be already canceled (since each is canceled as new work is added). + Contract.ThrowIfFalse(queueItems.All(t => !t.FrozenPartialSemantics)); + Contract.ThrowIfFalse(queueItems.All(t => t.NonFrozenComputationToken != null)); + Contract.ThrowIfFalse(queueItems.Take(queueItems.Count - 1).All(t => t.NonFrozenComputationToken!.Value.IsCancellationRequested)); + + var lastNonFrozenComputationToken = queueItems[^1].NonFrozenComputationToken!.Value; + + // Need a dedicated try/catch here since we're operating on a different token than the queue's token. + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(lastNonFrozenComputationToken, cancellationToken); + try + { + return await ComputeModelAndSelectItemAsync(frozenPartialSemantics: false, linkedTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException ex) when (ExceptionUtilities.IsCurrentOperationBeingCancelled(ex, linkedTokenSource.Token)) + { + return null; + } + } + else + { + // Normal request to either compute nav-bar items using frozen partial semantics. + var model = await ComputeModelAndSelectItemAsync(frozenPartialSemantics: true, cancellationToken).ConfigureAwait(false); + + // After that completes, enqueue work to compute *without* frozen partial snapshots so we move to accurate + // results shortly. Create and pass along a new cancellation token for this expensive work so that it can be + // canceled by future lightweight work. + _computeModelQueue.AddWork(new NavigationBarQueueItem(FrozenPartialSemantics: false, _nonFrozenComputationCancellationSeries.CreateNext(default))); + + return model; + } + } + + /// + /// Starts a new task to compute the model based on the current text. + /// + private async ValueTask ComputeModelAndSelectItemAsync( + bool frozenPartialSemantics, CancellationToken cancellationToken) { // Jump back to the UI thread to determine what snapshot the user is processing. await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); @@ -53,19 +102,13 @@ internal partial class NavigationBarController async Task ComputeModelAsync() { - // When computing items just get the partial semantics workspace. This will ensure we can get data for this - // file, and hopefully have enough loaded to get data for other files in the case of partial types. In the - // event the other files aren't available, then partial-type information won't be correct. That's ok though - // as this is just something that happens during solution load and will pass once that is over. By using - // partial semantics, we can ensure we don't spend an inordinate amount of time computing and using full - // compilation data (like skeleton assemblies). - var forceFrozenPartialSemanticsForCrossProcessOperations = true; - var workspace = textSnapshot.TextBuffer.GetWorkspace(); if (workspace is null) return null; - var document = textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken); + var document = frozenPartialSemantics + ? textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken) + : textSnapshot.AsText().GetOpenDocumentInCurrentContextWithChanges(); if (document == null) return null; @@ -90,7 +133,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( var items = await itemService.GetItemsAsync( document, workspace.CanApplyChange(ApplyChangesKind.ChangeDocument), - forceFrozenPartialSemanticsForCrossProcessOperations, + frozenPartialSemantics, textSnapshot.Version, cancellationToken).ConfigureAwait(false); return new NavigationBarModel(itemService, items); diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarQueueItem.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarQueueItem.cs new file mode 100644 index 0000000000000..6f3d80861a552 --- /dev/null +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarQueueItem.cs @@ -0,0 +1,16 @@ +// 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.Threading; + +namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar; + +/// Indicates if we should compute with frozen partial semantics or +/// not. +/// If is false, then this is a +/// cancellation token that can cancel the expensive work being done if new frozen-partial work is +/// requested. +internal readonly record struct NavigationBarQueueItem( + bool FrozenPartialSemantics, + CancellationToken? NonFrozenComputationToken); diff --git a/src/EditorFeatures/Test2/NavigationBar/TestHelpers.vb b/src/EditorFeatures/Test2/NavigationBar/TestHelpers.vb index 348828e251976..c353602dcce20 100644 --- a/src/EditorFeatures/Test2/NavigationBar/TestHelpers.vb +++ b/src/EditorFeatures/Test2/NavigationBar/TestHelpers.vb @@ -39,7 +39,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.NavigationBar Dim service = document.GetLanguageService(Of INavigationBarItemService)() Dim actualItems = Await service.GetItemsAsync( - document, workspaceSupportsDocumentChanges:=True, forceFrozenPartialSemanticsForCrossProcessOperations:=False, snapshot.Version, Nothing) + document, workspaceSupportsDocumentChanges:=True, frozenPartialSemantics:=False, snapshot.Version, Nothing) AssertEqual(expectedItems, actualItems, document.GetLanguageService(Of ISyntaxFactsService)().IsCaseSensitive) End Using @@ -58,7 +58,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.NavigationBar Dim service = document.GetLanguageService(Of INavigationBarItemService)() Dim items = Await service.GetItemsAsync( - document, workspaceSupportsDocumentChanges:=True, forceFrozenPartialSemanticsForCrossProcessOperations:=False, snapshot.Version, Nothing) + document, workspaceSupportsDocumentChanges:=True, frozenPartialSemantics:=False, snapshot.Version, Nothing) Dim hostDocument = workspace.Documents.Single(Function(d) d.CursorPosition.HasValue) Dim model As New NavigationBarModel(service, items) @@ -92,7 +92,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.NavigationBar Dim service = document.GetLanguageService(Of INavigationBarItemService)() Dim items = Await service.GetItemsAsync( - document, workspaceSupportsDocumentChanges:=True, forceFrozenPartialSemanticsForCrossProcessOperations:=False, snapshot.Version, Nothing) + document, workspaceSupportsDocumentChanges:=True, frozenPartialSemantics:=False, snapshot.Version, Nothing) Dim leftItem = items.Single(Function(i) i.Text = leftItemToSelectText) Dim rightItem = selectRightItem(leftItem.ChildItems) @@ -121,7 +121,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.NavigationBar Dim service = DirectCast(sourceDocument.GetLanguageService(Of INavigationBarItemService)(), AbstractEditorNavigationBarItemService) Dim items = Await service.GetItemsAsync( - sourceDocument, workspaceSupportsDocumentChanges:=True, forceFrozenPartialSemanticsForCrossProcessOperations:=False, snapshot.Version, Nothing) + sourceDocument, workspaceSupportsDocumentChanges:=True, frozenPartialSemantics:=False, snapshot.Version, Nothing) Dim leftItem = items.Single(Function(i) i.Text = leftItemToSelectText) Dim rightItem = leftItem.ChildItems.Single(Function(i) i.Text = rightItemToSelectText) diff --git a/src/Features/Core/Portable/NavigationBar/AbstractNavigationBarItemService.cs b/src/Features/Core/Portable/NavigationBar/AbstractNavigationBarItemService.cs index 7344822c3d5ed..3903bae77da32 100644 --- a/src/Features/Core/Portable/NavigationBar/AbstractNavigationBarItemService.cs +++ b/src/Features/Core/Portable/NavigationBar/AbstractNavigationBarItemService.cs @@ -18,7 +18,7 @@ internal abstract class AbstractNavigationBarItemService : INavigationBarItemSer { protected abstract Task> GetItemsInCurrentProcessAsync(Document document, bool supportsCodeGeneration, CancellationToken cancellationToken); - public async Task> GetItemsAsync(Document document, bool supportsCodeGeneration, bool forceFrozenPartialSemanticsForCrossProcessOperations, CancellationToken cancellationToken) + public async Task> GetItemsAsync(Document document, bool supportsCodeGeneration, bool frozenPartialSemantics, CancellationToken cancellationToken) { var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false); if (client != null) @@ -28,7 +28,7 @@ public async Task> GetItemsAsync(Documen var documentId = document.Id; var result = await client.TryInvokeAsync>( document.Project, - (service, solutionInfo, cancellationToken) => service.GetItemsAsync(solutionInfo, documentId, supportsCodeGeneration, forceFrozenPartialSemanticsForCrossProcessOperations, cancellationToken), + (service, solutionInfo, cancellationToken) => service.GetItemsAsync(solutionInfo, documentId, supportsCodeGeneration, frozenPartialSemantics, cancellationToken), cancellationToken).ConfigureAwait(false); return result.HasValue diff --git a/src/Features/Core/Portable/NavigationBar/INavigationBarItemService.cs b/src/Features/Core/Portable/NavigationBar/INavigationBarItemService.cs index af7d401458937..6b335a6b5e4eb 100644 --- a/src/Features/Core/Portable/NavigationBar/INavigationBarItemService.cs +++ b/src/Features/Core/Portable/NavigationBar/INavigationBarItemService.cs @@ -11,5 +11,5 @@ namespace Microsoft.CodeAnalysis.NavigationBar; internal interface INavigationBarItemService : ILanguageService { - Task> GetItemsAsync(Document document, bool supportsCodeGeneration, bool forceFrozenPartialSemanticsForCrossProcessOperations, CancellationToken cancellationToken); + Task> GetItemsAsync(Document document, bool supportsCodeGeneration, bool frozenPartialSemantics, CancellationToken cancellationToken); } diff --git a/src/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs b/src/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs index 7ac98297f2823..d9cd5fa1a5768 100644 --- a/src/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs @@ -44,7 +44,7 @@ public async Task HandleRequestAsync(RoslynDocumentSymbolParams reques var clientCapabilities = context.GetRequiredClientCapabilities(); var navBarService = document.Project.Services.GetRequiredService(); - var navBarItems = await navBarService.GetItemsAsync(document, supportsCodeGeneration: false, forceFrozenPartialSemanticsForCrossProcessOperations: false, cancellationToken).ConfigureAwait(false); + var navBarItems = await navBarService.GetItemsAsync(document, supportsCodeGeneration: false, frozenPartialSemantics: false, cancellationToken).ConfigureAwait(false); if (navBarItems.IsEmpty) return []; diff --git a/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpNavigationBarItemService.cs b/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpNavigationBarItemService.cs index e83cf9830f36d..8cbee65073e5d 100644 --- a/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpNavigationBarItemService.cs +++ b/src/Tools/ExternalAccess/FSharp/Internal/Editor/FSharpNavigationBarItemService.cs @@ -41,7 +41,7 @@ public FSharpNavigationBarItemService( public Task> GetItemsAsync(Document document, ITextVersion textVersion, CancellationToken cancellationToken) { return ((INavigationBarItemService)this).GetItemsAsync( - document, workspaceSupportsDocumentChanges: true, forceFrozenPartialSemanticsForCrossProcessOperations: false, textVersion, cancellationToken); + document, workspaceSupportsDocumentChanges: true, frozenPartialSemantics: false, textVersion, cancellationToken); } async Task> INavigationBarItemService.GetItemsAsync( diff --git a/src/Workspaces/Remote/ServiceHub/Services/NavigationBar/RemoteNavigationBarItemService.cs b/src/Workspaces/Remote/ServiceHub/Services/NavigationBar/RemoteNavigationBarItemService.cs index 0839972677023..4786cf00b8a2a 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/NavigationBar/RemoteNavigationBarItemService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/NavigationBar/RemoteNavigationBarItemService.cs @@ -9,40 +9,33 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Remote +namespace Microsoft.CodeAnalysis.Remote; + +internal sealed class RemoteNavigationBarItemService(in BrokeredServiceBase.ServiceConstructionArguments arguments) + : BrokeredServiceBase(arguments), IRemoteNavigationBarItemService { - internal sealed class RemoteNavigationBarItemService : BrokeredServiceBase, IRemoteNavigationBarItemService + internal sealed class Factory : FactoryBase { - internal sealed class Factory : FactoryBase - { - protected override IRemoteNavigationBarItemService CreateService(in ServiceConstructionArguments arguments) - => new RemoteNavigationBarItemService(arguments); - } - - public RemoteNavigationBarItemService(in ServiceConstructionArguments arguments) - : base(arguments) - { - } + protected override IRemoteNavigationBarItemService CreateService(in ServiceConstructionArguments arguments) + => new RemoteNavigationBarItemService(arguments); + } - public ValueTask> GetItemsAsync( - Checksum solutionChecksum, DocumentId documentId, bool supportsCodeGeneration, bool forceFrozenPartialSemanticsForCrossProcessOperations, CancellationToken cancellationToken) + public ValueTask> GetItemsAsync( + Checksum solutionChecksum, DocumentId documentId, bool supportsCodeGeneration, bool frozenPartialSemantics, CancellationToken cancellationToken) + { + return RunServiceAsync(solutionChecksum, async solution => { - return RunServiceAsync(solutionChecksum, async solution => - { - var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(document); + var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(document); - if (forceFrozenPartialSemanticsForCrossProcessOperations) - { - // Frozen partial semantics is not automatically passed to OOP, so enable it explicitly when desired - document = document.WithFrozenPartialSemantics(cancellationToken); - } + // Frozen partial semantics is not automatically passed to OOP, so enable it explicitly when desired + if (frozenPartialSemantics) + document = document.WithFrozenPartialSemantics(cancellationToken); - var navigationBarService = document.GetRequiredLanguageService(); - var result = await navigationBarService.GetItemsAsync(document, supportsCodeGeneration, forceFrozenPartialSemanticsForCrossProcessOperations, cancellationToken).ConfigureAwait(false); + var navigationBarService = document.GetRequiredLanguageService(); + var result = await navigationBarService.GetItemsAsync(document, supportsCodeGeneration, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); - return SerializableNavigationBarItem.Dehydrate(result); - }, cancellationToken); - } + return SerializableNavigationBarItem.Dehydrate(result); + }, cancellationToken); } }