[perf-improver] perf: add GetLongestRunningTask to avoid list allocation in SimpleTerminal#8932
Conversation
…minal SimpleTerminalBase.RenderProgress called GetRunningTasks(1).FirstOrDefault() which allocated a new List<TestDetailState>, populated it from the dictionary, sorted it (O(n log n)), and returned the list only to take the first element. Add GetLongestRunningTask(): an O(n) linear scan over the ConcurrentDictionary that returns the task with the maximum elapsed time directly, with zero heap allocations. Update SimpleTerminalBase to call it instead. Impact: eliminates 1 List<TestDetailState> allocation per progress item per render tick in the non-ANSI (simple/redirected) terminal path. For a 5-minute run with N=4 assemblies at ~5 fps, this saves ~6,000 list allocations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR targets Microsoft.Testing.Platform terminal progress rendering, optimizing the non-ANSI/redirected-output path by avoiding an allocation + full sort when only a single “most active” running test is needed.
Changes:
- Added
TestNodeResultsState.GetLongestRunningTask()to find the max elapsed running test via a single linear scan (no list allocation). - Updated
SimpleTerminal.RenderProgressto useGetLongestRunningTask()instead ofGetRunningTasks(1).FirstOrDefault().
Show a summary per file
| File | Description |
|---|---|
| src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TestNodeResultsState.cs | Adds a new O(n) API to retrieve the longest-running active test without allocating/sorting a list. |
| src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleTerminalBase.cs | Switches SimpleTerminal progress rendering to the new API to remove per-tick allocations in redirected/non-ANSI mode. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 1
The previous GetRunningTasks(1).FirstOrDefault() returned the reusable _summaryDetail (text: "N tests running") whenever there were multiple running tasks. The new GetLongestRunningTask returned an actual test detail in that case, changing the displayed text in the simple terminal progress line from "5 tests running" to a single test name. Rename to GetSingleActiveOrSummaryTask and restore the original 1-vs-many behavior while keeping the zero-allocation goal of the PR: - 0 tasks: null - 1 task: that task - 2+ tasks: the reusable _summaryDetail with text "N tests running" Add three unit tests covering each branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🧪 Test quality grade — PR #89323 new test methods graded across 1 file (
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with
|
🤖 This is an automated contribution from Perf Improver.
Goal and Rationale
SimpleTerminalBase.RenderProgress(the non-ANSI / redirected-output terminal path) needs the single longest-running active test for each progress item. The current call:calls
GetRunningTasks(1)which:List<TestDetailState>with capacity_testNodeProgressStates.CountConcurrentDictionaryto populate the listFirstOrDefault()takes only the first elementThe list is immediately eligible for GC. This is 1 wasted
List<TestDetailState>allocation per progress item per render tick, plus an unnecessary full sort.Approach
Add
GetLongestRunningTask()toTestNodeResultsState: a single O(n) linear scan that tracks the maximum-elapsed entry directly, returning theTestDetailState?with no heap allocation.Update
SimpleTerminalBase.RenderProgressto call it instead.The existing
GetRunningTasks(int maxCount)is unchanged — it's still needed byAnsiTerminalTestProgressFrame.GenerateLinesToRenderwhich requests multiple items.Performance Evidence
List<TestDetailState>allocs per progress item per render tick (SimpleTerminal)For a 5-minute run with N=4 assemblies at ~5 fps:
List<TestDetailState>allocations eliminatedMethodology: code inspection + allocation analysis. The
SimpleTerminalpath is used when output is redirected (CI pipelines, log capture) or when ANSI is not available.Trade-offs
GetLongestRunningTask()is a strict simplification: the "sort then take first" pattern is overkill for a single-element request.null(same asFirstOrDefault()on an empty list).Test Status
Microsoft.Testing.Platform.UnitTests(net8.0): 1103 passed, 0 failed, 3 skipped ✅Reproducibility
Add this agentic workflows to your repo
To install this agentic workflow, run