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 allocations from newline information allocated by ChangedText.GetLinesCore #74728

Merged
merged 15 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
88 changes: 88 additions & 0 deletions src/Compilers/Core/CodeAnalysisTest/Text/CompositeTextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.Text;

public sealed class CompositeTextTests
{
[Theory]
[InlineData("abcdefghijkl")]
[InlineData(["\r\r\r\r\r\r\r\r\r\r\r\r"])]
[InlineData(["\n\n\n\n\n\n\n\n\n\n\n\n"])]
[InlineData(["\r\n\r\n\r\n\r\n\r\n\r\n"])]
[InlineData(["\n\r\n\r\n\r\n\r\n\r\n\r"])]
[InlineData(["a\r\nb\r\nc\r\nd\r\n"])]
[InlineData(["\ra\n\rb\n\rc\n\rd\n"])]
[InlineData(["\na\r\nb\r\nc\r\nd\r"])]
[InlineData(["ab\r\ncd\r\nef\r\n"])]
[InlineData(["ab\r\r\ncd\r\r\nef"])]
[InlineData(["ab\n\n\rcd\n\n\ref"])]
public void CompositeTextLinesEqualSourceTextLinesPermutations(string contents)
{
// Please try to limit the inputs to this method to around 12 chars or less, as much longer than that
// will blow up the number of potential permutations.
foreach (var (sourceText, compositeText) in CreateSourceAndCompositeTexts(contents))
{
var sourceLinesText = GetLinesTexts(sourceText.Lines);
var compositeLinesText = GetLinesTexts(compositeText.Lines);

Assert.True(sourceLinesText.SequenceEqual(compositeLinesText));

for (var i = 0; i < sourceText.Length; i++)
{
Assert.Equal(sourceText.Lines.IndexOf(i), compositeText.Lines.IndexOf(i));
}
}
}

private static IEnumerable<string> GetLinesTexts(TextLineCollection textLines)
{
return textLines.Select(l => l.Text!.ToString(l.SpanIncludingLineBreak));
}

// Returns all possible permutations of contents into SourceText arrays of length between minSourceTextCount and maxSourceTextCount
private static IEnumerable<(SourceText, CompositeText)> CreateSourceAndCompositeTexts(string contents, int minSourceTextCount = 2, int maxSourceTextCount = 4)
{
var sourceText = SourceText.From(contents);

for (var sourceTextCount = minSourceTextCount; sourceTextCount <= Math.Min(maxSourceTextCount, contents.Length); sourceTextCount++)
{
foreach (var sourceTexts in CreateSourceTextPermutations(contents, sourceTextCount))
{
var sourceTextsBuilder = ArrayBuilder<SourceText>.GetInstance();
sourceTextsBuilder.AddRange(sourceTexts);

var compositeText = (CompositeText)CompositeText.ToSourceText(sourceTextsBuilder, sourceText, adjustSegments: false);
yield return (sourceText, compositeText);
}
}
}

private static IEnumerable<SourceText[]> CreateSourceTextPermutations(string contents, int requestedSourceTextCount)
{
if (requestedSourceTextCount == 1)
{
yield return [SourceText.From(contents)];
}
else
{
var maximalSourceTextLength = (contents.Length - requestedSourceTextCount) + 1;
for (int i = 1; i <= maximalSourceTextLength; i++)
{
var sourceText = SourceText.From(contents[..i]);
foreach (var otherSourceTexts in CreateSourceTextPermutations(contents.Substring(i), requestedSourceTextCount - 1))
{
yield return [sourceText, .. otherSourceTexts];
}
}
}
}
}
4 changes: 2 additions & 2 deletions src/Compilers/Core/CodeAnalysisTest/Text/TextChangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,14 @@ public void TestOptimizedSourceTextLinesRemoveCrLf()
}

[Fact]
public void TestOptimizedSourceTextLinesBrakeCrLf()
public void TestOptimizedSourceTextLinesBreakCrLf()
{
AssertChangedTextLinesHelper("Test\r\nMessage",
new TextChange(new TextSpan(5, 0), "aaaaaa"));
}

[Fact]
public void TestOptimizedSourceTextLinesBrakeCrLfWithLfPrefixedAndCrSuffixed()
public void TestOptimizedSourceTextLinesBreakCrLfWithLfPrefixedAndCrSuffixed()
{
AssertChangedTextLinesHelper("Test\r\nMessage",
new TextChange(new TextSpan(5, 0), "\naaaaaa\r"));
Expand Down
104 changes: 1 addition & 103 deletions src/Compilers/Core/Portable/Text/ChangedText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Text
Expand Down Expand Up @@ -264,108 +262,8 @@ private static ImmutableArray<TextChangeRange> Merge(IReadOnlyList<ImmutableArra
return merged;
}

/// <summary>
/// Computes line starts faster given already computed line starts from text before the change.
/// </summary>
protected override TextLineCollection GetLinesCore()
{
SourceText? oldText;
TextLineCollection? oldLineInfo;

if (!_info.WeakOldText.TryGetTarget(out oldText) || !oldText.TryGetLines(out oldLineInfo))
{
// no old line starts? do it the hard way.
return base.GetLinesCore();
}

// compute line starts given changes and line starts already computed from previous text
var lineStarts = new SegmentedList<int>(capacity: oldLineInfo.Count)
{
0
};

// position in the original document
var position = 0;

// delta generated by already processed changes (position in the new document = position + delta)
var delta = 0;

// true if last segment ends with CR and we need to check for CR+LF code below assumes that both CR and LF are also line breaks alone
var endsWithCR = false;

foreach (var change in _info.ChangeRanges)
{
// include existing line starts that occur before this change
if (change.Span.Start > position)
{
if (endsWithCR && _newText[position + delta] == '\n')
{
// remove last added line start (it was due to previous CR)
// a new line start including the LF will be added next
lineStarts.RemoveAt(lineStarts.Count - 1);
}

var lps = oldLineInfo.GetLinePositionSpan(TextSpan.FromBounds(position, change.Span.Start));
for (int i = lps.Start.Line + 1; i <= lps.End.Line; i++)
{
lineStarts.Add(oldLineInfo[i].Start + delta);
}

endsWithCR = oldText[change.Span.Start - 1] == '\r';

// in case change is inserted between CR+LF we treat CR as line break alone,
// but this line break might be retracted and replaced with new one in case LF is inserted
if (endsWithCR && change.Span.Start < oldText.Length && oldText[change.Span.Start] == '\n')
{
lineStarts.Add(change.Span.Start + delta);
}
}

// include line starts that occur within newly inserted text
if (change.NewLength > 0)
{
var changeStart = change.Span.Start + delta;
var text = GetSubText(new TextSpan(changeStart, change.NewLength));

if (endsWithCR && text[0] == '\n')
{
// remove last added line start (it was due to previous CR)
// a new line start including the LF will be added next
lineStarts.RemoveAt(lineStarts.Count - 1);
}

// Skip first line (it is always at offset 0 and corresponds to the previous line)
for (int i = 1; i < text.Lines.Count; i++)
{
lineStarts.Add(changeStart + text.Lines[i].Start);
}

endsWithCR = text[change.NewLength - 1] == '\r';
}

position = change.Span.End;
delta += (change.NewLength - change.Span.Length);
}

// include existing line starts that occur after all changes
if (position < oldText.Length)
{
if (endsWithCR && _newText[position + delta] == '\n')
{
// remove last added line start (it was due to previous CR)
// a new line start including the LF will be added next
lineStarts.RemoveAt(lineStarts.Count - 1);
}

var lps = oldLineInfo.GetLinePositionSpan(TextSpan.FromBounds(position, oldText.Length));
for (int i = lps.Start.Line + 1; i <= lps.End.Line; i++)
{
lineStarts.Add(oldLineInfo[i].Start + delta);
}
}

return new LineInfo(this, lineStarts);
}
=> _newText.Lines;

internal static class TestAccessor
{
Expand Down
Loading
Loading