From db6b65e95559f3f043d96317860a71c88664b3cb Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 10:33:17 -0700 Subject: [PATCH 01/33] Initial work to support frozen partial tagging --- ...actAsynchronousTaggerProvider.TagSource.cs | 6 +-- ...ousTaggerProvider.TagSource_ProduceTags.cs | 51 +++++++++++++------ .../AbstractAsynchronousTaggerProvider.cs | 5 ++ .../Core/Tagging/TaggerContext.cs | 20 +++++--- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index 79e233429623f..dd1514018afa6 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -79,7 +79,7 @@ private sealed partial class TagSource /// up event change notifications and only dispatch one recomputation every /// to actually produce the latest set of tags. /// - private readonly AsyncBatchingWorkQueue _eventChangeQueue; + private readonly AsyncBatchingWorkQueue<(bool highPriority, bool frozenPartialSemantics)> _eventChangeQueue; #endregion @@ -164,10 +164,10 @@ public TagSource( // // PERF: Use AsyncBatchingWorkQueue instead of AsyncBatchingWorkQueue because // the latter has an async state machine that rethrows a very common cancellation exception. - _eventChangeQueue = new AsyncBatchingWorkQueue( + _eventChangeQueue = new AsyncBatchingWorkQueue<(bool highPriority, bool frozenPartialSemantics)>( dataSource.EventChangeDelay.ComputeTimeDelay(), ProcessEventChangeAsync, - EqualityComparer.Default, + EqualityComparer<(bool highPriority, bool frozenPartialSemantics)>.Default, asyncListener, _disposalTokenSource.Token); diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 0c74f79bdae22..ef1ec04f53f38 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -168,13 +168,17 @@ private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) => EnqueueWork(highPriority: false); private void EnqueueWork(bool highPriority) - => _eventChangeQueue.AddWork(highPriority, _dataSource.CancelOnNewWork); + => EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics); - private ValueTask ProcessEventChangeAsync(ImmutableSegmentedList changes, CancellationToken cancellationToken) + private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) + => _eventChangeQueue.AddWork((highPriority, frozenPartialSemantics), _dataSource.CancelOnNewWork); + + private async ValueTask ProcessEventChangeAsync(ImmutableSegmentedList<(bool highPriority, bool frozenPartialSemantics)> changes, CancellationToken cancellationToken) { // If any of the requests was high priority, then compute at that speed. - var highPriority = changes.Contains(true); - return new ValueTask(RecomputeTagsAsync(highPriority, cancellationToken)); + var highPriority = changes.Contains(x => x.highPriority); + var frozenPartialSemantics = changes.Contains(t => t.frozenPartialSemantics); + await RecomputeTagsAsync(highPriority, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); } /// @@ -191,11 +195,14 @@ private ValueTask ProcessEventChangeAsync(ImmutableSegmentedList /// If this tagging request should be processed as quickly as possible with no extra delays added for it. /// - private async Task RecomputeTagsAsync(bool highPriority, CancellationToken cancellationToken) + private async Task RecomputeTagsAsync( + bool highPriority, + bool frozenPartialSemantics, + CancellationToken cancellationToken) { await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); if (cancellationToken.IsCancellationRequested) - return default; + return; // if we're tagging documents that are not visible, then introduce a long delay so that we avoid // consuming machine resources on work the user isn't likely to see. ConfigureAwait(true) so that if @@ -211,12 +218,12 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( _dataSource.ThreadingContext, _dataSource.AsyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(captureContext: true); if (cancellationToken.IsCancellationRequested) - return default; + return; } _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); if (cancellationToken.IsCancellationRequested) - return default; + return; using (Logger.LogBlock(FunctionId.Tagger_TagSource_RecomputeTags, cancellationToken)) { @@ -234,15 +241,22 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( await TaskScheduler.Default; if (cancellationToken.IsCancellationRequested) - return default; + return; + + if (frozenPartialSemantics) + { + spansToTag = spansToTag.SelectAsArray(ds => new DocumentSnapshotSpan( + ds.Document?.WithFrozenPartialSemantics(cancellationToken), + ds.SnapshotSpan)); + } // Create a context to store pass the information along and collect the results. var context = new TaggerContext( - oldState, spansToTag, caretPosition, textChangeRange, oldTagTrees); + oldState, frozenPartialSemantics, spansToTag, caretPosition, textChangeRange, oldTagTrees); await ProduceTagsAsync(context, cancellationToken).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) - return default; + return; // Process the result to determine what changed. var newTagTrees = ComputeNewTagTrees(oldTagTrees, context); @@ -251,7 +265,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // Then switch back to the UI thread to update our state and kick off the work to notify the editor. await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); if (cancellationToken.IsCancellationRequested) - return default; + return; // Once we assign our state, we're uncancellable. We must report the changed information // to the editor. The only case where it's ok not to is if the tagger itself is disposed. @@ -275,9 +289,14 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // Once we've computed tags, pause ourselves if we're no longer visible. That way we don't consume any // machine resources that the user won't even notice. PauseIfNotVisible(); - } - return default; + // If we were computing with frozen partial semantics here, enqueue work to compute *without* frozen + // partial snapshots so we move to accurate results shortly. Note: when the queue goes to process this + // message, if it sees any other events asking for frozen-partial-semantics, it will process in that + // mode again, kicking the can down the road to finally end with non-frozen-partial computation + if (frozenPartialSemantics) + this.EnqueueWork(highPriority, frozenPartialSemantics: false); + } } private ImmutableArray GetSpansAndDocumentsToTag() @@ -560,8 +579,8 @@ private DiffResult ComputeDifference( !this.CachedTagTrees.TryGetValue(buffer, out _)) { // Compute this as a high priority work item to have the lease amount of blocking as possible. - _dataSource.ThreadingContext.JoinableTaskFactory.Run(() => - this.RecomputeTagsAsync(highPriority: true, _disposalTokenSource.Token)); + _dataSource.ThreadingContext.JoinableTaskFactory.Run(async () => + await this.RecomputeTagsAsync(highPriority: true, _dataSource.SupportsFrozenPartialSemantics, _disposalTokenSource.Token).ConfigureAwait(false)); } _firstTagsRequest = false; diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs index 68a64bd1f494d..e70958e040552 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs @@ -95,6 +95,11 @@ internal abstract partial class AbstractAsynchronousTaggerProvider where T /// protected virtual bool CancelOnNewWork { get; } + /// + /// Whether or not this tagger would like to use frozen-partial snapshots to compute tags. TODO: doc more before submitting. + /// + protected virtual bool SupportsFrozenPartialSemantics { get; } + protected virtual void BeforeTagsChanged(ITextSnapshot snapshot) { } diff --git a/src/EditorFeatures/Core/Tagging/TaggerContext.cs b/src/EditorFeatures/Core/Tagging/TaggerContext.cs index de20723c6349e..e3e955f2c0625 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerContext.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerContext.cs @@ -4,10 +4,7 @@ #nullable disable -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; @@ -15,7 +12,6 @@ using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Tagging; @@ -26,6 +22,7 @@ internal class TaggerContext where TTag : ITag internal ImmutableArray _spansTagged; public readonly SegmentedList> TagSpans = []; + public bool FrozenPartialSemantics { get; } public ImmutableArray SpansToTag { get; } public SnapshotPoint? CaretPosition { get; } @@ -48,22 +45,31 @@ internal class TaggerContext where TTag : ITag // For testing only. internal TaggerContext( - Document document, ITextSnapshot snapshot, + Document document, + ITextSnapshot snapshot, + bool frozenPartialSemantics, SnapshotPoint? caretPosition = null, TextChangeRange? textChangeRange = null) - : this(state: null, [new DocumentSnapshotSpan(document, snapshot.GetFullSpan())], - caretPosition, textChangeRange, existingTags: null) + : this( + state: null, + frozenPartialSemantics, + [new DocumentSnapshotSpan(document, snapshot.GetFullSpan())], + caretPosition, + textChangeRange, + existingTags: null) { } internal TaggerContext( object state, + bool frozenPartialSemantics, ImmutableArray spansToTag, SnapshotPoint? caretPosition, TextChangeRange? textChangeRange, ImmutableDictionary> existingTags) { this.State = state; + this.FrozenPartialSemantics = frozenPartialSemantics; this.SpansToTag = spansToTag; this.CaretPosition = caretPosition; this.TextChangeRange = textChangeRange; From 0dab34b1db28ff24069062b71eeea5f448ebfe9f Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 11:22:31 -0700 Subject: [PATCH 02/33] Update tests --- .../InteractiveBraceHighlightingTests.cs | 4 ++- ...mbeddedClassificationViewTaggerProvider.cs | 20 ++++++++++++- .../Semantic/ClassificationUtilities.cs | 13 --------- ...ReferenceHighlightingViewTaggerProvider.cs | 11 +++++++- ...erProvider.SingleViewportTaggerProvider.cs | 3 ++ .../AsynchronousViewportTaggerProvider.cs | 3 ++ .../Test/Options/GlobalOptionsTests.cs | 4 ++- .../Test/Structure/StructureTaggerTests.cs | 2 +- .../AbstractBraceHighlightingTests.cs | 2 +- .../HighlightingOptions.cs | 7 ++--- .../AbstractInheritanceMarginService.cs | 6 ++-- ...bstractInheritanceMarginService_Helpers.cs | 4 +-- .../SemanticTokens/SemanticTokensHelpers.cs | 2 +- .../InheritanceMarginTaggerProvider.cs | 12 ++++---- .../Classification/ClassificationOptions.cs | 2 +- .../Portable/Workspace/Solution/Solution.cs | 9 +++++- .../CoreTest/SolutionTests/SolutionTests.cs | 28 +++++++++++++++++++ .../RemoteDocumentHighlightsService.cs | 5 ++-- .../RemoteSemanticClassificationService.cs | 6 ++-- 19 files changed, 100 insertions(+), 43 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/Interactive/BraceMatching/InteractiveBraceHighlightingTests.cs b/src/EditorFeatures/CSharpTest/Interactive/BraceMatching/InteractiveBraceHighlightingTests.cs index 4a68299a80d32..09876451f7782 100644 --- a/src/EditorFeatures/CSharpTest/Interactive/BraceMatching/InteractiveBraceHighlightingTests.cs +++ b/src/EditorFeatures/CSharpTest/Interactive/BraceMatching/InteractiveBraceHighlightingTests.cs @@ -45,7 +45,9 @@ private static async Task>> ProduceTagsA var context = new TaggerContext( buffer.CurrentSnapshot.GetRelatedDocumentsWithChanges().FirstOrDefault(), - buffer.CurrentSnapshot, new SnapshotPoint(buffer.CurrentSnapshot, position)); + buffer.CurrentSnapshot, + frozenPartialSemantics: false, + new SnapshotPoint(buffer.CurrentSnapshot, position)); await producer.GetTestAccessor().ProduceTagsAsync(context); return context.TagSpans; diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index 275ae3569a345..bc9bc90754f08 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -53,6 +53,12 @@ protected AbstractSemanticOrEmbeddedClassificationViewTaggerProvider( protected sealed override TaggerDelay EventChangeDelay => TaggerDelay.Short; + /// + /// We support frozen partial semantics, so we can quickly get classification items without building SG docs. We + /// will still run a tagging pass after the frozen-pass where we run again on non-frozen docs. + /// + protected sealed override bool SupportsFrozenPartialSemantics => true; + protected sealed override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) { this.ThreadingContext.ThrowIfNotOnUIThread(); @@ -92,7 +98,19 @@ protected sealed override Task ProduceTagsAsync( if (isLspSemanticTokensEnabled) return Task.CompletedTask; - var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language); + // Don't block getting classifications on building the full compilation. This may take a significant amount + // of time and can cause a very latency sensitive operation (copying) to block the user while we wait on this + // work to happen. + // + // It's also a better experience to get classifications to the user faster versus waiting a potentially large + // amount of time waiting for all the compilation information to be built. For example, we can classify types + // that we've parsed in other files, or partially loaded from metadata, even if we're still parsing/loading. + // For cross language projects, this also produces semantic classifications more quickly as we do not have to + // wait on skeletons to be built. + var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language) with + { + FrozenPartialSemantics = context.FrozenPartialSemantics, + }; return ClassificationUtilities.ProduceTagsAsync( context, spanToTag, classificationService, _typeMap, classificationOptions, _type, cancellationToken); } diff --git a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs index 98aac8074ef6a..f3961a268fda0 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs @@ -49,19 +49,6 @@ public static async Task ProduceTagsAsync( if (document == null) return; - // Don't block getting classifications on building the full compilation. This may take a significant amount - // of time and can cause a very latency sensitive operation (copying) to block the user while we wait on this - // work to happen. - // - // It's also a better experience to get classifications to the user faster versus waiting a potentially - // large amount of time waiting for all the compilation information to be built. For example, we can - // classify types that we've parsed in other files, or partially loaded from metadata, even if we're still - // parsing/loading. For cross language projects, this also produces semantic classifications more quickly - // as we do not have to wait on skeletons to be built. - - document = document.WithFrozenPartialSemantics(cancellationToken); - options = options with { ForceFrozenPartialSemanticsForCrossProcessOperations = true }; - var classified = await TryClassifyContainingMemberSpanAsync( context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); if (classified) diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs index f3fafcae3ed66..1d677cdc98e42 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs @@ -57,6 +57,12 @@ internal sealed partial class ReferenceHighlightingViewTaggerProvider( protected override TaggerDelay EventChangeDelay => TaggerDelay.Medium; + /// + /// We support frozen partial semantics, so we can quickly get reference highlights without building SG docs. We + /// will still run a tagging pass after the frozen-pass where we run again on non-frozen docs. + /// + protected override bool SupportsFrozenPartialSemantics => true; + protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) { // Note: we don't listen for OnTextChanged. Text changes to this buffer will get @@ -129,7 +135,10 @@ protected override Task ProduceTagsAsync( } // Otherwise, we need to go produce all tags. - var options = _globalOptions.GetHighlightingOptions(document.Project.Language); + var options = _globalOptions.GetHighlightingOptions(document.Project.Language) with + { + FrozenPartialSemantics = context.FrozenPartialSemantics, + }; return ProduceTagsAsync(context, caretPosition, document, options, cancellationToken); } diff --git a/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.SingleViewportTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.SingleViewportTaggerProvider.cs index b925dcce7ecda..6e9e4672dad6c 100644 --- a/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.SingleViewportTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.SingleViewportTaggerProvider.cs @@ -47,6 +47,9 @@ protected override TaggerTextChangeBehavior TextChangeBehavior protected override SpanTrackingMode SpanTrackingMode => _callback.SpanTrackingMode; + protected override bool SupportsFrozenPartialSemantics + => _callback.SupportsFrozenPartialSemantics; + protected override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) => _callback.CreateEventSource(textView, subjectBuffer); diff --git a/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.cs index 30fef15c33246..0ff2f128f2294 100644 --- a/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AsynchronousViewportTaggerProvider.cs @@ -103,6 +103,9 @@ SingleViewportTaggerProvider CreateSingleViewportTaggerProvider(ViewPortToTag vi /// protected virtual SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; + /// + protected virtual bool SupportsFrozenPartialSemantics { get; } + /// /// Indicates whether a tagger should be created for this text view and buffer. /// diff --git a/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs b/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs index fecd138428d1e..46ec36302d24f 100644 --- a/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs +++ b/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs @@ -18,6 +18,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.DocumentationComments; +using Microsoft.CodeAnalysis.DocumentHighlighting; using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.UnitTests; using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; @@ -165,7 +166,8 @@ private static bool IsStoredInGlobalOptions(PropertyInfo property, string? langu property.DeclaringType == typeof(AddImportPlacementOptions) && property.Name == nameof(AddImportPlacementOptions.UsingDirectivePlacement) && language == LanguageNames.VisualBasic || property.DeclaringType == typeof(DocumentFormattingOptions) && property.Name == nameof(DocumentFormattingOptions.FileHeaderTemplate) || property.DeclaringType == typeof(DocumentFormattingOptions) && property.Name == nameof(DocumentFormattingOptions.InsertFinalNewLine) || - property.DeclaringType == typeof(ClassificationOptions) && property.Name == nameof(ClassificationOptions.ForceFrozenPartialSemanticsForCrossProcessOperations) || + property.DeclaringType == typeof(ClassificationOptions) && property.Name == nameof(ClassificationOptions.FrozenPartialSemantics) || + property.DeclaringType == typeof(HighlightingOptions) && property.Name == nameof(HighlightingOptions.FrozenPartialSemantics) || property.DeclaringType == typeof(BlockStructureOptions) && property.Name == nameof(BlockStructureOptions.IsMetadataAsSource)); /// diff --git a/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs b/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs index 2190803b49cc7..793df59acf65f 100644 --- a/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs +++ b/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs @@ -336,7 +336,7 @@ private static async Task> GetTagsFromWorkspaceAsyn var provider = workspace.ExportProvider.GetExportedValue(); var document = workspace.CurrentSolution.GetDocument(hostdoc.Id); - var context = new TaggerContext(document, view.TextSnapshot); + var context = new TaggerContext(document, view.TextSnapshot, frozenPartialSemantics: false); await provider.GetTestAccessor().ProduceTagsAsync(context); return context.TagSpans.Select(x => x.Tag).OrderBy(t => t.OutliningSpan.Value.Start).ToList(); diff --git a/src/EditorFeatures/TestUtilities/BraceHighlighting/AbstractBraceHighlightingTests.cs b/src/EditorFeatures/TestUtilities/BraceHighlighting/AbstractBraceHighlightingTests.cs index 198342c5d430f..fd0d09b17e18f 100644 --- a/src/EditorFeatures/TestUtilities/BraceHighlighting/AbstractBraceHighlightingTests.cs +++ b/src/EditorFeatures/TestUtilities/BraceHighlighting/AbstractBraceHighlightingTests.cs @@ -51,7 +51,7 @@ protected async Task TestBraceHighlightingAsync( var buffer = testDocument.GetTextBuffer(); var document = buffer.CurrentSnapshot.GetRelatedDocumentsWithChanges().FirstOrDefault(); var context = new TaggerContext( - document, buffer.CurrentSnapshot, + document, buffer.CurrentSnapshot, frozenPartialSemantics: false, new SnapshotPoint(buffer.CurrentSnapshot, cursorPosition)); await provider.GetTestAccessor().ProduceTagsAsync(context); diff --git a/src/Features/Core/Portable/DocumentHighlighting/HighlightingOptions.cs b/src/Features/Core/Portable/DocumentHighlighting/HighlightingOptions.cs index 867e8dbca91ff..9a73a3bbc7044 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/HighlightingOptions.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/HighlightingOptions.cs @@ -7,14 +7,11 @@ namespace Microsoft.CodeAnalysis.DocumentHighlighting; [DataContract] -internal readonly record struct HighlightingOptions +internal readonly record struct HighlightingOptions() { [DataMember] public bool HighlightRelatedRegexComponentsUnderCursor { get; init; } = true; [DataMember] public bool HighlightRelatedJsonComponentsUnderCursor { get; init; } = true; - - public HighlightingOptions() - { - } + [DataMember] public bool FrozenPartialSemantics { get; init; } public static HighlightingOptions Default = new(); } diff --git a/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService.cs b/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService.cs index 9bcdcc876919e..23b62c110b1c7 100644 --- a/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService.cs +++ b/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService.cs @@ -57,7 +57,7 @@ public async ValueTask> GetInheritanceMemb var result = await remoteClient.TryInvokeAsync>( solution, (service, solutionInfo, cancellationToken) => - service.GetInheritanceMarginItemsAsync(solutionInfo, document.Id, spanToSearch, includeGlobalImports: includeGlobalImports, frozenPartialSemantics: frozenPartialSemantics, cancellationToken), + service.GetInheritanceMarginItemsAsync(solutionInfo, document.Id, spanToSearch, includeGlobalImports, frozenPartialSemantics, cancellationToken), cancellationToken).ConfigureAwait(false); if (!result.HasValue) @@ -72,8 +72,8 @@ public async ValueTask> GetInheritanceMemb return await GetInheritanceMarginItemsInProcessAsync( document, spanToSearch, - includeGlobalImports: includeGlobalImports, - frozenPartialSemantics: frozenPartialSemantics, + includeGlobalImports, + frozenPartialSemantics, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs b/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs index 6cd3cf5740f80..815e2bf225fc3 100644 --- a/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs +++ b/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs @@ -133,7 +133,7 @@ private async Task> GetInheritanceMarginIt using var _ = ArrayBuilder.GetInstance(out var result); if (includeGlobalImports && !remapped) - result.AddRange(await GetGlobalImportsItemsAsync(document, spanToSearch, frozenPartialSemantics: frozenPartialSemantics, cancellationToken).ConfigureAwait(false)); + result.AddRange(await GetGlobalImportsItemsAsync(document, spanToSearch, frozenPartialSemantics, cancellationToken).ConfigureAwait(false)); if (!symbolAndLineNumbers.IsEmpty) { @@ -141,7 +141,7 @@ private async Task> GetInheritanceMarginIt remappedProject, document: remapped ? null : document, symbolAndLineNumbers, - frozenPartialSemantics: frozenPartialSemantics, + frozenPartialSemantics, cancellationToken).ConfigureAwait(false)); } diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs index 428806dc57f70..694d7166f02c8 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs @@ -60,7 +60,7 @@ public static async Task HandleRequestHelperAsync(Document document, Immu // If the full compilation is not yet available, we'll try getting a partial one. It may contain inaccurate // results but will speed up how quickly we can respond to the client's request. document = document.WithFrozenPartialSemantics(cancellationToken); - options = options with { ForceFrozenPartialSemanticsForCrossProcessOperations = true }; + options = options with { FrozenPartialSemantics = true }; // The results from the range handler should not be cached since we don't want to cache // partial token results. In addition, a range request is only ever called with a whole diff --git a/src/VisualStudio/Core/Def/InheritanceMargin/InheritanceMarginTaggerProvider.cs b/src/VisualStudio/Core/Def/InheritanceMargin/InheritanceMarginTaggerProvider.cs index f3d1a2a29c881..8b6808ef4df2d 100644 --- a/src/VisualStudio/Core/Def/InheritanceMargin/InheritanceMarginTaggerProvider.cs +++ b/src/VisualStudio/Core/Def/InheritanceMargin/InheritanceMarginTaggerProvider.cs @@ -54,6 +54,12 @@ public InheritanceMarginTaggerProvider( protected override TaggerDelay EventChangeDelay => TaggerDelay.OnIdle; + /// + /// We support frozen partial semantics, so we can quickly get inheritance margin items without building SG docs. + /// We will still run a tagging pass after the frozen-pass where we run again on non-frozen docs. + /// + protected override bool SupportsFrozenPartialSemantics => true; + protected override bool CanCreateTagger(ITextView textView, ITextBuffer buffer) { // Match criterion InheritanceMarginViewMarginProvider uses to determine whether @@ -96,17 +102,13 @@ protected override async Task ProduceTagsAsync( var includeGlobalImports = GlobalOptions.GetOption(InheritanceMarginOptionsStorage.InheritanceMarginIncludeGlobalImports, document.Project.Language); - // Use FrozenSemantics Version of document to get the semantics ready, therefore we could have faster - // response. (Since the full load might take a long time) - document = document.WithFrozenPartialSemantics(cancellationToken); - var spanToSearch = spanToTag.SnapshotSpan.Span.ToTextSpan(); var stopwatch = SharedStopwatch.StartNew(); var inheritanceMemberItems = await inheritanceMarginInfoService.GetInheritanceMemberItemsAsync( document, spanToSearch, includeGlobalImports, - frozenPartialSemantics: true, + context.FrozenPartialSemantics, cancellationToken).ConfigureAwait(false); var elapsed = stopwatch.Elapsed; diff --git a/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs b/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs index 417ec439686f0..e35ff55dd5f1a 100644 --- a/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs +++ b/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs @@ -13,7 +13,7 @@ internal readonly record struct ClassificationOptions [DataMember] public bool ClassifyObsoleteSymbols { get; init; } = true; [DataMember] public bool ColorizeRegexPatterns { get; init; } = true; [DataMember] public bool ColorizeJsonPatterns { get; init; } = true; - [DataMember] public bool ForceFrozenPartialSemanticsForCrossProcessOperations { get; init; } = false; + [DataMember] public bool FrozenPartialSemantics { get; init; } = false; public ClassificationOptions() { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index ac73cfebc6f1b..494abb1d470c0 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -1519,7 +1519,14 @@ static AsyncLazy CreateLazyFrozenSolution(SolutionCompilationState com static Solution ComputeFrozenSolution(SolutionCompilationState compilationState, DocumentId documentId, CancellationToken cancellationToken) { var newCompilationState = compilationState.WithFrozenPartialCompilationIncludingSpecificDocument(documentId, cancellationToken); - return new Solution(newCompilationState); + var solution = new Solution(newCompilationState); + + // ensure that this document is within the frozen-partial-document for the solution we're creating. That + // way, if we ask to freeze it again, we'll just the same document back. + Contract.ThrowIfTrue(solution._documentIdToFrozenSolution.Count != 0); + solution._documentIdToFrozenSolution.Add(documentId, AsyncLazy.Create(solution)); + + return solution; } } diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index bb890884334b4..4525cc4fc420b 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -3459,6 +3459,34 @@ await documentToFreeze.Project.GetSemanticVersionAsync(), await frozenDocument.Project.GetSemanticVersionAsync()); } + [Fact] + public async Task TestFreezingTwiceGivesSameDocument() + { + using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics(); + var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp); + project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project; + + var documentToFreeze = project.AddDocument("DocumentToFreeze.cs", SourceText.From("")); + var frozenDocument = documentToFreeze.WithFrozenPartialSemantics(CancellationToken.None); + + // Because we had no compilation produced yet, we expect that only the DocumentToFreeze is in the compilation + Assert.NotSame(frozenDocument, documentToFreeze); + var tree = Assert.Single((await frozenDocument.Project.GetCompilationAsync()).SyntaxTrees); + Assert.Equal("DocumentToFreeze.cs", tree.FilePath); + + // Versions should be different + Assert.NotEqual( + await documentToFreeze.Project.GetDependentSemanticVersionAsync(), + await frozenDocument.Project.GetDependentSemanticVersionAsync()); + + Assert.NotEqual( + await documentToFreeze.Project.GetSemanticVersionAsync(), + await frozenDocument.Project.GetSemanticVersionAsync()); + + var frozenDocument2 = frozenDocument.WithFrozenPartialSemantics(CancellationToken.None); + Assert.Same(frozenDocument, frozenDocument2); + } + [Fact] public async Task TestFrozenPartialProjectHasDifferentSemanticVersions_ChangedDoc1() { diff --git a/src/Workspaces/Remote/ServiceHub/Services/DocumentHighlights/RemoteDocumentHighlightsService.cs b/src/Workspaces/Remote/ServiceHub/Services/DocumentHighlights/RemoteDocumentHighlightsService.cs index 9a630c45e737d..e75bccf52ca21 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DocumentHighlights/RemoteDocumentHighlightsService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DocumentHighlights/RemoteDocumentHighlightsService.cs @@ -2,9 +2,7 @@ // 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.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.DocumentHighlighting; @@ -36,6 +34,9 @@ public ValueTask> GetDocumentHigh return RunServiceAsync(solutionChecksum, async solution => { var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + document = options.FrozenPartialSemantics ? document.WithFrozenPartialSemantics(cancellationToken) : document; + solution = document.Project.Solution; + var documentsToSearch = await documentIdsToSearch.SelectAsArrayAsync(id => solution.GetDocumentAsync(id, includeSourceGenerated: true, cancellationToken)).ConfigureAwait(false); var documentsToSearchSet = ImmutableHashSet.CreateRange(documentsToSearch.WhereNotNull()); diff --git a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs index e4174e8f2d1cf..c291b0fd0eeb7 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs @@ -33,11 +33,9 @@ public ValueTask GetClassificationsAsync( var document = solution.GetDocument(documentId) ?? await solution.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); Contract.ThrowIfNull(document); - if (options.ForceFrozenPartialSemanticsForCrossProcessOperations) - { - // Frozen partial semantics is not automatically passed to OOP, so enable it explicitly when desired + // Frozen partial semantics is not automatically passed to OOP, so enable it explicitly when desired + if (options.FrozenPartialSemantics) document = document.WithFrozenPartialSemantics(cancellationToken); - } using var _ = Classifier.GetPooledList(out var temp); await AbstractClassificationService.AddClassificationsInCurrentProcessAsync( From 209a95c02ebe2e2a084828ed2cd3478663c7e846 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 12:00:27 -0700 Subject: [PATCH 03/33] Add tests --- .../Test/Tagging/AsynchronousTaggerTests.cs | 119 ++++++++++++++++-- 1 file changed, 111 insertions(+), 8 deletions(-) diff --git a/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs b/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs index 02355240a236d..465aaf5402787 100644 --- a/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs +++ b/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs @@ -12,7 +12,6 @@ using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Tagging; using Microsoft.CodeAnalysis.Editor.UnitTests; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; @@ -66,14 +65,15 @@ void M() WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(LargeNumberOfSpans)} creates asynchronous taggers"); - var eventSource = CreateEventSource(); + var eventSource = new TestTaggerEventSource(); var taggerProvider = new TestTaggerProvider( workspace.GetService(), - (s, c) => Enumerable + (_, s) => Enumerable .Range(0, tagsProduced) - .Select(i => new TagSpan(new SnapshotSpan(s.Snapshot, new Span(50 + i * 2, 1)), new TextMarkerTag($"Test{i}"))), + .Select(i => new TagSpan(new SnapshotSpan(s.SnapshotSpan.Snapshot, new Span(50 + i * 2, 1)), new TextMarkerTag($"Test{i}"))), eventSource, workspace.GetService(), + supportsFrozenPartialSemantics: false, asyncListener); var document = workspace.Documents.First(); @@ -141,27 +141,130 @@ class Program Assert.Equal(2, tags.Count()); } - private static TestTaggerEventSource CreateEventSource() - => new(); + [WpfFact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2016199")] + public async Task TestFrozenPartialSemantics1() + { + using var workspace = EditorTestWorkspace.CreateCSharp(""" + class Program + { + } + """); + + var asyncListener = new AsynchronousOperationListener(); + + WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(TestFrozenPartialSemantics1)} creates asynchronous taggers"); + + var eventSource = new TestTaggerEventSource(); + var callbackCounter = 0; + var taggerProvider = new TestTaggerProvider( + workspace.GetService(), + (c, s) => + { + Assert.True(callbackCounter <= 1); + if (callbackCounter is 0) + { + Assert.True(c.FrozenPartialSemantics); + } + else + { + Assert.False(c.FrozenPartialSemantics); + } + + callbackCounter++; + return [new TagSpan(new SnapshotSpan(s.SnapshotSpan.Snapshot, new Span(0, 1)), new TextMarkerTag($"Test"))]; + }, + eventSource, + workspace.GetService(), + supportsFrozenPartialSemantics: true, + asyncListener); + + var document = workspace.Documents.First(); + var textBuffer = document.GetTextBuffer(); + var snapshot = textBuffer.CurrentSnapshot; + using var tagger = taggerProvider.CreateTagger(textBuffer); + Contract.ThrowIfNull(tagger); + + var snapshotSpans = new NormalizedSnapshotSpanCollection( + snapshot.GetFullSpan()); + + eventSource.SendUpdateEvent(); + + await asyncListener.ExpeditedWaitAsync(); + + var tags = tagger.GetTags(snapshotSpans); + Assert.Equal(1, tags.Count()); + Assert.Equal(2, callbackCounter); + } + + [WpfFact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2016199")] + public async Task TestFrozenPartialSemantics2() + { + using var workspace = EditorTestWorkspace.CreateCSharp(""" + class Program + { + } + """); + + var asyncListener = new AsynchronousOperationListener(); + + WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(TestFrozenPartialSemantics2)} creates asynchronous taggers"); + + var eventSource = new TestTaggerEventSource(); + var callbackCounter = 0; + var taggerProvider = new TestTaggerProvider( + workspace.GetService(), + (c, s) => + { + Assert.True(callbackCounter == 0); + Assert.False(c.FrozenPartialSemantics); + + callbackCounter++; + return [new TagSpan(new SnapshotSpan(s.SnapshotSpan.Snapshot, new Span(0, 1)), new TextMarkerTag($"Test"))]; + }, + eventSource, + workspace.GetService(), + supportsFrozenPartialSemantics: false, + asyncListener); + + var document = workspace.Documents.First(); + var textBuffer = document.GetTextBuffer(); + var snapshot = textBuffer.CurrentSnapshot; + using var tagger = taggerProvider.CreateTagger(textBuffer); + Contract.ThrowIfNull(tagger); + + var snapshotSpans = new NormalizedSnapshotSpanCollection( + snapshot.GetFullSpan()); + + eventSource.SendUpdateEvent(); + + await asyncListener.ExpeditedWaitAsync(); + + var tags = tagger.GetTags(snapshotSpans); + Assert.Equal(1, tags.Count()); + Assert.Equal(1, callbackCounter); + } private sealed class TestTaggerProvider( IThreadingContext threadingContext, - Func>> callback, + Func, DocumentSnapshotSpan, IEnumerable>> callback, ITaggerEventSource eventSource, IGlobalOptionService globalOptions, + bool supportsFrozenPartialSemantics, IAsynchronousOperationListener asyncListener) : AsynchronousTaggerProvider(threadingContext, globalOptions, visibilityTracker: null, asyncListener) { protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; + protected override bool SupportsFrozenPartialSemantics => supportsFrozenPartialSemantics; + protected override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer) => eventSource; protected override Task ProduceTagsAsync( TaggerContext context, DocumentSnapshotSpan snapshotSpan, int? caretPosition, CancellationToken cancellationToken) { - foreach (var tag in callback(snapshotSpan.SnapshotSpan, cancellationToken)) + foreach (var tag in callback(context, snapshotSpan)) context.AddTag(tag); return Task.CompletedTask; From 14114344c2786d60a6dda346e7917b0ea6f21d22 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 12:12:32 -0700 Subject: [PATCH 04/33] Update tests --- .../Test/Tagging/AsynchronousTaggerTests.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs b/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs index 465aaf5402787..6833e685ded8c 100644 --- a/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs +++ b/src/EditorFeatures/Test/Tagging/AsynchronousTaggerTests.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.Editor.Tagging; using Microsoft.CodeAnalysis.Editor.UnitTests; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text.Shared.Extensions; @@ -154,6 +155,8 @@ class Program WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(TestFrozenPartialSemantics1)} creates asynchronous taggers"); + var testDocument = workspace.Documents.First(); + var eventSource = new TestTaggerEventSource(); var callbackCounter = 0; var taggerProvider = new TestTaggerProvider( @@ -161,13 +164,17 @@ class Program (c, s) => { Assert.True(callbackCounter <= 1); + var document = workspace.CurrentSolution.GetRequiredDocument(testDocument.Id); if (callbackCounter is 0) { Assert.True(c.FrozenPartialSemantics); + // Should be getting a frozen document here. + Assert.NotSame(document, c.SpansToTag.First().Document); } else { Assert.False(c.FrozenPartialSemantics); + Assert.Same(document, c.SpansToTag.First().Document); } callbackCounter++; @@ -178,22 +185,17 @@ class Program supportsFrozenPartialSemantics: true, asyncListener); - var document = workspace.Documents.First(); - var textBuffer = document.GetTextBuffer(); - var snapshot = textBuffer.CurrentSnapshot; + var textBuffer = testDocument.GetTextBuffer(); using var tagger = taggerProvider.CreateTagger(textBuffer); Contract.ThrowIfNull(tagger); - var snapshotSpans = new NormalizedSnapshotSpanCollection( - snapshot.GetFullSpan()); - eventSource.SendUpdateEvent(); await asyncListener.ExpeditedWaitAsync(); + Assert.Equal(2, callbackCounter); - var tags = tagger.GetTags(snapshotSpans); + var tags = tagger.GetTags(new NormalizedSnapshotSpanCollection(textBuffer.CurrentSnapshot.GetFullSpan())); Assert.Equal(1, tags.Count()); - Assert.Equal(2, callbackCounter); } [WpfFact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2016199")] @@ -209,14 +211,18 @@ class Program WpfTestRunner.RequireWpfFact($"{nameof(AsynchronousTaggerTests)}.{nameof(TestFrozenPartialSemantics2)} creates asynchronous taggers"); + var testDocument = workspace.Documents.First(); + var eventSource = new TestTaggerEventSource(); var callbackCounter = 0; var taggerProvider = new TestTaggerProvider( workspace.GetService(), (c, s) => { + var document = workspace.CurrentSolution.GetRequiredDocument(testDocument.Id); Assert.True(callbackCounter == 0); Assert.False(c.FrozenPartialSemantics); + Assert.Same(document, c.SpansToTag.First().Document); callbackCounter++; return [new TagSpan(new SnapshotSpan(s.SnapshotSpan.Snapshot, new Span(0, 1)), new TextMarkerTag($"Test"))]; @@ -226,22 +232,17 @@ class Program supportsFrozenPartialSemantics: false, asyncListener); - var document = workspace.Documents.First(); - var textBuffer = document.GetTextBuffer(); - var snapshot = textBuffer.CurrentSnapshot; + var textBuffer = testDocument.GetTextBuffer(); using var tagger = taggerProvider.CreateTagger(textBuffer); Contract.ThrowIfNull(tagger); - var snapshotSpans = new NormalizedSnapshotSpanCollection( - snapshot.GetFullSpan()); - eventSource.SendUpdateEvent(); await asyncListener.ExpeditedWaitAsync(); + Assert.Equal(1, callbackCounter); - var tags = tagger.GetTags(snapshotSpans); + var tags = tagger.GetTags(new NormalizedSnapshotSpanCollection(textBuffer.CurrentSnapshot.GetFullSpan())); Assert.Equal(1, tags.Count()); - Assert.Equal(1, callbackCounter); } private sealed class TestTaggerProvider( From be9305dacece1f3a897dc9e16385576bb4fc55f1 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:00:03 -0700 Subject: [PATCH 05/33] in progress --- ...actAsynchronousTaggerProvider.TagSource.cs | 21 +++-- ...ousTaggerProvider.TagSource_ProduceTags.cs | 91 +++++++++++++++++-- .../Core/Tagging/TagSourceQueueItem.cs | 23 +++++ 3 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index dd1514018afa6..c62e54f2aa1e1 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -7,10 +7,8 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Text; @@ -75,11 +73,18 @@ private sealed partial class TagSource private readonly AsyncBatchingWorkQueue _normalPriTagsChangedQueue; /// - /// Boolean specifies if this is the initial set of tags being computed or not. This queue is used to batch - /// up event change notifications and only dispatch one recomputation every - /// to actually produce the latest set of tags. + /// This queue is used to batch up event change notifications and only dispatch one recomputation every to actually produce the latest set of tags. /// - private readonly AsyncBatchingWorkQueue<(bool highPriority, bool frozenPartialSemantics)> _eventChangeQueue; + private readonly AsyncBatchingWorkQueue _eventChangeQueue; + + /// + /// For taggers that support tagging frozen and non-frozen snapshots, this cancellation series controls the + /// non-frozen tagging pass. We want this to be separately cancellable so that if new events come in that we + /// cancel the expensive non-frozen tagging pass (which might be computing skeletons, SG docs, etc.), do the + /// next cheap frozen-tagging-pass, and then push the expensive-nonfrozen-tagging-pass to the end again. + /// + private readonly CancellationSeries _nonFrozenComputationCancellationSeries = new(); #endregion @@ -164,10 +169,10 @@ public TagSource( // // PERF: Use AsyncBatchingWorkQueue instead of AsyncBatchingWorkQueue because // the latter has an async state machine that rethrows a very common cancellation exception. - _eventChangeQueue = new AsyncBatchingWorkQueue<(bool highPriority, bool frozenPartialSemantics)>( + _eventChangeQueue = new AsyncBatchingWorkQueue( dataSource.EventChangeDelay.ComputeTimeDelay(), ProcessEventChangeAsync, - EqualityComparer<(bool highPriority, bool frozenPartialSemantics)>.Default, + EqualityComparer.Default, asyncListener, _disposalTokenSource.Token); diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index ef1ec04f53f38..e8a9f5f300d35 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; @@ -168,17 +169,89 @@ private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) => EnqueueWork(highPriority: false); private void EnqueueWork(bool highPriority) - => EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics); + { + // We're enqueuing another request to do lightweight frozen-partial tagging. Stop any expensive non-frozen + // tagging pass currently in progress. It will get re-enqueues after this next frozen partial pass finishes. + _nonFrozenComputationCancellationSeries.CreateNext(); - private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) - => _eventChangeQueue.AddWork((highPriority, frozenPartialSemantics), _dataSource.CancelOnNewWork); + EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics, nonFrozenComputationToken: null); + } - private async ValueTask ProcessEventChangeAsync(ImmutableSegmentedList<(bool highPriority, bool frozenPartialSemantics)> changes, CancellationToken cancellationToken) + private void EnqueueWork(bool highPriority, bool frozenPartialSemantics, CancellationToken? nonFrozenComputationToken) { + // If we support frozen partial semantics, but this is the request to do expensive work, then this must come + // with an associated cancellation token to cancel this work + if (_dataSource.SupportsFrozenPartialSemantics && !frozenPartialSemantics) + { + Contract.ThrowIfNull(nonFrozenComputationToken); + } + + _eventChangeQueue.AddWork( + new TagSourceQueueItem(highPriority, frozenPartialSemantics, nonFrozenComputationToken), + _dataSource.CancelOnNewWork); + } + + private async ValueTask ProcessEventChangeAsync( + ImmutableSegmentedList changes, CancellationToken cancellationToken) + { + if (changes.Count == 0) + return; + // If any of the requests was high priority, then compute at that speed. - var highPriority = changes.Contains(x => x.highPriority); - var frozenPartialSemantics = changes.Contains(t => t.frozenPartialSemantics); - await RecomputeTagsAsync(highPriority, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); + var highPriority = changes.Contains(x => x.HighPriority); + + // 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 = changes.Contains(t => t.FrozenPartialSemantics); + + if (frozenPartialSemantics) + { + // If we were asking for frozen partial semantics, then just proceed as normal, getting those tags + // quickly, but inaccurately. + await RecomputeTagsAsync(highPriority, frozenPartialSemantics: true, cancellationToken).ConfigureAwait(false); + } + else + { + if (!_dataSource.SupportsFrozenPartialSemantics) + { + // We're asking for normal expensive full tags, and this tagger doesn't support frozen partial + // tagging anyways, so just proceed as normal, asking for expensive tags. + await RecomputeTagsAsync(highPriority, frozenPartialSemantics: false, cancellationToken).ConfigureAwait(false); + } + else + { + // We're asking for expensive tags, and this tagger supports frozen partial tags. Kick off the work + // to do this expensive tagging, but attach ourselves to the requested cancellation token so this + // expensive work can be canceled if new requests for frozen partial work come in. + + // We must have at least one request asking for full semantics. + Contract.ThrowIfFalse(changes.Any(c => !c.FrozenPartialSemantics)); + + // All those requests should have cancellation tokens provided with them. + Contract.ThrowIfFalse(changes.Where(c => !c.FrozenPartialSemantics).All(c => c.NonFrozenComputationToken != null)); + + // Get the first non-cancelled token if present. + var nonFrozenComputationToken = changes.FirstOrNull(t => t.NonFrozenComputationToken?.IsCancellationRequested is false)?.NonFrozenComputationToken; + + // If there is no non-frozen cancellation token that has not been triggered yet, then all non-frozen work + // has been canceled, and we can immediately bail out. + if (nonFrozenComputationToken is null) + return; + + // Otherwise, link this token with the main queue cancellation token and do the actual tagging. + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, nonFrozenComputationToken.Value); + + // Need a dedicated try/catch here since we're operating on a different token than the queue's token. + try + { + await RecomputeTagsAsync(highPriority, frozenPartialSemantics, linkedTokenSource.Token).ConfigureAwait(false); + } + catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, linkedTokenSource.Token)) + { + } + } + } } /// @@ -295,7 +368,9 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // message, if it sees any other events asking for frozen-partial-semantics, it will process in that // mode again, kicking the can down the road to finally end with non-frozen-partial computation if (frozenPartialSemantics) - this.EnqueueWork(highPriority, frozenPartialSemantics: false); + { + this.EnqueueWork(highPriority, frozenPartialSemantics: false, _nonFrozenComputationCancellationSeries.CreateNext(CancellationToken.None)); + } } } diff --git a/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs b/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs new file mode 100644 index 0000000000000..7c087963cc017 --- /dev/null +++ b/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs @@ -0,0 +1,23 @@ +// 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.Tagging; + +internal partial class AbstractAsynchronousTaggerProvider +{ + private sealed partial class TagSource + { + /// Specifies if this is the initial set of tags being computed or not, and no + /// artificial delays should be inserted when computing the tags. + /// Indicates if we should + /// compute with frozen partial semantics or not. + /// If is false, and this + /// queue does support computing frozen partial semantics (see ) + /// then this is a cancellation token that can cancel the expensive work being done if new frozen-partial work + /// is requested. + private record struct TagSourceQueueItem(bool HighPriority, bool FrozenPartialSemantics, CancellationToken? NonFrozenComputationToken); + } +} From 303982ac6b7af9aaca1da1be19d0f548b560cb26 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:03:51 -0700 Subject: [PATCH 06/33] Cancellation series --- ...ousTaggerProvider.TagSource_ProduceTags.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index e8a9f5f300d35..4ad867ae605df 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -170,20 +170,24 @@ private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) private void EnqueueWork(bool highPriority) { - // We're enqueuing another request to do lightweight frozen-partial tagging. Stop any expensive non-frozen - // tagging pass currently in progress. It will get re-enqueues after this next frozen partial pass finishes. - _nonFrozenComputationCancellationSeries.CreateNext(); - EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics, nonFrozenComputationToken: null); + EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics); } - private void EnqueueWork(bool highPriority, bool frozenPartialSemantics, CancellationToken? nonFrozenComputationToken) + private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) { - // If we support frozen partial semantics, but this is the request to do expensive work, then this must come - // with an associated cancellation token to cancel this work - if (_dataSource.SupportsFrozenPartialSemantics && !frozenPartialSemantics) + // Cancellation token if this expensive work that we want to be cancellable when cheap work comes in. + CancellationToken? nonFrozenComputationToken = null; + + if (_dataSource.SupportsFrozenPartialSemantics) { - Contract.ThrowIfNull(nonFrozenComputationToken); + // We do support frozen partial work. Cancel any expensive work in flight as new work has come in. + var nextToken = _nonFrozenComputationCancellationSeries.CreateNext(); + + // If this is new work *is* the expensive work, then use this token to allow cancellation of it when + // more new work comes in. + if (!frozenPartialSemantics) + nonFrozenComputationToken = nextToken; } _eventChangeQueue.AddWork( @@ -369,7 +373,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // mode again, kicking the can down the road to finally end with non-frozen-partial computation if (frozenPartialSemantics) { - this.EnqueueWork(highPriority, frozenPartialSemantics: false, _nonFrozenComputationCancellationSeries.CreateNext(CancellationToken.None)); + this.EnqueueWork(highPriority, frozenPartialSemantics: false); } } } From 06116b2cdec8c9ffb053e2f86f256750387a6c38 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:05:22 -0700 Subject: [PATCH 07/33] comments --- ...rEmbeddedClassificationViewTaggerProvider.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index bc9bc90754f08..48a35c21ee0f7 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -98,15 +98,20 @@ protected sealed override Task ProduceTagsAsync( if (isLspSemanticTokensEnabled) return Task.CompletedTask; - // Don't block getting classifications on building the full compilation. This may take a significant amount - // of time and can cause a very latency sensitive operation (copying) to block the user while we wait on this - // work to happen. + // Because of the `SupportsFrozenPartialSemantics => true` property above, we'll do classification in two + // passes. In the first pass we do not block getting classifications on building the full compilation. This + // may take a significant amount of time and can cause a very latency sensitive operation (copying) to block the + // user while we wait on this work to happen. // // It's also a better experience to get classifications to the user faster versus waiting a potentially large // amount of time waiting for all the compilation information to be built. For example, we can classify types - // that we've parsed in other files, or partially loaded from metadata, even if we're still parsing/loading. - // For cross language projects, this also produces semantic classifications more quickly as we do not have to - // wait on skeletons to be built. + // that we've parsed in other files, or partially loaded from metadata, even if we're still parsing/loading. For + // cross language projects, this also produces semantic classifications more quickly as we do not have to wait + // on skeletons to be built. + // + // In the second pass though, we will go and do things without frozen-partial semantics, so that we do always + // snap to a final correct state. Note: the expensive second pass will be kicked down the road as new events + // come in to classify things. var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language) with { FrozenPartialSemantics = context.FrozenPartialSemantics, From b9761557f9901483f2f1689bf660f0e2b19b472c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:06:50 -0700 Subject: [PATCH 08/33] move docs --- ...mbeddedClassificationViewTaggerProvider.cs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index 48a35c21ee0f7..f5b1dc7bd4ee7 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -54,8 +54,21 @@ protected AbstractSemanticOrEmbeddedClassificationViewTaggerProvider( protected sealed override TaggerDelay EventChangeDelay => TaggerDelay.Short; /// - /// We support frozen partial semantics, so we can quickly get classification items without building SG docs. We - /// will still run a tagging pass after the frozen-pass where we run again on non-frozen docs. + /// We do classification in two passes. In the first pass we do not block getting classifications on building the + /// full compilation. This may take a significant amount of time and can cause a very latency sensitive operation + /// (copying) to block the user while we wait on this work to happen. + /// + /// It's also a better experience to get classifications to the user faster versus waiting a potentially large + /// amount of time waiting for all the compilation information to be built. For example, we can classify types that + /// we've parsed in other files, or partially loaded from metadata, even if we're still parsing/loading. For cross + /// language projects, this also produces semantic classifications more quickly as we do not have to wait on + /// skeletons to be built. + /// + /// + /// In the second pass though, we will go and do things without frozen-partial semantics, so that we do always snap + /// to a final correct state. Note: the expensive second pass will be kicked down the road as new events come in to + /// classify things. + /// /// protected sealed override bool SupportsFrozenPartialSemantics => true; @@ -98,20 +111,6 @@ protected sealed override Task ProduceTagsAsync( if (isLspSemanticTokensEnabled) return Task.CompletedTask; - // Because of the `SupportsFrozenPartialSemantics => true` property above, we'll do classification in two - // passes. In the first pass we do not block getting classifications on building the full compilation. This - // may take a significant amount of time and can cause a very latency sensitive operation (copying) to block the - // user while we wait on this work to happen. - // - // It's also a better experience to get classifications to the user faster versus waiting a potentially large - // amount of time waiting for all the compilation information to be built. For example, we can classify types - // that we've parsed in other files, or partially loaded from metadata, even if we're still parsing/loading. For - // cross language projects, this also produces semantic classifications more quickly as we do not have to wait - // on skeletons to be built. - // - // In the second pass though, we will go and do things without frozen-partial semantics, so that we do always - // snap to a final correct state. Note: the expensive second pass will be kicked down the road as new events - // come in to classify things. var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language) with { FrozenPartialSemantics = context.FrozenPartialSemantics, From 4e3d729f542a25c6d5a7469c91d2123f644a39e5 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:07:57 -0700 Subject: [PATCH 09/33] Simplify --- ...stractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 4ad867ae605df..99f3a9e255ddb 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -169,10 +169,7 @@ private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) => EnqueueWork(highPriority: false); private void EnqueueWork(bool highPriority) - { - - EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics); - } + => EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics); private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) { From d86916808e2f6d0c90ac98bd1ca622d44698f86a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:09:44 -0700 Subject: [PATCH 10/33] ASserts --- ...AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 3 +-- .../Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue`2.cs | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 99f3a9e255ddb..a5012373fdc60 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -195,8 +195,7 @@ private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) private async ValueTask ProcessEventChangeAsync( ImmutableSegmentedList changes, CancellationToken cancellationToken) { - if (changes.Count == 0) - return; + Contract.ThrowIfTrue(changes.IsEmpty); // If any of the requests was high priority, then compute at that speed. var highPriority = changes.Contains(x => x.HighPriority); diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue`2.cs b/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue`2.cs index f0cab2c4b4f33..858e3f85d4ed3 100644 --- a/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue`2.cs +++ b/src/Workspaces/Core/Portable/Shared/Utilities/AsyncBatchingWorkQueue`2.cs @@ -98,6 +98,8 @@ internal class AsyncBatchingWorkQueue #endregion + /// Callback to process queued work items. The list of items passed in is + /// guaranteed to always be non-empty. public AsyncBatchingWorkQueue( TimeSpan delay, Func, CancellationToken, ValueTask> processBatchAsync, From 7c8bf357660683e9c21d5222dd254c5b954e95b2 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:10:57 -0700 Subject: [PATCH 11/33] Update src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs --- .../AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index a5012373fdc60..69a8bfcbc1f3d 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -368,9 +368,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // message, if it sees any other events asking for frozen-partial-semantics, it will process in that // mode again, kicking the can down the road to finally end with non-frozen-partial computation if (frozenPartialSemantics) - { this.EnqueueWork(highPriority, frozenPartialSemantics: false); - } } } From 3ec35301c01cd2872c47f7e09cf118f65148daeb Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:11:51 -0700 Subject: [PATCH 12/33] Simplify --- ...bstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index a5012373fdc60..11040d0fc5ec1 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -654,8 +654,8 @@ private DiffResult ComputeDifference( !this.CachedTagTrees.TryGetValue(buffer, out _)) { // Compute this as a high priority work item to have the lease amount of blocking as possible. - _dataSource.ThreadingContext.JoinableTaskFactory.Run(async () => - await this.RecomputeTagsAsync(highPriority: true, _dataSource.SupportsFrozenPartialSemantics, _disposalTokenSource.Token).ConfigureAwait(false)); + _dataSource.ThreadingContext.JoinableTaskFactory.Run(() => + this.RecomputeTagsAsync(highPriority: true, _dataSource.SupportsFrozenPartialSemantics, _disposalTokenSource.Token)); } _firstTagsRequest = false; From 24c34e588ffe14edc3a79813f9b11a4465aed083 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:52:01 -0700 Subject: [PATCH 13/33] move helpers --- ...mbeddedClassificationViewTaggerProvider.cs | 183 +++++++++++++++++- .../Semantic/ClassificationUtilities.cs | 181 ----------------- 2 files changed, 174 insertions(+), 190 deletions(-) diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index f5b1dc7bd4ee7..26cb1f005cbbd 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -2,22 +2,30 @@ // 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.Collections; using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Tagging; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.CodeAnalysis.Workspaces; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Tagging; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Classification; @@ -72,6 +80,9 @@ protected AbstractSemanticOrEmbeddedClassificationViewTaggerProvider( /// protected sealed override bool SupportsFrozenPartialSemantics => true; + protected override bool TagEquals(IClassificationTag tag1, IClassificationTag tag2) + => tag1.ClassificationType.Classification == tag2.ClassificationType.Classification; + protected sealed override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer) { this.ThreadingContext.ThrowIfNotOnUIThread(); @@ -86,39 +97,193 @@ protected sealed override ITaggerEventSource CreateEventSource(ITextView textVie TaggerEventSources.OnGlobalOptionChanged(_globalOptions, ClassificationOptionsStorage.ClassifyObsoleteSymbols)); } - protected sealed override Task ProduceTagsAsync( + protected sealed override async Task ProduceTagsAsync( TaggerContext context, DocumentSnapshotSpan spanToTag, CancellationToken cancellationToken) { var document = spanToTag.Document; if (document == null) - return Task.CompletedTask; + return; // Attempt to get a classification service which will actually produce the results. // If we can't (because we have no Document, or because the language doesn't support // this service), then bail out immediately. var classificationService = document.GetLanguageService(); if (classificationService == null) - return Task.CompletedTask; + return; // The LSP client will handle producing tags when running under the LSP editor. // Our tagger implementation should return nothing to prevent conflicts. var workspaceContextService = document.Project.Solution.Services.GetRequiredService(); if (workspaceContextService?.IsInLspEditorContext() == true) - return Task.CompletedTask; + return; // If the LSP semantic tokens feature flag is enabled, return nothing to prevent conflicts. var isLspSemanticTokensEnabled = _globalOptions.GetOption(LspOptionsStorage.LspSemanticTokensFeatureFlag); if (isLspSemanticTokensEnabled) - return Task.CompletedTask; + return; var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language) with { FrozenPartialSemantics = context.FrozenPartialSemantics, }; - return ClassificationUtilities.ProduceTagsAsync( - context, spanToTag, classificationService, _typeMap, classificationOptions, _type, cancellationToken); + + await ProduceTagsAsync( + context, spanToTag, classificationService, classificationOptions, cancellationToken).ConfigureAwait(false); } - protected override bool TagEquals(IClassificationTag tag1, IClassificationTag tag2) - => tag1.ClassificationType.Classification == tag2.ClassificationType.Classification; + public async Task ProduceTagsAsync( + TaggerContext context, + DocumentSnapshotSpan spanToTag, + IClassificationService classificationService, + ClassificationOptions options, + CancellationToken cancellationToken) + { + var document = spanToTag.Document; + if (document == null) + return; + + var classified = await TryClassifyContainingMemberSpanAsync( + context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false); + if (classified) + { + return; + } + + // We weren't able to use our specialized codepaths for semantic classifying. + // Fall back to classifying the full span that was asked for. + await ClassifySpansAsync( + context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false); + } + + private async Task TryClassifyContainingMemberSpanAsync( + TaggerContext context, + Document document, + SnapshotSpan snapshotSpan, + IClassificationService classificationService, + ClassificationOptions options, + CancellationToken cancellationToken) + { + var range = context.TextChangeRange; + if (range == null) + { + // There was no text change range, we can't just reclassify a member body. + return false; + } + + // there was top level edit, check whether that edit updated top level element + if (!document.SupportsSyntaxTree) + return false; + + var lastSemanticVersion = (VersionStamp?)context.State; + if (lastSemanticVersion != null) + { + var currentSemanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + if (lastSemanticVersion.Value != currentSemanticVersion) + { + // A top level change was made. We can't perform this optimization. + return false; + } + } + + var service = document.GetRequiredLanguageService(); + + // perf optimization. Check whether all edits since the last update has happened within + // a member. If it did, it will find the member that contains the changes and only refresh + // that member. If possible, try to get a speculative binder to make things even cheaper. + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var changedSpan = new TextSpan(range.Value.Span.Start, range.Value.NewLength); + var member = service.GetContainingMemberDeclaration(root, changedSpan.Start); + if (member == null || !member.FullSpan.Contains(changedSpan)) + { + // The edit was not fully contained in a member. Reclassify everything. + return false; + } + + var memberBodySpan = service.GetMemberBodySpanForSpeculativeBinding(member); + if (memberBodySpan.IsEmpty) + { + // Wasn't a member we could reclassify independently. + return false; + } + + // TODO(cyrusn): Unclear what this logic is for. It looks like it's just trying to narrow the span down + // slightly from the full member, just to its body. Unclear if this provides any substantive benefits. But + // keeping for now to preserve long standing logic. + var memberSpanToClassify = memberBodySpan.Contains(changedSpan) + ? memberBodySpan.ToSpan() + : member.FullSpan.ToSpan(); + + // Take the subspan we know we want to classify, and intersect that with the actual span being asked for. + // That way if we're only asking for a portion of a method, we still only classify that, and not the whole + // method. + var finalSpanToClassify = memberSpanToClassify.Intersection(snapshotSpan.Span); + if (finalSpanToClassify is null) + return false; + + var subSpanToTag = new SnapshotSpan(snapshotSpan.Snapshot, finalSpanToClassify.Value); + + // re-classify only the member we're inside. + await ClassifySpansAsync( + context, document, subSpanToTag, classificationService, options, cancellationToken).ConfigureAwait(false); + return true; + } + + private async Task ClassifySpansAsync( + TaggerContext context, + Document document, + SnapshotSpan snapshotSpan, + IClassificationService classificationService, + ClassificationOptions options, + CancellationToken cancellationToken) + { + try + { + using (Logger.LogBlock(FunctionId.Tagger_SemanticClassification_TagProducer_ProduceTags, cancellationToken)) + { + using var _ = Classifier.GetPooledList(out var classifiedSpans); + + await AddClassificationsAsync( + classificationService, options, document, snapshotSpan, classifiedSpans, cancellationToken).ConfigureAwait(false); + + foreach (var span in classifiedSpans) + context.AddTag(ClassificationUtilities.Convert(_typeMap, snapshotSpan.Snapshot, span)); + + var version = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); + + // Let the context know that this was the span we actually tried to tag. + context.SetSpansTagged([snapshotSpan]); + context.State = version; + } + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) + { + throw ExceptionUtilities.Unreachable(); + } + } + + private async Task AddClassificationsAsync( + IClassificationService classificationService, + ClassificationOptions options, + Document document, + SnapshotSpan snapshotSpan, + SegmentedList classifiedSpans, + CancellationToken cancellationToken) + { + if (_type == ClassificationType.Semantic) + { + await classificationService.AddSemanticClassificationsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); + } + else if (_type == ClassificationType.EmbeddedLanguage) + { + await classificationService.AddEmbeddedLanguageClassificationsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); + } + else + { + throw ExceptionUtilities.UnexpectedValue(_type); + } + } } diff --git a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs index f3961a268fda0..c2f2c54f32c78 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/ClassificationUtilities.cs @@ -2,28 +2,10 @@ // 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.Generic; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Classification; -using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Editor; -using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.Editor.Tagging; -using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Classification; @@ -35,167 +17,4 @@ public static TagSpan Convert(IClassificationTypeMap typeMap classifiedSpan.TextSpan.ToSnapshotSpan(snapshot), new ClassificationTag(typeMap.GetClassificationType(classifiedSpan.ClassificationType))); } - - public static async Task ProduceTagsAsync( - TaggerContext context, - DocumentSnapshotSpan spanToTag, - IClassificationService classificationService, - ClassificationTypeMap typeMap, - ClassificationOptions options, - ClassificationType type, - CancellationToken cancellationToken) - { - var document = spanToTag.Document; - if (document == null) - return; - - var classified = await TryClassifyContainingMemberSpanAsync( - context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); - if (classified) - { - return; - } - - // We weren't able to use our specialized codepaths for semantic classifying. - // Fall back to classifying the full span that was asked for. - await ClassifySpansAsync( - context, document, spanToTag.SnapshotSpan, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); - } - - private static async Task TryClassifyContainingMemberSpanAsync( - TaggerContext context, - Document document, - SnapshotSpan snapshotSpan, - IClassificationService classificationService, - ClassificationTypeMap typeMap, - ClassificationOptions options, - ClassificationType type, - CancellationToken cancellationToken) - { - var range = context.TextChangeRange; - if (range == null) - { - // There was no text change range, we can't just reclassify a member body. - return false; - } - - // there was top level edit, check whether that edit updated top level element - if (!document.SupportsSyntaxTree) - return false; - - var lastSemanticVersion = (VersionStamp?)context.State; - if (lastSemanticVersion != null) - { - var currentSemanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - if (lastSemanticVersion.Value != currentSemanticVersion) - { - // A top level change was made. We can't perform this optimization. - return false; - } - } - - var service = document.GetRequiredLanguageService(); - - // perf optimization. Check whether all edits since the last update has happened within - // a member. If it did, it will find the member that contains the changes and only refresh - // that member. If possible, try to get a speculative binder to make things even cheaper. - - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var changedSpan = new TextSpan(range.Value.Span.Start, range.Value.NewLength); - var member = service.GetContainingMemberDeclaration(root, changedSpan.Start); - if (member == null || !member.FullSpan.Contains(changedSpan)) - { - // The edit was not fully contained in a member. Reclassify everything. - return false; - } - - var memberBodySpan = service.GetMemberBodySpanForSpeculativeBinding(member); - if (memberBodySpan.IsEmpty) - { - // Wasn't a member we could reclassify independently. - return false; - } - - // TODO(cyrusn): Unclear what this logic is for. It looks like it's just trying to narrow the span down - // slightly from the full member, just to its body. Unclear if this provides any substantive benefits. But - // keeping for now to preserve long standing logic. - var memberSpanToClassify = memberBodySpan.Contains(changedSpan) - ? memberBodySpan.ToSpan() - : member.FullSpan.ToSpan(); - - // Take the subspan we know we want to classify, and intersect that with the actual span being asked for. - // That way if we're only asking for a portion of a method, we still only classify that, and not the whole - // method. - var finalSpanToClassify = memberSpanToClassify.Intersection(snapshotSpan.Span); - if (finalSpanToClassify is null) - return false; - - var subSpanToTag = new SnapshotSpan(snapshotSpan.Snapshot, finalSpanToClassify.Value); - - // re-classify only the member we're inside. - await ClassifySpansAsync( - context, document, subSpanToTag, classificationService, typeMap, options, type, cancellationToken).ConfigureAwait(false); - return true; - } - - private static async Task ClassifySpansAsync( - TaggerContext context, - Document document, - SnapshotSpan snapshotSpan, - IClassificationService classificationService, - ClassificationTypeMap typeMap, - ClassificationOptions options, - ClassificationType type, - CancellationToken cancellationToken) - { - try - { - using (Logger.LogBlock(FunctionId.Tagger_SemanticClassification_TagProducer_ProduceTags, cancellationToken)) - { - using var _ = Classifier.GetPooledList(out var classifiedSpans); - - await AddClassificationsAsync( - classificationService, options, document, snapshotSpan, classifiedSpans, type, cancellationToken).ConfigureAwait(false); - - foreach (var span in classifiedSpans) - context.AddTag(Convert(typeMap, snapshotSpan.Snapshot, span)); - - var version = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - - // Let the context know that this was the span we actually tried to tag. - context.SetSpansTagged([snapshotSpan]); - context.State = version; - } - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) - { - throw ExceptionUtilities.Unreachable(); - } - } - - private static async Task AddClassificationsAsync( - IClassificationService classificationService, - ClassificationOptions options, - Document document, - SnapshotSpan snapshotSpan, - SegmentedList classifiedSpans, - ClassificationType type, - CancellationToken cancellationToken) - { - if (type == ClassificationType.Semantic) - { - await classificationService.AddSemanticClassificationsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); - } - else if (type == ClassificationType.EmbeddedLanguage) - { - await classificationService.AddEmbeddedLanguageClassificationsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); - } - else - { - throw ExceptionUtilities.UnexpectedValue(type); - } - } } From 3de17645f13e02825d1e6d541fa0006c53e80283 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:53:08 -0700 Subject: [PATCH 14/33] indentatin --- ...bstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index 26cb1f005cbbd..c9ab7239cf4d9 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -143,7 +143,7 @@ public async Task ProduceTagsAsync( return; var classified = await TryClassifyContainingMemberSpanAsync( - context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false); + context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false); if (classified) { return; From 84c7aab431ba46a36bda99c74e7b25c198de6144 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:55:05 -0700 Subject: [PATCH 15/33] REmove type --- .../Core/Tagging/TagSourceQueueItem.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs b/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs index 7c087963cc017..84bb54b767d02 100644 --- a/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs +++ b/src/EditorFeatures/Core/Tagging/TagSourceQueueItem.cs @@ -8,16 +8,13 @@ namespace Microsoft.CodeAnalysis.Editor.Tagging; internal partial class AbstractAsynchronousTaggerProvider { - private sealed partial class TagSource - { - /// Specifies if this is the initial set of tags being computed or not, and no - /// artificial delays should be inserted when computing the tags. - /// Indicates if we should - /// compute with frozen partial semantics or not. - /// If is false, and this - /// queue does support computing frozen partial semantics (see ) - /// then this is a cancellation token that can cancel the expensive work being done if new frozen-partial work - /// is requested. - private record struct TagSourceQueueItem(bool HighPriority, bool FrozenPartialSemantics, CancellationToken? NonFrozenComputationToken); - } + /// Specifies if this is the initial set of tags being computed or not, and no + /// artificial delays should be inserted when computing the tags. + /// Indicates if we should + /// compute with frozen partial semantics or not. + /// If is false, and this + /// queue does support computing frozen partial semantics (see ) + /// then this is a cancellation token that can cancel the expensive work being done if new frozen-partial work + /// is requested. + private record struct TagSourceQueueItem(bool HighPriority, bool FrozenPartialSemantics, CancellationToken? NonFrozenComputationToken); } From 5469dee646dcae34b6e84bb7c284539430928e4c Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:56:37 -0700 Subject: [PATCH 16/33] Docs --- src/EditorFeatures/Core/Tagging/TaggerContext.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/EditorFeatures/Core/Tagging/TaggerContext.cs b/src/EditorFeatures/Core/Tagging/TaggerContext.cs index e3e955f2c0625..fab148c0c801a 100644 --- a/src/EditorFeatures/Core/Tagging/TaggerContext.cs +++ b/src/EditorFeatures/Core/Tagging/TaggerContext.cs @@ -22,7 +22,14 @@ internal class TaggerContext where TTag : ITag internal ImmutableArray _spansTagged; public readonly SegmentedList> TagSpans = []; + /// + /// If the client should compute tags using frozen partial semantics. This generally should have no effect if tags + /// are computed within this process as the provided will be given the right frozen or + /// unfrozen documents. However, this is relevant when making calls to our external OOP server to ensure that it + /// also does the same when processing the request on its side. + /// public bool FrozenPartialSemantics { get; } + public ImmutableArray SpansToTag { get; } public SnapshotPoint? CaretPosition { get; } From 766085b7a3d2690da734cedcba7acd069974c505 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:57:40 -0700 Subject: [PATCH 17/33] REmove unused usings --- src/EditorFeatures/Test/Options/GlobalOptionsTests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs b/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs index 46ec36302d24f..9de1873576955 100644 --- a/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs +++ b/src/EditorFeatures/Test/Options/GlobalOptionsTests.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.Serialization; @@ -14,14 +13,11 @@ using Microsoft.CodeAnalysis.BraceMatching; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.DocumentationComments; using Microsoft.CodeAnalysis.DocumentHighlighting; using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.UnitTests; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.ExtractMethod; using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Formatting; From d9c84547e4d4677f4a5c7c02c83106c17392e13b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 13:59:38 -0700 Subject: [PATCH 18/33] Consistentcy --- .../RemoteSemanticClassificationService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs index c291b0fd0eeb7..2db59acf92969 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs @@ -34,8 +34,8 @@ public ValueTask GetClassificationsAsync( Contract.ThrowIfNull(document); // Frozen partial semantics is not automatically passed to OOP, so enable it explicitly when desired - if (options.FrozenPartialSemantics) - document = document.WithFrozenPartialSemantics(cancellationToken); + document = options.FrozenPartialSemantics ? document.WithFrozenPartialSemantics(cancellationToken) : document; + solution = document.Project.Solution; using var _ = Classifier.GetPooledList(out var temp); await AbstractClassificationService.AddClassificationsInCurrentProcessAsync( From 02bd1ee070b2b60c8d63c93db6b1f9d2980204f5 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 4 Apr 2024 14:47:10 -0700 Subject: [PATCH 19/33] lint --- .../KeywordHighlighting/AbstractKeywordHighlightingTests.vb | 2 +- .../ReferenceHighlighting/AbstractReferenceHighlightingTests.vb | 2 +- .../VisualBasicTest/BraceMatching/BraceHighlightingTests.vb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EditorFeatures/Test2/KeywordHighlighting/AbstractKeywordHighlightingTests.vb b/src/EditorFeatures/Test2/KeywordHighlighting/AbstractKeywordHighlightingTests.vb index 72aacd6fcf71a..cfb36ab2cfafb 100644 --- a/src/EditorFeatures/Test2/KeywordHighlighting/AbstractKeywordHighlightingTests.vb +++ b/src/EditorFeatures/Test2/KeywordHighlighting/AbstractKeywordHighlightingTests.vb @@ -40,7 +40,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.KeywordHighlighting visibilityTracker:=Nothing, AsynchronousOperationListenerProvider.NullProvider) - Dim context = New TaggerContext(Of KeywordHighlightTag)(document, snapshot, New SnapshotPoint(snapshot, caretPosition)) + Dim context = New TaggerContext(Of KeywordHighlightTag)(document, snapshot, frozenPartialSemantics:=False, New SnapshotPoint(snapshot, caretPosition)) Await tagProducer.GetTestAccessor().ProduceTagsAsync(context) Dim producedTags = From tag In context.TagSpans diff --git a/src/EditorFeatures/Test2/ReferenceHighlighting/AbstractReferenceHighlightingTests.vb b/src/EditorFeatures/Test2/ReferenceHighlighting/AbstractReferenceHighlightingTests.vb index 86e949be586ea..e8777830bcc54 100644 --- a/src/EditorFeatures/Test2/ReferenceHighlighting/AbstractReferenceHighlightingTests.vb +++ b/src/EditorFeatures/Test2/ReferenceHighlighting/AbstractReferenceHighlightingTests.vb @@ -42,7 +42,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.ReferenceHighlighting Dim document = workspace.CurrentSolution.GetDocument(hostDocument.Id) Dim context = New TaggerContext(Of NavigableHighlightTag)( - document, snapshot, New SnapshotPoint(snapshot, caretPosition)) + document, snapshot, frozenPartialSemantics:=False, New SnapshotPoint(snapshot, caretPosition)) Await tagProducer.GetTestAccessor().ProduceTagsAsync(context) Dim producedTags = From tag In context.TagSpans diff --git a/src/EditorFeatures/VisualBasicTest/BraceMatching/BraceHighlightingTests.vb b/src/EditorFeatures/VisualBasicTest/BraceMatching/BraceHighlightingTests.vb index 374c6fb2dd999..e38c168775ee7 100644 --- a/src/EditorFeatures/VisualBasicTest/BraceMatching/BraceHighlightingTests.vb +++ b/src/EditorFeatures/VisualBasicTest/BraceMatching/BraceHighlightingTests.vb @@ -36,7 +36,7 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.BraceMatching Dim doc = buffer.CurrentSnapshot.GetRelatedDocumentsWithChanges().FirstOrDefault() Dim context = New TaggerContext(Of BraceHighlightTag)( - doc, buffer.CurrentSnapshot, New SnapshotPoint(buffer.CurrentSnapshot, position)) + doc, buffer.CurrentSnapshot, frozenPartialSemantics:=False, New SnapshotPoint(buffer.CurrentSnapshot, position)) Await producer.GetTestAccessor().ProduceTagsAsync(context) Return context.TagSpans End Function From 54485a1cc24216b7acc5c54160237b1dc93bc53a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 00:06:15 -0700 Subject: [PATCH 20/33] Use disposal source --- .../Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index c62e54f2aa1e1..210786c40ee89 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -84,7 +84,7 @@ private sealed partial class TagSource /// cancel the expensive non-frozen tagging pass (which might be computing skeletons, SG docs, etc.), do the /// next cheap frozen-tagging-pass, and then push the expensive-nonfrozen-tagging-pass to the end again. /// - private readonly CancellationSeries _nonFrozenComputationCancellationSeries = new(); + private readonly CancellationSeries _nonFrozenComputationCancellationSeries; #endregion @@ -161,6 +161,7 @@ public TagSource( _visibilityTracker = visibilityTracker; _dataSource = dataSource; _asyncListener = asyncListener; + _nonFrozenComputationCancellationSeries = new(_disposalTokenSource.Token); _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer()); From bb87d24d9f1e9cdd25d8d66453c43d1a934be198 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:29:02 -0700 Subject: [PATCH 21/33] Switch to any --- ...bstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 441cd28880d3e..79657ff986f24 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -198,12 +198,12 @@ private async ValueTask ProcessEventChangeAsync( Contract.ThrowIfTrue(changes.IsEmpty); // If any of the requests was high priority, then compute at that speed. - var highPriority = changes.Contains(x => x.HighPriority); + var highPriority = changes.Any(x => x.HighPriority); // 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 = changes.Contains(t => t.FrozenPartialSemantics); + var frozenPartialSemantics = changes.Any(t => t.FrozenPartialSemantics); if (frozenPartialSemantics) { From f4a0212054b4aba77b39fc6f38d12104cc830c62 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:35:27 -0700 Subject: [PATCH 22/33] simplify logic --- ...ousTaggerProvider.TagSource_ProduceTags.cs | 68 ++++++++----------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 79657ff986f24..6c2abdc59a498 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -205,53 +205,36 @@ private async ValueTask ProcessEventChangeAsync( // to do the slow-but-accurate pass. var frozenPartialSemantics = changes.Any(t => t.FrozenPartialSemantics); - if (frozenPartialSemantics) + if (!frozenPartialSemantics && _dataSource.SupportsFrozenPartialSemantics) { - // If we were asking for frozen partial semantics, then just proceed as normal, getting those tags - // quickly, but inaccurately. - await RecomputeTagsAsync(highPriority, frozenPartialSemantics: true, cancellationToken).ConfigureAwait(false); - } - else - { - if (!_dataSource.SupportsFrozenPartialSemantics) - { - // We're asking for normal expensive full tags, and this tagger doesn't support frozen partial - // tagging anyways, so just proceed as normal, asking for expensive tags. - await RecomputeTagsAsync(highPriority, frozenPartialSemantics: false, cancellationToken).ConfigureAwait(false); - } - else - { - // We're asking for expensive tags, and this tagger supports frozen partial tags. Kick off the work - // to do this expensive tagging, but attach ourselves to the requested cancellation token so this - // expensive work can be canceled if new requests for frozen partial work come in. - - // We must have at least one request asking for full semantics. - Contract.ThrowIfFalse(changes.Any(c => !c.FrozenPartialSemantics)); - - // All those requests should have cancellation tokens provided with them. - Contract.ThrowIfFalse(changes.Where(c => !c.FrozenPartialSemantics).All(c => c.NonFrozenComputationToken != null)); + // We're asking for expensive tags, and this tagger supports frozen partial tags. Kick off the work + // to do this expensive tagging, but attach ourselves to the requested cancellation token so this + // expensive work can be canceled if new requests for frozen partial work come in. - // Get the first non-cancelled token if present. - var nonFrozenComputationToken = changes.FirstOrNull(t => t.NonFrozenComputationToken?.IsCancellationRequested is false)?.NonFrozenComputationToken; + // 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(changes.All(t => !t.FrozenPartialSemantics)); + Contract.ThrowIfFalse(changes.All(t => t.NonFrozenComputationToken != null)); + Contract.ThrowIfFalse(changes.Take(changes.Count - 1).All(t => t.NonFrozenComputationToken!.Value.IsCancellationRequested)); - // If there is no non-frozen cancellation token that has not been triggered yet, then all non-frozen work - // has been canceled, and we can immediately bail out. - if (nonFrozenComputationToken is null) - return; + var lastNonFrozenComputationToken = changes[^1].NonFrozenComputationToken!.Value; - // Otherwise, link this token with the main queue cancellation token and do the actual tagging. - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, nonFrozenComputationToken.Value); - - // Need a dedicated try/catch here since we're operating on a different token than the queue's token. - try - { - await RecomputeTagsAsync(highPriority, frozenPartialSemantics, linkedTokenSource.Token).ConfigureAwait(false); - } - catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, linkedTokenSource.Token)) - { - } + // 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 + { + await RecomputeTagsAsync(highPriority, frozenPartialSemantics, linkedTokenSource.Token).ConfigureAwait(false); + } + catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, linkedTokenSource.Token)) + { } } + else + { + // Normal request to either compute frozen partial tags, or compute normal tags in a tagger that does + // *not* support frozen partial tagging. + await RecomputeTagsAsync(highPriority, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); + } } /// @@ -273,6 +256,9 @@ private async Task RecomputeTagsAsync( bool frozenPartialSemantics, CancellationToken cancellationToken) { + if (cancellationToken.IsCancellationRequested) + return; + await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); if (cancellationToken.IsCancellationRequested) return; From ba2fb3ab06b59f4ecd291e2967eccc28e51881f7 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:41:32 -0700 Subject: [PATCH 23/33] move option lower, and inline method --- ...mbeddedClassificationViewTaggerProvider.cs | 50 +++++++------------ .../SemanticTokens/SemanticTokensHelpers.cs | 1 - 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index c9ab7239cf4d9..393f3ed6ab7f1 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -122,11 +122,7 @@ protected sealed override async Task ProduceTagsAsync( if (isLspSemanticTokensEnabled) return; - var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language) with - { - FrozenPartialSemantics = context.FrozenPartialSemantics, - }; - + var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language); await ProduceTagsAsync( context, spanToTag, classificationService, classificationOptions, cancellationToken).ConfigureAwait(false); } @@ -244,8 +240,24 @@ private async Task ClassifySpansAsync( { using var _ = Classifier.GetPooledList(out var classifiedSpans); - await AddClassificationsAsync( - classificationService, options, document, snapshotSpan, classifiedSpans, cancellationToken).ConfigureAwait(false); + // Ensure that if we're producing tags for frozen/partial documents, that we pass along that info so + // that we preserve that same behavior in OOP if we end up computing the tags there. + options = options with { FrozenPartialSemantics = context.FrozenPartialSemantics }; + + if (_type == ClassificationType.Semantic) + { + await classificationService.AddSemanticClassificationsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); + } + else if (_type == ClassificationType.EmbeddedLanguage) + { + await classificationService.AddEmbeddedLanguageClassificationsAsync( + document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); + } + else + { + throw ExceptionUtilities.UnexpectedValue(_type); + } foreach (var span in classifiedSpans) context.AddTag(ClassificationUtilities.Convert(_typeMap, snapshotSpan.Snapshot, span)); @@ -262,28 +274,4 @@ await AddClassificationsAsync( throw ExceptionUtilities.Unreachable(); } } - - private async Task AddClassificationsAsync( - IClassificationService classificationService, - ClassificationOptions options, - Document document, - SnapshotSpan snapshotSpan, - SegmentedList classifiedSpans, - CancellationToken cancellationToken) - { - if (_type == ClassificationType.Semantic) - { - await classificationService.AddSemanticClassificationsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); - } - else if (_type == ClassificationType.EmbeddedLanguage) - { - await classificationService.AddEmbeddedLanguageClassificationsAsync( - document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); - } - else - { - throw ExceptionUtilities.UnexpectedValue(_type); - } - } } diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs index 694d7166f02c8..52442b4d8fa9d 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs @@ -60,7 +60,6 @@ public static async Task HandleRequestHelperAsync(Document document, Immu // If the full compilation is not yet available, we'll try getting a partial one. It may contain inaccurate // results but will speed up how quickly we can respond to the client's request. document = document.WithFrozenPartialSemantics(cancellationToken); - options = options with { FrozenPartialSemantics = true }; // The results from the range handler should not be cached since we don't want to cache // partial token results. In addition, a range request is only ever called with a whole From 271758567abf4fbac569dea679cfd93a8183da12 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:44:01 -0700 Subject: [PATCH 24/33] do the same for highlighting --- .../ReferenceHighlightingViewTaggerProvider.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs index 1d677cdc98e42..efcd5eaa36c50 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs @@ -135,10 +135,7 @@ protected override Task ProduceTagsAsync( } // Otherwise, we need to go produce all tags. - var options = _globalOptions.GetHighlightingOptions(document.Project.Language) with - { - FrozenPartialSemantics = context.FrozenPartialSemantics, - }; + var options = _globalOptions.GetHighlightingOptions(document.Project.Language); return ProduceTagsAsync(context, caretPosition, document, options, cancellationToken); } @@ -158,9 +155,14 @@ private static async Task ProduceTagsAsync( var service = document.GetLanguageService(); if (service != null) { + // Ensure that if we're producing tags for frozen/partial documents, that we pass along that info so + // that we preserve that same behavior in OOP if we end up computing the tags there. + options = options with { FrozenPartialSemantics = context.FrozenPartialSemantics }; + // We only want to search inside documents that correspond to the snapshots // we're looking at var documentsToSearch = ImmutableHashSet.CreateRange(context.SpansToTag.Select(vt => vt.Document).WhereNotNull()); + var documentHighlightsList = await service.GetDocumentHighlightsAsync( document, position, documentsToSearch, options, cancellationToken).ConfigureAwait(false); if (documentHighlightsList != null) From 5ae41b5183544274bcf1b17a1e85c93fe371a605 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:47:13 -0700 Subject: [PATCH 25/33] voidresult --- ...actAsynchronousTaggerProvider.TagSource.cs | 9 +++---- ...ousTaggerProvider.TagSource_ProduceTags.cs | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index 210786c40ee89..1a85731845ef7 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -165,12 +165,9 @@ public TagSource( _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer()); - // Collapse all booleans added to just a max of two ('true' or 'false') representing if we're being - // asked for initial tags or not - // - // PERF: Use AsyncBatchingWorkQueue instead of AsyncBatchingWorkQueue because - // the latter has an async state machine that rethrows a very common cancellation exception. - _eventChangeQueue = new AsyncBatchingWorkQueue( + // PERF: Use AsyncBatchingWorkQueue<_, VoidResult> instead of AsyncBatchingWorkQueue<_> because the latter + // has an async state machine that rethrows a very common cancellation exception. + _eventChangeQueue = new AsyncBatchingWorkQueue( dataSource.EventChangeDelay.ComputeTimeDelay(), ProcessEventChangeAsync, EqualityComparer.Default, diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 6c2abdc59a498..0e51a718d161a 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -192,7 +192,7 @@ private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) _dataSource.CancelOnNewWork); } - private async ValueTask ProcessEventChangeAsync( + private async ValueTask ProcessEventChangeAsync( ImmutableSegmentedList changes, CancellationToken cancellationToken) { Contract.ThrowIfTrue(changes.IsEmpty); @@ -223,17 +223,18 @@ private async ValueTask ProcessEventChangeAsync( using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(lastNonFrozenComputationToken, cancellationToken); try { - await RecomputeTagsAsync(highPriority, frozenPartialSemantics, linkedTokenSource.Token).ConfigureAwait(false); + return await RecomputeTagsAsync(highPriority, frozenPartialSemantics, linkedTokenSource.Token).ConfigureAwait(false); } - catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, linkedTokenSource.Token)) + catch (OperationCanceledException ex) when (ExceptionUtilities.IsCurrentOperationBeingCancelled(ex, linkedTokenSource.Token)) { + return default; } } else { // Normal request to either compute frozen partial tags, or compute normal tags in a tagger that does // *not* support frozen partial tagging. - await RecomputeTagsAsync(highPriority, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); + return await RecomputeTagsAsync(highPriority, frozenPartialSemantics, cancellationToken).ConfigureAwait(false); } } @@ -251,17 +252,17 @@ private async ValueTask ProcessEventChangeAsync( /// /// If this tagging request should be processed as quickly as possible with no extra delays added for it. /// - private async Task RecomputeTagsAsync( + private async Task RecomputeTagsAsync( bool highPriority, bool frozenPartialSemantics, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) - return; + return default; await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); if (cancellationToken.IsCancellationRequested) - return; + return default; // if we're tagging documents that are not visible, then introduce a long delay so that we avoid // consuming machine resources on work the user isn't likely to see. ConfigureAwait(true) so that if @@ -277,12 +278,12 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( _dataSource.ThreadingContext, _dataSource.AsyncListener, _subjectBuffer, DelayTimeSpan.NonFocus, cancellationToken).NoThrowAwaitable(captureContext: true); if (cancellationToken.IsCancellationRequested) - return; + return default; } _dataSource.ThreadingContext.ThrowIfNotOnUIThread(); if (cancellationToken.IsCancellationRequested) - return; + return default; using (Logger.LogBlock(FunctionId.Tagger_TagSource_RecomputeTags, cancellationToken)) { @@ -300,7 +301,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( await TaskScheduler.Default; if (cancellationToken.IsCancellationRequested) - return; + return default; if (frozenPartialSemantics) { @@ -315,7 +316,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( await ProduceTagsAsync(context, cancellationToken).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) - return; + return default; // Process the result to determine what changed. var newTagTrees = ComputeNewTagTrees(oldTagTrees, context); @@ -324,7 +325,7 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // Then switch back to the UI thread to update our state and kick off the work to notify the editor. await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); if (cancellationToken.IsCancellationRequested) - return; + return default; // Once we assign our state, we're uncancellable. We must report the changed information // to the editor. The only case where it's ok not to is if the tagger itself is disposed. @@ -355,6 +356,8 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // mode again, kicking the can down the road to finally end with non-frozen-partial computation if (frozenPartialSemantics) this.EnqueueWork(highPriority, frozenPartialSemantics: false); + + return default; } } From 05d0c6fdd2bd519d9393cbef8d3ec79c54276467 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:48:34 -0700 Subject: [PATCH 26/33] Update src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs --- .../ReferenceHighlightingViewTaggerProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs index efcd5eaa36c50..eb1e30f29407a 100644 --- a/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlightingViewTaggerProvider.cs @@ -162,7 +162,6 @@ private static async Task ProduceTagsAsync( // We only want to search inside documents that correspond to the snapshots // we're looking at var documentsToSearch = ImmutableHashSet.CreateRange(context.SpansToTag.Select(vt => vt.Document).WhereNotNull()); - var documentHighlightsList = await service.GetDocumentHighlightsAsync( document, position, documentsToSearch, options, cancellationToken).ConfigureAwait(false); if (documentHighlightsList != null) From df8d68a84680a7a78bc6f8f7ac8e618bd097b73b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:49:10 -0700 Subject: [PATCH 27/33] voidresult --- .../Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs index 1a85731845ef7..7f19fe9ea29a5 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource.cs @@ -76,7 +76,7 @@ private sealed partial class TagSource /// This queue is used to batch up event change notifications and only dispatch one recomputation every to actually produce the latest set of tags. /// - private readonly AsyncBatchingWorkQueue _eventChangeQueue; + private readonly AsyncBatchingWorkQueue _eventChangeQueue; /// /// For taggers that support tagging frozen and non-frozen snapshots, this cancellation series controls the From f31e40f8491c4dbc17f23d16abf2782ee3053417 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:55:15 -0700 Subject: [PATCH 28/33] Simplify --- ...ousTaggerProvider.TagSource_ProduceTags.cs | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 0e51a718d161a..7488b288fd13a 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -169,28 +169,18 @@ private void OnEventSourceChanged(object? _1, TaggerEventArgs _2) => EnqueueWork(highPriority: false); private void EnqueueWork(bool highPriority) - => EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics); - - private void EnqueueWork(bool highPriority, bool frozenPartialSemantics) { - // Cancellation token if this expensive work that we want to be cancellable when cheap work comes in. - CancellationToken? nonFrozenComputationToken = null; - - if (_dataSource.SupportsFrozenPartialSemantics) - { - // We do support frozen partial work. Cancel any expensive work in flight as new work has come in. - var nextToken = _nonFrozenComputationCancellationSeries.CreateNext(); - - // If this is new work *is* the expensive work, then use this token to allow cancellation of it when - // more new work comes in. - if (!frozenPartialSemantics) - nonFrozenComputationToken = nextToken; - } + // Cancel any expensive, in-flight, tagging 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(); + EnqueueWork(highPriority, _dataSource.SupportsFrozenPartialSemantics, nonFrozenComputationToken: null); + } - _eventChangeQueue.AddWork( + private void EnqueueWork(bool highPriority, bool frozenPartialSemantics, CancellationToken? nonFrozenComputationToken) + => _eventChangeQueue.AddWork( new TagSourceQueueItem(highPriority, frozenPartialSemantics, nonFrozenComputationToken), _dataSource.CancelOnNewWork); - } private async ValueTask ProcessEventChangeAsync( ImmutableSegmentedList changes, CancellationToken cancellationToken) @@ -351,11 +341,10 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( PauseIfNotVisible(); // If we were computing with frozen partial semantics here, enqueue work to compute *without* frozen - // partial snapshots so we move to accurate results shortly. Note: when the queue goes to process this - // message, if it sees any other events asking for frozen-partial-semantics, it will process in that - // mode again, kicking the can down the road to finally end with non-frozen-partial computation + // 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. if (frozenPartialSemantics) - this.EnqueueWork(highPriority, frozenPartialSemantics: false); + this.EnqueueWork(highPriority, frozenPartialSemantics: false, _nonFrozenComputationCancellationSeries.CreateNext(default)); return default; } From 6e8da4c7d16b3794891ed7619b2f637fb78dab82 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:56:04 -0700 Subject: [PATCH 29/33] REvert --- ...bstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 7488b288fd13a..8c19889962559 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -345,9 +345,9 @@ await _visibilityTracker.DelayWhileNonVisibleAsync( // token for this expensive work so that it can be canceled by future lightweight work. if (frozenPartialSemantics) this.EnqueueWork(highPriority, frozenPartialSemantics: false, _nonFrozenComputationCancellationSeries.CreateNext(default)); - - return default; } + + return default; } private ImmutableArray GetSpansAndDocumentsToTag() From b78d0a801e51465030785b345298466d43da28b4 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:57:00 -0700 Subject: [PATCH 30/33] revert --- .../Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs index 52442b4d8fa9d..694d7166f02c8 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs @@ -60,6 +60,7 @@ public static async Task HandleRequestHelperAsync(Document document, Immu // If the full compilation is not yet available, we'll try getting a partial one. It may contain inaccurate // results but will speed up how quickly we can respond to the client's request. document = document.WithFrozenPartialSemantics(cancellationToken); + options = options with { FrozenPartialSemantics = true }; // The results from the range handler should not be cached since we don't want to cache // partial token results. In addition, a range request is only ever called with a whole From c58a6046874512e4d87bbe3c7a85bf9e8409e87a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 09:59:16 -0700 Subject: [PATCH 31/33] remove redundant check --- ...AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs index 8c19889962559..3dabafc4cab6d 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs @@ -247,9 +247,6 @@ private async Task RecomputeTagsAsync( bool frozenPartialSemantics, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) - return default; - await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); if (cancellationToken.IsCancellationRequested) return default; From a9b19d36d3ac3c2a2ef31788c69ba24874a5ff4e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 10:42:41 -0700 Subject: [PATCH 32/33] Docs --- .../Tagging/AbstractAsynchronousTaggerProvider.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs index e70958e040552..65413e0f96bd3 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs @@ -96,7 +96,16 @@ internal abstract partial class AbstractAsynchronousTaggerProvider where T protected virtual bool CancelOnNewWork { get; } /// - /// Whether or not this tagger would like to use frozen-partial snapshots to compute tags. TODO: doc more before submitting. + /// Whether or not this tagger would like to use frozen-partial snapshots to compute tags. If , tagging behaves normally, with a single call to after a batch of events comes in. If then tagging will happen in two passes. A first pass operating with frozen documents, + /// allowing the tagger to actually compute tags quickly, without waiting on skeleton references or source generated + /// documents to be up to date. Followed by a second, slower, pass on non-frozen documents that will then produce + /// the final accurate tags. Because this second pass is more expensive, it will be aggressively canceled and + /// pushed to the end when new normal work comes in. That way, when the user is doing things like typing, they'll + /// continuously be getting frozen-partial results quickly, but always with the final, full, correct results coming + /// at the end once enough idle time has passed. /// protected virtual bool SupportsFrozenPartialSemantics { get; } From 0e8d4131f7d5818595ee78b30e961df21fb036ea Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 5 Apr 2024 11:44:15 -0700 Subject: [PATCH 33/33] Update src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs Co-authored-by: Sam Harwell --- .../Core/Tagging/AbstractAsynchronousTaggerProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs index 65413e0f96bd3..1f37b5a69b2a2 100644 --- a/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs +++ b/src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs @@ -107,7 +107,7 @@ internal abstract partial class AbstractAsynchronousTaggerProvider where T /// continuously be getting frozen-partial results quickly, but always with the final, full, correct results coming /// at the end once enough idle time has passed. /// - protected virtual bool SupportsFrozenPartialSemantics { get; } + protected virtual bool SupportsFrozenPartialSemantics => false; protected virtual void BeforeTagsChanged(ITextSnapshot snapshot) {