Skip to content

Commit

Permalink
Merge pull request #73699 from CyrusNajmabadi/lessMainThreadSwitch
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored May 25, 2024
2 parents 6e4546d + 900ed3d commit 6c9936c
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 338 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -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 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))
{
Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

protected override TaggerDelay EventChangeDelay => TaggerDelay.NearImmediate;

protected override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;

#endregion

#region Fields that can only be accessed from the foreground thread
Expand Down Expand Up @@ -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;
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"/>,
Expand Down Expand Up @@ -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();
};

// Now hook up this tagger to all interesting events.
Connect();
Expand All @@ -225,8 +238,11 @@ void Connect()

_eventSource.Changed += OnEventSourceChanged;

if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.TrackTextChanges))
if (_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveAllTags) ||
_dataSource.TextChangeBehavior.HasFlag(TaggerTextChangeBehavior.RemoveTagsThatIntersectEdits))
{
_subjectBuffer.Changed += OnSubjectBufferChanged;
}

if (_dataSource.CaretChangeBehavior.HasFlag(TaggerCaretChangeBehavior.RemoveAllTagsOnCaretMoveOutsideOfTag))
{
Expand Down Expand Up @@ -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))
{
_subjectBuffer.Changed -= OnSubjectBufferChanged;
}

_eventSource.Changed -= OnEventSourceChanged;

Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 6c9936c

Please sign in to comment.