-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update tagging to avoid jumping back to the UI thread when finished. #73699
Changes from all commits
a625300
45198ea
da8522c
594cf95
96813b9
1dfb46e
6b3608c
3ad1639
1d544fb
4d1998a
ba6cbe9
4e01406
13b9493
5723ae1
5ef1b2a
4eed867
addb628
2b83bd9
0e6342d
e08f41f
900ed3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,9 +39,6 @@ internal abstract class AbstractSemanticOrEmbeddedClassificationViewTaggerProvid | |
private readonly IGlobalOptionService _globalOptions; | ||
private readonly ClassificationType _type; | ||
|
||
// We want to track text changes so that we can try to only reclassify a method body if | ||
// all edits were contained within one. | ||
protected sealed override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.TrackTextChanges; | ||
protected sealed override ImmutableArray<IOption2> Options { get; } = [SemanticColorizerOptionsStorage.SemanticColorizer]; | ||
|
||
protected AbstractSemanticOrEmbeddedClassificationViewTaggerProvider( | ||
|
@@ -137,8 +134,9 @@ public async Task ProduceTagsAsync( | |
if (document == null) | ||
return; | ||
|
||
var currentSemanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); | ||
var classified = await TryClassifyContainingMemberSpanAsync( | ||
context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false); | ||
context, document, spanToTag.SnapshotSpan, classificationService, options, currentSemanticVersion, cancellationToken).ConfigureAwait(false); | ||
if (classified) | ||
{ | ||
return; | ||
|
@@ -147,7 +145,7 @@ public async Task ProduceTagsAsync( | |
// 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); | ||
context, document, spanToTag.SnapshotSpan, classificationService, options, currentSemanticVersion, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
private async Task<bool> TryClassifyContainingMemberSpanAsync( | ||
|
@@ -156,39 +154,39 @@ private async Task<bool> TryClassifyContainingMemberSpanAsync( | |
SnapshotSpan snapshotSpan, | ||
IClassificationService classificationService, | ||
ClassificationOptions options, | ||
VersionStamp currentSemanticVersion, | ||
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 a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of having this range be tracked by the context, and passed in. we can fairly trivially compute it if we store the previous snapshot we computed against, and determine the change ranges between it and the current snapshot we're classifying. |
||
|
||
// 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; | ||
} | ||
} | ||
// No cached state, so we can't check if the edits were just inside a member. | ||
if (context.State is null) | ||
return false; | ||
|
||
// Retrieve the information about the last time we classified this document. | ||
var (lastSemanticVersion, lastTextImageVersion) = ((VersionStamp, ITextImageVersion))context.State; | ||
|
||
// if a top level change was made. We can't perform this optimization. | ||
if (lastSemanticVersion != currentSemanticVersion) | ||
return false; | ||
|
||
var service = document.GetRequiredLanguageService<ISyntaxFactsService>(); | ||
|
||
// 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. | ||
// 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 currentTextImageVersion = GetTextImageVersion(snapshotSpan); | ||
|
||
var textChangeRanges = ITextImageHelpers.GetChangeRanges(lastTextImageVersion, currentTextImageVersion); | ||
var collapsedRange = TextChangeRange.Collapse(textChangeRanges); | ||
|
||
var changedSpan = new TextSpan(collapsedRange.Span.Start, collapsedRange.NewLength); | ||
|
||
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)) | ||
{ | ||
|
@@ -221,16 +219,20 @@ private async Task<bool> TryClassifyContainingMemberSpanAsync( | |
|
||
// re-classify only the member we're inside. | ||
await ClassifySpansAsync( | ||
context, document, subSpanToTag, classificationService, options, cancellationToken).ConfigureAwait(false); | ||
context, document, subSpanToTag, classificationService, options, currentSemanticVersion, cancellationToken).ConfigureAwait(false); | ||
return true; | ||
} | ||
|
||
private static ITextImageVersion GetTextImageVersion(SnapshotSpan snapshotSpan) | ||
=> ((ITextSnapshot2)snapshotSpan.Snapshot).TextImage.Version; | ||
|
||
private async Task ClassifySpansAsync( | ||
TaggerContext<IClassificationTag> context, | ||
Document document, | ||
SnapshotSpan snapshotSpan, | ||
IClassificationService classificationService, | ||
ClassificationOptions options, | ||
VersionStamp currentSemanticVersion, | ||
CancellationToken cancellationToken) | ||
{ | ||
try | ||
|
@@ -243,29 +245,33 @@ private async Task ClassifySpansAsync( | |
// that we preserve that same behavior in OOP if we end up computing the tags there. | ||
options = options with { FrozenPartialSemantics = context.FrozenPartialSemantics }; | ||
|
||
var span = snapshotSpan.Span; | ||
var snapshot = snapshotSpan.Snapshot; | ||
|
||
if (_type == ClassificationType.Semantic) | ||
{ | ||
await classificationService.AddSemanticClassificationsAsync( | ||
document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); | ||
document, span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); | ||
} | ||
else if (_type == ClassificationType.EmbeddedLanguage) | ||
{ | ||
await classificationService.AddEmbeddedLanguageClassificationsAsync( | ||
document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false); | ||
document, 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)); | ||
|
||
var version = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); | ||
foreach (var classifiedSpan in classifiedSpans) | ||
context.AddTag(ClassificationUtilities.Convert(_typeMap, snapshot, classifiedSpan)); | ||
|
||
// Let the context know that this was the span we actually tried to tag. | ||
context.SetSpansTagged([snapshotSpan]); | ||
context.State = version; | ||
|
||
// Store the semantic version and text-image-version we used to produce these tags. We can use this in | ||
// the future to try to limit what we classify, if all edits were made within a single member. | ||
context.State = (currentSemanticVersion, GetTextImageVersion(snapshotSpan)); | ||
} | ||
} | ||
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,10 +40,6 @@ internal partial class ActiveStatementTaggerProvider( | |
[Import(AllowDefault = true)] ITextBufferVisibilityTracker? visibilityTracker, | ||
IAsynchronousOperationListenerProvider listenerProvider) : AsynchronousTaggerProvider<ITextMarkerTag>(threadingContext, globalOptions, visibilityTracker, listenerProvider.GetListener(FeatureAttribute.Classification)) | ||
{ | ||
// We want to track text changes so that we can try to only reclassify a method body if | ||
// all edits were contained within one. | ||
protected override TaggerTextChangeBehavior TextChangeBehavior => TaggerTextChangeBehavior.TrackTextChanges; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was never used. this asked the tagger infrastructure to track this info. but then it was never used in this tagger. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: it's clear that this was just a copy/paste from the semantic-classifier |
||
|
||
protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate; | ||
|
||
protected override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,9 @@ | |
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Threading; | ||
using Microsoft.CodeAnalysis.Editor.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Editor.Shared.Tagging; | ||
|
@@ -27,12 +29,12 @@ internal partial class AbstractAsynchronousTaggerProvider<TTag> | |
/// tagging infrastructure. It is the coordinator between <see cref="ProduceTagsAsync(TaggerContext{TTag}, CancellationToken)"/>s, | ||
/// <see cref="ITaggerEventSource"/>s, and <see cref="ITagger{T}"/>s.</para> | ||
/// | ||
/// <para>The <see cref="TagSource"/> is the type that actually owns the | ||
/// list of cached tags. When an <see cref="ITaggerEventSource"/> says tags need to be recomputed, | ||
/// the tag source starts the computation and calls <see cref="ProduceTagsAsync(TaggerContext{TTag}, CancellationToken)"/> to build | ||
/// the new list of tags. When that's done, the tags are stored in <see cref="CachedTagTrees"/>. The | ||
/// tagger, when asked for tags from the editor, then returns the tags that are stored in | ||
/// <see cref="CachedTagTrees"/></para> | ||
/// <para>The <see cref="TagSource"/> is the type that actually owns the list of cached tags. When an <see | ||
/// cref="ITaggerEventSource"/> says tags need to be recomputed, the tag source starts the computation and calls | ||
/// <see cref="ProduceTagsAsync(TaggerContext{TTag}, CancellationToken)"/> to build the new list of tags. When | ||
/// that's done, the tags are stored in <see cref="_cachedTagTrees_mayChangeFromAnyThread"/>. The tagger, when asked | ||
/// for tags from the editor, then returns the tags that are stored in <see | ||
/// cref="_cachedTagTrees_mayChangeFromAnyThread"/></para> | ||
/// | ||
/// <para>There is a one-to-many relationship between <see cref="TagSource"/>s | ||
/// and <see cref="ITagger{T}"/>s. Special cases, like reference highlighting (which processes multiple | ||
|
@@ -86,6 +88,20 @@ private sealed partial class TagSource | |
/// </summary> | ||
private readonly CancellationSeries _nonFrozenComputationCancellationSeries; | ||
|
||
/// <summary> | ||
/// The last tag trees that we computed per buffer. Note: this can be read/written from any thread. Because of | ||
/// that, we have to use safe operations to actually read or write it. This includes using looping "compare and | ||
/// swap" algorithms to make sure that it is consistently moved forward no matter which thread is trying to | ||
/// mutate it. | ||
/// </summary> | ||
private ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>> _cachedTagTrees_mayChangeFromAnyThread = ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>>.Empty; | ||
|
||
#endregion | ||
|
||
#region Mutable state. Only accessed from _eventChangeQueue | ||
|
||
private object? _state_accessOnlyFromEventChangeQueueCallback; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a move/rename. We used to require this have UI thread affinity. But there's no need for that. It's only read/written from the _eventChangeQueue callbacks, so it is safe to have no jumps for this. |
||
|
||
#endregion | ||
|
||
#region Fields that can only be accessed from the foreground thread | ||
|
@@ -121,13 +137,6 @@ private sealed partial class TagSource | |
|
||
#region Mutable state. Can only be accessed from the foreground thread | ||
|
||
/// <summary> | ||
/// accumulated text changes since last tag calculation | ||
/// </summary> | ||
private TextChangeRange? _accumulatedTextChanges_doNotAccessDirectly; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed entirely. |
||
private ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>> _cachedTagTrees_doNotAccessDirectly = ImmutableDictionary.Create<ITextBuffer, TagSpanIntervalTree<TTag>>(); | ||
private object? _state_doNotAccessDirecty; | ||
|
||
/// <summary> | ||
/// Keep track of if we are processing the first <see cref="ITagger{T}.GetTags"/> request. If our provider returns | ||
/// <see langword="true"/> for <see cref="AbstractAsynchronousTaggerProvider{TTag}.ComputeInitialTagsSynchronously"/>, | ||
|
@@ -202,9 +211,13 @@ public TagSource( | |
// Create the tagger-specific events that will cause the tagger to refresh. | ||
_eventSource = CreateEventSource(); | ||
|
||
// any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause | ||
// themselves immediately afterwards. | ||
_onVisibilityChanged = () => ResumeIfVisible(); | ||
// Any time visibility changes try to pause us if we're not visible, or resume us if we are. | ||
_onVisibilityChanged = () => | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
PauseIfNotVisible(); | ||
ResumeIfVisible(); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. previously, we would jump back to the UI thread after reporting tags to pause ourselves. Now, we just pause/unpause based on the events we get from the visibility service. This matches what we just did in anvbars. |
||
|
||
// Now hook up this tagger to all interesting events. | ||
Connect(); | ||
|
@@ -225,8 +238,11 @@ void Connect() | |
|
||
_eventSource.Changed += OnEventSourceChanged; | ||
|
||
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed the top flag. |
||
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags) || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can change in followup. |
||
_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) | ||
{ | ||
_subjectBuffer.Changed += OnSubjectBufferChanged; | ||
} | ||
|
||
if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag)) | ||
{ | ||
|
@@ -270,8 +286,11 @@ void Disconnect() | |
_textView.Caret.PositionChanged -= OnCaretPositionChanged; | ||
} | ||
|
||
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) | ||
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags) || | ||
_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) | ||
{ | ||
Comment on lines
+289
to
+291
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it easier to skip this condition and just always do the delegate remove? That way there's no risk of the conditions getting out of sync. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i wasn't 100% certain the semantics of dleegate removal. and i was a tiny bit concerned that might introduce more chance of a problem... i'd like to keep this way for now if htat's ok! |
||
_subjectBuffer.Changed -= OnSubjectBufferChanged; | ||
} | ||
|
||
_eventSource.Changed -= OnEventSourceChanged; | ||
|
||
|
@@ -336,51 +355,6 @@ private ITaggerEventSource CreateEventSource() | |
return TaggerEventSources.Compose(optionChangedEventSources); | ||
} | ||
|
||
private TextChangeRange? AccumulatedTextChanges | ||
{ | ||
get | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
return _accumulatedTextChanges_doNotAccessDirectly; | ||
} | ||
|
||
set | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
_accumulatedTextChanges_doNotAccessDirectly = value; | ||
} | ||
} | ||
|
||
private ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>> CachedTagTrees | ||
{ | ||
get | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
return _cachedTagTrees_doNotAccessDirectly; | ||
} | ||
|
||
set | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
_cachedTagTrees_doNotAccessDirectly = value; | ||
} | ||
} | ||
|
||
private object? State | ||
{ | ||
get | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
return _state_doNotAccessDirecty; | ||
} | ||
|
||
set | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
_state_doNotAccessDirecty = value; | ||
} | ||
} | ||
|
||
private void RaiseTagsChanged(ITextBuffer buffer, DiffResult difference) | ||
{ | ||
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i removed the text-change-tracking that tagging did (which required the UI thread), and made it something only the SemanticClassifier does (since it is the only feature that needs it).