Skip to content

[Efficiency Improver] perf: skip PropertyBag construction in BFSTestNodeVisitor when filter has no property expressionsΒ #7883

@github-actions

Description

@github-actions

πŸ€– Daily Efficiency Improver β€” automated AI assistant.

Goal and Rationale

When BFSTestNodeVisitor walks the test tree with a TreeNodeFilter, it calls MatchesFilter for every node. Prior to this change, it always constructed a PropertyBag from the node's IProperty[] β€” even though the PropertyBag is only inspected when the filter contains [Trait=Value] property expressions.

A PropertyBag backed by N properties allocates 1 PropertyBag object + N Property linked-list nodes per call. In a tree of 1 000 test nodes, each carrying 3 properties, that is 4 000 short-lived heap objects created just to be immediately discarded for the common case where no property filter is active.

Focus Area

Code-Level Efficiency β€” eliminate unnecessary object creation in a per-node hot path.

Approach

  1. TreeNodeFilter.ContainsPropertyFilters (new internal property) β€” computed once during filter parsing using _filters.Any(HasPropertyFilterExpression). A recursive helper HasPropertyFilterExpression walks the parsed FilterExpression tree looking for any ValueAndPropertyExpression node (the only kind that inspects PropertyBag contents at match time). Cost: one extra boolean field + O(depth Γ— width) traversal at construction time β€” amortised to zero over the lifetime of the filter.

  2. BFSTestNodeVisitor β€” when ContainsPropertyFilters is false, the new static EmptyPropertyBag singleton is reused instead of constructing a per-node bag. When true, a PropertyBag is constructed as before (no regression for users who do use property filters).

Energy Efficiency Evidence

Proxy metric: heap allocations per test-node traversal with a path-only TreeNodeFilter (e.g. /AssemblyName/ClassName/MethodName).

Before After
PropertyBag objects / node 1 0
Property linked-list nodes / node N (β‰ˆ3–5) 0
Total allocations / 1 000-node tree ~4 000–6 000 0

Reasoning: Fewer short-lived heap objects β†’ less GC pressure β†’ fewer GC pauses β†’ lower CPU/energy draw per test run. This directly maps to the Hardware Efficiency principle from the Green Software Foundation: avoid unnecessary computation and allocation on the critical path.

The static EmptyPropertyBag is allocated exactly once (class static initialiser), so it adds no ongoing cost.

Trade-offs

  • Minimal: the change adds one boolean field and one helper method to TreeNodeFilter, and changes two lines in BFSTestNodeVisitor. No new public API surface.
  • EmptyPropertyBag is mutable; however, it is only ever read (never written to) via the MatchesFilter code path when ContainsPropertyFilters is false, so sharing is safe.

Reproducibility

export PATH="$PWD/.dotnet:$PATH"
dotnet test test/UnitTests/MSTest.Engine.UnitTests/MSTest.Engine.UnitTests.csproj -p:TargetFramework=net8.0 --filter "BFSTestNodeVisitor"
dotnet test test/UnitTests/Microsoft.Testing.Platform.UnitTests/Microsoft.Testing.Platform.UnitTests.csproj -p:TargetFramework=net8.0 --filter "TreeNodeFilter"

Test Status

  • βœ… 10 / 10 BFSTestNodeVisitorTests pass (net8.0)
  • βœ… 45 / 45 TreeNodeFilterTests pass (net8.0)

Note

πŸ”’ Integrity filter blocked 1 item

The following item 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".

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 Β· ● 4.7M Β· β—·


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/skip-propertybag-when-no-property-filter-37db331d9e78483b.

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 (107 of 107 lines)
From 1b3325b8397454c41178beba09cbf32de0a56af8 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 28 Apr 2026 04:39:09 +0000
Subject: [PATCH] perf: skip PropertyBag construction in BFSTestNodeVisitor
 when filter has no property expressions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add ContainsPropertyFilters to TreeNodeFilter β€” computed once at parse time.
When false, BFSTestNodeVisitor reuses a static empty PropertyBag instead of
allocating a new PropertyBag (+ N Property linked-list nodes) per traversed
test node.

Proxy metric: heap allocations per test node during BFS traversal with a
TreeNodeFilter that contains no [Trait=Value] property conditions (the common
case).
  Before: 1 PropertyBag + N Property nodes per node (N = number of properties)
  After:  0 allocations (static empty bag reused)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../MSTest.Engine/Engine/BFSTestNodeVisitor.cs  | 17 +++++------------
 .../Requests/TreeNodeFilter/TreeNodeFilter.cs   | 12 ++++++++++++
 2 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs
index dc98be1..d443281 100644
--- a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs
+++ b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs
@@ -11,6 +11,7 @@ namespace Microsoft.Testing.Framework;
 internal sealed class BFSTestNodeVisitor
 {
     private static readonly string PathSeparatorString = TreeNodeFilter.PathSeparator.ToString();
+    private static readonly PropertyBag EmptyPropertyBag = new();
 
     private readonly IEnumerable<TestNode> _rootTestNodes;
     private readonly ITestExecutionFilter _testExecutionFilter;
@@ -72,7 +73,10 @@ public async Task VisitAsync(Func<TestNode, TestNodeUid?, Task> onIncludedTestNo
             // When we are filtering as tr
... (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