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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
Amaury Levé (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
Amaury Levé (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

MetricBeforeAfter
Dictionary<IAsyncConsumerDataProcessor, long> allocated per drain10
Processor visits in DisableAsync (N consumers × M types)N×MN
Processor visits in Dispose (N consumers × M types)N×MN
Inner-loop access pattern in drainhash-table lookuparray 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 / 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

./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>
CopilotAI review requested due to automatic review settings May 30, 2026 14:18
@EvangelinkAmaury Levé (Evangelink) added area/performance Runtime / build performance / efficiency. type/automation Created or maintained by an agentic workflow. labels May 30, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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
FileDescription
src/Platform/Microsoft.Testing.Platform/Messages/AsynchronousMessageBus.csCaches distinct processors and reuses arrays for drain/disable/dispose paths.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 2

@Evangelink
Amaury Levé (Evangelink) marked this pull request as ready for review May 31, 2026 06:37
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Copilot address review comments

… regression test
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>

CopilotAI commented May 31, 2026

Copy link
Copy Markdown
Contributor

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
MemberAuthor

/review

@github-actions

github-actionsBot commented May 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ 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
Amaury Levé (Evangelink) merged commit ad1a191 into mainMay 31, 2026
25 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the perf-assist/messagebus-drain-alloc-reduction-7ab8f9a5ecfc5fd8 branch May 31, 2026 18:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/performanceRuntime / build performance / efficiency.type/automationCreated or maintained by an agentic workflow.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink