Skip to content
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

Cache semantic classifications and display from the cache while a solution is loading. #46955

Merged
merged 60 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
cc95ab4
Initialize OOP telemetry session lazily after in-proc one is initialized
tmat Aug 18, 2020
a77649f
Cache semantic classifications across VS sessions
CyrusNajmabadi Aug 19, 2020
1168cb8
Merge branch 'cachedSemanticClassification' into merged
CyrusNajmabadi Aug 19, 2020
fc91948
move code
CyrusNajmabadi Aug 19, 2020
794d5df
Merge branch 'cachedSemanticClassification' into merged
CyrusNajmabadi Aug 19, 2020
d3b1a6c
free after
CyrusNajmabadi Aug 19, 2020
59561fd
Immediately display initial tags.
CyrusNajmabadi Aug 19, 2020
80861ef
Add docs
CyrusNajmabadi Aug 19, 2020
ea9027c
Remove unnecessary code.
CyrusNajmabadi Aug 19, 2020
fb6b1a5
Cleanup
CyrusNajmabadi Aug 19, 2020
5e9472d
Add docs
CyrusNajmabadi Aug 19, 2020
7716245
Add docs
CyrusNajmabadi Aug 19, 2020
130e91e
Revert
CyrusNajmabadi Aug 19, 2020
5f82327
doc
CyrusNajmabadi Aug 19, 2020
b94811b
Revert
CyrusNajmabadi Aug 19, 2020
25c063d
Rename
CyrusNajmabadi Aug 19, 2020
cfe9e69
Docs + rename
CyrusNajmabadi Aug 19, 2020
001e3e0
Comma
CyrusNajmabadi Aug 19, 2020
0f4ba2a
pass listener
CyrusNajmabadi Aug 19, 2020
490aac7
Rework
CyrusNajmabadi Aug 19, 2020
f6d09c1
Docs
CyrusNajmabadi Aug 19, 2020
7f6ca86
Add docs
CyrusNajmabadi Aug 19, 2020
c4a39ab
rename
CyrusNajmabadi Aug 19, 2020
3806349
simplify
CyrusNajmabadi Aug 19, 2020
0eb445a
Simplify
CyrusNajmabadi Aug 19, 2020
99b3374
lint
CyrusNajmabadi Aug 19, 2020
c119353
Update docs
CyrusNajmabadi Aug 19, 2020
e6e67b8
Merge remote-tracking branch 'upstream/master' into merged
CyrusNajmabadi Aug 19, 2020
3e4c61d
Primary
CyrusNajmabadi Aug 19, 2020
8db8648
Better check
CyrusNajmabadi Aug 19, 2020
079dd21
Add classification cache.
CyrusNajmabadi Aug 25, 2020
5d09a45
Have classification attempt to retrieve cached semantics during proje…
CyrusNajmabadi Aug 25, 2020
428ab75
Merge branch 'classificationCaching2' into merged
CyrusNajmabadi Aug 25, 2020
5c63c4d
Revert
CyrusNajmabadi Aug 25, 2020
c8902fd
Delete
CyrusNajmabadi Aug 25, 2020
6437570
Revert
CyrusNajmabadi Aug 25, 2020
8a42a3e
Revert
CyrusNajmabadi Aug 25, 2020
954ae44
add docs
CyrusNajmabadi Aug 25, 2020
0088f3f
Move to vs to drive
CyrusNajmabadi Aug 25, 2020
f6f3767
Extract files
CyrusNajmabadi Aug 25, 2020
4511da1
Store data in memory.
CyrusNajmabadi Aug 25, 2020
23f9bce
Doc
CyrusNajmabadi Aug 26, 2020
27e0938
Update src/VisualStudio/Core/Def/Implementation/SemanticClassificatio…
CyrusNajmabadi Aug 26, 2020
0cc749a
Add assert
CyrusNajmabadi Aug 26, 2020
7f872f2
Update src/VisualStudio/Core/Def/Implementation/SemanticClassificatio…
CyrusNajmabadi Aug 26, 2020
81aa94d
Cleanup
CyrusNajmabadi Aug 26, 2020
9c4881f
Merge branch 'merged' of https://github.com/CyrusNajmabadi/roslyn int…
CyrusNajmabadi Aug 26, 2020
096dcf8
Update src/Workspaces/Core/Portable/Remote/WellKnownServiceHubService.cs
CyrusNajmabadi Aug 26, 2020
7739bb5
Delete
CyrusNajmabadi Aug 26, 2020
45aba61
Merge branch 'merged' of https://github.com/CyrusNajmabadi/roslyn int…
CyrusNajmabadi Aug 26, 2020
6d578ab
Merge remote-tracking branch 'upstream/master' into merged
CyrusNajmabadi Aug 27, 2020
c7565ae
Fix mangling
CyrusNajmabadi Aug 27, 2020
6ce1f81
Fire and forget
CyrusNajmabadi Aug 27, 2020
c5b2bd0
Lint
CyrusNajmabadi Aug 27, 2020
8da4227
Revert
CyrusNajmabadi Aug 27, 2020
9e1f351
Revert
CyrusNajmabadi Aug 27, 2020
5310f4b
PR feedback
CyrusNajmabadi Aug 27, 2020
c67aeeb
Compress two values.
CyrusNajmabadi Aug 27, 2020
bb85569
Doc
CyrusNajmabadi Aug 27, 2020
dc06f8a
Update src/Workspaces/Remote/ServiceHub/Services/CodeAnalysis/CodeAna…
CyrusNajmabadi Aug 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
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.LanguageServices;
using Microsoft.CodeAnalysis.PersistentStorage;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
Expand All @@ -22,6 +27,17 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.Classification
{
internal static class SemanticClassificationUtilities
{
/// <summary>
/// Mapping from workspaces to a task representing when they are fully loaded. While this task is not complete,
/// the workspace is still loading. Once complete the workspace is loaded. We store the task around, instead
/// of just awaiting <see cref="IWorkspaceStatusService.IsFullyLoadedAsync"/> as actually awaiting that call
/// takes non-neglible time (upwards of several hundred ms, to a second), whereas we can actually just use the
/// status of the <see cref="IWorkspaceStatusService.WaitUntilFullyLoadedAsync"/> task to know when we have
/// actually transitioned to a loaded state.
/// </summary>
private static readonly ConditionalWeakTable<Workspace, Task> s_workspaceToFullyLoadedStateTask =
jasonmalinowski marked this conversation as resolved.
Show resolved Hide resolved
new ConditionalWeakTable<Workspace, Task>();

public static async Task ProduceTagsAsync(
TaggerContext<IClassificationTag> context,
DocumentSnapshotSpan spanToTag,
Expand Down Expand Up @@ -131,8 +147,8 @@ private static async Task ClassifySpansAsync(
{
var classifiedSpans = ClassificationUtilities.GetOrCreateClassifiedSpanList();

await classificationService.AddSemanticClassificationsAsync(
document, snapshotSpan.Span.ToTextSpan(), classifiedSpans, cancellationToken: cancellationToken).ConfigureAwait(false);
await AddSemanticClassificationsAsync(
document, snapshotSpan.Span.ToTextSpan(), classificationService, classifiedSpans, cancellationToken: cancellationToken).ConfigureAwait(false);

ClassificationUtilities.Convert(typeMap, snapshotSpan.Snapshot, classifiedSpans, context.AddTag);
ClassificationUtilities.ReturnClassifiedSpanList(classifiedSpans);
Expand All @@ -149,5 +165,57 @@ await classificationService.AddSemanticClassificationsAsync(
throw ExceptionUtilities.Unreachable;
}
}

private static async Task AddSemanticClassificationsAsync(
Document document,
TextSpan textSpan,
IClassificationService classificationService,
List<ClassifiedSpan> classifiedSpans,
CancellationToken cancellationToken)
{
var workspace = document.Project.Solution.Workspace;
var fullyLoadedStateTask = s_workspaceToFullyLoadedStateTask.GetValue(
workspace, w =>
{
var workspaceLoadedService = workspace.Services.GetRequiredService<IWorkspaceStatusService>();
return workspaceLoadedService.WaitUntilFullyLoadedAsync(CancellationToken.None);
});

// If we're not fully loaded try to read from the cache instead so that classifications appear up to date.
// New code will not be semantically classified, but will eventually when the project fully loads.
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
var isFullyLoaded = fullyLoadedStateTask.IsCompleted;
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
if (await TryAddSemanticClassificationsFromCacheAsync(document, textSpan, classifiedSpans, isFullyLoaded, cancellationToken).ConfigureAwait(false))
return;

await classificationService.AddSemanticClassificationsAsync(
document, textSpan, classifiedSpans, cancellationToken).ConfigureAwait(false);
}

private static async Task<bool> TryAddSemanticClassificationsFromCacheAsync(
Document document,
TextSpan textSpan,
List<ClassifiedSpan> classifiedSpans,
bool isFullyLoaded,
CancellationToken cancellationToken)
{
// Don't use the cache if we're fully loaded. We should just compute values normally.
if (isFullyLoaded)
return false;

var semanticCacheService = document.Project.Solution.Workspace.Services.GetService<ISemanticClassificationCacheService>();
if (semanticCacheService == null)
return false;

var checksums = await document.State.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false);
var checksum = checksums.Text;

var result = await semanticCacheService.GetCachedSemanticClassificationsAsync(
(DocumentKey)document, textSpan, checksum, cancellationToken).ConfigureAwait(false);
if (result.IsDefault)
return false;

classifiedSpans.AddRange(result);
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,6 @@ private sealed partial class TagSource : ForegroundThreadAffinitizedObject
/// </summary>
private readonly CancellationTokenSource _initialComputationCancellationTokenSource = new CancellationTokenSource();

/// <summary>
/// Whether or not we've gotten any change notifications from our <see cref="ITaggerEventSource"/>.
/// The first time we hear about changes, we fast track getting tags and reporting
/// them to the UI.
///
/// We use an int so we can use <see cref="Interlocked.CompareExchange(ref int, int, int)"/>
/// to read/set this.
/// </summary>
private int _seenEventSourceChanged;
CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved

public TaggerDelay AddedTagNotificationDelay => _dataSource.AddedTagNotificationDelay;
public TaggerDelay RemovedTagNotificationDelay => _dataSource.RemovedTagNotificationDelay;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
Expand Down Expand Up @@ -59,24 +60,13 @@ private void OnUIUpdatesResumed(object sender, EventArgs e)

private void OnEventSourceChanged(object sender, TaggerEventArgs e)
{
var result = Interlocked.CompareExchange(ref _seenEventSourceChanged, value: 1, comparand: 0);
if (result == 0)
{
// this is the first time we're hearing about changes from our event-source.
// Don't have any delay here. We want to just compute the tags and display
// them as soon as we possibly can.
ComputeInitialTags();
}
else
{
// First, cancel any previous requests (either still queued, or started). We no longer
// want to continue it if new changes have come in.
_workQueue.CancelCurrentWork();
RegisterNotification(
() => RecomputeTagsForeground(initialTags: false),
(int)e.Delay.ComputeTimeDelay().TotalMilliseconds,
GetCancellationToken(initialTags: false));
}
// First, cancel any previous requests (either still queued, or started). We no longer
// want to continue it if new changes have come in.
_workQueue.CancelCurrentWork();
RegisterNotification(
() => RecomputeTagsForeground(initialTags: false),
(int)e.Delay.ComputeTimeDelay().TotalMilliseconds,
GetCancellationToken(initialTags: false));
}

private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
Expand Down Expand Up @@ -560,10 +550,28 @@ private void ProcessNewTagTrees(
// If we're on the foreground already, we can just update our internal state directly.
UpdateStateAndReportChanges(newTagTrees, bufferToChanges, newState, initialTags);
}
else if (initialTags)
{
// If this is the initial set of tags, we fast-track a notification about whatever initial tags we
// computed. This way the UI is updated quickly for that initial set, and we don't have to wait a
// potentially very long time as the foreground-thread-queue makes it way to our notification.
//
// Do this in a fire and forget manner, but ensure we notify the test harness of this so that it
// doesn't try to acquire tag results prior to this work finishing.
var asyncToken = this._asyncListener.BeginAsyncOperation(nameof(ProcessNewTagTrees));
Task.Run(async () =>
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
UpdateStateAndReportChanges(newTagTrees, bufferToChanges, newState, initialTags);
}).CompletesAsyncOperation(asyncToken);
}
else
{
// Otherwise report back on the foreground asap to update the state and let our
// clients know about the change.
// Otherwise report back on the foreground to update the state and let our clients know about the
// change. This will go to the end of the foreground processing queue. This will normally process
// quickly once VS is loaded, but it may take some time initially when VS is loading and the UI
// thread is highly occupied. This helps ensure that we don't oversaturate the UI during a very
// contended period of time.
RegisterNotification(() => UpdateStateAndReportChanges(
newTagTrees, bufferToChanges, newState, initialTags),
delay: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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.

#nullable enable

using System;
using System.Composition;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SolutionCrawler;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.SemanticClassificationCache
{
[ExportIncrementalAnalyzerProvider(nameof(SemanticClassificationCacheIncrementalAnalyzerProvider), new[] { WorkspaceKind.Host }), Shared]
internal class SemanticClassificationCacheIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public SemanticClassificationCacheIncrementalAnalyzerProvider()
{
}

public IIncrementalAnalyzer? CreateIncrementalAnalyzer(Workspace workspace)
{
if (workspace is not VisualStudioWorkspace)
return null;

return new SemanticClassificationCacheIncrementalAnalyzer();
}

private class SemanticClassificationCacheIncrementalAnalyzer : IncrementalAnalyzerBase
{
public override async Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, InvocationReasons reasons, CancellationToken cancellationToken)
{
if (!document.IsOpen())
return;

var solution = document.Project.Solution;
var client = await RemoteHostClient.TryGetClientAsync(solution.Workspace, cancellationToken).ConfigureAwait(false);
if (client == null)
{
// We don't do anything if we fail to get the external process. That's the case when something has gone
// wrong, or the user is explicitly choosing to run inproc only. In neither of those cases do we want
// to bog down the VS process with the work to semantically classify files.
return;
}

var statusService = document.Project.Solution.Workspace.Services.GetRequiredService<IWorkspaceStatusService>();
var isFullyLoaded = await statusService.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
Debug.Assert(isFullyLoaded, "We should only be called by the incremental analyzer once the solution is fully loaded.");

await client.RunRemoteAsync(
WellKnownServiceHubService.CodeAnalysis,
nameof(IRemoteSemanticClassificationCacheService.CacheSemanticClassificationsAsync),
document.Project.Solution,
arguments: new object[] { document.Id, isFullyLoaded },
callbackTarget: null,
cancellationToken).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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.

#nullable enable

using System;
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Implementation.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PersistentStorage;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.SemanticClassificationCache
{
[ExportWorkspaceService(typeof(ISemanticClassificationCacheService), ServiceLayer.Host), Shared]
internal class VisualStudioSemanticClassificationCacheService
: ForegroundThreadAffinitizedObject, ISemanticClassificationCacheService
{
private readonly VisualStudioWorkspaceImpl _workspace;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VisualStudioSemanticClassificationCacheService(
VisualStudioWorkspaceImpl workspace,
IThreadingContext threadingContext)
: base(threadingContext)
{
_workspace = workspace;
}

public async Task<ImmutableArray<ClassifiedSpan>> GetCachedSemanticClassificationsAsync(
DocumentKey documentKey,
TextSpan textSpan,
Checksum checksum,
CancellationToken cancellationToken)
{
var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false);
if (client == null)
{
// We don't do anything if we fail to get the external process. That's the case when something has gone
// wrong, or the user is explicitly choosing to run inproc only. In neither of those cases do we want
// to bog down the VS process with the work to semantically classify files.
return default;
}

var classifiedSpans = await client.RunRemoteAsync<SerializableClassifiedSpans>(
WellKnownServiceHubService.CodeAnalysis,
nameof(IRemoteSemanticClassificationCacheService.GetCachedSemanticClassificationsAsync),
solution: null,
arguments: new object[] { documentKey.Dehydrate(), textSpan, checksum },
callbackTarget: null,
cancellationToken).ConfigureAwait(false);
if (classifiedSpans == null)
return default;

var list = ClassificationUtilities.GetOrCreateClassifiedSpanList();
classifiedSpans.Rehydrate(list);

var result = list.ToImmutableArray();
ClassificationUtilities.ReturnClassifiedSpanList(list);
return result;
}
}
}
Loading