Skip to content

Commit a1d79df

Browse files
EvangelinkCopilot
andauthored
perf: eliminate per-tick allocations in GenerateLinesToRender via cached buffers
Cache the four working buffers (List<object>, TestProgressState[], int[], List<TestDetailState>?[]) and the sort comparer as instance fields on AnsiTerminalTestProgressFrame so they are allocated once per frame object rather than on every render tick. - _linesToRenderBuffer: reused List<object> (was: new list each tick) - _progressItemsBuffer: grown-only array (was: new array each tick) - _sortedIndicesBuffer: grown-only array (was: new array each tick) - _detailItemsBuffer: grown-only array (was: new array each tick) - _progressCountComparer: cached IComparer<int> instance used with Array.Sort(array, offset, count, comparer) so no closure is captured (was: Array.Sort(array, Comparison<T> lambda) → 1 closure/tick) At ~2 fps with N assemblies this removes ~5N allocations per second. For a typical run with 4 assemblies over 5 minutes, this is roughly ~12 000 allocations eliminated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 741048f commit a1d79df

1 file changed

Lines changed: 49 additions & 18 deletions

File tree

src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminalTestProgressFrame.cs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ private static string[] CreateMoveCursorBackwardCache()
3030
return cache;
3131
}
3232

33+
// Reusable working buffers for GenerateLinesToRender, cached across render ticks on the same frame
34+
// object to eliminate 4+ per-tick heap allocations (3 arrays + 1 List, plus the sort comparer).
35+
private readonly List<object> _linesToRenderBuffer = [];
36+
private readonly ProgressCountComparer _progressCountComparer = new();
37+
private TestProgressState[] _progressItemsBuffer = [];
38+
private int[] _sortedIndicesBuffer = [];
39+
private List<TestDetailState>?[] _detailItemsBuffer = [];
40+
3341
public int Width { get; private set; }
3442

3543
public int Height { get; private set; }
@@ -50,6 +58,7 @@ internal void Reset(int width, int height)
5058
Width = Math.Min(width, MaxColumn);
5159
Height = height;
5260
RenderedLines?.Clear();
61+
_linesToRenderBuffer.Clear();
5362
}
5463

5564
public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgressItem currentLine, AnsiTerminal terminal)
@@ -360,8 +369,6 @@ public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressStat
360369

361370
private List<object> GenerateLinesToRender(TestProgressState?[] progress)
362371
{
363-
var linesToRender = new List<object>(progress.Length);
364-
365372
// Note: We want to render the list of active tests, but this can easily fill up the full screen.
366373
// As such, we should balance the number of active tests shown per project.
367374
// We do this by distributing the remaining lines for each projects.
@@ -376,47 +383,71 @@ private List<object> GenerateLinesToRender(TestProgressState?[] progress)
376383
}
377384
}
378385

379-
var progressItems = new TestProgressState[itemCount];
386+
// Grow cached working buffers when more capacity is needed; never shrink to avoid churn.
387+
if (_progressItemsBuffer.Length < itemCount)
388+
{
389+
_progressItemsBuffer = new TestProgressState[itemCount];
390+
_sortedIndicesBuffer = new int[itemCount];
391+
_detailItemsBuffer = new List<TestDetailState>?[itemCount];
392+
}
393+
380394
int idx = 0;
381395
for (int j = 0; j < progress.Length; j++)
382396
{
383397
if (progress[j] is not null)
384398
{
385-
progressItems[idx++] = progress[j]!;
399+
_progressItemsBuffer[idx++] = progress[j]!;
386400
}
387401
}
388402

389-
int linesToDistribute = (int)(Height * 0.7) - 1 - progressItems.Length;
390-
var detailItems = new List<TestDetailState>[progressItems.Length];
403+
int linesToDistribute = (int)(Height * 0.7) - 1 - itemCount;
391404

392405
// Sort indices by detail count ascending to distribute lines fairly,
393406
// without LINQ Enumerable.Range + OrderBy allocation.
394-
int[] sortedItemsIndices = new int[progressItems.Length];
395-
for (int j = 0; j < progressItems.Length; j++)
407+
for (int j = 0; j < itemCount; j++)
396408
{
397-
sortedItemsIndices[j] = j;
409+
_sortedIndicesBuffer[j] = j;
398410
}
399411

400-
Array.Sort(sortedItemsIndices, (a, b) => (progressItems[a].TestNodeResultsState?.Count ?? 0).CompareTo(progressItems[b].TestNodeResultsState?.Count ?? 0));
412+
// _progressCountComparer is a cached instance — no per-tick allocation.
413+
_progressCountComparer.Buffer = _progressItemsBuffer;
414+
Array.Sort(_sortedIndicesBuffer, 0, itemCount, _progressCountComparer);
401415

402-
foreach (int sortedItemIndex in sortedItemsIndices)
416+
int linesPerItem = itemCount > 0 ? linesToDistribute / itemCount : 0;
417+
for (int j = 0; j < itemCount; j++)
403418
{
404-
detailItems[sortedItemIndex] = progressItems[sortedItemIndex].TestNodeResultsState?.GetRunningTasks(
405-
linesToDistribute / progressItems.Length)
406-
?? [];
419+
int sortedItemIndex = _sortedIndicesBuffer[j];
420+
_detailItemsBuffer[sortedItemIndex] = _progressItemsBuffer[sortedItemIndex].TestNodeResultsState?.GetRunningTasks(linesPerItem) ?? [];
407421
}
408422

409-
for (int progressI = 0; progressI < progressItems.Length; progressI++)
423+
for (int progressI = 0; progressI < itemCount; progressI++)
410424
{
411-
linesToRender.Add(progressItems[progressI]);
412-
linesToRender.AddRange(detailItems[progressI]);
425+
_linesToRenderBuffer.Add(_progressItemsBuffer[progressI]);
426+
if (_detailItemsBuffer[progressI] is { } details)
427+
{
428+
_linesToRenderBuffer.AddRange(details);
429+
_detailItemsBuffer[progressI] = null; // release to avoid holding stale GC roots
430+
}
413431
}
414432

415-
return linesToRender;
433+
return _linesToRenderBuffer;
416434
}
417435

418436
public void Clear() => RenderedLines?.Clear();
419437

438+
/// <summary>
439+
/// Reusable comparer for sorting progress-item indices by running-task count.
440+
/// Cached as a field to avoid a new allocations on every render tick.
441+
/// </summary>
442+
private sealed class ProgressCountComparer : IComparer<int>
443+
{
444+
internal TestProgressState[] Buffer { get; set; } = [];
445+
446+
public int Compare(int a, int b)
447+
=> (Buffer[a].TestNodeResultsState?.Count ?? 0)
448+
.CompareTo(Buffer[b].TestNodeResultsState?.Count ?? 0);
449+
}
450+
420451
internal sealed class RenderedProgressItem
421452
{
422453
public RenderedProgressItem(long id, long version)

0 commit comments

Comments
 (0)