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

Reduce LOH allocs when applying code fixes. #73424

Merged
merged 24 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 17 additions & 26 deletions src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,45 +205,36 @@ private async ValueTask SynchronizeTextChangesAsync(

async ValueTask SynchronizeTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken)
{
// this pushes text changes to the remote side if it can.
// this is purely perf optimization. whether this pushing text change
// worked or not doesn't affect feature's functionality.
// this pushes text changes to the remote side if it can. this is purely perf optimization. whether this
// pushing text change worked or not doesn't affect feature's functionality.
//
// this basically see whether it can cheaply find out text changes
// between 2 snapshots, if it can, it will send out that text changes to
// remote side.
// this basically see whether it can cheaply find out text changes between 2 snapshots, if it can, it will
// send out that text changes to remote side.
//
// the remote side, once got the text change, will again see whether
// it can use that text change information without any high cost and
// create new snapshot from it.
// the remote side, once got the text change, will again see whether it can use that text change information
// without any high cost and create new snapshot from it.
//
// otherwise, it will do the normal behavior of getting full text from
// VS side. this optimization saves times we need to do full text
// synchronization for typing scenario.
// otherwise, it will do the normal behavior of getting full text from VS side. this optimization saves
// times we need to do full text synchronization for typing scenario.

if ((oldDocument.TryGetText(out var oldText) == false) ||
(newDocument.TryGetText(out var newText) == false))
if (!oldDocument.TryGetText(out var oldText) ||
!newDocument.TryGetText(out var newText))
{
// we only support case where text already exist
return;
}

// get text changes
var textChanges = newText.GetTextChanges(oldText).AsImmutable();
if (textChanges.Length == 0)
{
// no changes
// Avoid allocating text before seeing if we can bail out.
var changeRanges = newText.GetChangeRanges(oldText).AsImmutable();
if (changeRanges.Length == 0)
return;
}

// whole document case
if (textChanges.Length == 1 && textChanges[0].Span.Length == oldText.Length)
{
// no benefit here. pulling from remote host is more efficient
// no benefit here. pulling from remote host is more efficient
if (changeRanges is [{ Span.Length: var singleChangeLength }] && singleChangeLength == oldText.Length)
return;
}

// only cancelled when remote host gets shutdown
var textChanges = newText.GetTextChanges(oldText).AsImmutable();

var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false);
if (client == null)
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,113 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.UnitTests.LinkedFiles
namespace Microsoft.CodeAnalysis.Editor.UnitTests.LinkedFiles;

public sealed class LinkedFileDiffMergingEditorTests : AbstractCodeActionTest
{
public partial class LinkedFileDiffMergingEditorTests : AbstractCodeActionTest
{
private const string WorkspaceXml = @"<Workspace>
<Project Language=""C#"" CommonReferences=""true"" AssemblyName=""CSProj"" PreprocessorSymbols=""Proj1"">
<Document FilePath = ""C.cs""><![CDATA[public class [|C|] { }]]></Document>
</Project>
<Project Language = ""C#"" CommonReferences=""true"" PreprocessorSymbols=""Proj2"">
<Document IsLinkFile = ""true"" LinkAssemblyName=""CSProj"" LinkFilePath=""C.cs""/>
</Project>
</Workspace>";

protected internal override string GetLanguage()
=> LanguageNames.CSharp;

protected override CodeRefactoringProvider CreateCodeRefactoringProvider(EditorTestWorkspace workspace, TestParameters parameters)
=> new TestCodeRefactoringProvider();

[WpfFact]
public async Task TestCodeActionPreviewAndApply()
private const string WorkspaceXml = """
<Workspace>
<Project Language="C#" CommonReferences="true" AssemblyName="CSProj" PreprocessorSymbols="Proj1">
<Document FilePath = "C.cs"><![CDATA[public class [|C|]
{
public class D
{
}
}]]></Document>
</Project>
<Project Language = "C#" CommonReferences="true" PreprocessorSymbols="Proj2">
<Document IsLinkFile = "true" LinkAssemblyName="CSProj" LinkFilePath="C.cs"/>
</Project>
</Workspace>
""";

private const string s_expectedCode = """
internal class C
{
// TODO: WPF required due to https://github.com/dotnet/roslyn/issues/46153
using var workspace = EditorTestWorkspace.Create(WorkspaceXml, composition: EditorTestCompositions.EditorFeaturesWpf);
var codeIssueOrRefactoring = await GetCodeRefactoringAsync(workspace, new TestParameters());
private class D
{
}
}
""";

var expectedCode = "private class D { }";
protected internal override string GetLanguage()
=> LanguageNames.CSharp;

await TestActionOnLinkedFiles(
workspace,
expectedText: expectedCode,
action: codeIssueOrRefactoring.CodeActions[0].action,
expectedPreviewContents: expectedCode);
}
protected override CodeRefactoringProvider CreateCodeRefactoringProvider(EditorTestWorkspace workspace, TestParameters parameters)
=> new TestCodeRefactoringProvider();

[Fact]
public async Task TestWorkspaceTryApplyChangesDirectCall()
{
using var workspace = EditorTestWorkspace.Create(WorkspaceXml);
var solution = workspace.CurrentSolution;
[WpfFact]
public async Task TestCodeActionPreviewAndApply()
{
// TODO: WPF required due to https://github.com/dotnet/roslyn/issues/46153
using var workspace = EditorTestWorkspace.Create(WorkspaceXml, composition: EditorTestCompositions.EditorFeaturesWpf);
var codeIssueOrRefactoring = await GetCodeRefactoringAsync(workspace, new TestParameters());

await TestActionOnLinkedFiles(
workspace,
expectedText: s_expectedCode,
action: codeIssueOrRefactoring.CodeActions[0].action,
expectedPreviewContents: """
internal class C
{
private class D
{
...
""");
}

var documentId = workspace.Documents.Single(d => !d.IsLinkFile).Id;
var text = await workspace.CurrentSolution.GetDocument(documentId).GetTextAsync();
[Fact]
public async Task TestWorkspaceTryApplyChangesDirectCall()
{
using var workspace = EditorTestWorkspace.Create(WorkspaceXml);
var solution = workspace.CurrentSolution;

var linkedDocumentId = workspace.Documents.Single(d => d.IsLinkFile).Id;
var linkedText = await workspace.CurrentSolution.GetDocument(linkedDocumentId).GetTextAsync();
var documentId = workspace.Documents.Single(d => !d.IsLinkFile).Id;
var text = await workspace.CurrentSolution.GetRequiredDocument(documentId).GetTextAsync();

var newSolution = solution
.WithDocumentText(documentId, text.Replace(13, 1, "D"))
.WithDocumentText(linkedDocumentId, linkedText.Replace(0, 6, "private"));
var linkedDocumentId = workspace.Documents.Single(d => d.IsLinkFile).Id;
var linkedText = await workspace.CurrentSolution.GetRequiredDocument(linkedDocumentId).GetTextAsync();

workspace.TryApplyChanges(newSolution);
var textString = linkedText.ToString();

var expectedMergedText = "private class D { }";
Assert.Equal(expectedMergedText, (await workspace.CurrentSolution.GetDocument(documentId).GetTextAsync()).ToString());
Assert.Equal(expectedMergedText, (await workspace.CurrentSolution.GetDocument(linkedDocumentId).GetTextAsync()).ToString());
}
var newSolution = solution
.WithDocumentText(documentId, text.Replace(textString.IndexOf("public"), "public".Length, "internal"))
.WithDocumentText(linkedDocumentId, linkedText.Replace(textString.LastIndexOf("public"), "public".Length, "private"));

workspace.TryApplyChanges(newSolution);

protected override ParseOptions GetScriptOptions()
=> throw new NotSupportedException();
Assert.Equal(s_expectedCode, (await workspace.CurrentSolution.GetRequiredDocument(documentId).GetTextAsync()).ToString());
Assert.Equal(s_expectedCode, (await workspace.CurrentSolution.GetRequiredDocument(linkedDocumentId).GetTextAsync()).ToString());
}

protected override ParseOptions GetScriptOptions()
=> throw new NotSupportedException();

private class TestCodeRefactoringProvider : CodeRefactorings.CodeRefactoringProvider
private sealed class TestCodeRefactoringProvider : CodeRefactoringProvider
{
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var document = context.Document;
var linkedDocument = document.Project.Solution.Projects.Single(p => p != document.Project).Documents.Single();
var document = context.Document;
var linkedDocument = document.Project.Solution.Projects.Single(p => p != document.Project).Documents.Single();
var sourceText = await linkedDocument.GetTextAsync();
var textString = sourceText.ToString();

var newSolution = document.Project.Solution
.WithDocumentText(document.Id, (await document.GetTextAsync()).Replace(13, 1, "D"))
.WithDocumentText(linkedDocument.Id, (await linkedDocument.GetTextAsync()).Replace(0, 6, "private"));
var newSolution = document.Project.Solution
.WithDocumentText(document.Id, (await document.GetTextAsync()).Replace(textString.IndexOf("public"), "public".Length, "internal"))
.WithDocumentText(linkedDocument.Id, sourceText.Replace(textString.LastIndexOf("public"), "public".Length, "private"));

#pragma warning disable RS0005
context.RegisterRefactoring(CodeAction.Create("Description", (ct) => Task.FromResult(newSolution)), context.Span);
#pragma warning restore RS0005
}
context.RegisterRefactoring(CodeAction.Create("Description", _ => Task.FromResult(newSolution)), context.Span);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,6 @@ protected static string MakeUnique(string baseName, INamedTypeSymbol containingT
return NameGenerator.GenerateUniqueName(baseName, containingTypeMemberNames.ToSet(), StringComparer.Ordinal);
}

internal override IEnumerable<SyntaxNode> GetConstructorNodes(INamedTypeSymbol containingType)
protected override IEnumerable<SyntaxNode> GetConstructorNodes(INamedTypeSymbol containingType)
=> containingType.Constructors.SelectMany(c => c.DeclaringSyntaxReferences.Select(d => d.GetSyntax()));
}
Loading
Loading