Skip to content

[perf-improver] perf: add GetLongestRunningTask to avoid list allocation in SimpleTerminal#8932

Draft
Evangelink wants to merge 1 commit into
mainfrom
perf-assist/longest-running-task-linear-scan-de0e1495961cdcfb
Draft

[perf-improver] perf: add GetLongestRunningTask to avoid list allocation in SimpleTerminal#8932
Evangelink wants to merge 1 commit into
mainfrom
perf-assist/longest-running-task-linear-scan-de0e1495961cdcfb

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 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:

TestDetailState? activeTest = p.TestNodeResultsState?.GetRunningTasks(1).FirstOrDefault();

calls GetRunningTasks(1) which:

  1. Allocates a new List<TestDetailState> with capacity _testNodeProgressStates.Count
  2. Iterates the entire ConcurrentDictionary to populate the list
  3. Sorts the list descending by elapsed time (O(n log n))
  4. Returns the list — then FirstOrDefault() takes only the first element

The 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() to TestNodeResultsState: a single O(n) linear scan that tracks the maximum-elapsed entry directly, returning the TestDetailState? with no heap allocation.

Update SimpleTerminalBase.RenderProgress to call it instead.

The existing GetRunningTasks(int maxCount) is unchanged — it's still needed by AnsiTerminalTestProgressFrame.GenerateLinesToRender which requests multiple items.

Performance Evidence

Metric Before After
List<TestDetailState> allocs per progress item per render tick (SimpleTerminal) 1 0
Algorithm for finding the max O(n log n) sort + take first O(n) linear scan

For a 5-minute run with N=4 assemblies at ~5 fps:

  • ~6,000 List<TestDetailState> allocations eliminated
  • Sort overhead replaced by a single pass

Methodology: code inspection + allocation analysis. The SimpleTerminal path is used when output is redirected (CI pipelines, log capture) or when ANSI is not available.

Trade-offs

  • None. GetLongestRunningTask() is a strict simplification: the "sort then take first" pattern is overkill for a single-element request.
  • The new method's semantics match the existing call exactly: when the dictionary is empty it returns null (same as FirstOrDefault() on an empty list).

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1103 passed, 0 failed, 3 skipped
  • Build (all TFMs): 0 warnings, 0 errors

Reproducibility

./build.sh -build -c Debug
artifacts/bin/Microsoft.Testing.Platform.UnitTests/Debug/net8.0/Microsoft.Testing.Platform.UnitTests

Generated by Perf Improver

Generated by Perf Improver · sonnet46 4M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/perf-improver.md@main

…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>
Copilot AI review requested due to automatic review settings June 8, 2026 14:59
@Evangelink Evangelink added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.RenderProgress to use GetLongestRunningTask() instead of GetRunningTasks(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

}

TestDetailState? activeTest = p.TestNodeResultsState?.GetRunningTasks(1).FirstOrDefault();
TestDetailState? activeTest = p.TestNodeResultsState?.GetLongestRunningTask();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants