This document provides a comprehensive analysis of intermediate heap allocations in Terminal.Gui, focusing on the TextFormatter and LineCanvas classes as reported in the issue.
The allocation issues identified are significant performance concerns that affect:
Terminal.Gui/Text/TextFormatter.cs)Location: Line 126 (in Draw method)
string[] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
Location: Line 934 (in GetDrawRegion method)
string [] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
Additional Allocation Points:
List<string> graphemes = GraphemeHelper.GetGraphemes (text).ToList (); in SplitNewLinestring [] graphemes = GraphemeHelper.GetGraphemes (text).ToArray (); in ClipOrPadList<string> graphemes = GraphemeHelper.GetGraphemes (StripCRLF (text)).ToList (); in WordWrapTextList<string> graphemes = GraphemeHelper.GetGraphemes (text).ToList (); in ClipAndJustifystring [] graphemes = GraphemeHelper.GetGraphemes (text).ToArray (); in GetSumMaxCharWidthstring [] graphemes = GraphemeHelper.GetGraphemes (lines [lineIdx]).ToArray (); in GetMaxColsForWidthTotal Count: 9 distinct allocation points in TextFormatter alone
The Draw method is called:
With a typical progress bar updating at 10-30 times per second, and potentially multiple text elements on screen, this can result in hundreds to thousands of allocations per second.
Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs)Location: Lines 219-222 (in GetMap(Rectangle inArea) method)
IntersectionDefinition [] intersects = _lines
.Select (l => l.Intersects (x, y))
.OfType<IntersectionDefinition> ()
.ToArray ();
The GetCellMap() method (line 162) was already optimized:
List<IntersectionDefinition> intersectionsBufferList = [];
// ... reuses list with Clear() ...
ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan(intersectionsBufferList);
This is the correct pattern - reusing a buffer and using spans to avoid allocations.
Terminal.Gui/Drawing/Cell.cs)Location: Line 30
if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1)
The core issue is that GraphemeHelper.GetGraphemes() returns an IEnumerable<string>, which is then immediately materialized to arrays or lists. This pattern appears throughout the codebase.
The fundamental issue is the design pattern:
GetGraphemes() returns IEnumerable<string> (lazy enumeration).ToArray() or .ToList() to materialize itThe GetMap(Rectangle inArea) method has a particularly problematic nested loop structure:
.ToArray() allocationThis is a classic O(n²) allocation problem where the allocation count grows quadratically with area size.
Assuming:
Draw() which allocates an arrayResult: 40 array allocations per second, just for the progress bar
Add a clock display updating once per second, status messages, etc., and we easily reach hundreds of allocations per second in a moderately complex UI.
A typical dialog window:
Result: 260 allocations per border redraw
If the dialog is redrawn 10 times per second (e.g., with animated content inside), that's 2,600 allocations per second just for one border.
The issue mentions that allocations "increased drastically" on the v2_develop branch, particularly from LineCanvas. This is consistent with the findings:
The allocations fall into several categories:
string[] from .ToArray()List<string> from .ToList()With Gen0 collections potentially happening multiple times per second due to these allocations:
ReadOnlySpan<string> variant where possibleTo quantify the impact:
| Scenario | Severity | Reason |
|---|---|---|
| Static UI (no updates) | LOW | Allocations only on initial render |
| Progress bars / animations | CRITICAL | Continuous allocations 10-60 Hz |
| Text-heavy UI | HIGH | Many text elements = many allocations |
| Border-heavy UI | HIGH | Per-pixel allocations in LineCanvas |
| Simple forms | MEDIUM | Periodic allocations on interaction |
The heap allocation issue is real and significant, particularly for:
The good news is that the patterns for fixing this are well-established:
The LineCanvas.GetMap() issue is particularly straightforward to fix by applying the same pattern already used in GetCellMap().
Priority order:
Based on this analysis, the recommendation is to:
Analysis Date: 2025-12-03
Analyzed By: GitHub Copilot
Codebase: Terminal.Gui (v2_develop branch)