Table of Contents

Performance Considerations

This guide covers strategies for optimizing the performance of your WebDriverBiDi.NET automation.

Overview

Performance in browser automation involves multiple factors:

  • Command execution time
  • Network communication overhead
  • Event processing efficiency
  • Resource management
  • Browser responsiveness

Understanding and optimizing these factors can significantly improve automation speed and reliability.

Connection Type Performance

WebDriverBiDi.NET primarily uses WebSocket connections for browser communication. An alternative pipe-based connection is available for specialized scenarios.

WebSocket connections are the standard transport mechanism for WebDriver BiDi:

Characteristics:

  • Universal compatibility: Supported by all browsers with WebDriver BiDi
  • Network flexibility: Connect to local or remote browsers
  • Simple setup: Just provide a WebSocket URL
  • Low latency: a command to a local browser costs a serialize, a socket write, and the browser's own processing. No figure here is measured: the benchmarks in this repository deliberately measure no real connection

Performance:

  • Command execution: dominated by the browser's processing of the command rather than by the library
  • Event delivery: Real-time with minimal overhead
  • Suitable for all automation scenarios
// Standard WebSocket connection
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync("ws://localhost:9515/session/abc-123");

Pipe Connections (Advanced)

For specific scenarios where the browser and test runner are co-located, pipe connections provide an alternative transport:

When to consider:

  • Browser implementation supports pipe protocol (currently only Chromium-based browsers)
  • Both browser and tests run on the same machine
  • Absolute minimum latency is critical

Trade-offs:

  • Slightly lower latency: no socket, no WebSocket framing, and no handshake
  • No network stack overhead
  • Requires process lifecycle management
  • Limited browser support
  • No remote debugging capability

The sample uses MyChromiumPipeLauncher, the launcher of your own sketched in Browser Setup, which launches Chromium with a pipe connection:

// Your own IPipeServerProcessProvider, which launches Chromium with --remote-debugging-pipe and whose
// CreateTransport() installs a BiDi-over-CDP mapper (see Browser Setup).
MyChromiumPipeLauncher launcher = new MyChromiumPipeLauncher();

await launcher.StartAsync();
await launcher.LaunchBrowserAsync();

BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30), launcher.CreateTransport());
await driver.StartAsync("pipes");

Recommendation: Use WebSocket connections unless you have specific requirements for pipe-based transport and are using a compatible browser implementation.

Command Optimization

Parallel Command Execution

Execute independent commands in parallel:

// ❌ Slow: Sequential execution
var tree = await driver.BrowsingContext.GetTreeAsync(new());
var status = await driver.Session.StatusAsync(new());
var cookies = await driver.Storage.GetCookiesAsync(new());

// ✅ Fast: Parallel execution
Task<GetTreeCommandResult> treeTask = driver.BrowsingContext.GetTreeAsync(new());
Task<StatusCommandResult> statusTask = driver.Session.StatusAsync(new());
Task<GetCookiesCommandResult> cookiesTask = driver.Storage.GetCookiesAsync(new());

await Task.WhenAll(treeTask, statusTask, cookiesTask);

var parallelTree = treeTask.Result;
var parallelStatus = statusTask.Result;
var parallelCookies = cookiesTask.Result;

Batch Operations

// ❌ Slow: Multiple round trips
foreach (string url in urls)
{
    await driver.BrowsingContext.NavigateAsync(
        new NavigateCommandParameters(contextId, url));
}

// ✅ Fast: Parallel navigation in different contexts
List<Task<NavigateCommandResult>> navigationTasks = new();

foreach (var (url, context) in urls.Zip(contextIds))
{
    navigationTasks.Add(driver.BrowsingContext.NavigateAsync(
        new NavigateCommandParameters(context, url)));
}

await Task.WhenAll(navigationTasks);

Use Appropriate Readiness States

// For content-only needs
NavigateCommandParameters parameters1 = new NavigateCommandParameters(contextId, url)
{
    Wait = ReadinessState.Interactive  // Don't wait for images/CSS
};

// For visual validation
NavigateCommandParameters parameters2 = new NavigateCommandParameters(contextId, url)
{
    Wait = ReadinessState.Complete  // Wait for all resources
};

// For fastest possible navigation
NavigateCommandParameters parameters3 = new NavigateCommandParameters(contextId, url)
{
    Wait = ReadinessState.None  // Return once the navigation is committed
};

Smart Waiting

// ❌ Slow: Fixed delays
await driver.BrowsingContext.NavigateAsync(navParams);
await Task.Delay(5000);  // Always waits 5 seconds

// ✅ Fast: Wait for specific condition
await driver.BrowsingContext.NavigateAsync(navParams);

string waitScript = """
    new Promise((resolve) => {
        if (document.querySelector('.content-loaded')) {
            resolve(true);
        } else {
            const observer = new MutationObserver(() => {
                if (document.querySelector('.content-loaded')) {
                    observer.disconnect();
                    resolve(true);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
    })
    """;

await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(waitScript, new ContextTarget(contextId), true));

Script Execution Optimization

Minimize Script Calls

// ❌ Slow: Multiple script calls
var title = await GetScriptValue("document.title");
var url = await GetScriptValue("window.location.href");
var linkCount = await GetScriptValue("document.querySelectorAll('a').length");

// ✅ Fast: Single script call
string script = """
    ({
        title: document.title,
        url: window.location.href,
        linkCount: document.querySelectorAll('a').length
    })
    """;

EvaluateResult result = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(script, new ContextTarget(contextId), true));

if (result is EvaluateResultSuccess success &&
    success.Result is KeyValuePairCollectionRemoteValue remoteValue &&
    remoteValue.Value is RemoteValueDictionary data)
{
    string actualTitle = data["title"].As<StringRemoteValue>().Value;
    string actualUrl = data["url"].As<StringRemoteValue>().Value;
    long actualLinkCount = data["linkCount"].As<NumberRemoteValue>();
}

Efficient Element Operations

// ❌ Slow: Multiple element queries
for (int i = 0; i < 10; i++)
{
    var element = await FindElementAsync($".item-{i}");
    await ClickElementAsync(element);
}

// ✅ Fast: Batch element operations
string script = """
    Array.from(document.querySelectorAll('[class^=""item-""]'))
        .slice(0, 10)
        .forEach(el => el.click());
    """;

await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(script, new ContextTarget(contextId), false));

Event Processing Optimization

Use Async Event Handlers

// ❌ Slow: Synchronous handler blocks message processing
driver.Network.OnBeforeRequestSent.AddObserver((e) =>
{
    Thread.Sleep(1000);  // Blocks for 1 second!
    ProcessRequest(e);
});

// ✅ Fast: Async handler doesn't block
driver.Network.OnBeforeRequestSent.AddObserver(async (e) =>
{
    await Task.Delay(1000);  // Doesn't block message processing
    await ProcessRequestAsync(e);
},
ObservableEventHandlerOptions.RunHandlerAsynchronously);

Filter Events Early

// ❌ Slow: the expensive work runs for every response, including the ones thrown away
driver.Network.OnResponseCompleted.AddObserver((e) =>
{
    string summary = SummarizeResponse(e);
    if (e.Response.Url.Contains(".json"))
    {
        RecordSummary(summary);
    }
});

// ✅ Fast: decide first, so the expensive work runs only for the responses that are kept
driver.Network.OnResponseCompleted.AddObserver((e) =>
{
    if (!e.Response.Url.Contains(".json"))
    {
        return;  // Exit early
    }

    RecordSummary(SummarizeResponse(e));
});

Selective Event Subscription

// ❌ Slow: Subscribe to everything
SubscribeCommandParameters subscribe =
    new SubscribeCommandParameters(driver.Network.OnBeforeRequestSent.EventName);
subscribe.Events.Add(driver.Network.OnResponseStarted.EventName);
subscribe.Events.Add(driver.Network.OnResponseCompleted.EventName);
subscribe.Events.Add(driver.Network.OnFetchError.EventName);

// ✅ Fast: Only subscribe to what you need
SubscribeCommandParameters subscribeParameters =
    new SubscribeCommandParameters(driver.Network.OnResponseCompleted.EventName);

// Even better: Subscribe only for specific contexts
subscribeParameters.Contexts.Add(contextId);

Message Queue and High-Throughput Scenarios

Understanding the Transport Message Queue

WebDriverBiDi.NET uses an unbounded message queue to buffer incoming messages from the browser. This design choice provides flexibility and avoids blocking the connection, but has important implications for high-throughput scenarios.

Architecture:

  • Messages received from the WebSocket connection are queued immediately
  • A dedicated reader task processes messages sequentially
  • No limit on queue depth (unbounded)
  • Single-reader, single-writer for optimal throughput

Normal Operation: In typical usage, message processing is fast enough that the queue remains nearly empty. Messages arrive, get processed within milliseconds, and the queue clears immediately.

High-Throughput Risks:

// ⚠️ Problematic: Thousands of rapid events with slow handlers
driver.Network.OnBeforeRequestSent.AddObserver((e) =>
{
    // Slow synchronous operation (200ms)
    Thread.Sleep(200);
    ProcessRequest(e);
});

// If 100 events arrive per second:
// - Processing rate: 5 events/second (200ms each)
// - Queue growth: 95 events/second
// - After 10 seconds: ~950 messages queued
// - Memory usage grows unbounded

Symptoms of Queue Backlog

Monitor for these indicators:

  1. Rising Incoming Queue Depth: BiDiDriver.TransportDiagnostics.IncomingQueueDepth climbs and does not recover (see Monitoring and Diagnostics)
  2. Rising In-Flight Handler Count: The AsyncHandlerTaskCount EventSource event reports a persistently high value
  3. Increasing Memory Usage: Process memory grows during high-event periods
  4. Event Lag: Events processed long after they occurred
  5. Delayed Command Responses: Commands take longer as queue backs up
  6. OutOfMemoryException: In extreme cases with thousands of queued messages

Preventing Queue Backlog

1. Use Asynchronous Event Handlers

// ✅ Good: Async handler doesn't block message thread
driver.Network.OnBeforeRequestSent.AddObserver(
    async (e) =>
    {
        // Runs on task pool, doesn't block queue processing
        await Task.Delay(200);
        await ProcessRequestAsync(e);
    },
    ObservableEventHandlerOptions.RunHandlerAsynchronously
);

Impact:

  • Message processing thread continues immediately
  • Queue stays near-empty even with slow handlers
  • Multiple events can be processed concurrently

2. Keep Event Handlers Fast

// ❌ Bad: Heavy processing in handler
driver.Log.OnEntryAdded.AddObserver((e) =>
{
    // CPU-intensive operation
    var analysis = PerformComplexAnalysis(e.Text);
    SaveToDatabase(analysis);  // I/O operation
    SendNotification(analysis); // Network call
});

// ✅ Good: Offload heavy work
ConcurrentQueue<EntryAddedEventArgs> logQueue = new();

driver.Log.OnEntryAdded.AddObserver((e) =>
{
    // Fast: Just queue for later processing
    logQueue.Enqueue(e);
});

// Separate background task processes the queue
Task.Run(async () =>
{
    while (!cancellationToken.IsCancellationRequested)
    {
        if (logQueue.TryDequeue(out var logEvent))
        {
            var analysis = await PerformComplexAnalysisAsync(logEvent.Text ?? string.Empty);
            await SaveToDatabaseAsync(analysis);
            await SendNotificationAsync(analysis);
        }
        else
        {
            await Task.Delay(10);
        }
    }
});

3. Reduce Event Subscriptions

// ❌ Bad: Subscribe to high-frequency events you don't need
subscribe.Events.Add(driver.Network.OnBeforeRequestSent.EventName);   // Very high frequency
subscribe.Events.Add(driver.Network.OnResponseStarted.EventName);     // Very high frequency
subscribe.Events.Add(driver.Network.OnResponseCompleted.EventName);   // High frequency

// ✅ Good: Only subscribe to events you actually use
subscribe.Events.Add(driver.Network.OnResponseCompleted.EventName);   // Only this one

// ✅ Even better: Scope to specific contexts
subscribe.Events.Add(driver.Network.OnResponseCompleted.EventName);
subscribe.Contexts.Add(contextId);  // Only for this tab

4. Implement Event Throttling

// Throttle high-frequency events
DateTime lastProcessed = DateTime.MinValue;
TimeSpan throttleInterval = TimeSpan.FromMilliseconds(100);

driver.Network.OnBeforeRequestSent.AddObserver((e) =>
{
    DateTime now = DateTime.Now;
    if (now - lastProcessed < throttleInterval)
    {
        return;  // Skip this event
    }

    lastProcessed = now;
    ProcessRequest(e);
});

5. Use Event Sampling

// Sample 10% of events for analysis
Random random = new Random();

driver.Network.OnResponseCompleted.AddObserver((e) =>
{
    if (random.Next(100) < 10)  // 10% sample rate
    {
        AnalyzeResponse(e);
    }
});

Monitoring and Diagnostics

WebDriverBiDi.NET exposes two built-in signals for detecting message-processing backlog, plus process-level memory as a supplementary guardrail.

Incoming Queue Depth (TransportDiagnostics.IncomingQueueDepth)

BiDiDriver.TransportDiagnostics.IncomingQueueDepth returns the number of messages that have been received from the connection but not yet picked up by the reader task. Poll it on a timer to catch backlog directly:

// Poll the queue depth on a timer to detect backlog early. The property is
// safe to read concurrently with message production and consumption, and
// never throws, whatever point of the lifecycle the driver is at.
Timer queueDepthMonitor = new Timer(_ =>
{
    int depth = driver.TransportDiagnostics.IncomingQueueDepth;

    if (depth > 100)
    {
        Console.WriteLine($"⚠️ Incoming message queue depth: {depth}");
    }
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

The property is safe to read concurrently with message production and consumption. Each call to ConnectAsync installs a fresh queue whose depth begins at zero, so the value always reports the current connection's backlog alone; reading it before the first connect returns the count of any messages an already-open, adopted connection delivered ahead of ConnectAsync (normally 0; ConnectAsync discards them with a warning, as Connection Management describes), and reading it after a disconnect returns the depth of the remaining (normally fully drained) queue rather than throwing.

In-Flight Async Handler Tasks (AsyncHandlerTaskCount EventSource event)

When handlers are registered with ObservableEventHandlerOptions.RunHandlerAsynchronously, the reader task does not wait for them to complete — so a growing queue is not the only backlog symptom. The second symptom is a growing set of running async handler tasks. The WebDriverBiDi EventSource publishes an AsyncHandlerTaskCount event (verbose level) each time this count changes; subscribe with an EventListener:

private sealed class AsyncHandlerBacklogListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Name == "WebDriverBiDi")
        {
            this.EnableEvents(eventSource, EventLevel.Verbose);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.EventName == "AsyncHandlerTaskCount"
            && eventData.Payload is [int inFlightCount]
            && inFlightCount > 100)
        {
            Console.WriteLine($"⚠️ In-flight async handler tasks: {inFlightCount}");
        }
    }
}

Construct it where you want the counting to begin, and dispose it when you are finished:

// Subscribe to the AsyncHandlerTaskCount event raised by WebDriverBiDiEventSource
// (provider name: "WebDriverBiDi") to observe how many async handler tasks are
// in flight across all BiDiDriver instances in the process.
AsyncHandlerBacklogListener listener = new AsyncHandlerBacklogListener();

This counter is process-global across all BiDiDriver instances.

Process Memory (Supplementary)

Use process memory as an outer guardrail when the two signals above are not available to your diagnostic pipeline:

// Monitor memory usage
Process currentProcess = Process.GetCurrentProcess();

Timer memoryMonitor = new Timer(_ =>
{
    currentProcess.Refresh();
    long memoryMB = currentProcess.WorkingSet64 / (1024 * 1024);

    if (memoryMB > 500)  // Alert if over 500MB
    {
        Console.WriteLine($"⚠️ High memory usage: {memoryMB} MB");
    }
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

Practical Guidelines

For Low-Traffic Applications (< 100 events/second):

  • Default configuration works well
  • No special considerations needed
  • Synchronous event handlers are acceptable for quick operations

For Medium-Traffic Applications (100-1000 events/second):

  • Use RunHandlerAsynchronously for all handlers doing I/O
  • Keep handlers under 10ms for synchronous execution
  • Monitor memory usage during peak load

For High-Traffic Applications (> 1000 events/second):

  • Use RunHandlerAsynchronously for ALL event handlers
  • Implement event sampling or throttling
  • Consider reducing event subscriptions
  • Offload processing to background queues
  • Monitor memory continuously
  • Consider multiple driver instances to distribute load

Example: High-Throughput Network Monitoring

// Efficient high-throughput event handling
public class NetworkMonitor
{
    private readonly ConcurrentQueue<ResponseData> responseQueue = new();
    private readonly SemaphoreSlim processingSignal = new(0);
    private readonly CancellationTokenSource cancellation = new();
    private int eventCount = 0;

    public async Task StartAsync(BiDiDriver driver)
    {
        // Lightweight event handler - just queue
        driver.Network.OnResponseCompleted.AddObserver(
            async (e) =>
            {
                responseQueue.Enqueue(e.Response);
                processingSignal.Release();
                Interlocked.Increment(ref eventCount);
            },
            ObservableEventHandlerOptions.RunHandlerAsynchronously
        );

        // Background processor - handles heavy work
        _ = Task.Run(async () =>
        {
            while (!cancellation.Token.IsCancellationRequested)
            {
                await processingSignal.WaitAsync(cancellation.Token);

                if (responseQueue.TryDequeue(out var response))
                {
                    try
                    {
                        await AnalyzeResponseAsync(response);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Analysis error: {ex.Message}");
                    }
                }
            }
        });

        // Metrics reporter
        _ = Task.Run(async () =>
        {
            while (!cancellation.Token.IsCancellationRequested)
            {
                await Task.Delay(TimeSpan.FromSeconds(10));
                int count = Interlocked.Exchange(ref eventCount, 0);
                int queueDepth = responseQueue.Count;
                Console.WriteLine($"Events/sec: {count / 10.0:F1}, Queue depth: {queueDepth}");
            }
        });
    }

    private async Task AnalyzeResponseAsync(ResponseData response)
    {
        // Heavy analysis happens here, off the message thread
        await Task.Delay(50);  // Simulated analysis
    }

    public void Stop()
    {
        cancellation.Cancel();
    }
}

Resource Management

Connection Pooling

public class DriverPool
{
    private readonly Stack<BiDiDriver> availableDrivers = new();
    private readonly int maxPoolSize = 5;
    private readonly SemaphoreSlim semaphore;

    public DriverPool()
    {
        semaphore = new SemaphoreSlim(maxPoolSize, maxPoolSize);
    }

    public async Task<BiDiDriver> AcquireAsync(string webSocketUrl)
    {
        await semaphore.WaitAsync();

        lock (availableDrivers)
        {
            if (availableDrivers.Count > 0)
            {
                return availableDrivers.Pop();
            }
        }

        // Create new driver if none available
        BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
        await driver.StartAsync(webSocketUrl);
        return driver;
    }

    public async Task ReleaseAsync(BiDiDriver driver)
    {
        bool returnedToPool;
        lock (availableDrivers)
        {
            returnedToPool = availableDrivers.Count < maxPoolSize;
            if (returnedToPool)
            {
                availableDrivers.Push(driver);
            }
        }

        // Shut the surplus driver down outside the lock, and await it rather than blocking with
        // .Wait(): blocking here would hold the lock across I/O and can deadlock, and a faulted
        // StopAsync would surface as an AggregateException instead of the original exception.
        if (!returnedToPool)
        {
            await driver.StopAsync();
        }

        semaphore.Release();
    }
}
// Usage
DriverPool pool = new DriverPool();

BiDiDriver driver = await pool.AcquireAsync(websocketUrl);
try
{
    // Use driver
    await driver.BrowsingContext.NavigateAsync(navParams);
}
finally
{
    await pool.ReleaseAsync(driver);
}

Memory Management

// Always clean up data collectors
AddDataCollectorCommandResult collector =
    await driver.Network.AddDataCollectorAsync(collectorParams);

try
{
    // Use collector
    await CaptureNetworkTraffic();

    // Read the collected data, disowning it so the collector frees it as it is retrieved. This has
    // to happen while the collector still exists, so it belongs here rather than after the removal.
    GetDataCommandParameters getDataParams =
        new GetDataCommandParameters(requestId, DataType.Request)
    {
        CollectorId = collector.CollectorId,
        DisownCollectedData = true  // Free memory after retrieval
    };
    GetDataCommandResult data = await driver.Network.GetDataAsync(getDataParams);
    Console.WriteLine($"Collected {data.Bytes.Value.Length} bytes");
}
finally
{
    await driver.Network.RemoveDataCollectorAsync(
        new RemoveDataCollectorCommandParameters(collector.CollectorId));
}

Observer Cleanup

public class ManagedObserver<T> : IDisposable where T : WebDriverBiDiEventArgs
{
    private EventObserver<T>? observer;

    public ManagedObserver(ObservableEvent<T> observableEvent, Func<T, Task> handler)
    {
        observer = observableEvent.AddObserver(handler);
    }

    public void Dispose()
    {
        observer?.Unobserve();
        observer = null;
    }
}
// Usage with automatic cleanup
using (new ManagedObserver<EntryAddedEventArgs>(
    driver.Log.OnEntryAdded,
    (e) => { Console.WriteLine(e.Text); return Task.CompletedTask; }))
{
    // Observer active here
    await driver.BrowsingContext.NavigateAsync(navParams);
}
// Observer automatically removed

Network Optimization

Reduce Network Interception Overhead

// ❌ Slow: Intercept everything
AddInterceptCommandParameters slowIntercept =
    new AddInterceptCommandParameters(InterceptPhase.BeforeRequestSent);
await driver.Network.AddInterceptAsync(slowIntercept);

// ✅ Fast: Intercept only what you need
AddInterceptCommandParameters fastIntercept =
    new AddInterceptCommandParameters(InterceptPhase.BeforeRequestSent);
fastIntercept.UrlPatterns.AddRange(
[
    new UrlPatternPattern { HostName = "api.example.com" }
]);
await driver.Network.AddInterceptAsync(fastIntercept);

Block Unnecessary Resources

URL patterns cannot select requests by resource type or file extension, so blocking by type means intercepting every request, as in the slow case above, and letting through each one the handler does not block. Every request then waits for a round trip to your handler; weigh that against the downloads the blocking saves.

// Speed up page loads by blocking images, CSS, fonts. URL patterns cannot match a
// kind of resource or a file extension, so this intercepts every request and
// chooses by the request's destination; every other request is let through.
HashSet<string> blockedDestinations = ["image", "style", "font"];
AddInterceptCommandParameters intercept =
    new AddInterceptCommandParameters(InterceptPhase.BeforeRequestSent);

await driver.Network.AddInterceptAsync(intercept);

driver.Network.OnBeforeRequestSent.AddObserver(async (e) =>
{
    if (!e.IsBlocked)
    {
        return;
    }

    if (blockedDestinations.Contains(e.Request.Destination))
    {
        await driver.Network.FailRequestAsync(
            new FailRequestCommandParameters(e.Request.RequestId));
    }
    else
    {
        await driver.Network.ContinueRequestAsync(
            new ContinueRequestCommandParameters(e.Request.RequestId));
    }
},
ObservableEventHandlerOptions.RunHandlerAsynchronously);

Browser Context Optimization

Reuse Contexts

// ❌ Slow: Create new context for each test
[Test]
public async Task SlowTest1()
{
    CreateCommandResult ctx = await driver.BrowsingContext.CreateAsync(
        new CreateCommandParameters(CreateType.Tab));
    // Test...
    await driver.BrowsingContext.CloseAsync(new CloseCommandParameters(ctx.BrowsingContextId));
}

// ✅ Fast: Reuse context, just navigate
private string sharedContextId;

[SetUp]
public async Task Setup()
{
    CreateCommandResult ctx = await driver.BrowsingContext.CreateAsync(
        new CreateCommandParameters(CreateType.Tab));
    sharedContextId = ctx.BrowsingContextId;
}

[Test]
public async Task FastTest1()
{
    await driver.BrowsingContext.NavigateAsync(
        new NavigateCommandParameters(sharedContextId, url));
    // Test...
}

[TearDown]
public async Task Teardown()
{
    await driver.BrowsingContext.CloseAsync(
        new CloseCommandParameters(sharedContextId));
}

Use User Contexts for Isolation

// Create isolated user context once
CreateUserContextCommandResult userContext =
    await driver.Browser.CreateUserContextAsync(new());

// Create multiple tabs in same user context (shares cache, cookies)
List<string> contextIds = new();
for (int i = 0; i < 5; i++)
{
    CreateCommandResult tab = await driver.BrowsingContext.CreateAsync(
        new CreateCommandParameters(CreateType.Tab)
        {
            UserContextId = userContext.UserContextId
        });
    contextIds.Add(tab.BrowsingContextId);
}

// All tabs share cookies and cache = faster subsequent loads

Measurement and Profiling

Performance Tracking

public class PerformanceTracker
{
    private readonly Dictionary<string, List<TimeSpan>> metrics = new();

    public async Task<T> TrackAsync<T>(string operationName, Func<Task<T>> operation)
    {
        DateTime start = DateTime.Now;

        try
        {
            return await operation();
        }
        finally
        {
            TimeSpan duration = DateTime.Now - start;

            if (!metrics.ContainsKey(operationName))
            {
                metrics[operationName] = new List<TimeSpan>();
            }

            metrics[operationName].Add(duration);
        }
    }

    public void PrintStats()
    {
        Console.WriteLine("\n Performance Statistics");
        Console.WriteLine("========================");

        foreach (var kvp in metrics)
        {
            var durations = kvp.Value;
            double avgMs = durations.Average(d => d.TotalMilliseconds);
            double minMs = durations.Min(d => d.TotalMilliseconds);
            double maxMs = durations.Max(d => d.TotalMilliseconds);

            Console.WriteLine($"\n{kvp.Key}:");
            Console.WriteLine($"  Count: {durations.Count}");
            Console.WriteLine($"  Avg:   {avgMs:F2}ms");
            Console.WriteLine($"  Min:   {minMs:F2}ms");
            Console.WriteLine($"  Max:   {maxMs:F2}ms");
        }
    }
}
// Usage
PerformanceTracker tracker = new PerformanceTracker();

await tracker.TrackAsync("Navigation", async () =>
    await driver.BrowsingContext.NavigateAsync(navParams));

await tracker.TrackAsync("Script Execution", async () =>
    await driver.Script.EvaluateAsync(evalParams));

tracker.PrintStats();

Bottleneck Identification

public async Task AnalyzePageLoadAsync(BiDiDriver driver, string contextId, string url)
{
    Dictionary<string, int> resourceCounts = new();
    Dictionary<string, long> resourceSizes = new();
    DateTime startTime = DateTime.Now;

    driver.Network.OnResponseCompleted.AddObserver((e) =>
    {
        Uri uri = new Uri(e.Response.Url);
        string extension = Path.GetExtension(uri.LocalPath).ToLower();

        resourceCounts[extension] = resourceCounts.GetValueOrDefault(extension) + 1;
        resourceSizes[extension] = resourceSizes.GetValueOrDefault(extension) +
            (long)e.Response.BytesReceived;
    });

    SubscribeCommandParameters subscribe =
        new SubscribeCommandParameters(driver.Network.OnResponseCompleted.EventName);
    await driver.Session.SubscribeAsync(subscribe);

    await driver.BrowsingContext.NavigateAsync(
        new NavigateCommandParameters(contextId, url)
        { Wait = ReadinessState.Complete });

    TimeSpan loadTime = DateTime.Now - startTime;

    Console.WriteLine($"\nPage Load Analysis for {url}");
    Console.WriteLine($"Total Time: {loadTime.TotalSeconds:F2}s");
    Console.WriteLine("\nResources by Type:");

    foreach (var kvp in resourceCounts.OrderByDescending(x => x.Value))
    {
        long sizeKB = resourceSizes[kvp.Key] / 1024;
        Console.WriteLine($"  {kvp.Key,-10} {kvp.Value,3} files  {sizeKB,6} KB");
    }
}

Caching Strategies

Cache Repeated Queries

public class CachedContextInfo
{
    private readonly Dictionary<string, BrowsingContextInfo> cache = new();
    private readonly BiDiDriver driver;

    public async Task<BrowsingContextInfo> GetContextInfoAsync(string contextId)
    {
        if (cache.TryGetValue(contextId, out var cachedInfo))
        {
            return cachedInfo;
        }

        GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
            new GetTreeCommandParameters { RootBrowsingContextId = contextId });

        if (tree.ContextTree.Count > 0)
        {
            cache[contextId] = tree.ContextTree[0];
            return tree.ContextTree[0];
        }

        throw new Exception("Context not found");
    }

    public void InvalidateCache(string contextId)
    {
        cache.Remove(contextId);
    }
}
// Usage
BrowsingContextInfo info = await cachedInfo.GetContextInfoAsync(contextId);
cachedInfo.InvalidateCache(contextId);

Best Practices Summary

  1. Parallelize Independent Operations: Use Task.WhenAll for concurrent execution
  2. Minimize Round Trips: Batch operations when possible
  3. Use Appropriate Wait States: Don't wait for more than you need
  4. Filter Events Early: Process only relevant events
  5. Clean Up Resources: Remove observers, collectors, and intercepts
  6. Reuse Contexts: Don't create/destroy contexts unnecessarily
  7. Block Unnecessary Resources: Speed up loads by blocking images/CSS
  8. Use Async Handlers: Don't block message processing
  9. Cache Repeated Queries: Store frequently accessed data
  10. Profile Your Code: Measure to find actual bottlenecks

Performance Checklist

  • [ ] Commands executed in parallel where possible?
  • [ ] Appropriate navigation readiness state used?
  • [ ] Event subscriptions limited to necessary events?
  • [ ] Event handlers are async for long operations?
  • [ ] Resources cleaned up properly?
  • [ ] Contexts reused across tests?
  • [ ] Network interception scoped appropriately?
  • [ ] Data collectors removed when done?
  • [ ] Observers unsubscribed when no longer needed?
  • [ ] Performance metrics collected and analyzed?

Next Steps