Skip to content

[perf-improver] perf: cache distinct processors in AsynchronousMessageBus to eliminate per-drain allocation#8704

Merged
Evangelink merged 2 commits into
mainfrom
perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8
May 31, 2026
Merged

[perf-improver] perf: cache distinct processors in AsynchronousMessageBus to eliminate per-drain allocation#8704
Evangelink merged 2 commits into
mainfrom
perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8

Conversation

@Evangelink
Copy link
Copy Markdown
Member

🤖 This is an automated contribution from Perf Improver.

Goal and Rationale

AsynchronousMessageBus.DrainDataAsync() allocated a fresh Dictionary<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 after InitAsync, this dictionary can be replaced with a cached array pair (IAsyncConsumerDataProcessor[] + long[]) allocated once.

Additionally, DisableAsync and Dispose iterated _dataTypeConsumers.Values with 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 _distinctProcessors array fixes this.

Approach

  1. Add two new fields initialized at end of InitAsync:

    • _distinctProcessors = [.. _consumerProcessor.Values] — the unique set of processors
    • _drainLastReceived = new long[_distinctProcessors.Length] — reusable count snapshot
  2. DrainDataAsync: replace new Dictionary<IAsyncConsumerDataProcessor, long>() + hash-table lookups with direct array indexing over _distinctProcessors.

  3. DisableAsync / Dispose: replace double nested foreach over _dataTypeConsumers.Values with a single foreach over _distinctProcessors.

Performance Evidence

Metric Before After
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain 1 0
Processor visits in DisableAsync (N consumers × M types) N×M N
Processor visits in Dispose (N consumers × M types) N×M N
Inner-loop access pattern in drain hash-table lookup array index (cache-friendly)

DrainDataAsync is called once at the end of every test run. The dictionary allocation and hash operations are eliminated; the reused long[] snapshot costs only a stack read per iteration.

Methodology: code inspection + allocation diff (directly verifiable from the diff).

Trade-offs

  • Two new fields per AsynchronousMessageBus instance (one pointer + one pointer — negligible).
  • No behavioral change for DrainDataAsync.
  • DisableAsync / Dispose now call CompleteAddingAsync / Dispose exactly 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

./build.sh
artifacts/bin/Microsoft.Testing.Platform.UnitTests/Debug/net8.0/Microsoft.Testing.Platform.UnitTests

Generated by Perf Improver · sonnet46

Generated by Perf Improver · sonnet46 3.1M ·

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/perf-improver.md@main

…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>
Copilot AI review requested due to automatic review settings May 30, 2026 14:18
@Evangelink Evangelink added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _distinctProcessors and _drainLastReceived arrays initialized in InitAsync.
  • Replaces per-drain dictionary allocation with indexed array snapshots.
  • Updates DisableAsync and Dispose to 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

@Evangelink Evangelink marked this pull request as ready for review May 31, 2026 06:37
@Evangelink
Copy link
Copy Markdown
Member Author

@copilot address review comments

… regression test

Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 31, 2026

@copilot address review comments

Both review comments addressed in commit fix: clear _distinctProcessors in Dispose and add multi-type consumer regression test:

  1. Dispose retains references / double-dispose: _distinctProcessors = [] and _drainLastReceived = [] are now cleared alongside the existing dictionary clears.

  2. Missing multi-type consumer test: Added DisableAsync_WithConsumerSubscribedToMultipleDataTypes_ShouldCompleteProcessorOnce — a consumer subscribed to DataTypeA and DataTypeB publishes one of each, drains, asserts both were received, then calls DisableAsync. All 5 AsynchronousMessageBus tests pass.

@Evangelink
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 31, 2026

⚠️ Security scanning failed for Expert Code Review (command). Review the logs for details.

Expert-reviewer launched in background for PR #8704. It will post the review directly.

@Evangelink Evangelink merged commit ad1a191 into main May 31, 2026
25 checks passed
@Evangelink Evangelink deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants