Sitelet https://github.com/SeleniumHQ/selenium/pull/17213
Skip to content

[dotnet] [bidi] Parallel event handlers - #17213

Merged
nvborisenko merged 4 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-parallel-event-handlers
Mar 12, 2026
Merged

nvborisenko merged 4 commits into
SeleniumHQ:trunkfrom
nvborisenko:bidi-parallel-event-handlers

Conversation

@nvborisenko

Copy link
Copy Markdown
Member

Now all event handlers are executed in parallel, non blocking each others.

Why:
I have 100 tests, each test is executed in isolated userContext environment. Each test is executed in parallel sharing one single bidi connection. Each test wants to listen to network traffic (just an example). So, ideally any event handler should not block others, never.

Experiment:
I have executed this kind of simulation. Performance is great!

💥 What does this PR do?

This pull request introduces improvements to the event handling mechanism in the EventDispatcher class, focusing on better management of asynchronous event handler tasks and ensuring proper cleanup during disposal.

💡 Additional Considerations

It is still considerable to configure this behaviour.

🔄 Types of changes

  • Bug fix (backwards compatible)
  • New feature (non-breaking change which adds functionality and tests!)
  • Breaking change (fix or feature that would cause existing functionality to change)

Copilot AI review requested due to automatic review settings March 12, 2026 18:29
@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Implement parallel event handler execution in BiDi EventDispatcher

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Execute event handlers in parallel without blocking each other
• Track running handler tasks for proper cleanup during disposal
• Extract handler invocation logic into separate method with error handling
• Ensure all pending handlers complete before EventDispatcher disposal

Grey Divider

File Changes

1. dotnet/src/webdriver/BiDi/EventDispatcher.cs ✨ Enhancement +26/-11

Parallel event handler execution with task tracking

• Added _runningHandlers concurrent dictionary to track in-flight handler tasks
• Modified ProcessEventsAwaiterAsync() to invoke handlers without awaiting, enabling parallel
 execution
• Extracted handler invocation into new InvokeHandlerAsync() method with centralized error
 handling
• Updated DisposeAsync() to wait for all running handlers to complete before disposal

dotnet/src/webdriver/BiDi/EventDispatcher.cs


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Logs raw exception ex 📘 Rule violation ✧ Quality
Description
The new handler invocation path logs the full exception object ({ex}), which can include
sensitive/nullable message content and treats expected cancellations as errors. This reduces
operational safety and can leak data into logs.
Code

dotnet/src/webdriver/BiDi/EventDispatcher.cs[R109-114]

+        catch (Exception ex)
+        {
+            if (_logger.IsEnabled(LogEventLevel.Error))
+            {
+                _logger.Error($"Unhandled error processing BiDi event handler: {ex}");
            }
Evidence
PR Compliance ID 9 requires resilient error handling/logging that avoids logging raw exception
messages and handles expected cancellation intentionally; the added InvokeHandlerAsync catch block
logs {ex} directly.

dotnet/src/webdriver/BiDi/EventDispatcher.cs[103-114]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`InvokeHandlerAsync` logs the full exception (`{ex}`), which can include sensitive/nullable exception message content and does not distinguish expected cancellation during shutdown.

## Issue Context
Compliance requires resilient error handling and logging: prefer stable identifiers (exception type, event name/handler id), and handle expected `OperationCanceledException` intentionally (usually not an error during disposal/shutdown).

## Fix Focus Areas
- dotnet/src/webdriver/BiDi/EventDispatcher.cs[103-115]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unbounded handler concurrency 🐞 Bug ⛯ Reliability
Description
ProcessEventsAwaiterAsync no longer awaits handler execution and can schedule handler tasks faster
than they complete, removing implicit backpressure and enabling unbounded concurrent work. With an
unbounded channel, high-frequency events can cause _runningHandlers/task count to grow without
limit, degrading performance and potentially exhausting memory/threadpool resources.
Code

dotnet/src/webdriver/BiDi/EventDispatcher.cs[R86-97]

+                if (_eventRegistrations.TryGetValue(result.Method, out var registration))
                {
-                    if (_eventRegistrations.TryGetValue(result.Method, out var registration))
+                    foreach (var handler in registration.GetHandlers()) // copy-on-write array, safe to iterate
                    {
-                        foreach (var handler in registration.GetHandlers()) // copy-on-write array, safe to iterate
+                        var runningHandlerTask = InvokeHandlerAsync(handler, result.EventArgs);
+                        if (!runningHandlerTask.IsCompleted)
                        {
-                            await handler.InvokeAsync(result.EventArgs).ConfigureAwait(false);
+                            _runningHandlers.TryAdd(runningHandlerTask, 0);
+                            _ = runningHandlerTask.ContinueWith(static (t, state) => ((ConcurrentDictionary<Task, byte>)state!).TryRemove(t, out _),
+                                _runningHandlers, TaskContinuationOptions.ExecuteSynchronously);
                        }
                    }
Evidence
The dispatcher uses an unbounded channel for events and, after this PR, reads events in a tight loop
while starting handler tasks without awaiting them; this allows the consumer to outpace handler
completion and accumulate concurrent tasks. While DisposeAsync awaits currently tracked handler
tasks, it does not prevent unbounded growth during steady-state operation.

dotnet/src/webdriver/BiDi/EventDispatcher.cs[39-43]
dotnet/src/webdriver/BiDi/EventDispatcher.cs[82-99]
dotnet/src/webdriver/BiDi/Broker.cs[219-238]
dotnet/src/webdriver/BiDi/EventDispatcher.cs[118-126]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProcessEventsAwaiterAsync` schedules handler tasks without awaiting or throttling them. Because `_pendingEvents` is created with `Channel.CreateUnbounded`, the dispatcher can enqueue an unbounded number of concurrent handler tasks under high event throughput (e.g., network events), leading to threadpool starvation and memory pressure.

### Issue Context
The PR’s goal is parallel/non-blocking handler execution, but we still need bounded resource usage during steady state. `DisposeAsync` already waits for in-flight handler tasks; the gap is runtime backpressure/concurrency limiting.

### Fix Focus Areas
- dotnet/src/webdriver/BiDi/EventDispatcher.cs[39-99]
- dotnet/src/webdriver/BiDi/EventDispatcher.cs[118-126]

### Suggested approach
- Add a configurable maximum concurrency (e.g., `int maxConcurrentHandlers = ...`) and a `SemaphoreSlim`.
- Before starting each handler task, `await semaphore.WaitAsync()` (this provides backpressure when saturated).
- Ensure the semaphore is released in `finally` inside `InvokeHandlerAsync`.
- Optionally keep `_runningHandlers` tracking, but consider tracking only after acquiring the semaphore.
- Alternative: per-event parallelism with `Task.WhenAll(handlersForThisEvent)` to keep handler parallelism while restoring per-event backpressure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copilot AI 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 changes the .NET BiDi EventDispatcher so event handlers are invoked without awaiting them, allowing multiple handlers (and multiple events) to be processed concurrently rather than serially blocking the event loop.

Changes:

  • Dispatch BiDi event handlers in a fire-and-forget fashion instead of awaiting each handler inline.
  • Track in-flight handler tasks and await their completion during DisposeAsync.
  • Centralize handler exception logging in a dedicated InvokeHandlerAsync wrapper.

Comment thread dotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment thread dotnet/src/webdriver/BiDi/EventDispatcher.cs
Comment thread dotnet/src/webdriver/BiDi/EventDispatcher.cs
@nvborisenko
nvborisenko merged commit d7b436a into SeleniumHQ:trunk Mar 12, 2026
20 of 21 checks passed
@nvborisenko
nvborisenko deleted the bidi-parallel-event-handlers branch March 12, 2026 19:00
krishnamohan-kothapalli pushed a commit to krishnamohan-kothapalli/selenium that referenced this pull request Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-dotnet .NET Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants