From dc46ab9bd611ca063f35207eb8851e34f7c0e04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 17 Jun 2026 00:40:04 +0200 Subject: [PATCH 1/3] perf: defer GetTestName() to failure branches and avoid OfType<> alloc in AzureDevOpsReporter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConsumeAsync() previously called GetTestName() eagerly for every TestNodeUpdateMessage — including passing, skipped, discovered and in-progress nodes — even though the result was only used inside the four failure-state switch branches. GetTestName() itself called OfType() which (a) allocates a SerializableKeyValuePairStringProperty[] and (b) walks the full PropertyBag linked list before the caller filters by key with FirstOrDefault(). Two changes applied: 1. Move GetTestName() inside each failure-state branch so it is never invoked for passing/skipped/non-terminal test results. 2. Replace OfType().FirstOrDefault(predicate) with a zero-allocation GetStructEnumerator() walk that short-circuits on the first matching key. Before (per passing test in --report-azdo runs): - 1 PropertyBag walk (SingleOrDefault) - 1 PropertyBag walk (GetTestName → OfType<>) - 1 SerializableKeyValuePairStringProperty[] heap alloc After (per passing test): - 1 PropertyBag walk (SingleOrDefault) - 0 extra walks, 0 extra allocs After (per failing test): - 1 PropertyBag walk (SingleOrDefault) - 1 PropertyBag walk (GetTestName → GetStructEnumerator, early exit) - 0 heap allocs For 1 000 passing tests: ~1 000 linked-list walks and ~1 000 array allocations eliminated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureDevOpsReporter.cs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs index ede01491c1..72852806ec 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs @@ -131,23 +131,24 @@ public async Task ConsumeAsync(IDataProducer dataProducer, IData value, Cancella EnsureEnabledConfigurationLoaded(); TestNodeStateProperty? nodeState = nodeUpdateMessage.TestNode.Properties.SingleOrDefault(); string testDisplayName = nodeUpdateMessage.TestNode.DisplayName; - string testName = GetTestName(nodeUpdateMessage.TestNode); + // Defer GetTestName() to failure branches only: for passing/skipped/in-progress tests + // nodeState falls through the switch with no match and testName is never needed. switch (nodeState) { case FailedTestNodeStateProperty failed: - await WriteExceptionAsync(testDisplayName, testName, failed.Explanation, failed.Exception, cancellationToken).ConfigureAwait(false); + await WriteExceptionAsync(testDisplayName, GetTestName(nodeUpdateMessage.TestNode), failed.Explanation, failed.Exception, cancellationToken).ConfigureAwait(false); break; case ErrorTestNodeStateProperty error: - await WriteExceptionAsync(testDisplayName, testName, error.Explanation, error.Exception, cancellationToken).ConfigureAwait(false); + await WriteExceptionAsync(testDisplayName, GetTestName(nodeUpdateMessage.TestNode), error.Explanation, error.Exception, cancellationToken).ConfigureAwait(false); break; #pragma warning disable CS0618, MTP0001 // Type or member is obsolete case CancelledTestNodeStateProperty cancelled: #pragma warning restore CS0618, MTP0001 // Type or member is obsolete - await WriteExceptionAsync(testDisplayName, testName, cancelled.Explanation, cancelled.Exception, cancellationToken).ConfigureAwait(false); + await WriteExceptionAsync(testDisplayName, GetTestName(nodeUpdateMessage.TestNode), cancelled.Explanation, cancelled.Exception, cancellationToken).ConfigureAwait(false); break; case TimeoutTestNodeStateProperty timeout: - await WriteExceptionAsync(testDisplayName, testName, timeout.Explanation, timeout.Exception, cancellationToken).ConfigureAwait(false); + await WriteExceptionAsync(testDisplayName, GetTestName(nodeUpdateMessage.TestNode), timeout.Explanation, timeout.Exception, cancellationToken).ConfigureAwait(false); break; } } @@ -434,10 +435,22 @@ private Regex[] LoadUserStackFrameFilters() } private static string GetTestName(TestNode testNode) - => testNode.Properties - .OfType() - .FirstOrDefault(static property => property.Key == FullyQualifiedNamePropertyKey)?.Value - ?? testNode.DisplayName; + { + // Walk the PropertyBag once with the zero-allocation struct enumerator and short-circuit + // on the first matching key, avoiding the SerializableKeyValuePairStringProperty[] heap + // allocation that OfType() would incur. + PropertyBag.PropertyBagEnumerator enumerator = testNode.Properties.GetStructEnumerator(); + while (enumerator.MoveNext()) + { + if (enumerator.Current is SerializableKeyValuePairStringProperty kvp + && kvp.Key == FullyQualifiedNamePropertyKey) + { + return kvp.Value; + } + } + + return testNode.DisplayName; + } /// /// Formats the reporter message so the test name lands on its own line. From a66e35fa32ac2946dc55f7c6b2a3a6062b8463b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 17 Jun 2026 09:48:19 +0200 Subject: [PATCH 2/3] Add using to PropertyBagEnumerator in GetTestName to satisfy IDisposable contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureDevOpsReporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs index 72852806ec..18ba49b5f8 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs @@ -439,7 +439,7 @@ private static string GetTestName(TestNode testNode) // Walk the PropertyBag once with the zero-allocation struct enumerator and short-circuit // on the first matching key, avoiding the SerializableKeyValuePairStringProperty[] heap // allocation that OfType() would incur. - PropertyBag.PropertyBagEnumerator enumerator = testNode.Properties.GetStructEnumerator(); + using PropertyBag.PropertyBagEnumerator enumerator = testNode.Properties.GetStructEnumerator(); while (enumerator.MoveNext()) { if (enumerator.Current is SerializableKeyValuePairStringProperty kvp From 8a027fd21f29ebec797bb45ac7edb71e16af118b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 17 Jun 2026 11:57:04 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../TestNodeIdentity.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TestNodeIdentity.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TestNodeIdentity.cs index bff0edd25e..9e9f263c55 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TestNodeIdentity.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TestNodeIdentity.cs @@ -17,8 +17,8 @@ internal static class TestNodeIdentity public static string GetTestName(TestNode testNode) { // Walk the PropertyBag once with the zero-allocation struct enumerator and short-circuit - // on the first matching key, avoiding the SerializableKeyValuePairStringProperty[] heap - // allocation that OfType() would incur. + // on the first matching key, avoiding the LINQ iterator/boxed-enumerator allocations that + // OfType().FirstOrDefault(...) would incur. using PropertyBag.PropertyBagEnumerator enumerator = testNode.Properties.GetStructEnumerator(); while (enumerator.MoveNext()) {