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 (); }