Skip to content

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

Merged
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
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.
public TProperty? FirstOrDefault<TProperty>()
    where TProperty : IProperty
{
    if (_testNodeStateProperty is TProperty testNodeStateProperty)
        return testNodeStateProperty;
    if (typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)))
        return default;
    Property? current = _property;
    while (current is not null)
    {
        if (current.Current is TProperty match)
            return match;
        current = current.Next;
    }
    return default;
}

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

Energy Efficiency Evidence

Proxy metric: Heap allocations eliminated per test update message.

Location Before After
VideoRecorderSessionHandler L128 TestNodeStateProperty[] allocated + LINQ enumeration Direct field read (_testNodeStateProperty), O(1), 0 alloc
VideoRecorderSessionHandler L480 TimingProperty[] allocated + LINQ enumeration Linked-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>
Copilot AI review requested due to automatic review settings June 28, 2026 22:17
@Evangelink Evangelink added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels Jun 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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
File Description
src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt Tracks newly added PropertyBag.FirstOrDefault<TProperty>() API for net target.
src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt Tracks newly added PropertyBag.FirstOrDefault<TProperty>() API for general PublicAPI.
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs Adds FirstOrDefault<TProperty>() implementation with fast-path and linked-list traversal.
src/Platform/Microsoft.Testing.Extensions.VideoRecorder/VideoRecorderSessionHandler.cs Replaces LINQ OfType().FirstOrDefault() with PropertyBag.FirstOrDefault<T>().

Review details

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

Comment thread src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
Comment thread src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
@Evangelink

Copy link
Copy Markdown
Member Author

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

File Role
PublicAPI/PublicAPI.Unshipped.txt:3 Base file — covers all target frameworks
PublicAPI/net/PublicAPI.Unshipped.txt:2 TFM-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)

Code File Line Message
RS0025 PublicAPI/PublicAPI.Unshipped.txt 3 Symbol PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once
RS0025 PublicAPI/PublicAPI.Unshipped.txt 3 (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)
Code Project File:Line Message
RS0025 Microsoft.Testing.Platform PublicAPI/PublicAPI.Unshipped.txt:3 The symbol Microsoft.Testing.Platform.Extensions.Messages.PropertyBag.FirstOrDefault<TProperty>() -> TProperty? appears more than once in the public API files
RS0025 Microsoft.Testing.Platform PublicAPI/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 · [◷]( · )

@Evangelink Evangelink left a comment

Copy link
Copy Markdown
Member Author

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 ·

Comment thread src/Platform/Microsoft.Testing.Platform/PublicAPI/net/PublicAPI.Unshipped.txt Outdated
@Evangelink Evangelink marked this pull request as ready for review June 29, 2026 10:10
@Evangelink Evangelink enabled auto-merge (squash) June 29, 2026 10:10
@Evangelink 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>

@Evangelink Evangelink left a comment

Copy link
Copy Markdown
Member Author

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.

# Dimension Verdict
13 Test 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.

Comment thread src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
# Conflicts:
#	src/Platform/Microsoft.Testing.Platform/PublicAPI/PublicAPI.Unshipped.txt
Copilot AI review requested due to automatic review settings June 29, 2026 12:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

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

Comment thread src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Evangelink

Copy link
Copy Markdown
Member Author

🧪 Test quality grade — PR #9488

ΔTestGradeBandNotes
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
FirstObject_
WhenMultipleMatchesExist
B 80–89 Disjunctive assertion doesn't pin which match is "first"; test name implies first-element semantics.
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
CorrectObject_
WhenSingleMatchExists
A 90–100 No issues found.
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenBagIsEmpty
A 90–100 No issues found.
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenNoMatchExists
A 90–100 No issues found.
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenSubtype_
NotPresent
A 90–100 No issues found.
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
Null_
WhenTestNodeStateProperty_
NotPresent
A 90–100 No issues found.
new PropertyBagTests.
FirstOrDefault_
Should_
Return_
TestNodeStateProperty_
WhenPresent
A 90–100 No 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 Evangelink merged commit a28e8b1 into main Jun 29, 2026
53 checks passed
@Evangelink Evangelink deleted the efficiency/propertybag-firstordefault-b36a19e9bbe579d0 branch June 29, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performance Runtime / build performance / efficiency. state/needs-review Awaiting review from the team. type/automation Created or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants