From c1e1f74557accbbd5ee77daac162d2ec0c08980d Mon Sep 17 00:00:00 2001 From: Enciphered Date: Tue, 5 May 2026 23:13:12 +0500 Subject: [PATCH] Stess testing added, it wasn't very helpful cause the server could easily handle 100 chrome instances at the same time. May be would be better to stress test with pure http requests instead. Leaving this for later. Co-authored-by: Copilot --- Testing/stress-test-01/Program.cs | 193 +++++++++++++++++++ Testing/stress-test-01/README.md | 48 +++++ Testing/stress-test-01/stress-test-01.csproj | 13 ++ 3 files changed, 254 insertions(+) create mode 100644 Testing/stress-test-01/Program.cs create mode 100644 Testing/stress-test-01/README.md create mode 100644 Testing/stress-test-01/stress-test-01.csproj diff --git a/Testing/stress-test-01/Program.cs b/Testing/stress-test-01/Program.cs new file mode 100644 index 0000000..4d96e87 --- /dev/null +++ b/Testing/stress-test-01/Program.cs @@ -0,0 +1,193 @@ +using Microsoft.Playwright; + +const string defaultSelector = "button:has-text('Click to')"; + +var targetUrl = PromptForTargetUrl(); +var options = StressOptions.Parse(args, defaultSelector, targetUrl); + +Console.WriteLine($"URL: {options.TargetUrl}"); +Console.WriteLine($"Instances: {options.InstanceCount}"); +Console.WriteLine($"Interval: {options.IntervalMs}ms"); +Console.WriteLine($"Selector: {options.ButtonSelector}"); +Console.WriteLine($"Headless: {options.Headless}"); +Console.WriteLine("Press Ctrl+C to stop."); + +using var cts = new CancellationTokenSource(); +Console.CancelKeyPress += (_, eventArgs) => +{ + eventArgs.Cancel = true; + cts.Cancel(); +}; + +var workers = Enumerable.Range(1, options.InstanceCount) + .Select(instanceId => RunWorkerAsync(instanceId, options, cts.Token)) + .ToArray(); + +await Task.WhenAll(workers); + +static string PromptForTargetUrl() +{ + while (true) + { + Console.Write("Enter target URL: "); + var input = Console.ReadLine(); + + if (!string.IsNullOrWhiteSpace(input) + && Uri.TryCreate(input, UriKind.Absolute, out var uri) + && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + { + return uri.ToString(); + } + + Console.WriteLine("Invalid URL. Please enter a full http/https URL."); + } +} + +static async Task RunWorkerAsync(int instanceId, StressOptions options, CancellationToken cancellationToken) +{ + try + { + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = options.Headless + }); + + var page = await browser.NewPageAsync(); + await page.GotoAsync(options.TargetUrl, new PageGotoOptions + { + WaitUntil = WaitUntilState.DOMContentLoaded, + Timeout = 30000 + }); + + Console.WriteLine($"[{instanceId}] connected"); + var clickCount = 0; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + // HTMX can replace the button after each request, so resolve it fresh every loop. + var button = page.Locator(options.ButtonSelector).First; + await button.WaitForAsync(new LocatorWaitForOptions + { + State = WaitForSelectorState.Visible, + Timeout = 5000 + }); + await button.ClickAsync(new LocatorClickOptions + { + Timeout = 5000, + Force = true + }); + + clickCount += 1; + if (clickCount % 25 == 0) + { + Console.WriteLine($"[{instanceId}] clicks sent: {clickCount}"); + } + } + catch (TimeoutException) + { + Console.WriteLine($"[{instanceId}] click timeout"); + } + catch (PlaywrightException ex) + { + Console.WriteLine($"[{instanceId}] click error: {ex.Message}"); + } + + try + { + await Task.Delay(options.IntervalMs, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + } + + await page.CloseAsync(); + Console.WriteLine($"[{instanceId}] stopped"); + } + catch (OperationCanceledException) + { + Console.WriteLine($"[{instanceId}] canceled"); + } + catch (Exception ex) + { + Console.WriteLine($"[{instanceId}] fatal: {ex.Message}"); + if (ex.Message.Contains("Executable doesn't exist", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Install browser binaries first:"); + Console.WriteLine("./bin/Debug/net10.0/.playwright/node/linux-x64/node ./bin/Debug/net10.0/.playwright/package/cli.js install chromium"); + } + } +} + +internal sealed record StressOptions( + string TargetUrl, + int InstanceCount, + int IntervalMs, + string ButtonSelector, + bool Headless) +{ + public static StressOptions Parse(string[] args, string defaultSelector, string targetUrl) + { + var instanceCount = ParsePositiveInt(Environment.GetEnvironmentVariable("INSTANCE_COUNT"), 20); + var intervalMs = ParsePositiveInt(Environment.GetEnvironmentVariable("CLICK_INTERVAL_MS"), 200); + var buttonSelector = Environment.GetEnvironmentVariable("BUTTON_SELECTOR") ?? defaultSelector; + var headless = ParseBool(Environment.GetEnvironmentVariable("HEADLESS"), true); + + foreach (var arg in args) + { + var split = arg.Split('=', 2, StringSplitOptions.TrimEntries); + if (split.Length != 2) + { + continue; + } + + var key = split[0].TrimStart('-', '/').ToLowerInvariant(); + var value = split[1]; + + switch (key) + { + case "instances": + case "instancecount": + instanceCount = ParsePositiveInt(value, instanceCount); + break; + case "interval": + case "intervalms": + intervalMs = ParsePositiveInt(value, intervalMs); + break; + case "selector": + case "buttonselector": + buttonSelector = value; + break; + case "headless": + headless = ParseBool(value, headless); + break; + } + } + + return new StressOptions(targetUrl, instanceCount, intervalMs, buttonSelector, headless); + } + + private static int ParsePositiveInt(string? rawValue, int fallback) + { + if (!string.IsNullOrWhiteSpace(rawValue) && int.TryParse(rawValue, out var parsed) && parsed > 0) + { + return parsed; + } + + return fallback; + } + + private static bool ParseBool(string? rawValue, bool fallback) + { + if (!string.IsNullOrWhiteSpace(rawValue) && bool.TryParse(rawValue, out var parsed)) + { + return parsed; + } + + return fallback; + } +} diff --git a/Testing/stress-test-01/README.md b/Testing/stress-test-01/README.md new file mode 100644 index 0000000..6ae962a --- /dev/null +++ b/Testing/stress-test-01/README.md @@ -0,0 +1,48 @@ +# stress-test-01 + +C# .NET console app that launches 20 Playwright Chromium instances, navigates to the deployed URL, and clicks a button every 200ms. + +## Defaults + +- URL: prompted from user input every run +- Instances: `20` +- Interval: `200` ms +- Button selector: `button:visible` (first match) +- Headless: `true` + +## Run + +```bash +cd Testing/stress-test-01 +dotnet restore +dotnet build +./bin/Debug/net10.0/.playwright/node/linux-x64/node ./bin/Debug/net10.0/.playwright/package/cli.js install chromium +dotnet run +``` + +PowerShell alternative: + +```bash +pwsh bin/Debug/net10.0/playwright.ps1 install chromium +``` + +The app will prompt: + +```text +Enter target URL: +``` + +## Optional overrides + +Use either environment variables or CLI args: + +- `INSTANCE_COUNT` or `--instances=` +- `CLICK_INTERVAL_MS` or `--intervalms=` +- `BUTTON_SELECTOR` or `--selector=` +- `HEADLESS` or `--headless=` + +Example: + +```bash +INSTANCE_COUNT=20 CLICK_INTERVAL_MS=200 dotnet run +``` diff --git a/Testing/stress-test-01/stress-test-01.csproj b/Testing/stress-test-01/stress-test-01.csproj new file mode 100644 index 0000000..7eef001 --- /dev/null +++ b/Testing/stress-test-01/stress-test-01.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + enable + enable + latest + + + + + +