-
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
Have the workspace cache the semantic model (after it is acquired) for the active document (if one exists). #73074
Have the workspace cache the semantic model (after it is acquired) for the active document (if one exists). #73074
Conversation
@@ -7,8 +7,8 @@ | |||
using System.Linq; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Microsoft.CodeAnalysis.Editor.Test; |
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.
helper type moved into common location.
Also, view with whitespcace off.
@@ -24,7 +24,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.NavigateTo | |||
[Trait(Traits.Feature, Traits.Features.NavigateTo)] | |||
public sealed class NavigateToSearcherTests | |||
{ | |||
private static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(FirstDocIsActiveAndVisibleDocumentTrackingService.Factory)); | |||
private static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(FirstDocumentIsActiveAndVisibleDocumentTrackingService.Factory)); |
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.
renamed to not have abbrs.
/// <summary> | ||
/// Queue to push out text changes in a batched fashion when we hear about them. Because these should be short | ||
/// operations (only syncing text changes) we don't cancel this when we enter the paused state. We simply don't | ||
/// start queuing more requests into this until we become unpaused. | ||
/// </summary> | ||
private readonly AsyncBatchingWorkQueue<(Document? oldDocument, Document? newDocument)> _textChangeQueue; | ||
private readonly AsyncBatchingWorkQueue<(Document oldDocument, Document newDocument)> _textChangeQueue; |
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.
change this queue so that we don't bother adding to it if the doc is null, instead of filtering those out on the other end when we process th queue items.
|
||
var solution = _workspace.CurrentSolution; | ||
await client.TryInvokeAsync<IRemoteAssetSynchronizationService>( | ||
(service, cancellationToken) => service.SynchronizeActiveDocumentAsync(activeDocument, cancellationToken), |
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: this call doesn't use CurrentSolution, or pass over any solution data that needs to sync. It's solely about passing along the current active doc id.
@@ -194,15 +231,15 @@ async ValueTask SynchronizeTextChangesAsync(Document oldDocument, Document newDo | |||
} | |||
|
|||
// get text changes | |||
var textChanges = newText.GetTextChanges(oldText); | |||
if (textChanges.Count == 0) | |||
var textChanges = newText.GetTextChanges(oldText).AsImmutable(); |
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.
changed the API to take an immutable array as well.
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.
Would something like ToImmutableArrayOrEmpty prevent recreating the array?
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.
That's what AsImmutable should do. If not, we need ot unify :)
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.
Yes, I find it very confusing that there as so many ways to convert from IEnumerable to ImmutableArray, but I think most of them don't do the check for it already being an immutable array
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 bottoms at at:
public static ImmutableArray<T> CreateRange<T>(IEnumerable<T> items)
{
Requires.NotNull(items, "items");
if (items is IImmutableArray immutableArray)
{
Array array = immutableArray.Array;
if (array == null)
{
throw new InvalidOperationException(System.SR.InvalidOperationOnDefaultArray);
}
return new ImmutableArray<T>((T[])array);
}
So it should allow moving direct to a new immutablearray backed by the same array.
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.
ultimately, it's the underlying array that matters. as boxing and unboxing the immutable array doesn't actually give the same IA again for obvious reasons.
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.
Oh, I didn't track it into CreateRange, I didn't think ImmutableArray had that built in. Nice!
namespace Microsoft.CodeAnalysis.Editor.Test; | ||
|
||
[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Test), Shared, PartNotDiscoverable] | ||
internal sealed class TestDocumentTrackingService : IDocumentTrackingService |
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. t hsi type already existed.
// 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; |
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 moved to Workspace level, but was cleaned up so much, it shows as a remove+add.
/// <summary> | ||
/// Raised when a text buffer that's not part of a workspace is changed. | ||
/// </summary> | ||
event EventHandler<EventArgs> NonRoslynBufferTextChanged; |
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.
never used. removed.
|
||
internal interface IDocumentTrackingService : IWorkspaceService | ||
{ | ||
bool SupportsDocumentTracking { get; } |
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.
not needed. removed.
using var workspace = TestWorkspace.CreateCSharp(code); | ||
using var remoteWorkspace = CreateRemoteWorkspace(); | ||
[Fact] | ||
public void TestNoActiveDocumentSemanticModelNotCached() |
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.
a bunch of tests to validate the strong vs weak holding of semantic models based on active-doc, along with tests that oop respects that.
/// return altogether. Hosts are free to implement this service to do nothing at all, always returning empty/default | ||
/// values for the members within. As per the above, this should never affect correctness, but it may impede a | ||
/// feature's ability to provide results in as timely a manner as possible for a client. | ||
/// </summary> |
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 docs for clarity.
/// hold on weakly. Given how much work happens on the active document, this can help avoid the remote side from | ||
/// continually creating and then throwing away that data. | ||
/// </summary> | ||
ValueTask SynchronizeActiveDocumentAsync(DocumentId? documentId, CancellationToken cancellationToken); |
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 api.
{ | ||
var documentTrackingService = GetWorkspace().Services.GetRequiredService<IDocumentTrackingService>() as RemoteDocumentTrackingService; | ||
documentTrackingService?.SetActiveDocument(documentId); | ||
return ValueTaskFactory.CompletedTask; |
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.
impl on OOP side is trivial. we literally just set the value in the oop doc tracking service.
src/EditorFeatures/TestUtilities/DocumentTracking/TestDocumentTrackingService.cs
Outdated
Show resolved
Hide resolved
[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Host), Shared] | ||
[method: ImportingConstructor] | ||
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
internal sealed class RemoteDocumentTrackingService() : IDocumentTrackingService |
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.
if i do all this work myself, what work will be left for the poor AIs?
@ToddGrun what features were you seeing that were doing nullable analysis? I want to check if those are candidates for opting out of that analysis. |
SemanticModel? semanticModel; | ||
if (disableNullableAnalysis) | ||
var semanticModel = await GetSemanticModelWorkerAsync().ConfigureAwait(false); | ||
this.Project.Solution.OnSemanticModelObtained(this.Id, semanticModel); |
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.
Never mind, it's never queried from that cache, strictly used as a mechanism to keep it alive.
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.
(although it looks like this won't be able to keep alive both the semantic models for the active document)
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.
Finally looked in the SemanticModelCaching code, and it's able to detect that and keeps both. Nice!
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.
Haha. A wild journey in four steps. :-)
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.
Will check the perf results tomorrow morning and move forward if things are good. |
…adi/roslyn into semanticModelCaching
No regressions. Merging in. |
@jasonmalinowski For review when you get back. |
We're seeing a lot of cases where both the local solution and OOP solution end up recreating the semantic model for the active document repeatedly. This is because we intentionally do not hold onto semantic models strongly, so tehy can drop away after use. this is valuable so that features that access lots of docs don't end up rooting a huge amount of memory. However, it has the downside that heavily used, and very important docs (like the user's actively edited document) aren't held onto, leading to features getting the semantic model, and then having it drop away prior to the next feature waking up and obtaining it. As features run on very different cadences, and GCs are happening very regularly, these models often drop away.
In prior versions of roslyn, we had complex caches with complex rules for populating and purging. These caches were not well understood, and often caused major problems for us (like rooting 10s of GB of memory, and only releasing them on some long time-based cadence). We ended up removing them, getting to a much better point where our memory usage would now peak far lower, and would stay relatively stable.
This PR adds back in a cache, but in a very targeted fashion. It only caches one semantic model around, only if the requested model is for the active document. This concept works in both the local host and in the OOP server, allowing both to keep that sole model around once asked until either edits come in, or hte active doc changes. The logic here is then quite simple, and the amount of memory used will be bounded to the whatever that single model needs.
--
Going to run speedometer tests to ensure this causes no issue (and we're hoping to see benefits here as well).
PR1: https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_build/results?buildId=9434937&view=results
PR2: https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_build/results?buildId=9434940&view=results