Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all \x3Cpre>\x3Ccode> blocks (function() { function addCopyButtons() { document.querySelectorAll('pre code').forEach(function(codeBlock) { if (codeBlock.parentElement.hasAttribute('data-copy-added')) return; codeBlock.parentElement.setAttribute('data-copy-added', 'true'); var btn = document.createElement('button'); btn.textContent = 'Copy'; btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;'; btn.onmouseover = function() { this.style.opacity = '1'; }; btn.onmouseout = function() { this.style.opacity = '0.7'; }; btn.onclick = function() { navigator.clipboard.writeText(codeBlock.textContent).then(function() { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); }); }; codeBlock.parentElement.style.position = 'relative'; codeBlock.parentElement.appendChild(btn); }); } addCopyButtons(); // Re-run on dynamic content var observer = new MutationObserver(addCopyButtons); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + ' [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters by Evangelink · Pull Request #8823 · microsoft/testfx · GitHub
Skip to content

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters - #8823

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7
Jun 5, 2026
Merged

[perf-improver] perf: single-pass message iteration in VSTestBridge ObjectModelConverters#8823
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7

Conversation

@Evangelink

Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

When converting a VSTest TestResult to a TestNode in the VSTestBridge adapter, testResult.Messages was iterated twice when TRX reporting was enabled:

  1. A LINQ Select pass to build a TrxMessage[] for TrxMessagesProperty — creates a SelectIterator<> state machine and an intermediate array via [.. ...] collection spread
  2. A separate foreach to collect standardErrorMessages / standardOutputMessages for StandardErrorProperty / StandardOutputProperty

This also removes a dead addVSTestProviderProperties variable that was declared but its value was never read (the call-site was removed at some earlier point).

Approach

Replace the two separate iterations with a single foreach that builds all three collections in one pass:

  • When isTrxEnabled, transform each message to a TrxMessage and collect it in a pre-allocated List<TrxMessage>
  • Simultaneously collect standard-error and standard-output strings (as before)
  • After the loop, use the collected List<TrxMessage> to create TrxMessagesProperty

Performance Evidence

PathBeforeAfter
testResult.Messages iterations (TRX enabled)2 (LINQ Select + foreach)1 (single foreach)
Allocations per call (TRX enabled)SelectIterator<> + intermediate TrxMessage[] + dead bool + List<string>? × 2List<TrxMessage> (pre-allocated) + List<string>? × 2
Allocations per call (TRX disabled)dead bool + List<string>? × 2List<string>? × 2

For a typical CI run with 1 000 tests and TRX enabled, this eliminates ≥1 000 SelectIterator state machines and ≥1 000 intermediate TrxMessage[] allocations.

Methodology: code inspection + diff review. ToTestNode(TestResult) is called once per test result in the VSTestBridge execution path.

Trade-offs

  • Slightly more verbose loop body when TRX is enabled (explicit TrxMessage trxMsg = switch { ... }), but the logic is clearer than splitting across two separate iterations.
  • No change to observable behaviour: the same properties are added in the same order.

Test Status

  • Microsoft.Testing.Platform.UnitTests (net8.0): 1079 passed, 0 failed, 3 skipped
  • Microsoft.Testing.Extensions.UnitTests (net8.0): 392 passed, 0 failed, 7 skipped
  • Build: 0 warnings, 0 errors

Reproducibility

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

Generated by Perf Improver

Generated by Perf Improver · sonnet46 10.8M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

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

…ters
When converting a VSTest TestResult to a TestNode, testResult.Messages was
iterated twice when TRX reporting was enabled:
1. A LINQ Select pass to build TrxMessage[] (creates SelectIterator state
machine + intermediate array via collection spread)
2. A separate foreach to collect standard-error/standard-output strings
Replace with a single foreach that collects all three outputs in one pass.
Also removes a dead addVSTestProviderProperties variable whose value was
never read after the usage was previously removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 4, 2026 14:42
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 4, 2026

CopilotAI 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 optimizes the VSTestBridge adapter’s conversion from VSTest TestResult to MTP TestNode by consolidating testResult.Messages processing into a single pass, reducing iterator/collection allocations when TRX reporting is enabled.

Changes:

  • Replaces the prior Select(...)+[..] TRX message projection plus a separate foreach with a single foreach that collects TRX messages, stdout, and stderr together.
  • Removes an unused addVSTestProviderProperties local that was computed but never read.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.csConsolidates message processing into one loop and removes a dead local to reduce allocations in the VSTestBridge conversion path.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 4, 2026 16:40
@Evangelink
Amaury Levé (Evangelink) enabled auto-merge (squash) June 4, 2026 16:41
Pre-allocate the trxMessages list with the exact message count to avoid List<T> growth reallocations, and switch the loop guard from isTrxEnabled to a direct null check on trxMessages so the null-forgiveness operator is no longer needed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Workflow run: https://github.com/microsoft/testfx/actions/runs/26975085514

❌ Root Cause: IDE0028 — Collection initialization can be simplified

The build fails with 3 errors (one per target framework: netstandard2.0, net8.0, net9.0), all pointing to the same single location:

src/Platform/Microsoft.Testing.Extensions.VSTestBridge/ObjectModel/ObjectModelConverters.cs(145,56): error IDE0028

The newly introduced line:

List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;

triggers IDE0028 ("Collection initialization can be simplified") because .editorconfig sets dotnet_style_prefer_collection_expression = true and the type List<TrxMessage> is redundant — it can be inferred from the left-hand side. In CI the project builds with /p:TreatWarningsAsErrors=true, promoting this warning to an error.

✅ Fix

Use a target-typed new expression to drop the redundant type annotation while preserving the capacity hint:

// Before (triggers IDE0028)List<TrxMessage>?trxMessages=isTrxEnabled?newList<TrxMessage>(testResult.Messages.Count):null;// AfterList<TrxMessage>?trxMessages=isTrxEnabled?new(testResult.Messages.Count):null;

This is the minimal one-character change that satisfies the analyzer, keeps the pre-sizing optimisation intact (important on netstandard2.0 where EnsureCapacity is unavailable), and is idiomatic C#.

An inline suggestion has been posted directly on the offending line below.

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M ·

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Generated by Build Failure Analysis for issue #8823 · sonnet46 2.6M

The new List<TrxMessage>(testResult.Messages.Count) constructor call
triggers IDE0028 (warn-as-error in CI) which suggests a collection
expression. Capacity hints are not expressible in collection-expression
form, so wrap the line with the same pragma pattern other call sites in
this repo already use (TestContextImplementation.cs, TelemetryCollector.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 5, 2026 07:39

CopilotAI 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.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

…ception ordering
Addresses review comment on PR #8823: the original pre-PR code threw
InvalidOperationException from FullyQualifiedName parsing before iterating
testResult.Messages, so an UnreachableException from an unexpected msg.Category
could not fire in the malformed-FQN case. The previous version of the
single-pass refactor moved the foreach above the FQN validation, which changed
the observable exception order for error paths.
This commit hoists the TRX-only property additions (TrxExceptionProperty,
TrxTestDefinitionName, TrxFullyQualifiedTypeNameProperty) above the message
loop while keeping TrxMessagesProperty after the loop so it can consume the
collected list. Behavior is now identical to the pre-PR code for both happy
and error paths, and the single-pass perf win is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink
Amaury Levé (Evangelink) merged commit 073d794 into mainJun 5, 2026
48 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/vstest-bridge-single-pass-messages-7ba386ead473d7a7 branch June 5, 2026 12:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@JanKrivanek