Skip to content

Commit

Permalink
Reduce allocations from newline information allocated by ChangedText.…
Browse files Browse the repository at this point in the history
…GetLinesCore (#74728)

* Reduce allocations from newline information allocated by ChangedText.GetLinesCore.

The typing scenario in the speedometer scrolling test shows this as 10% of allocations.

The general idea here is that ChangedText doesn't need to keep track of line information as the SourceText that it wraps has that information. The complexity that was in ChangedText around newline splitting now needs to sit in both CompositeText and SubText as they need to understand how to expose their line collections.
  • Loading branch information
ToddGrun committed Aug 22, 2024
1 parent 439bea0 commit 8e98fc9
Show file tree
Hide file tree
Showing 5 changed files with 419 additions and 106 deletions.
90 changes: 90 additions & 0 deletions src/Compilers/Core/CodeAnalysisTest/Text/CompositeTextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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"])]
[InlineData(["ab\u0085cdef\u2028ijkl\u2029op"])]
[InlineData(["\u0085\u2028\u2029\u0085\u2028\u2029\u0085\u2028\u2029\u0085\u2028\u2029"])]
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

0 comments on commit 8e98fc9

Please sign in to comment.