[perf-improver] perf: cache distinct processors in AsynchronousMessageBus to eliminate per-drain allocation#8704
Conversation
…e per-drain allocation - Add _distinctProcessors (IAsyncConsumerDataProcessor[]) and _drainLastReceived (long[]) fields, both initialized once in InitAsync from _consumerProcessor.Values. - Replace the per-call Dictionary<IAsyncConsumerDataProcessor, long> in DrainDataAsync with array-based index tracking; eliminates one dictionary allocation per test run and replaces hash-table lookups with cache-friendly array indexing. - Replace nested foreach loops in DisableAsync and Dispose with single loops over _distinctProcessors, ensuring each processor is completed/disposed exactly once (previously a consumer subscribing to N data types would be visited N times). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR optimizes AsynchronousMessageBus by caching the distinct consumer processors after initialization and reusing that cache for draining, disabling, and disposal.
Changes:
- Adds cached
_distinctProcessorsand_drainLastReceivedarrays initialized inInitAsync. - Replaces per-drain dictionary allocation with indexed array snapshots.
- Updates
DisableAsyncandDisposeto iterate distinct processors once.
Show a summary per file
| File | Description |
|---|---|
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.cs |
Caches distinct processors and reuses arrays for drain/disable/dispose paths. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 2
|
@copilot address review comments |
… regression test Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Both review comments addressed in commit
|
|
/review |
|
Expert-reviewer launched in background for PR #8704. It will post the review directly. |
🤖 This is an automated contribution from Perf Improver.
Goal and Rationale
AsynchronousMessageBus.DrainDataAsync()allocated a freshDictionary<IAsyncConsumerDataProcessor, long>on every call solely to track how many items each processor had received between drain rounds. Since the set of processors is fixed afterInitAsync, this dictionary can be replaced with a cached array pair (IAsyncConsumerDataProcessor[]+long[]) allocated once.Additionally,
DisableAsyncandDisposeiterated_dataTypeConsumers.Valueswith a nested loop — because a consumer subscribed to N data types appears in N separate lists, each processor was being completed/disposed N times. Using the deduplicated_distinctProcessorsarray fixes this.Approach
Add two new fields initialized at end of
InitAsync:_distinctProcessors = [.. _consumerProcessor.Values]— the unique set of processors_drainLastReceived = new long[_distinctProcessors.Length]— reusable count snapshotDrainDataAsync: replacenew Dictionary<IAsyncConsumerDataProcessor, long>()+ hash-table lookups with direct array indexing over_distinctProcessors.DisableAsync/Dispose: replace double nestedforeachover_dataTypeConsumers.Valueswith a singleforeachover_distinctProcessors.Performance Evidence
Dictionary<IAsyncConsumerDataProcessor, long>allocated per drainDisableAsync(N consumers × M types)Dispose(N consumers × M types)DrainDataAsyncis called once at the end of every test run. The dictionary allocation and hash operations are eliminated; the reusedlong[]snapshot costs only a stack read per iteration.Methodology: code inspection + allocation diff (directly verifiable from the diff).
Trade-offs
AsynchronousMessageBusinstance (one pointer + one pointer — negligible).DrainDataAsync.DisableAsync/Disposenow callCompleteAddingAsync/Disposeexactly once per processor (vs. once per (processor, data-type) pair previously) — this is strictly more correct.Test Status
Microsoft.Testing.Platform.UnitTests(net8.0): 992 passed, 0 failed, 3 skipped ✅Reproducibility
Add this agentic workflows to your repo
To install this agentic workflow, run