Browse Source

StripCRLF early exit when no newline to avoid StringBuilder allocation

Tonttu 4 months ago
parent
commit
7d317ba550
2 changed files with 34 additions and 21 deletions
  1. 5 2
      Benchmarks/Text/TextFormatter/StripCRLF.cs
  2. 29 19
      Terminal.Gui/Text/TextFormatter.cs

+ 5 - 2
Benchmarks/Text/TextFormatter/StripCRLF.cs

@@ -4,6 +4,9 @@ using Tui = Terminal.Gui;
 
 
 namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
 namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
 
 
+/// <summary>
+/// Benchmarks for <see cref="Tui.TextFormatter.StripCRLF"/> performance fine-tuning.
+/// </summary>
 [MemoryDiagnoser]
 [MemoryDiagnoser]
 public class StripCRLF
 public class StripCRLF
 {
 {
@@ -31,7 +34,7 @@ public class StripCRLF
     }
     }
 
 
     /// <summary>
     /// <summary>
-    /// Previous implementation with intermediate list allocation.
+    /// Previous implementation with intermediate rune list.
     /// </summary>
     /// </summary>
     private static string RuneListToString (string str, bool keepNewLine = false)
     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."
             "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)
         foreach (string text in textPermutations)
         {
         {

+ 29 - 19
Terminal.Gui/Text/TextFormatter.cs

@@ -11,7 +11,7 @@ namespace Terminal.Gui;
 public class TextFormatter
 public class TextFormatter
 {
 {
     // Utilized in CRLF related helper methods for faster newline char index search.
     // Utilized in CRLF related helper methods for faster newline char index search.
-    private static readonly SearchValues<char> NewLineSearchValues = SearchValues.Create(['\r', '\n']);
+    private static readonly SearchValues<char> NewlineSearchValues = SearchValues.Create(['\r', '\n']);
 
 
     private Key _hotKey = new ();
     private Key _hotKey = new ();
     private int _hotKeyPos = -1;
     private int _hotKeyPos = -1;
@@ -1191,31 +1191,39 @@ public class TextFormatter
     // TODO: Move to StringExtensions?
     // TODO: Move to StringExtensions?
     internal static string StripCRLF (string str, bool keepNewLine = false)
     internal static string StripCRLF (string str, bool keepNewLine = false)
     {
     {
+        ReadOnlySpan<char> 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();
         StringBuilder stringBuilder = new();
+        ReadOnlySpan<char> 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..];
 
 
-        ReadOnlySpan<char> remaining = str.AsSpan ();
         while (remaining.Length > 0)
         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;
                 break;
             }
             }
 
 
-            ReadOnlySpan<char> slice = remaining.Slice (0, nextLineBreakIndex);
-            stringBuilder.Append (slice);
+            ReadOnlySpan<char> segment = remaining[..newlineCharIndex];
+            stringBuilder.Append (segment);
 
 
+            int stride = segment.Length;
             // Evaluate how many line break characters to preserve.
             // 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)
                 if (keepNewLine)
                 {
                 {
                     stringBuilder.Append ('\n');
                     stringBuilder.Append ('\n');
@@ -1223,10 +1231,11 @@ public class TextFormatter
             }
             }
             else // '\r'
             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)
                 if (crlf)
                 {
                 {
-                    stride = 2;
+                    stride += 2;
                     if (keepNewLine)
                     if (keepNewLine)
                     {
                     {
                         stringBuilder.Append ('\n');
                         stringBuilder.Append ('\n');
@@ -1234,15 +1243,16 @@ public class TextFormatter
                 }
                 }
                 else
                 else
                 {
                 {
-                    stride = 1;
+                    stride++;
                     if (keepNewLine)
                     if (keepNewLine)
                     {
                     {
                         stringBuilder.Append ('\r');
                         stringBuilder.Append ('\r');
                     }
                     }
                 }
                 }
             }
             }
-            remaining = remaining.Slice (slice.Length + stride);
+            remaining = remaining [stride..];
         }
         }
+        stringBuilder.Append (remaining);
         return stringBuilder.ToString ();
         return stringBuilder.ToString ();
     }
     }