You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
🤖 This is an automated pull request from Daily Efficiency Improver, an AI assistant focused on reducing the energy consumption and computational footprint of this repository.
Goal and Rationale
Eliminate unnecessary heap allocations in the BFS test-node traversal loop (BFSTestNodeVisitor.VisitAsync) and the CreateWrappedName helper. These code paths run for every test node during discovery and execution, so allocation savings compound linearly with test-suite size.
1. BFSTestNodeVisitor — replace StringBuilder with string in BFS queue
Before: Each queued tuple held a StringBuilder representing the parent's full path. On dequeue, a newStringBuilder was allocated to copy the parent path, the separator and the encoded node name were appended, then ToString() was called.
// BeforeQueue<(TestNode,TestNodeUid?,StringBuilderNodeFullPath)>queue=new();queue.Enqueue((node,null,new()));// one empty StringBuilder per root node// ...StringBuildernodeFullPathForChildren=newStringBuilder().Append(nodeFullPath);// copy per nodenodeFullPathForChildren.Append('/');nodeFullPathForChildren.Append(EncodeString(...));stringcurrentNodeFullPath=nodeFullPathForChildren.ToString();// one string per node
After: The queue holds string directly. Child nodes receive an already-materialised string as their base path.
// AfterQueue<(TestNode,TestNodeUid?,stringNodeFullPath)>queue=new();queue.Enqueue((node,null,string.Empty));// zero extra allocations per root node// ...stringcurrentNodeFullPath=nodeFullPath.Length==0||nodeFullPath[^1]!=TreeNodeFilter.PathSeparator?string.Concat(nodeFullPath,"/",encodedName)// one string.Concat per node:string.Concat(nodeFullPath,encodedName);
Savings per test node traversal (tree-filter path):
Before
After
Objects per node
1 StringBuilder + 1 string
1 string
Allocations eliminated
1 StringBuilder per node
—
For a test suite of n nodes: n fewer object allocations, plus the associated internal char[] buffer inside each StringBuilder.
2. BFSTestNodeVisitor — replace HashSet<PlatformTestNodeUid> with HashSet<string>
Before: The UID filter set was HashSet<PlatformTestNodeUid>. Every lookup called currentNode.StableUid.ToPlatformTestNodeUid(), allocating a new PlatformTestNodeUid wrapper object on every node just to check membership.
After: The filter set is HashSet<string> populated with uid.Value strings. Lookups use currentNode.StableUid.Value directly — no wrapper allocation.
Savings: 1 PlatformTestNodeUid allocation per node in UID-filter mode (re-run / filtered runs).
3. TestArgumentsManager — simplify CreateWrappedName to avoid StringBuilder
Savings: 1 StringBuilder allocation per argument entry (data-driven tests), replaced by a single string.Concat call (1 allocation) or no allocation at all when wrapping is not needed.
Energy Efficiency Evidence
Proxy metric: heap allocations per test node traversal.
Allocation reduction is the primary proxy for energy efficiency here:
Fewer heap allocations → less GC pressure → fewer GC pause cycles → lower CPU usage
StringBuilder objects include an internal char[] buffer allocation (typically 16+ chars minimum), not just the object header
Per-node allocations scale linearly with test-suite size
To measure allocations before/after, use dotnet-trace with the gc-collect profile or BenchmarkDotNet with [MemoryDiagnoser] on a large synthetic test tree.
Green Software Foundation Context
Per the Hardware Efficiency principle (use as little physical resources as possible): reducing per-node GC pressure means the GC threads can spend less energy on collection cycles, and DRAM refresh energy is reduced by lowering the live heap size.
Trade-offs
string.Concat for path building creates one string per level in the tree rather than incrementally building with StringBuilder. For very deep trees (depth > ~100), this is equivalent in total character copies. For typical test trees (depth ≤ 10), the savings clearly dominate.
The change is a net simplification — the code is shorter and easier to reason about.
No behavioural changes; all existing BFSTestNodeVisitorTests pass.
Test Status
Build: ✅ MSTest.Engine builds cleanly (0 warnings, 0 errors) on net8.0
Tests: ✅ All 10 BFSTestNodeVisitorTests pass on net8.0
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/bfstestnode-string-path-tracking-f474c342a1a32973.
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 (153 of 153 lines)
From b13f883d4e7b531c5222611ac13a36b86e7e74f2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sat, 25 Apr 2026 04:21:44 +0000
Subject: [PATCH] perf: reduce allocations in BFSTestNodeVisitor path tracking
and CreateWrappedName
- Replace StringBuilder in the BFS queue with string, eliminating one
StringBuilder allocation per test node during tree traversal. Each
node's full path is now an immutable string shared directly with
child nodes as their base path.
- Replace HashSet<PlatformTestNodeUid> with HashSet<string> for UID
filter lookups. This avoids allocating a temporary PlatformTestNodeUid
wrapper object on every node lookup in the HashSet.
- Simplify CreateWrappedName in TestArgumentsManager to a conditional
string.Concat, removing a StringBuilder allocation per argument entry.
Proxy metric: heap allocations per test node during BFS traversal.
Before: 1 StringBuilder + 1 string per node (path) + 1 PlatformTestNodeUid
per node (filter lookup).
After: 1 string per node (path), no PlatformTestNodeUid per lookup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Engine/BFSTestNodeVisitor.cs | 43 ++++++++++---------
.../Engine/TestArgumentsManager.cs | 18 +-------
2 files changed, 24 insertions(+), 37 deletions(-)
diff --git a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs
index cdf5a26bb..33dd922ad 100644
--- a/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs+++ b/src/Adapter/MSTest.Engine/Engine/BFSTestNodeVisitor.cs@@ -3,7 +3,6 @@
using System.Web;
-using Microsoft.Testing.Framework.Helpers;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Requests;
@@ -34,22 +33,32 @@ public BFSTestNodeVisitor(IEnumerable<TestNode> rootTestNodes, ITestExecutionFil
public async Task VisitAsync(Func<TestNode, TestNodeUid?, Task
... (truncated)
🤖 This is an automated pull request from Daily Efficiency Improver, an AI assistant focused on reducing the energy consumption and computational footprint of this repository.
Goal and Rationale
Eliminate unnecessary heap allocations in the BFS test-node traversal loop (
BFSTestNodeVisitor.VisitAsync) and theCreateWrappedNamehelper. These code paths run for every test node during discovery and execution, so allocation savings compound linearly with test-suite size.Focus Area
Code-Level Efficiency — unnecessary object creation / allocation.
Changes
1.
BFSTestNodeVisitor— replaceStringBuilderwithstringin BFS queueBefore: Each queued tuple held a
StringBuilderrepresenting the parent's full path. On dequeue, a newStringBuilderwas allocated to copy the parent path, the separator and the encoded node name were appended, thenToString()was called.After: The queue holds
stringdirectly. Child nodes receive an already-materialised string as their base path.Savings per test node traversal (tree-filter path):
StringBuilder+ 1stringstringStringBuilderper nodeFor a test suite of n nodes: n fewer object allocations, plus the associated internal
char[]buffer inside eachStringBuilder.2.
BFSTestNodeVisitor— replaceHashSet<PlatformTestNodeUid>withHashSet<string>Before: The UID filter set was
HashSet<PlatformTestNodeUid>. Every lookup calledcurrentNode.StableUid.ToPlatformTestNodeUid(), allocating a newPlatformTestNodeUidwrapper object on every node just to check membership.After: The filter set is
HashSet<string>populated withuid.Valuestrings. Lookups usecurrentNode.StableUid.Valuedirectly — no wrapper allocation.Savings: 1
PlatformTestNodeUidallocation per node in UID-filter mode (re-run / filtered runs).3.
TestArgumentsManager— simplifyCreateWrappedNameto avoidStringBuilderBefore:
After:
Savings: 1
StringBuilderallocation per argument entry (data-driven tests), replaced by a singlestring.Concatcall (1 allocation) or no allocation at all when wrapping is not needed.Energy Efficiency Evidence
Proxy metric: heap allocations per test node traversal.
Allocation reduction is the primary proxy for energy efficiency here:
StringBuilderobjects include an internalchar[]buffer allocation (typically 16+ chars minimum), not just the object headerReproducibility:
To measure allocations before/after, use
dotnet-tracewith thegc-collectprofile orBenchmarkDotNetwith[MemoryDiagnoser]on a large synthetic test tree.Green Software Foundation Context
Per the Hardware Efficiency principle (use as little physical resources as possible): reducing per-node GC pressure means the GC threads can spend less energy on collection cycles, and DRAM refresh energy is reduced by lowering the live heap size.
Trade-offs
string.Concatfor path building creates one string per level in the tree rather than incrementally building withStringBuilder. For very deep trees (depth > ~100), this is equivalent in total character copies. For typical test trees (depth ≤ 10), the savings clearly dominate.BFSTestNodeVisitorTestspass.Test Status
Build: ✅
MSTest.Enginebuilds cleanly (0 warnings, 0 errors) onnet8.0Tests: ✅ All 10
BFSTestNodeVisitorTestspass onnet8.0Note
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/bfstestnode-string-path-tracking-f474c342a1a32973.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 (153 of 153 lines)