[Efficiency Improver] perf: eliminate yield-iterator state machine in PropertyBag.OfType(T)()#7917
Conversation
Replace the yield-based iterator from Property.OfType<TProperty>() with a direct linked-list walk inside PropertyBag.OfType<TProperty>(). Before: _property.OfType<TProperty>() (yield return) allocates one state- machine heap object per call, even when no matching property is found. After: direct while-loop walk; no state-machine allocated; returns [] for the common case (no matching property) with zero heap allocations beyond the empty array constant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes a hot-path in Microsoft.Testing.Platform by removing the yield return-based iterator allocation from PropertyBag.OfType<TProperty>() and replacing it with a direct linked-list traversal.
Changes:
- Replace
[.. _property.OfType<TProperty>()](yield iterator) with a manualwhileloop over thePropertylinked list. - Preserve existing fast-path behavior for
TestNodeStatePropertylookups and the empty/nullproperty-list cases.
Show a summary per file
| File | Description |
|---|---|
| src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs | Reworks OfType<TProperty>() to avoid iterator state-machine allocations by directly walking the linked list. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 0
There was a problem hiding this comment.
Summary
Workflow: Test Expert Reviewer 🧪
Date: 2026-04-29
Repository: microsoft/testfx
Key Findings
The PR replaces the yield-based OfType<TProperty>() iterator with a direct linked-list walk, introducing three distinct return paths:
_testNodeStateProperty is TProperty→[testNodeStateProperty](unchanged)- New combined early-return:
typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)) || _property is null→[] - New while-loop:
!foundAny→[], single match →[first!], multiple →[.. overflow]
Paths 2 and 3 (both new) have gaps in PropertyBagTests:
| Gap | Missing path | Risk |
|---|---|---|
_property is null early-return |
OfType<DummyProperty>() on a bag with only TestNodeStateProperty |
Silent regression if early-return is dropped |
| Single-item fast path | OfType<DummyProperty>() with exactly 1 match |
[first!] returns wrong result/throws undetected |
| No-match path in walk | OfType<DummyProperty2>() on a non-empty bag |
Manual loop doesn't cover LINQ's implicitly empty-on-no-match |
The production logic itself looks correct — the overflow initialization overflow ??= [first!] correctly seeds the list with the first match before adding the second. No functional bugs detected.
Recommendations
- Add an assertion
Assert.IsEmpty(property.OfType<DummyProperty>())for a bag containing only aTestNodeStateProperty. - Add
OfType<DummyProperty>()with exactly 1DummyPropertyin the bag. - Add
Assert.IsEmpty(property.OfType<DummyProperty2>())to the existingOfType_Should_Return_CorrectObjecttest (one line, costs nothing).
Generated by Test Expert Reviewer
🧪 Test quality reviewed by Test Expert Reviewer 🧪
| return !foundAny | ||
| ? [] | ||
| : _property is null ? [] : [.. _property.OfType<TProperty>()]; | ||
| : overflow is not null ? [.. overflow] : [first!]; |
There was a problem hiding this comment.
[Coverage] The single-item fast path ([first!] when overflow is null) is not exercised by any existing test. OfType_Should_Return_CorrectObject only tests with 2 DummyProperty instances, which goes through the overflow branch. The [first!] path needs its own case.
Impact: A regression where a single-result bag returns [] or throws would not be caught.
Suggestion: Extend OfType_Should_Return_CorrectObject or add a dedicated test:
PropertyBag bag = new();
DummyProperty single = new();
bag.Add(single);
bag.Add(PassedTestNodeStateProperty.CachedInstance);
TProperty[] result = bag.OfType<DummyProperty>();
Assert.HasCount(1, result);
Assert.AreSame(single, result[0]);|
|
||
| // We don't want to allocate an array if we know that we're looking for a TestNodeStateProperty | ||
| return typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)) | ||
| if (typeof(TestNodeStateProperty).IsAssignableFrom(typeof(TProperty)) || _property is null) |
There was a problem hiding this comment.
[Coverage] The || _property is null branch is new (the original ternary had _property is null ? [] as its own arm). There's currently no test for calling OfType<SomeDummyProperty>() on a PropertyBag that contains only a TestNodeStateProperty (i.e., _testNodeStateProperty is set but _property is null).
Impact: The early-return path added here goes unverified. If a future refactor accidentally removes it, a silent linear walk over a null reference would produce incorrect results.
Suggestion: Add a test case to PropertyBagTests:
[TestMethod]
public void OfType_WithOnlyTestNodeStateProperty_ReturnsEmpty()
{
PropertyBag property = new();
property.Add(PassedTestNodeStateProperty.CachedInstance);
// _property is null; _testNodeStateProperty is set — exercises the new early-return path
Assert.IsEmpty(property.OfType<DummyProperty>());
}| current = current.Next; | ||
| } | ||
|
|
||
| return !foundAny |
There was a problem hiding this comment.
[Coverage] The !foundAny ? [] return at the end of the while-loop (no matching property found in a non-empty bag) is not tested. DummyProperty2 is used in Single / SingleOrDefault tests, but OfType<DummyProperty2>() is never called on a bag that has other properties.
Impact: The new manual traversal diverges from the old LINQ-based _property.OfType<TProperty>(). A bug that incorrectly returns null or throws instead of [] when no match is found would go undetected.
Suggestion: Add one assertion to the existing OfType_Should_Return_CorrectObject test:
// No DummyProperty2 in the bag — exercises the "no match found" path in the while-loop
Assert.IsEmpty(property.OfType<DummyProperty2>());|
@copilot address review comments |
Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/7ea4b52b-3b29-47b2-a116-5af4cdd3b156 Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/testfx/sessions/7ea4b52b-3b29-47b2-a116-5af4cdd3b156 Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Added the three missing test cases in commits 911b697 and 813c705:
|
… PropertyBag.OfType(T)() (#7917) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Replace the yield-based iterator from Property.OfType() with a direct linked-list walk inside PropertyBag.OfType().
Before: _property.OfType() (yield return) allocates one state- machine heap object per call, even when no matching property is found.
After: direct while-loop walk; no state-machine allocated; returns [] for the common case (no matching property) with zero heap allocations beyond the empty array constant.
Fixes #7914