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

Have the workspace cache the semantic model (after it is acquired) for the active document (if one exists). #73074

Merged
merged 29 commits into from
Apr 18, 2024

Conversation

CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi commented Apr 17, 2024

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

@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner April 17, 2024 22:24
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Apr 17, 2024
@@ -7,8 +7,8 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Test;
Copy link
Member Author

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));
Copy link
Member Author

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;
Copy link
Member Author

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),
Copy link
Member Author

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();
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

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 :)

Copy link
Contributor

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

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Contributor

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
Copy link
Member Author

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;
Copy link
Member Author

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;
Copy link
Member Author

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; }
Copy link
Member Author

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()
Copy link
Member Author

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>
Copy link
Member Author

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);
Copy link
Member Author

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;
Copy link
Member Author

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.

[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Host), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RemoteDocumentTrackingService() : IDocumentTrackingService
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteDocumentTrackingService

nit: rename file

Copy link
Member Author

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?

@CyrusNajmabadi
Copy link
Member Author

@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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnSemanticModelObtained

No need to indicate in the cache the value of disableNullableAnalysis?

Copy link
Contributor

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.

Copy link
Contributor

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)

Copy link
Contributor

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!

Copy link
Member Author

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. :-)

Copy link
Contributor

@ToddGrun ToddGrun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@CyrusNajmabadi
Copy link
Member Author

Will check the perf results tomorrow morning and move forward if things are good.

@CyrusNajmabadi
Copy link
Member Author

No regressions. Merging in.

@CyrusNajmabadi CyrusNajmabadi merged commit afa1eb6 into dotnet:main Apr 18, 2024
25 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the semanticModelCaching branch April 18, 2024 17:22
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Apr 18, 2024
@CyrusNajmabadi
Copy link
Member Author

@jasonmalinowski For review when you get back.

@dibarbet dibarbet modified the milestones: Next, 17.11 P1 Apr 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants