Run HasRefactoringsAsync out-of-process#40672
Conversation
4e3bebe to
71bb4e1
Compare
|
@tmat @CyrusNajmabadi This is the change I was mentioning in our meeting last week |
src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Remote/Core/Services/RemotePasteTrackingService.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_CodeRefactoring.cs
Outdated
Show resolved
Hide resolved
|
Overall 1000% for this. But i think we can simplify in a very easy way. |
src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_CodeRefactoring.cs
Outdated
Show resolved
Hide resolved
src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_CodeRefactoring.cs
Outdated
Show resolved
Hide resolved
| var pastedTextSpanOpt = | ||
| provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | ||
| ? (TextSpan?)pastedTextSpan | ||
| : null; |
There was a problem hiding this comment.
| var pastedTextSpanOpt = | |
| provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | |
| ? (TextSpan?)pastedTextSpan | |
| : null; | |
| var pastedTextSpanOpt = provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | |
| ? (TextSpan?)pastedTextSpan | |
| : null; |
The problem with this style is how difficult it is to distinguish the parent/child relationship. For exaple, this would be even more difficult to understand:
| var pastedTextSpanOpt = | |
| provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | |
| ? (TextSpan?)pastedTextSpan | |
| : null; | |
| var pastedTextSpanOpt = provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | |
| ? another.complex.expr() | |
| ? and.yet.another() | |
| ? baz | |
| : quuz | |
| : bar | |
| : null; |
As opposed to:
| var pastedTextSpanOpt = | |
| provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | |
| ? (TextSpan?)pastedTextSpan | |
| : null; | |
| var pastedTextSpanOpt = provider._pasteTrackingService.TryGetPastedTextSpan(sourceText.Container, out var pastedTextSpan) | |
| ? another.complex.expr() | |
| ? and.yet.another() | |
| ? baz | |
| : quuz | |
| : bar | |
| : null; |
Now i can very easily see what children are actually siblings, and i can easily tell the parent/child relationship.
i get that you continually push this strict adherence to indentation. But it's not going to fly with me. Custom indentation serves a valuable purpose and we do judiciously use it across the IDE codebase where appropriate because it really helps with clarity.
There was a problem hiding this comment.
This is all covered by #38255 (comment). The form I used is correct (covered by 1(ii)), as are the first and third examples here (covered by 1(iii)).
There was a problem hiding this comment.
i don't actually see the value of 1ii. in particular it seems to entirely not what i'd want for indenting whenever the expression itself starts on a new line. i.e. on a new line you'll always get:
....
startOfExpr
? whenTrue
: whenFalse
Which never seems desirable. This also doesn't really match teh examples shown in that linked post, none of which show the whenTrue/whenFalse parts aligning with the condition.
| // Cannot track two different pasted spans for the same container | ||
| throw ExceptionUtilities.UnexpectedValue(pastedTextSpan); |
There was a problem hiding this comment.
@CyrusNajmabadi The other option here is to assign null (no pasted text span) if multiple callers cannot agree on the same non-null span. As it stands, code that attempts to use a different span will fail if prior work has not yet completed.
| return textSpan; | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
this is a bit concerning that we're exposing a synchronous method over to the OOP side. in general we don't do that. can this be made async?
| var pasteTrackingService = (RemotePasteTrackingService?)mefHostExportProvider.GetExports<IPasteTrackingService>().SingleOrDefault()?.Value; | ||
| var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); | ||
| var container = sourceText.Container; | ||
| using var _ = pasteTrackingService?.SetPastedTextSpanForRemoteCall(container, pastedTextSpan); |
There was a problem hiding this comment.
is the ?. needed? can it actually fail to get the IPasteTrackingService?
| textSpan, | ||
| pastedTextSpan, | ||
| }, | ||
| callbackTarget: new CodeRefactoringServiceCallback(this), |
There was a problem hiding this comment.
tbh.... i don't get what this callback is for. i don't eally see anyone consuming it on the remote side. could you clarify?
| lock (_trackedSpans) | ||
| { | ||
| var result = _trackedSpans.TryGetValue(sourceTextContainer, out var spanAndCount); | ||
| if (!result || spanAndCount.referenceCount == 0 || spanAndCount.span is null) |
There was a problem hiding this comment.
the .referenceCount == 0 case seems impossible. wlud prefer we throw in that case vs bailing.
There was a problem hiding this comment.
Will add an assertion
| throw ExceptionUtilities.UnexpectedValue(container); | ||
|
|
||
| Debug.Assert(spanAndCount.referenceCount > 0); | ||
| if (spanAndCount.referenceCount <= 1) |
There was a problem hiding this comment.
the < case seems imposible. would prefer we throw in that case.
CyrusNajmabadi
left a comment
There was a problem hiding this comment.
Overall looking good. One thing i didn't understand, and a few places i think coudl clean up.
| { | ||
| if (await state.Target.Owner._codeRefactoringService.HasRefactoringsAsync( | ||
| document, selection.Value, cancellationToken).ConfigureAwait(false)) | ||
| document, selection.Value, pastedTextSpanOpt, cancellationToken).ConfigureAwait(false)) |
There was a problem hiding this comment.
so, generally, we've been trying to move to the model more where we have 'args' that can get passed along with the op, to avoid the 'just jam a ton of individual parameters along' . Just food for thought.
| if (result.HasValue) | ||
| { | ||
| return result.Value; | ||
| } |
There was a problem hiding this comment.
i believe we're no longer following this pattern. if the operation fails, we do not try to fallback to local processing. instead, we jsut bail out (As we will also have had a yellowbar here). note we may not be 100% consistent here, but i believe that Tomas' strong position on what this ll needs to be.
| // 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 |
| return new Releaser(this, container); | ||
| } | ||
|
|
||
| internal readonly struct Releaser : IDisposable |
There was a problem hiding this comment.
nit: my preference is public on all nested symbols that are made 'internal' by virtue of their container.
| }, cancellationToken); | ||
| } | ||
|
|
||
| public ValueTask<bool> HasRefactoringsAsync(PinnedSolutionInfo solutionInfo, DocumentId documentId, TextSpan textSpan, TextSpan? pastedTextSpan, CancellationToken cancellationToken) |
There was a problem hiding this comment.
there's a potential future optimizatino we can do/track if we're willing to make the statement that a refactoring should be computable without knowing about projects that depend on. Specifically, OOP supports the idea of quickly syncing only a project (and the projects it depends on). we could use that to more quickly sync and find out if there are refactorings. i believe all our refactorings would satisfy this. but some third party ones might not.
|
i have a general design concern here given that we run HasRefactorings OOP, but GetRefactorign in prc. It means that we cannot leverage all the work that HasRefactorings deos (in terms of warming up compilations, etc.) that happen externally. THis risks paying the same price again when we now actually do the work in proc to get the refactorings. That said, maybe it's ok. an A/B test with code markers would help here. |
Marked as a draft until we verify that it works for both built-in refactorings and refactorings provided by a VSIX package.
This change means refactorings no longer have access to Visual Studio services, but our options are somewhat limited. The infrastructure to execute
HasRefactoringsAsynccannot be executed in the limited process space of devenv.exe without risking OOM crashes in large sources. Moving out-of-process means the 64-bit address space can be leveraged when necessary.We could provide a hook for individual refactorings to run in process, but it will have a tremendous negative performance impact on those refactorings (they will no longer share a semantic model with other refactorings running on the same compilation, so moving a refactoring in-process means the entire burden of parsing+binding+analysis will fall on the refactoring that chose to run in process).
Refactorings that require Visual Studio services always have the option of interacting directly with the Light Bulb APIs instead of operating as a CodeRefactoringProvider.