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
TelemetryCollector.TrackAssertionCall is invoked unconditionally at the top of all 206 Assert/CollectionAssert/StringAssert methods — it is the single highest-frequency call site in the framework, executed once per assertion in every test. It used ConcurrentDictionary<string, long>.AddOrUpdate(key, 1, static (_, count) => count + 1), which internally performs a lookup, then a lock-striped write/CAS-retry loop plus a delegate invocation on every call — even though the common case (after warmup) is simply incrementing an already-existing counter.
Approach
Changed the backing store to ConcurrentDictionary<string, StrongBox<long>>. Once an assertion name's entry exists, subsequent calls only need a lock-free dictionary read (GetOrAdd fast path) plus a single Interlocked.Increment on the boxed value — no per-call delegate invocation and no internal per-update locking. DrainAssertionCallCounts was updated to unwrap the boxed values into the same Dictionary<string, long> snapshot shape as before; external behavior/API is unchanged.
Methodology: warmed up both paths first, then timed with Stopwatch, isolating just the dictionary-update call (representative of the real call site). No additional heap allocations in either path (both are steady-state, no growth).
Trade-offs
Slightly more code in DrainAssertionCallCounts (manual unwrap loop instead of the Dictionary(IEnumerable<KeyValuePair>) ctor) since values are now boxed in StrongBox<long> rather than plain long.
One-time extra allocation of a StrongBox<long> per distinct assertion name (bounded by ~120 known assertion names), not per call — negligible.
Reproducibility
The benchmark harness (a throwaway console app comparing the two dictionary-update strategies) was not committed; it can be reproduced by writing a small console app that runs both ConcurrentDictionary<string,long>.AddOrUpdate and the boxed GetOrAdd+Interlocked.Increment pattern in a loop over a small fixed set of string keys, both single-threaded and via Parallel.For.
🤖 Automated content by GitHub Copilot. Generated by the Perf Improver workflow. · auto · 227.9 AIC · ⌖ 12.7 AIC · ⊞ 18.6K · [◷]( · ◷) Comment /perf-assist to run again
Add this agentic workflow to your repo
To install this agentic workflow, run
gh aw add githubnext/agentics/workflows/perf-improver.md@main
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 perf-assist/telemetry-collector-counter-7d47e540211b0bf5.
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 (65 of 65 lines)
From f7e906f1198f6ceba6ad5b3ad1991b0a3f20ad38 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 11 Aug 2026 14:21:07 +0000
Subject: [PATCH] Reduce TelemetryCollector.TrackAssertionCall contention on
the assertion hot path
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Internal/TelemetryCollector.cs | 24 ++++++++++++-------
1 file changed, 15 insertions(+), 9 deletions(-)
diff --git a/src/TestFramework/TestFramework/Internal/TelemetryCollector.cs b/src/TestFramework/TestFramework/Internal/TelemetryCollector.cs
index faa1bf3..f95534e 100644
--- a/src/TestFramework/TestFramework/Internal/TelemetryCollector.cs+++ b/src/TestFramework/TestFramework/Internal/TelemetryCollector.cs@@ -15,7 +15,11 @@ internal static class TelemetryCollector
// happens at most once per process.
private static readonly Lazy<bool> IsEnabled = new(IsTelemetryEnabledFromEnvironment, LazyThreadSafetyMode.ExecutionAndPublication);
- private static ConcurrentDictionary<string, long> s_assertionCallCounts = new();+ // Values are boxed in a StrongBox<long> so that, once an assertion name's entry exists, repeat+ // calls only need a dictionary lookup (no write) plus a single Interlocked.Increment on the box.+ // This avoids ConcurrentDictionary.AddOrUpdate's internal per-update locking/CAS-retry loop and+ // the per-call lambda invocation on the (by far) most common case where the key already exists.+ private static ConcurrentDictionary<string, StrongBox<long>> s_assertionCallCounts = new();
/// <summary>
/// Records that an assertion method was called. This is on the hot path of every assertion,
@@ -35,7 +39,8 @@ internal static void TrackAssertionCall(string assertionName)
try
{
- s_assertionCallCounts.AddOrUpdate(assertionName, 1, static (_, count) => count + 1);+ StrongBox<long> box = s_assertionCallCoun
... (truncated)
Goal and rationale
TelemetryCollector.TrackAssertionCallis invoked unconditionally at the top of all 206Assert/CollectionAssert/StringAssertmethods — it is the single highest-frequency call site in the framework, executed once per assertion in every test. It usedConcurrentDictionary<string, long>.AddOrUpdate(key, 1, static (_, count) => count + 1), which internally performs a lookup, then a lock-striped write/CAS-retry loop plus a delegate invocation on every call — even though the common case (after warmup) is simply incrementing an already-existing counter.Approach
Changed the backing store to
ConcurrentDictionary<string, StrongBox<long>>. Once an assertion name's entry exists, subsequent calls only need a lock-free dictionary read (GetOrAddfast path) plus a singleInterlocked.Incrementon the boxed value — no per-call delegate invocation and no internal per-update locking.DrainAssertionCallCountswas updated to unwrap the boxed values into the sameDictionary<string, long>snapshot shape as before; external behavior/API is unchanged.Performance evidence
Standalone microbenchmark (
ConcurrentDictionarystring-key access pattern with 5 assertion names, 2M iterations, .NET 8 Release):AddOrUpdate)GetOrAdd+Interlocked.Increment)Methodology: warmed up both paths first, then timed with
Stopwatch, isolating just the dictionary-update call (representative of the real call site). No additional heap allocations in either path (both are steady-state, no growth).Trade-offs
DrainAssertionCallCounts(manual unwrap loop instead of theDictionary(IEnumerable<KeyValuePair>)ctor) since values are now boxed inStrongBox<long>rather than plainlong.StrongBox<long>per distinct assertion name (bounded by ~120 known assertion names), not per call — negligible.Reproducibility
The benchmark harness (a throwaway console app comparing the two dictionary-update strategies) was not committed; it can be reproduced by writing a small console app that runs both
ConcurrentDictionary<string,long>.AddOrUpdateand the boxedGetOrAdd+Interlocked.Incrementpattern in a loop over a small fixed set of string keys, both single-threaded and viaParallel.For.Test Status
dotnet build src/TestFramework/TestFramework/TestFramework.csproj(all TFMs: netstandard2.0, net462, net8.0, net9.0) — succeeded, 0 warnings/errors.dotnet run --project test/UnitTests/TestFramework.UnitTests -f net8.0— all 1506 tests passed, 0 failed.Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
southcentralus0.in.applicationinsights.azure.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.
Add this agentic workflow to your repo
To install this agentic workflow, run
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
perf-assist/telemetry-collector-counter-7d47e540211b0bf5.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 (65 of 65 lines)