-
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
Update tagging to avoid jumping back to the UI thread when finished. #73699
Conversation
var classified = await TryClassifyContainingMemberSpanAsync( | ||
context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false); | ||
context, document, spanToTag.SnapshotSpan, classificationService, options, currentSemanticVersion, cancellationToken).ConfigureAwait(false); |
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).
@@ -88,6 +88,12 @@ private sealed partial class TagSource | |||
|
|||
#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 comment
The 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.
@@ -124,9 +130,7 @@ private sealed partial class TagSource | |||
/// <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 comment
The reason will be displayed to describe this comment to others. Learn more.
removed entirely.
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
PauseIfNotVisible(); | ||
ResumeIfVisible(); | ||
}; |
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.
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.
// 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; |
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.
this is hte main win. we're not switching back to the ui thread when done.
await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); | ||
if (cancellationToken.IsCancellationRequested) | ||
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. | ||
cancellationToken = CancellationToken.None; | ||
|
||
this.CachedTagTrees = newTagTrees; |
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.
WIP: this is an immutable dictionary. need to ensure that this is safe for all the reading threads.
{ | ||
// 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 comment
The 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.
var oldTagTrees = this.CachedTagTrees; | ||
this.CachedTagTrees = ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>>.Empty; | ||
var oldTagTrees = Interlocked.Exchange( | ||
ref _cachedTagTrees_mayChangeFromAnyThread, ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>>.Empty); |
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.
because _cachedTagTrees may be mutated from several different locations (and threads) we have to switch to save ways of doing that atomically.
return; | ||
// Everything we're passing in here is synchronous. So we can assert that this must complete synchronously | ||
// as well. | ||
var (oldTagTrees, newTagTrees) = CompareAndSwapTagTreesAsync( |
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.
core helper is this CompareAndSwapTagTreesAsync function. It performs the normal compare-and-swap algorithm, reading teh current value of _cachedTagTrees, calling a user-provided function with it, and attempting to then update that field with the new result as long as it didn't change in between. If it did change, it spins until it finally wins and then returns the before/after state.
var (oldTagTrees, newTagTrees) = CompareAndSwapTagTreesAsync( | ||
oldTagTrees => | ||
{ | ||
if (oldTagTrees.TryGetValue(buffer, out var treeForBuffer)) |
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.
inverted these checks for clarity.
} | ||
} | ||
|
||
private async Task<(ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>> oldTagTrees, ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>> newTagTrees)> |
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 really wish i could have an alias for ImmutableDictionary<ITextBuffer, TagSpanIntervalTree<TTag>>
, but it references TTag
which is a generic type parameter of this type.
@@ -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 comment
The 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 comment
The 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
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.BufferToTagTree.cs
Outdated
Show resolved
Hide resolved
@@ -225,8 +235,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 comment
The reason will be displayed to describe this comment to others. Learn more.
removed the top flag.
} | ||
|
||
return default; | ||
} |
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.
core CompareAndSwap helper.
bool highPriority, | ||
bool frozenPartialSemantics, | ||
bool calledFromJtfRun, |
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.
new addition. want to make sure we dont' do unnecessary hops when in a JTF call.
CancellationToken cancellationToken) | ||
{ | ||
// Jump to the main thread, as we have to grab the spans to tag and the caret point. | ||
await _dataSource.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable(); |
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.
existing UI jump that we need to preserve.
Note this is a complimentary PR to #67259. That pr makes it so that we can batch as many taggers together and have them all execute their UI work at the same time. |
...s/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs
Outdated
Show resolved
Hide resolved
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags) || | ||
_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits)) | ||
{ |
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.
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 comment
The 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!
// If we're being called from within a blocking JTF.Run call, we don't want to switch to the background | ||
// if we can avoid it. | ||
if (!calledFromJtfRun) | ||
await TaskScheduler.Default; |
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.
You can also do this:
roslyn/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs
Lines 80 to 87 in 14a96f9
// The rest of this method can be ran off the UI thread. We'll only switch though if the UI thread isn't already blocked -- the legacy project | |
// system creates project synchronously, and during solution load we've seen traces where the thread pool is sufficiently saturated that this | |
// switch can't be completed quickly. For the rest of this method, we won't use ConfigureAwait(false) since we're expecting VS threading | |
// rules to apply. | |
if (!_threadingContext.JoinableTaskContext.IsMainThreadBlocked()) | |
{ | |
await TaskScheduler.Default; | |
} |
var lastSourceText = lastTextSnapshot.AsText(); | ||
var currentSourceText = snapshotSpan.Snapshot.AsText(); | ||
|
||
var textChangeRanges = currentSourceText.GetChangeRanges(lastSourceText); | ||
var collapsedRange = TextChangeRange.Collapse(textChangeRanges); | ||
|
||
var changedSpan = new TextSpan(collapsedRange.Span.Start, collapsedRange.NewLength); |
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.
Do we only need the span in the end? Because we can be a bit friendlier here on costs I think. That GetChangeRanges call is only looking at the ITextVersions I think:
INormalizedTextChangeCollection? changes = null; |
If we extract out that then the state could just be the snapshot version.
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.
yup. will do this. intiially it seemed complex. but it should be ok.
public TagSpanIntervalTree<TTag> this[ITextBuffer buffer] => Map[buffer]; | ||
|
||
internal static BufferToTagTree InterlockedExchange(ref BufferToTagTree location, BufferToTagTree value) | ||
=> new(Interlocked.Exchange(ref Unsafe.AsRef(in location.Map), value.Map)); |
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.
Is this just more sane to have the struct use a mutable field and then you don't need this directly? Or can dispense of the Unsafe.AsRef? I normally say that a mutable struct is terrible, but this is already effectively mutable so maybe it doesn't really matter.
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.
removed this all. back to ImmutableDictionaries.
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.BufferToTagTree.cs
Outdated
Show resolved
Hide resolved
var newTagTree = new TagSpanIntervalTree<TTag>( | ||
buffer, | ||
treeForBuffer.SpanTrackingMode, | ||
allTags.Except(tagsToRemove, comparer: this)); |
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.
Unrelated to your change but I wonder if we can/should make this processing cheaper. It seems we enumerate the tree twice and allocate that intermediate list and then create a whole new tree from scratch. I assume there's a better algorithm.
(assuming this even appears in a trace)
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.
haven't seen it yet. we likely can optimize more here though.
_dataSource.ThreadingContext.ThrowIfNotOnUIThread(); | ||
if (cancellationToken.IsCancellationRequested) | ||
return default; | ||
|
||
// Make a copy of all the data we need while we're on the foreground. Then switch to a threadpool | ||
// thread to do the computation. Finally, once new tags have been computed, then we update our state | ||
// again on the foreground. | ||
var spansToTag = GetSpansAndDocumentsToTag(); | ||
var caretPosition = _dataSource.GetCaretPoint(_textView, _subjectBuffer); |
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 wonder how hard it would be to also allow the editor to make this free-threaded.
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs
Outdated
Show resolved
Hide resolved
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs
Outdated
Show resolved
Hide resolved
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs
Outdated
Show resolved
Hide resolved
…icOrEmbeddedClassificationViewTaggerProvider.cs Co-authored-by: Jason Malinowski <[email protected]>
…adi/roslyn into lessMainThreadSwitch
/// </summary> | ||
TrackTextChanges = 1 << 0, | ||
RemoveTagsThatIntersectEdits = 1 << 1, |
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.
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.
No. Can fix later :-)
@@ -225,8 +238,11 @@ void Connect() | |||
|
|||
_eventSource.Changed += OnEventSourceChanged; | |||
|
|||
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges)) | |||
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags) || |
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.
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.
can change in followup.
|
||
// Compute the new tag trees, based on what the current tag trees are. Intentionally CA(true) here so | ||
// we stay on the UI thread if we're in a JTF blocking call. | ||
var newTagTrees = await callback(oldTagTrees).ConfigureAwait(true); |
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.
Can you walk me through this a bit, going off the common path with ConfigureAwait always confuses me.
Specifically, the case I'm curious about is when we enter this method on the main thread and the callback switches to a different thread to do it's work.
With ConfigureAwait(true), we get switched back to the main thread upon the callback return, right? Whereas, if ConfigureAwait(false), we stay on whatever thread the work completed on (or is always a threadpool thread)?
If that's so, wouldn't that be an extra switch to main thread, perhaps unnecessarilly in the case where the CompareExchange was successful? Or does the caller of this method expect to be on the main thread upon it's return?
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.
If that's so, wouldn't that be an extra switch to main thread, perhaps unnecessarilly in the case where the CompareExchange was successful? Or does the caller of this method expect to be on the main thread upon it's return?
The caller can be in one of two modes.
- Normal threadpool/bg tagging. In this case, CA(true) is fine, as we come back to that context and continue i nthe bg.
- blocking JTF call waiting for tags, for certain blocking editor APis (specifically, code-folding). In this case, we want to return to he ui thread whenever we can since that thread is already blocking on us. thsi allows that true thread to contribute progress to this work, instead of it just sitting there blocked, waiting on teh theradpool to finish work.
await TaskScheduler.Default; | ||
// If we're being called from within a blocking JTF.Run call, we don't want to switch to the background | ||
// if we can avoid it. | ||
if (!calledFromJtfRun) |
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.
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.
no. The first call to get tags is also high-pri.
{ | ||
if (changes != null) | ||
{ | ||
// Oops - more than one "textual" change between these snapshots, bail and try to find smallest changes span |
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.
// Oops - more than one "textual" change between these snapshots, bail and try to find smallest changes span
I know this comment isn't new in this PR, but I was trying to understand this code, and the "oops" part of this is confusing. I would think this would commonly happen if the versions sent in had some intermediary versions between.
Is it intended to instead call the GetChangeRanges method that accepts the forward parameter if you might have versions that aren't adjacent?
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.
@jasonmalinowski may know moer her.e
This takes us from two UI jumps per ProduceTagsAsync call to just one.