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 <copilot@github.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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=<value>`
|
||||
- `CLICK_INTERVAL_MS` or `--intervalms=<value>`
|
||||
- `BUTTON_SELECTOR` or `--selector=<value>`
|
||||
- `HEADLESS` or `--headless=<true|false>`
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
INSTANCE_COUNT=20 CLICK_INTERVAL_MS=200 dotnet run
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Playwright" Version="1.54.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user