# Heap Allocation Analysis for Terminal.Gui ## Executive Summary 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. ## Severity Assessment: **HIGH IMPACT** The allocation issues identified are significant performance concerns that affect: - Every frame redraw in UI scenarios - Any time-based updates (progress bars, timers, clocks) - Text rendering operations - Border and line drawing operations ## Key Findings ### 1. TextFormatter Class (`Terminal.Gui/Text/TextFormatter.cs`) #### Critical Allocation Hotspots **Location: Line 126 (in `Draw` method)** ```csharp string[] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray (); ``` - **Frequency**: Every time Draw is called (potentially 60+ times per second during animations) - **Impact**: Allocates a new string array for every line being drawn - **Called from**: View.Drawing.cs, Border.cs, TextField.cs, and other UI components **Location: Line 934 (in `GetDrawRegion` method)** ```csharp string [] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray (); ``` - **Frequency**: Every time region calculation is needed - **Impact**: Similar allocation for grapheme arrays **Additional Allocation Points:** - Line 1336: `List graphemes = GraphemeHelper.GetGraphemes (text).ToList ();` in `SplitNewLine` - Line 1407: `string [] graphemes = GraphemeHelper.GetGraphemes (text).ToArray ();` in `ClipOrPad` - Line 1460: `List graphemes = GraphemeHelper.GetGraphemes (StripCRLF (text)).ToList ();` in `WordWrapText` - Line 1726: `List graphemes = GraphemeHelper.GetGraphemes (text).ToList ();` in `ClipAndJustify` - Line 2191: `string [] graphemes = GraphemeHelper.GetGraphemes (text).ToArray ();` in `GetSumMaxCharWidth` - Line 2300: `string [] graphemes = GraphemeHelper.GetGraphemes (lines [lineIdx]).ToArray ();` in `GetMaxColsForWidth` **Total Count**: 9 distinct allocation points in TextFormatter alone #### Why This Matters The `Draw` method is called: 1. On every frame update for animated content 2. When any view needs to redraw its text 3. During progress bar updates (the example mentioned in the issue) 4. For real-time displays (clocks, status bars) 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**. ### 2. LineCanvas Class (`Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs`) #### Critical Allocation Hotspot **Location: Lines 219-222 (in `GetMap(Rectangle inArea)` method)** ```csharp IntersectionDefinition [] intersects = _lines .Select (l => l.Intersects (x, y)) .OfType () .ToArray (); ``` - **Frequency**: **Once per pixel in the area** (nested loop over x and y) - **Impact**: EXTREMELY HIGH - allocates array for every single pixel being evaluated - **Example**: A 80x24 terminal window border = 1,920 allocations per redraw - **Example**: A 120x40 dialog with borders = 4,800 allocations per redraw #### Good News The `GetCellMap()` method (line 162) was already optimized: ```csharp List intersectionsBufferList = []; // ... reuses list with Clear() ... ReadOnlySpan intersects = CollectionsMarshal.AsSpan(intersectionsBufferList); ``` This is the **correct pattern** - reusing a buffer and using spans to avoid allocations. ### 3. Cell Class (`Terminal.Gui/Drawing/Cell.cs`) **Location: Line 30** ```csharp if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1) ``` - **Frequency**: Every time Grapheme property is set - **Impact**: Moderate - validation code ### 4. GraphemeHelper Pattern The core issue is that `GraphemeHelper.GetGraphemes()` returns an `IEnumerable`, which is then immediately materialized to arrays or lists. This pattern appears throughout the codebase. ## Root Cause Analysis ### TextFormatter Allocations The fundamental issue is the design pattern: 1. `GetGraphemes()` returns `IEnumerable` (lazy enumeration) 2. Code immediately calls `.ToArray()` or `.ToList()` to materialize it 3. This happens on every draw call, creating garbage ### LineCanvas Allocations The `GetMap(Rectangle inArea)` method has a particularly problematic nested loop structure: - Outer loop: Y coordinates - Inner loop: X coordinates - **Inside inner loop**: LINQ query with `.ToArray()` allocation This is a classic O(n²) allocation problem where the allocation count grows quadratically with area size. ## Performance Impact Estimation ### TextFormatter in Progress Demo Assuming: - Progress bar updates 20 times/second - Each update redraws the bar (1 line) and percentage text (1 line) - Each line calls `Draw()` which allocates an array **Result**: 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. ### LineCanvas in Border Drawing A typical dialog window: - 100x30 character area - Border needs to evaluate 2×(100+30) = 260 pixels for the border - Each pixel: 1 array allocation **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. ## Comparison to v2_develop Branch The issue mentions that allocations "increased drastically" on the v2_develop branch, particularly from LineCanvas. This is consistent with the findings: 1. **GetMap(Rectangle)** method allocates per-pixel 2. If border drawing or line canvas usage increased in v2, this would multiply the allocation impact ## Memory Allocation Types The allocations fall into several categories: 1. **String Arrays**: `string[]` from `.ToArray()` 2. **String Lists**: `List` from `.ToList()` 3. **LINQ Enumerable Objects**: Intermediate enumerables in LINQ chains 4. **Dictionary/Collection Allocations**: Less critical but still present ## GC Impact With Gen0 collections potentially happening multiple times per second due to these allocations: 1. **Pause times**: GC pauses affect UI responsiveness 2. **CPU overhead**: GC work consumes CPU that could render content 3. **Memory pressure**: Constant allocation/collection cycle 4. **Cache pollution**: Reduces cache effectiveness ## Recommended Solutions (High-Level) ### For TextFormatter 1. **Use ArrayPool**: Rent arrays from pool instead of allocating 2. **Use Span**: Work with spans instead of materializing arrays 3. **Cache grapheme arrays**: If text doesn't change, cache the split 4. **Lazy evaluation**: Only materialize when truly needed ### For LineCanvas 1. **Apply GetCellMap pattern to GetMap**: Reuse buffer list, use spans 2. **Pool IntersectionDefinition arrays**: Similar to GetCellMap optimization 3. **Consider pixel-level caching**: Cache intersection results for static lines ### For GraphemeHelper 1. **Add GetGraphemesAsSpan**: Return `ReadOnlySpan` variant where possible 2. **Add TryGetGraphemeCount**: Count without allocation for validation 3. **Consider string pooling**: Pool common grapheme strings ## Measurement Recommendations To quantify the impact: 1. **Add BenchmarkDotNet tests**: Measure allocations for typical scenarios 2. **Profile with dotnet-trace**: Capture allocation profiles during Progress demo 3. **Memory profiler**: Use Visual Studio or JetBrains dotMemory ## Severity by Scenario | 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 | ## Conclusion The heap allocation issue is **real and significant**, particularly for: 1. **Any time-based updates** (progress bars, clocks, animations) 2. **Border/line-heavy UIs** due to LineCanvas per-pixel allocations 3. **Text-heavy interfaces** with frequent redraws The good news is that the patterns for fixing this are well-established: - ArrayPool usage - Span adoption - Buffer reuse (as demonstrated in GetCellMap) The LineCanvas.GetMap() issue is particularly straightforward to fix by applying the same pattern already used in GetCellMap(). ## Files Requiring Changes Priority order: 1. **Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs** (GetMap method) - CRITICAL 2. **Terminal.Gui/Text/TextFormatter.cs** (Draw method) - CRITICAL 3. **Terminal.Gui/Text/TextFormatter.cs** (other allocation points) - HIGH 4. **Terminal.Gui/Drawing/Cell.cs** (validation) - MEDIUM 5. **Terminal.Gui/Drawing/GraphemeHelper.cs** (add span-based APIs) - MEDIUM ## Next Steps Based on this analysis, the recommendation is to: 1. ✅ **Acknowledge the issue is real and significant** 2. Fix the most critical issue: LineCanvas.GetMap() per-pixel allocations 3. Fix TextFormatter.Draw() allocations 4. Add benchmarks to measure improvement 5. Consider broader architectural changes for grapheme handling --- **Analysis Date**: 2025-12-03 **Analyzed By**: GitHub Copilot **Codebase**: Terminal.Gui (v2_develop branch)