Skip to content

[Efficiency Improver] perf: eliminate state-machine allocations in PropertyBag.Single(T)() and SingleOrDefault(T)()Β #7854

@github-actions

Description

@github-actions

πŸ€– Daily Efficiency Improver β€” automated AI assistant focused on reducing energy consumption and computational footprint.

Goal and Rationale

PropertyBag is a core data structure in Microsoft.Testing.Platform: every test node carries a PropertyBag and the runtime iterates over properties on every test execution. Eliminating per-call heap allocations in the two most common lookup methods (Single<T>() and SingleOrDefault<T>()) reduces GC pressure and DRAM refresh cycles β€” a direct Hardware Efficiency improvement per the Green Software Foundation model.

Focus Area

Code-Level Efficiency β€” removing unnecessary object allocations in a hot path.

Approach

Single<T>() β€” before (3–4 state-machine allocations per call)

IEnumerable<TProperty> matchingValues = _property is null ? [] : _property.OfType<TProperty>(); // yield state machine
return !matchingValues.Any()          // enumerator #1
    ? throw ...
    : matchingValues.Skip(1).Any()    // enumerators #2 + #3
        ? throw ...
        : matchingValues.First();     // enumerator #4

Each call to Any(), Skip(1).Any(), and First() calls GetEnumerator() on the yield-generated object, allocating/re-entering a compiler-generated state machine.

SingleOrDefault<T>() β€” before (1 state-machine allocation per call)

IEnumerable<TProperty> matchingValues = _property.OfType<TProperty>(); // yield state machine
using IEnumerator<TProperty> enumerator = matchingValues.GetEnumerator();
...

Property.OfType<TProperty>() is a yield return method β€” calling it allocates a compiler-generated state machine on the heap.

After β€” both methods (0 allocations in all cases)

Both methods now walk the Property linked list directly, using the same pattern already established by Property.Any<T>() and Property.Contains():

Property? current = _property;
while (current is not null)
{
    if (current.Current is TProperty match) { ... }
    current = current.Next;
}

No iterators, no state machines, no LINQ delegates.

Energy Efficiency Evidence

Proxy metric: heap allocations per call (maps to energy via reduced GC invocations and lower DRAM refresh pressure).

Method Before After
Single<T>() 1 yield state machine + up to 3 LINQ enumerator objects 0
SingleOrDefault<T>() 1 yield state machine 0

The saving compounds with test count: at 10,000 test nodes, a single Single<T>() call per node previously generated up to 40,000 short-lived objects. After: 0.

🌱 GSF principle: Hardware Efficiency β€” avoid unnecessary work that causes the GC to run more frequently, consuming additional CPU time and energy.

Trade-offs

The direct walk is slightly more code than the LINQ one-liner. The logic is straightforward (matches the existing Property.Any<T>() pattern), and the performance benefit is clear and measurable.

Reproducibility

export PATH="$PWD/.dotnet:$PATH"
dotnet restore test/UnitTests/Microsoft.Testing.Platform.UnitTests/Microsoft.Testing.Platform.UnitTests.csproj -p:TargetFramework=net8.0
dotnet test test/UnitTests/Microsoft.Testing.Platform.UnitTests/Microsoft.Testing.Platform.UnitTests.csproj -p:TargetFramework=net8.0 --no-restore --filter PropertyBag

Test Status

βœ… All 12 PropertyBagTests pass on net8.0.

Note

πŸ”’ Integrity filter blocked 3 items

The following items were blocked because they don't meet the GitHub integrity level.

  • #6611 list_pull_requests: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • #4200 search_pull_requests: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
  • #510 search_pull_requests: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by Daily Efficiency Improver Β· ● 2.3M Β· β—·


Note

This was originally intended as a pull request, but GitHub Actions is not permitted to create or approve pull requests in this repository.
The changes have been pushed to branch efficiency/propertybag-single-no-alloc-474709edfa199a08.

Click here to create the pull request

To fix the permissions issue, go to Settings β†’ Actions β†’ General and enable Allow GitHub Actions to create and approve pull requests. See also: gh-aw FAQ

Show patch preview (114 of 114 lines)
From 3f02b787bc21ca86b567c540c47c299e7fdb59c4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Mon, 27 Apr 2026 04:32:31 +0000
Subject: [PATCH] perf: eliminate state-machine allocations in
 PropertyBag.Single<T>() and SingleOrDefault<T>()
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Replace triple-LINQ enumeration in Single<T>() and yield-iterator
enumeration in SingleOrDefault<T>() with direct linked-list walks.

Single<T>() previously called Any(), Skip(1).Any(), and First() on the
same IEnumerable returned by Property.OfType<T>() β€” allocating up to
four separate yield-state-machine objects per call.

SingleOrDefault<T>() previously called Property.OfType<T>() and then
GetEnumerator() on the result β€” allocating one state-machine object
per call.

Both methods now walk the Property linked list directly (the same
pattern already used by Property.Any<T>() and Property.Contains()),
allocating zero heap objects in the common case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../Messages/PropertyBag.cs                   | 57 +++++++++++++------
 1 file changed, 39 insertions(+), 18 deletions(-)

diff --git a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
index 63fe5cc..36f3a94 100644
--- a/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
+++ b/src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs
@@ -190,23 +190,27 @@ public bool Any<TProperty>()
             return default;
         }
 
-        if (_property is null || _property.Count == 0)
+        // Direct linked-list walk: avoids allocating a yield-iterator state machine.
+        TProperty? found = default;
+        bool foundAny = false;
+        Property? current = _property;
+        while (current is not null)
         {
-            return default;
-        }
+          
... (truncated)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/agentic-workflowsGitHub agentic workflow definitions under .github/workflows/*.md.area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions