Skip to content

[perf-improver] Reduce TelemetryCollector.TrackAssertionCall contention on the assertion hot path #10559

Description

@github-actions

Goal and rationale

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.

Performance evidence

Standalone microbenchmark (ConcurrentDictionary string-key access pattern with 5 assertion names, 2M iterations, .NET 8 Release):

Scenario Before (AddOrUpdate) After (GetOrAdd + Interlocked.Increment)
Single-threaded, 2M calls 197 ms 23 ms (~8.6x faster)
4 threads contending on same keys, 2M calls total 168 ms 34 ms (~5x faster)

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.

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.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "southcentralus0.in.applicationinsights.azure.com"

See Network Configuration for more information.

🤖 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.

Click here to create the pull request

To fix the permissions issue, go to SettingsActionsGeneral 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions