From 7d317ba5506440ca85116afce47cc2d4d743c227 Mon Sep 17 00:00:00 2001
From: Tonttu <15074459+TheTonttu@users.noreply.github.com>
Date: Fri, 14 Mar 2025 23:14:13 +0200
Subject: [PATCH] StripCRLF early exit when no newline to avoid StringBuilder
allocation
---
Benchmarks/Text/TextFormatter/StripCRLF.cs | 7 ++-
Terminal.Gui/Text/TextFormatter.cs | 50 +++++++++++++---------
2 files changed, 35 insertions(+), 22 deletions(-)
diff --git a/Benchmarks/Text/TextFormatter/StripCRLF.cs b/Benchmarks/Text/TextFormatter/StripCRLF.cs
index 0878083ce..64e1d4ed8 100644
--- a/Benchmarks/Text/TextFormatter/StripCRLF.cs
+++ b/Benchmarks/Text/TextFormatter/StripCRLF.cs
@@ -4,6 +4,9 @@ using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
+///
+/// Benchmarks for performance fine-tuning.
+///
[MemoryDiagnoser]
public class StripCRLF
{
@@ -31,7 +34,7 @@ public class StripCRLF
}
///
- /// Previous implementation with intermediate list allocation.
+ /// Previous implementation with intermediate rune list.
///
private static string RuneListToString (string str, bool keepNewLine = false)
{
@@ -98,7 +101,7 @@ public class StripCRLF
"Nullam semper tempor mi, nec semper quam fringilla eu. Aenean sit amet pretium augue, in posuere ante. Aenean convallis porttitor purus, et posuere velit dictum eu."
];
- bool[] newLinePermutations = { true, false };
+ bool[] newLinePermutations = [true, false];
foreach (string text in textPermutations)
{
diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs
index 5e48c128c..bd5da255e 100644
--- a/Terminal.Gui/Text/TextFormatter.cs
+++ b/Terminal.Gui/Text/TextFormatter.cs
@@ -11,7 +11,7 @@ namespace Terminal.Gui;
public class TextFormatter
{
// Utilized in CRLF related helper methods for faster newline char index search.
- private static readonly SearchValues NewLineSearchValues = SearchValues.Create(['\r', '\n']);
+ private static readonly SearchValues NewlineSearchValues = SearchValues.Create(['\r', '\n']);
private Key _hotKey = new ();
private int _hotKeyPos = -1;
@@ -1191,31 +1191,39 @@ public class TextFormatter
// TODO: Move to StringExtensions?
internal static string StripCRLF (string str, bool keepNewLine = false)
{
- StringBuilder stringBuilder = new();
-
ReadOnlySpan remaining = str.AsSpan ();
+ int firstNewlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+ // Early exit to avoid StringBuilder allocation if there are no newline characters.
+ if (firstNewlineCharIndex < 0)
+ {
+ return str;
+ }
+
+ StringBuilder stringBuilder = new();
+ ReadOnlySpan firstSegment = remaining[..firstNewlineCharIndex];
+ stringBuilder.Append (firstSegment);
+
+ // The first newline is not yet skipped because the "keepNewLine" condition has not been evaluated.
+ // This means there will be 1 extra iteration because the same newline index is checked again in the loop.
+ remaining = remaining [firstNewlineCharIndex..];
+
while (remaining.Length > 0)
{
- int nextLineBreakIndex = remaining.IndexOfAny (NewLineSearchValues);
- if (nextLineBreakIndex == -1)
+ int newlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+ if (newlineCharIndex == -1)
{
- if (str.Length == remaining.Length)
- {
- return str;
- }
- stringBuilder.Append (remaining);
break;
}
- ReadOnlySpan slice = remaining.Slice (0, nextLineBreakIndex);
- stringBuilder.Append (slice);
+ ReadOnlySpan segment = remaining[..newlineCharIndex];
+ stringBuilder.Append (segment);
+ int stride = segment.Length;
// Evaluate how many line break characters to preserve.
- int stride;
- char lineBreakChar = remaining [nextLineBreakIndex];
- if (lineBreakChar == '\n')
+ char newlineChar = remaining [newlineCharIndex];
+ if (newlineChar == '\n')
{
- stride = 1;
+ stride++;
if (keepNewLine)
{
stringBuilder.Append ('\n');
@@ -1223,10 +1231,11 @@ public class TextFormatter
}
else // '\r'
{
- bool crlf = (nextLineBreakIndex + 1) < remaining.Length && remaining [nextLineBreakIndex + 1] == '\n';
+ int nextCharIndex = newlineCharIndex + 1;
+ bool crlf = nextCharIndex < remaining.Length && remaining [nextCharIndex] == '\n';
if (crlf)
{
- stride = 2;
+ stride += 2;
if (keepNewLine)
{
stringBuilder.Append ('\n');
@@ -1234,15 +1243,16 @@ public class TextFormatter
}
else
{
- stride = 1;
+ stride++;
if (keepNewLine)
{
stringBuilder.Append ('\r');
}
}
}
- remaining = remaining.Slice (slice.Length + stride);
+ remaining = remaining [stride..];
}
+ stringBuilder.Append (remaining);
return stringBuilder.ToString ();
}