Skip to content

perf: replace global lock in EventReceiverRegistry with lock-free structure #5715

@thomhurst

Description

@thomhurst

Summary

EventReceiverRegistry.RegisterReceivers takes a global Lock once per test. With Environment.ProcessorCount * 4 concurrent tests this is a top source of contention in the trace: Monitor.Enter_Slowpath at 2.16% self. Estimated ~1.5% CPU + removes hot lock.

Evidence

Trace top exclusive:

7.  Monitor.Enter_Slowpath(...)                   2.16%  (self)

Code: TUnit.Engine/Events/EventReceiverRegistry.cs:29-96, 145-168. RegisterReceivers is called from TUnit.Engine/Services/TestExecution/TestCoordinator.cs:67 on every test start.

The registry is conceptually a copy-on-write Dictionary<Type, object[]> — currently guarded by a single shared lock.

Proposed fix

  • Switch to ConcurrentDictionary<Type, ImmutableArray<object>>.
  • Update via AddOrUpdate with ImmutableArray.Add or a snapshot-and-swap via Interlocked.CompareExchange.
  • Short-circuit RegisterReceivers entirely when every object in the incoming span is already in _initializedObjects (common case — most eligible objects are shared across tests).

Expected impact

  • ~1.5% CPU reduction on parallel paths.
  • Eliminates lock contention that scales with parallelism (gets worse on bigger machines / bigger suites).

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .net codeenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions