소스 검색

Merge pull request #4436 from gui-cs/copilot/fix-intermediate-heap-allocations

Tig 1 주 전
부모
커밋
bad2f02697
4개의 변경된 파일112개의 추가작업 그리고 31개의 파일을 삭제
  1. 1 1
      Terminal.Gui/Drawing/Cell.cs
  2. 23 0
      Terminal.Gui/Drawing/GraphemeHelper.cs
  3. 12 4
      Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
  4. 76 26
      Terminal.Gui/Text/TextFormatter.cs

+ 1 - 1
Terminal.Gui/Drawing/Cell.cs

@@ -27,7 +27,7 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, st
         readonly get => _grapheme;
         set
         {
-            if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1)
+            if (GraphemeHelper.GetGraphemeCount (value) > 1)
             {
                 throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell.");
             }

+ 23 - 0
Terminal.Gui/Drawing/GraphemeHelper.cs

@@ -46,4 +46,27 @@ public static class GraphemeHelper
             yield return element;
         }
     }
+
+    /// <summary>
+    ///     Counts the number of grapheme clusters in a string without allocating intermediate collections.
+    /// </summary>
+    /// <param name="text">The string to count graphemes in.</param>
+    /// <returns>The number of grapheme clusters, or 0 if the string is null or empty.</returns>
+    public static int GetGraphemeCount (string text)
+    {
+        if (string.IsNullOrEmpty (text))
+        {
+            return 0;
+        }
+
+        TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (text);
+        var count = 0;
+
+        while (enumerator.MoveNext ())
+        {
+            count++;
+        }
+
+        return count;
+    }
 }

+ 12 - 4
Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs

@@ -211,15 +211,23 @@ public class LineCanvas : IDisposable
     {
         Dictionary<Point, Rune> map = new ();
 
+        List<IntersectionDefinition> intersectionsBufferList = [];
+
         // walk through each pixel of the bitmap
         for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++)
         {
             for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
             {
-                IntersectionDefinition [] intersects = _lines
-                    .Select (l => l.Intersects (x, y))
-                    .OfType<IntersectionDefinition> () // automatically filters nulls and casts
-                    .ToArray ();
+                intersectionsBufferList.Clear ();
+                foreach (var line in _lines)
+                {
+                    if (line.Intersects (x, y) is { } intersect)
+                    {
+                        intersectionsBufferList.Add (intersect);
+                    }
+                }
+                // Safe as long as the list is not modified while the span is in use.
+                ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan(intersectionsBufferList);
 
                 Rune? rune = GetRuneForIntersects (intersects);
 

+ 76 - 26
Terminal.Gui/Text/TextFormatter.cs

@@ -123,11 +123,31 @@ public class TextFormatter
             }
 
             string strings = linesFormatted [line];
-            string[] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
+            
+            // Use ArrayPool to avoid per-draw allocations
+            int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters
+            string [] graphemes = ArrayPool<string>.Shared.Rent (estimatedCount);
+            var graphemeCount = 0;
 
-            // When text is justified, we lost left or right, so we use the direction to align.
+            try
+            {
+                foreach (string grapheme in GraphemeHelper.GetGraphemes (strings))
+                {
+                    if (graphemeCount >= graphemes.Length)
+                    {
+                        // Need larger array (rare case for complex text)
+                        string [] larger = ArrayPool<string>.Shared.Rent (graphemes.Length * 2);
+                        Array.Copy (graphemes, larger, graphemeCount);
+                        ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
+                        graphemes = larger;
+                    }
+
+                    graphemes [graphemeCount++] = grapheme;
+                }
+
+                // When text is justified, we lost left or right, so we use the direction to align.
 
-            int x = 0, y = 0;
+                int x = 0, y = 0;
 
             // Horizontal Alignment
             if (Alignment is Alignment.End)
@@ -214,7 +234,7 @@ public class TextFormatter
             {
                 if (isVertical)
                 {
-                    y = screen.Bottom - graphemes.Length;
+                    y = screen.Bottom - graphemeCount;
                 }
                 else
                 {
@@ -250,7 +270,7 @@ public class TextFormatter
             {
                 if (isVertical)
                 {
-                    int s = (screen.Height - graphemes.Length) / 2;
+                    int s = (screen.Height - graphemeCount) / 2;
                     y = screen.Top + s;
                 }
                 else
@@ -292,17 +312,17 @@ public class TextFormatter
                         continue;
                     }
 
-                    if (!FillRemaining && idx > graphemes.Length - 1)
+                    if (!FillRemaining && idx > graphemeCount - 1)
                     {
                         break;
                     }
 
                     if ((!isVertical
                          && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
-                             || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))
+                             || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))
                         || (isVertical
                             && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
-                                || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))))
+                                || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))))
                     {
                         break;
                     }
@@ -317,7 +337,7 @@ public class TextFormatter
 
                 if (isVertical)
                 {
-                    if (idx >= 0 && idx < graphemes.Length)
+                    if (idx >= 0 && idx < graphemeCount)
                     {
                         text = graphemes [idx];
                     }
@@ -368,7 +388,7 @@ public class TextFormatter
                 {
                     driver?.Move (current, y);
 
-                    if (idx >= 0 && idx < graphemes.Length)
+                    if (idx >= 0 && idx < graphemeCount)
                     {
                         text = graphemes [idx];
                     }
@@ -428,15 +448,20 @@ public class TextFormatter
                     current += runeWidth;
                 }
 
-                int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemes.Length
+                int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemeCount
                                         ? graphemes [idx + 1].GetColumns ()
                                         : 0;
 
-                if (!isVertical && idx + 1 < graphemes.Length && current + nextRuneWidth > start + size)
+                if (!isVertical && idx + 1 < graphemeCount && current + nextRuneWidth > start + size)
                 {
                     break;
                 }
             }
+            }
+            finally
+            {
+                ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
+            }
         }
     }
 
@@ -931,10 +956,30 @@ public class TextFormatter
             }
 
             string strings = linesFormatted [line];
-            string [] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
+            
+            // Use ArrayPool to avoid per-line allocations
+            int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters
+            string [] graphemes = ArrayPool<string>.Shared.Rent (estimatedCount);
+            var graphemeCount = 0;
 
-            // When text is justified, we lost left or right, so we use the direction to align.
-            int x = 0, y = 0;
+            try
+            {
+                foreach (string grapheme in GraphemeHelper.GetGraphemes (strings))
+                {
+                    if (graphemeCount >= graphemes.Length)
+                    {
+                        // Need larger array (rare case for complex text)
+                        string [] larger = ArrayPool<string>.Shared.Rent (graphemes.Length * 2);
+                        Array.Copy (graphemes, larger, graphemeCount);
+                        ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
+                        graphemes = larger;
+                    }
+
+                    graphemes [graphemeCount++] = grapheme;
+                }
+
+                // When text is justified, we lost left or right, so we use the direction to align.
+                int x = 0, y = 0;
 
             switch (Alignment)
             {
@@ -1011,7 +1056,7 @@ public class TextFormatter
             {
                 // Vertical Alignment
                 case Alignment.End when isVertical:
-                    y = screen.Bottom - graphemes.Length;
+                    y = screen.Bottom - graphemeCount;
 
                     break;
                 case Alignment.End:
@@ -1041,7 +1086,7 @@ public class TextFormatter
                     }
                 case Alignment.Center when isVertical:
                     {
-                        int s = (screen.Height - graphemes.Length) / 2;
+                        int s = (screen.Height - graphemeCount) / 2;
                         y = screen.Top + s;
 
                         break;
@@ -1081,22 +1126,22 @@ public class TextFormatter
                     continue;
                 }
 
-                if (!FillRemaining && idx > graphemes.Length - 1)
+                if (!FillRemaining && idx > graphemeCount - 1)
                 {
                     break;
                 }
 
                 if ((!isVertical
                      && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
-                         || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))
+                         || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))
                     || (isVertical
                         && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
-                            || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))))
+                            || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))))
                 {
                     break;
                 }
 
-                string text = idx >= 0 && idx < graphemes.Length ? graphemes [idx] : " ";
+                string text = idx >= 0 && idx < graphemeCount ? graphemes [idx] : " ";
                 int runeWidth = GetStringWidth (text, TabWidth);
 
                 if (isVertical)
@@ -1116,20 +1161,25 @@ public class TextFormatter
 
                 current += isVertical && runeWidth > 0 ? 1 : runeWidth;
 
-                int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemes.Length
+                int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemeCount
                                         ? graphemes [idx + 1].GetColumns ()
                                         : 0;
 
-                if (!isVertical && idx + 1 < graphemes.Length && current + nextStringWidth > start + size)
+                if (!isVertical && idx + 1 < graphemeCount && current + nextStringWidth > start + size)
                 {
                     break;
                 }
             }
 
-            // Add the line's drawn region to the overall region
-            if (lineWidth > 0 && lineHeight > 0)
+                // Add the line's drawn region to the overall region
+                if (lineWidth > 0 && lineHeight > 0)
+                {
+                    drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+                }
+            }
+            finally
             {
-                drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+                ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
             }
         }