[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation - #9488

Merged
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0
Jun 29, 2026
Merged

[efficiency-improver] perf: add PropertyBag.FirstOrDefault(T)() to eliminate per-call array allocation#9488
Amaury Levé (Evangelink) merged 4 commits into
mainfrom
efficiency/propertybag-firstordefault-b36a19e9bbe579d0

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Goal

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag and update VideoRecorderSessionHandler to use it, eliminating two heap allocations per test state-change message.

Focus Area

Code-Level Efficiency — unnecessary object creation on a hot path.

Problem

PropertyBag exposes OfType<T>() which materialises results into a TProperty[] array. Callers that only want the first match were writing:

Properties.OfType<TestNodeStateProperty>().FirstOrDefault()

This allocates a TProperty[] for every call, even though only the first element is ever used. The array is immediately discarded. VideoRecorderSessionHandler has two such call sites, both on the per-test-update hot path.

Approach

Add PropertyBag.FirstOrDefault<TProperty>() modelled after the existing SingleOrDefault<TProperty>():

  1. Fast path: If TProperty is TestNodeStateProperty (or a subtype), check _testNodeStateProperty directly — O(1), zero allocation.
  2. Linked-list walk: For all other types, walk the internal Property? linked list with an early-exit on the first match — no array, no throw on duplicates.
publicTProperty?FirstOrDefault<TProperty>()whereTProperty:IProperty{if(_testNodeStatePropertyisTPropertytestNodeStateProperty)returntestNodeStateProperty;if(typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))returndefault;Property?current=_property;while(currentis not null){if(current.CurrentisTPropertymatch)returnmatch;current=current.Next;}returndefault;}

VideoRecorderSessionHandler now calls Properties.FirstOrDefault<T>() directly at both call sites.

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

LocationBeforeAfter
VideoRecorderSessionHandler L128TestNodeStateProperty[] allocated + LINQ enumerationDirect field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480TimingProperty[] allocated + LINQ enumerationLinked-list walk, early-exit, 0 alloc

Eliminating heap allocations directly reduces GC pressure. Less GC means fewer stop-the-world pauses and fewer CPU cycles spent on collection — translating to lower energy per functional unit (test run).

Limitation: We do not have direct energy measurements. The reasoning is:

  • Fewer heap objects → shorter / less frequent GC collections → fewer CPU cycles on GC → reduced energy.
  • This is a well-established proxy relationship.

Green Software Foundation Context

Hardware Efficiency: Making better use of the hardware by avoiding unnecessary memory round-trips. Every array the GC does not have to scan, trace, and collect is CPU time reclaimed for useful work, reducing the energy per test execution.

Trade-offs

None: the new method is semantically equivalent to the previous pattern for the single-match case (which is the only realistic scenario given PropertyBag's enforcement of uniqueness for TestNodeStateProperty). The only behavioural difference is that this method does not throw when multiple properties of the same type are present — which is exactly the defensive behaviour the code comments already called for.

Reproducibility

# Measure allocations with dotnet-trace or BenchmarkDotNet (no perf benchmarks# currently exist for PropertyBag):
dotnet trace collect --providers Microsoft-DotNETRuntime:0x1:5 -- \
dotnet run --project test/...

Test Status

CI will validate. Changes are self-contained: new public API + two call-site updates.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Efficiency Improver workflow. · 3K AIC · ⌖ 39 AIC · ⊞ 58.8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

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

Add a zero-allocation FirstOrDefault<TProperty>() method to PropertyBag
that walks the internal linked list directly, returning the first match
without materialising a TProperty[] array.
Previously, callers used:
Properties.OfType<T>().FirstOrDefault()
PropertyBag.OfType<T>() allocates a TProperty[] even for the common
single-element case, and the subsequent LINQ .FirstOrDefault() iterates
it. This results in a heap allocation per call that is immediately
discarded.
The new method:
- Returns _testNodeStateProperty directly (O(1), zero alloc) when T
is TestNodeStateProperty or a subtype
- Walks the linked list with an early-exit on first match for all
other types — no intermediate array, no throw on duplicates
VideoRecorderSessionHandler had two call sites on the hot path
(once per test state-change message):
update.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault()
update.TestNode.Properties.OfType<TimingProperty>().FirstOrDefault()
Both are updated to use Properties.FirstOrDefault<T>() directly.
Proxy metric: heap allocations eliminated per test update message.
GSF principle: Hardware Efficiency — less GC pressure means the CPU
spends fewer cycles on collection, reducing energy per functional unit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 28, 2026 22:17
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 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

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a new PropertyBag.FirstOrDefault<TProperty>() API to retrieve the first matching property without throwing when duplicates exist, and updates the video recorder extension to use it instead of LINQ-based enumeration.

Changes:

  • Added PropertyBag.FirstOrDefault<TProperty>() as a public API (tracked in PublicAPI.Unshipped files).
  • Implemented an allocation-free linked-list walk for first-match lookup in PropertyBag.
  • Updated VideoRecorderSessionHandler to use the new method for TestNodeStateProperty and TimingProperty retrieval.
Show a summary per file
FileDescription
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txtTracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.csAdds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.csReplaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Low

@Evangelink

Copy link
Copy Markdown
MemberAuthor

🔍 Build Failure Analysis

Summary — The build fails with RS0025 because PropertyBag.FirstOrDefault<TProperty>() was registered in two Public API tracking files for the same project, causing the Roslyn PublicApiAnalyzers to see the symbol declared twice.

Root cause: Duplicate entry in PublicAPI.Unshipped.txt (RS0025)

The PR added the new public API declaration to both:

FileRole
PublicAPI/PublicAPI.Unshipped.txt:3Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2TFM-specific file — covers .NET only

When MSBuild compiles the net target framework of Microsoft.Testing.Platform, the analyzer reads both files and encounters PropertyBag.FirstOrDefault<TProperty>() -> TProperty? in each — triggering RS0025 ("symbol appears more than once in the public API files"). The error fires twice because the project is built for multiple target frameworks and both builds include the .net TFM-specific file alongside the base file.

Affected errors (2)

CodeFileLineMessage
RS0025PublicAPI/PublicAPI.Unshipped.txt3Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025PublicAPI/PublicAPI.Unshipped.txt3(same — second TFM build)

Proposed fix

FirstOrDefault<TProperty>() contains no #if NET-guarded code in PropertyBag.cs, so it is available on all target frameworks. It belongs only in the base PublicAPI/PublicAPI.Unshipped.txt. Remove the duplicate line from the TFM-specific file:

# src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt
#nullable enable
- Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty?

The base file already has the correct entry at line 3 — no change needed there.


Build overview
Build: FAILED | Duration: 177.2s | MSBuild: 18.7.0-preview
Projects: 48 | Errors: 3 | Warnings: 0
Failed projects:
✗ Build.proj
✗ NonWindowsTests.slnf
✗ Microsoft.Testing.Extensions.CrashDump.csproj
✗ Microsoft.Testing.Extensions.TrxReport.Abstractions.csproj
✗ Microsoft.Testing.Platform.csproj ← root cause here
All MSBuild errors (2)
CodeProjectFile:LineMessage
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025Microsoft.Testing.PlatformPublicAPI/PublicAPI.Unshipped.txt:3(same, second TFM evaluation)

🤖 Generated by the Build Failure Analysis workflow using [binlog-mcp]((dev.azure.com/redacted) · commit 119b52666c9723ac2c09d679b2a2dc1a2dc31998

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K · [◷]( · )

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.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Build Failure Analysis workflow. · 179.5 AIC · ⌖ 14 AIC · ⊞ 47K ·

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review June 29, 2026 10:10
@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jun 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

#DimensionVerdict
13Test Completeness & Coverage🟠 1 MAJOR

✅ 21/22 dimensions clean.

  • Test Completeness — PropertyBag.FirstOrDefault<TProperty>() ships with no unit tests; all other PropertyBag methods are thoroughly tested in PropertyBagTests.cs. See the inline comment for the exact scenarios needed.

Overall assessment: The implementation is correct, well-structured, and consistent with the existing SingleOrDefault<T> and Any<T> patterns. The algorithm handles all edge cases properly (TestNodeStateProperty fast path, subtype guard via IsAssignableFrom, linked-list walk with early return). The PublicAPI.Unshipped.txt declaration is present, and the VideoRecorder call-site changes are semantically equivalent to the replaced LINQ expressions. The only gap is test coverage for the new public method.

# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
CopilotAI review requested due to automatic review settings June 29, 2026 12:26

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.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Low

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
MemberAuthor

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B80–89Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A90–100No issues found.
newPropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A90–100No issues found.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 183.4 AIC · ⌖ 13.4 AIC · ⊞ 45.7K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit a28e8b1 into mainJun 29, 2026
53 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.state/needs-reviewAwaiting review from the team.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101