Skip to content

[perf-improver] perf: eliminate LINQ allocations in terminal progress render hot path#8799

Merged
Evangelink merged 2 commits into
mainfrom
perf-assist/terminal-progress-linq-removal-be8b0231ca8fb352
Jun 3, 2026
Merged

[perf-improver] perf: eliminate LINQ allocations in terminal progress render hot path#8799
Evangelink merged 2 commits into
mainfrom
perf-assist/terminal-progress-linq-removal-be8b0231ca8fb352

Conversation

@Evangelink
Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

The live-progress render loop runs every 500 ms while tests are executing. Two methods in this path were creating unnecessary LINQ iterator chains and iterator state machines per frame:

  1. TestNodeResultsState.GetRunningTasks — built a Select + OrderByDescending + ToList LINQ pipeline to sort running test tasks, then used yield return to expose them. Each call produced: a DictionaryEnumerator, a SelectIterator<>, an OrderedEnumerable<,>, a List<TestDetailState>, a second array from [.. Take(n)] (when too many items), and an IEnumerator state machine. Called once per assembly per frame.

  2. AnsiTerminalTestProgressFrame.GenerateLinesToRender — used progress.OfType<TestProgressState>() spread (OfTypeIterator + array copy) to filter nulls, and Enumerable.Range(0, n).OrderBy(...) (RangeIterator + OrderedEnumerable<,>) to sort indices by detail count. Called once per frame.

Approach

GetRunningTasks

  • Changed return type from IEnumerable<TestDetailState> to List<TestDetailState> — caller gets the list directly, no iterator state machine needed.
  • Build the list with a direct foreach over the ConcurrentDictionary instead of Select(d => d.Value).
  • Sort with List.Sort and a static lambda (no closure allocation) instead of LINQ OrderByDescending.
  • Truncate in-place via List.RemoveRange instead of [.. Take(n)] (avoids second array allocation).
  • Remove yield return — items returned from the list directly.

GenerateLinesToRender

  • Replace [.. progress.OfType<TestProgressState>()] with two null-counting passes over the array — avoids OfTypeIterator and collection-spread copy.
  • Replace Enumerable.Range(0, n).OrderBy(i => ...) with a pre-filled int[] sorted via Array.Sort — avoids RangeIterator and OrderedEnumerable allocations.
  • Update detailItems array type from IEnumerable<TestDetailState>[] to List<TestDetailState>[] to match the new return type.

Performance Evidence

Site Before (per frame) After
GetRunningTasks (per assembly) DictionaryEnumerator + SelectIterator + OrderedEnumerable + List + array (Take) + state machine = ~5–6 objects List only = 1 object
GenerateLinesToRender — null filter OfTypeIterator + array spread = 2 objects 0 (in-place loops)
GenerateLinesToRender — index sort RangeIterator + OrderedEnumerable = 2 objects int[] + closure = 2 objects (same count but avoids LINQ machinery overhead)

For a test run with 10 assemblies, this eliminates roughly 4–5 allocations per assembly per frame = ~40–50 fewer heap allocations per 500 ms tick in the terminal render loop.

Methodology: code inspection + diff review. The render loop is easily verified by running tests and observing a steady terminal.

Trade-offs

  • Slightly more verbose code in GenerateLinesToRender (two loops to count + fill the array vs. a one-liner spread).
  • GetRunningTasks return type change is internal — no public API impact.
  • The Array.Sort comparison lambda still captures progressItems (a local), but this is one allocation per frame total rather than per-item LINQ machinery.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1003 passed, 0 failed, 3 skipped
  • Build: 0 warnings, 0 errors
  • Note: Full repo build has a pre-existing infrastructure failure (XLF files out of sync with resx — unrelated to these changes).

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 4.4M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

- GetRunningTasks: replace Select+OrderByDescending+ToList LINQ chain
  with direct List<T> build + List.Sort using a static comparer, and
  change return type from IEnumerable<TestDetailState> to List<TestDetailState>
  to avoid iterator state-machine allocation from 'yield return'.
  Also replace 'sortedDetails = [.. sortedDetails.Take(n)]' with
  in-place List.RemoveRange to skip the second list/array allocation.

- GenerateLinesToRender: replace 'progress.OfType<TestProgressState>()'
  spread with a null-filtering manual loop (avoids LINQ OfTypeIterator +
  collection-spread array copy). Replace 'Enumerable.Range(0,n).OrderBy(...)'
  with a pre-filled int[] sorted via Array.Sort (avoids RangeIterator and
  OrderedEnumerable allocations).

Both sites are in the 500 ms live-progress render loop, so the
allocations are pressure that appears on every frame during a test run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 3, 2026 15:22
@Evangelink Evangelink added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 3, 2026
@Evangelink Evangelink marked this pull request as ready for review June 3, 2026 15:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@Evangelink
Copy link
Copy Markdown
Member Author

🔍 Build Failure Analysis

Summary — The build fails with 3 identical errors (one per target framework) because Resources/xlf/PlatformResources.cs.xlf (the Czech localization file) is out-of-date with Resources/PlatformResources.resx in Microsoft.Testing.Platform. This is not caused by this PR's code changes, which only touch AnsiTerminalTestProgressFrame.cs and TestNodeResultsState.cs.


Root cause: Localization XLF out-of-sync with RESX

The Microsoft.DotNet.XliffTasks targets enforce that every .xlf file's <source> strings exactly match the values in the corresponding .resx. The Czech XLF (PlatformResources.cs.xlf) appears to have drifted from PlatformResources.resx — likely because the OneLocBuild automation check-in (PR #8798, commit 726cc8a) updated the resx and xlf together, but a subtle mismatch remains (e.g., a changed source string value or whitespace/encoding difference in a recent addition).

The same error fires for all three TFMs (net8.0, netstandard2.0, net9.0) because the check runs once per build of Microsoft.Testing.Platform.csproj.

Affected project / errors

Code Project Message
(no code) Microsoft.Testing.Platform (net8.0) 'Resources/xlf/PlatformResources.cs.xlf' is out-of-date with 'Resources/PlatformResources.resx'
(no code) Microsoft.Testing.Platform (netstandard2.0) same
(no code) Microsoft.Testing.Platform (net9.0) same

Proposed fix

Regenerate the XLF files locally and commit the result. From the repository root:

dotnet msbuild src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj /t:UpdateXlf

Then commit the updated src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf (and any other updated xlf files). Do not hand-edit the xlf files directly.

Note: Since this PR doesn't modify any resource strings, it's also possible the mismatch is a pre-existing condition on the base branch. If UpdateXlf produces no diff, the fix is a simple re-run of the CI.


Build overview
  • Project: src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj
  • Target frameworks: net8.0, netstandard2.0, net9.0
  • Target that failed: CheckForOutdatedXlf (via Microsoft.DotNet.XliffTasks.targets:84)
  • Build time: 1m 28s
  • Exit code: 1
  • Errors: 3 | Warnings: 0
All MSBuild errors (3)
Code Project TFM File Message
Microsoft.Testing.Platform net8.0 XliffTasks.targets:84 'Resources/xlf/PlatformResources.cs.xlf' is out-of-date with 'Resources/PlatformResources.resx'
Microsoft.Testing.Platform netstandard2.0 XliffTasks.targets:84 same
Microsoft.Testing.Platform net9.0 XliffTasks.targets:84 same

🤖 Generated by the Build Failure Analysis workflow · commit a435e21

Generated by Build Failure Analysis for issue #8799 · sonnet46 2.7M ·

@Evangelink Evangelink enabled auto-merge (squash) June 3, 2026 20:33
@Evangelink Evangelink disabled auto-merge June 3, 2026 21:50
@Evangelink Evangelink merged commit d2ce1c5 into main Jun 3, 2026
27 checks passed
@Evangelink Evangelink deleted the perf-assist/terminal-progress-linq-removal-be8b0231ca8fb352 branch June 3, 2026 21:50
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