Skip to content
ConsoleMonster

Stress test

Keep thirty-two workers, session counters, CPU trends and captured output readable under sustained updates.

Demos Dashboards Showcase

Use it when a busy worker pool must remain readable while updates and tool output arrive together.

  • Live
  • 140×48
  • PowerShell 7.4+
  • .NET 8.0+
Stress test 140×4824-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; both languages draw the same cells.

PowerShellstress.ps1View on GitHub
# Stress test: thirty-two workers, session telemetry and captured tool output.
param(
    [switch]$StaticSnapshot,
    [ValidateRange(0, 6)][int]$Step = 0
)

Import-Module ConsoleMonster

# Data: fixed snapshot samples; the bounded live workload touches no services or files.
$workers = 32
$seconds = 6
$actions = 'parsing', 'resolving', 'evaluating', 'scoring', 'writing'
$sampleRates = @(0..47 | ForEach-Object { 18000 + ($_ * 7919 % 11000) })
$sampleCpu = @(0..47 | ForEach-Object { 42 + ($_ * 17 % 74) })
$messages = 'cache miss for key 412', 'retrying request 28 (attempt 2)', 'connection pool at 76% capacity', 'GC pause 8 ms', 'flushed 960 records'
$culture = [cultureinfo]::InvariantCulture
$snapshot = $StaticSnapshot -or [Console]::IsInputRedirected
$tick = if ($snapshot) { $Step } else { 0 }
$samples = $tick * 8
$posted = $tick * 128000L

# Layout: all thirty-two lanes, the update sparkline, CPU chart and session counters remain visible.
$lanes = New-ConsoleLanes -Name workers {
    foreach ($id in 0..31) {
        $lane = @{
            Key = "w$id"
            Label = 'worker {0:00}' -f ($id + 1)
            Total = 100000
        }
        if ($tick -gt 0) {
            $done = if ($tick -eq 6) { 100000 } else { $tick * 3200 + $id * 97 % 400 }
            $failed = [int][Math]::Truncate($done / 97)
            $warned = [int][Math]::Truncate($done / 17)
            $lane.Done = $done
            $lane.Failed = $failed
            $lane.Warned = $warned
            $lane.Passed = $done - $failed - $warned
            $lane.Status = if ($tick -eq 6) { 'Pass' } else { 'Running' }
            $lane.Detail = $actions[($id + $tick) % $actions.Count] + ' item ' + $done.ToString('#,0', $culture)
        }
        New-ConsoleLane @lane
    }
}
$stats = [ordered]@{
    'Updates posted' = $posted.ToString('#,0', $culture)
    'Updates applied' = $posted.ToString('#,0', $culture)
    'Update rate' = if ($samples) { $sampleRates[$samples - 1].ToString('#,0', $culture) + '/s' } else { '-' }
    Frames = ($tick * 15).ToString('#,0', $culture)
    'Frame rate' = if ($tick) { '15.0 fps' } else { '-' }
    CPU = if ($samples) { $sampleCpu[$samples - 1].ToString('#,0', $culture) + '%' } else { '-' }
    'CPU share' = if ($samples) { ($sampleCpu[$samples - 1] / 8).ToString('0.0', $culture) + '% of 8 cores' } else { '-' }
    'Stray writes' = ($tick * 4).ToString($culture)
    'Dropped / errors' = '0 / 0'
}
$dashboard = New-ConsoleDashboard -Title '[bold]Stress test[/]' -Subtitle '32 workers · captured tool output · 6s' -Footer '[muted]32 workers    15 frames/s    Ctrl+C stop[/]' {
    New-ConsoleColumns {
        New-ConsolePanel -Title Workers -Weight 2.2 { $lanes }
        New-ConsolePanel -Title Session {
            New-ConsoleKeyValue -Name stats -Data $stats
            New-ConsoleRule 'updates/s'
            $values = @($sampleRates | Select-Object -First $samples)
            New-ConsoleSparkline -Name rate -Height 3 -Unit '/s' -Value $values
            New-ConsoleRule 'CPU % (100% = one core)'
            New-ConsoleLineChart -Name cpu -Height 6 -Minimum 0 -Unit '%' {
                New-ConsoleChartSeries cpu -Tone Accent2 -Value @($sampleCpu | Select-Object -First $samples)
            }
        }
    }
    New-ConsolePanel -Title 'Captured output' -Fill {
        New-ConsoleLog -Height 6 -NoTime {
            for ($i = 0; $i -lt $tick * 4; $i++) {
                $level = if ($i % 7 -eq 0) { 'err' } else { 'info' }
                New-ConsoleLogEntry ('stray-' + ($i % 4) + "-${i}: FakeLib: " + $messages[$i % 5]) -Level $level
            }
        }
    }
}
$dashboard.ShowClock = $false

# Behaviour: each runspace posts native lane updates; worker zero samples real session and process counters.
if ($snapshot) {
    $dashboard | Write-ConsoleWidget
    return
}
0..31 | Start-ConsoleDashboard $dashboard -Parallel -ThrottleLimit $workers -ArgumentList $seconds, $actions, $messages {
    param($Id, $Seconds, $Actions, $Messages)
    $culture = [cultureinfo]::InvariantCulture
    $watch = [System.Diagnostics.Stopwatch]::StartNew()
    $null = Get-Random -SetSeed ($Id * 7919)
    $key = "w$Id"
    $done = 0
    $lastSample = 0.0
    $lastPosted = 0L
    $process = Get-Process -Id $PID
    $lastCpu = $process.TotalProcessorTime.TotalSeconds
    Update-ConsoleLane -Name workers -Key $key -Status Running -Detail starting
    try {
        while ($watch.Elapsed.TotalSeconds -lt $Seconds -and -not $ConsoleSession.Cancellation.IsCancellationRequested) {
            # Batch equal outcomes without giving up result counters or queue coalescing.
            $fail = (Get-Random -Minimum 0 -Maximum 3)
            $warn = (Get-Random -Minimum 1 -Maximum 5)
            $pass = 40 - $fail - $warn
            Update-ConsoleLane -Name workers -Key $key -Result Pass -Steps $pass -Detail ($Actions[$done % 5] + " item $done")
            if ($warn) { Update-ConsoleLane -Name workers -Key $key -Result Warn -Steps $warn }
            if ($fail) { Update-ConsoleLane -Name workers -Key $key -Result Fail -Steps $fail }
            $done += 40
            if ($Id -lt 4 -and $done % 400 -eq 0) {
                $line = "stray-${Id}-${done}: FakeLib: " + $Messages[($done / 400) % $Messages.Count]
                if ($done % 2800 -eq 0) { Write-Warning $line } else { Write-Host $line }
            }
            $now = $watch.Elapsed.TotalSeconds
            if ($Id -eq 0 -and $now - $lastSample -ge 0.25) {
                $process.Refresh()
                $cpuNow = $process.TotalProcessorTime.TotalSeconds
                $interval = $now - $lastSample
                $core = ($cpuNow - $lastCpu) / $interval * 100
                $posted = $ConsoleSession.EventsPosted
                $rate = ($posted - $lastPosted) / $interval
                Add-ConsoleValue -Name rate -Value $rate
                Add-ConsoleValue -Name cpu -Key cpu -Value $core
                Set-ConsoleValue -Name stats -Key 'Updates posted' -Value $posted.ToString('#,0', $culture)
                Set-ConsoleValue -Name stats -Key 'Updates applied' -Value $ConsoleSession.EventsApplied.ToString('#,0', $culture)
                Set-ConsoleValue -Name stats -Key 'Update rate' -Value ($rate.ToString('#,0', $culture) + '/s')
                Set-ConsoleValue -Name stats -Key Frames -Value $ConsoleSession.FramesRendered.ToString('#,0', $culture)
                Set-ConsoleValue -Name stats -Key 'Frame rate' -Value (($ConsoleSession.FramesRendered / $now).ToString('0.0', $culture) + ' fps')
                Set-ConsoleValue -Name stats -Key CPU -Value ($core.ToString('#,0', $culture) + '%')
                Set-ConsoleValue -Name stats -Key 'CPU share' -Value (($core / [Environment]::ProcessorCount).ToString('0.0', $culture) + '% of ' + [Environment]::ProcessorCount + ' cores')
                Set-ConsoleValue -Name stats -Key 'Stray writes' -Value 'capturing 4 writers'
                Set-ConsoleValue -Name stats -Key 'Dropped / errors' -Value ($ConsoleSession.EventsDropped.ToString() + ' / ' + $ConsoleSession.ApplyErrors)
                $lastSample = $now
                $lastCpu = $cpuNow
                $lastPosted = $posted
            }
            Start-Sleep -Milliseconds 8
        }
        Update-ConsoleLane -Name workers -Key $key -Status Pass
    } finally {
        $process.Dispose()
    }
}
C#Program.csView on GitHub
// Stress test: thirty-two workers, fast handles, session telemetry and captured tool output.
using System.Diagnostics;
using System.Globalization;
using ConsoleMonster;
using ConsoleMonster.Layout;
using ConsoleMonster.Live;
using ConsoleMonster.Styling;
using ConsoleMonster.Widgets;

// Data: seeded snapshot samples; live work is bounded and touches no external service or file.
const int Workers = 32;
const int Seconds = 6;
string[] actions = { "parsing", "resolving", "evaluating", "scoring", "writing" };
double[] sampleRates = Enumerable.Range(0, 48).Select(i => (double)(18000 + (i * 7919 % 11000))).ToArray();
double[] sampleCpu = Enumerable.Range(0, 48).Select(i => (double)(42 + (i * 17 % 74))).ToArray();
string[] messages = { "cache miss for key 412", "retrying request 28 (attempt 2)", "connection pool at 76% capacity", "GC pause 8 ms", "flushed 960 records" };
int stepAt = Array.IndexOf(args, "--step");
int step = stepAt < 0 ? 0 : int.Parse(args[stepAt + 1], CultureInfo.InvariantCulture);
if (step < 0 || step > 6) throw new ArgumentOutOfRangeException(nameof(step));
bool snapshot = args.Contains("--snapshot") || (!args.Contains("--interactive") && Console.IsInputRedirected);
string Number(double value) => value.ToString("#,0", CultureInfo.InvariantCulture);

// Layout: retain every worker row, the session counters, update sparkline, CPU chart and captured output.
Lanes lanes = new Lanes("workers");
for (int id = 0; id < Workers; id++) {
    Lane lane = lanes.Add("w" + id, "worker " + (id + 1).ToString("00", CultureInfo.InvariantCulture), 100000);
    if (snapshot && step > 0) {
        lane.Done = step == 6 ? 100000 : step * 3200 + id * 97 % 400;
        lane.Failed = (int)lane.Done / 97;
        lane.Warned = (int)lane.Done / 17;
        lane.Passed = (int)lane.Done - lane.Failed - lane.Warned;
        lane.Status = step == 6 ? StatusKind.Pass : StatusKind.Running;
        lane.Detail = actions[(id + step) % actions.Length] + " item " + Number(lane.Done);
    }
}
int samples = snapshot ? step * 8 : 0;
long posted = snapshot ? step * 128000L : 0;
KeyValueList stats = new KeyValueList("stats").Add("Updates posted", Number(posted))
    .Add("Updates applied", Number(posted)).Add("Update rate", samples == 0 ? "-" : Number(sampleRates[samples - 1]) + "/s")
    .Add("Frames", snapshot ? Number(step * 15) : "0").Add("Frame rate", step == 0 ? "-" : "15.0 fps")
    .Add("CPU", samples == 0 ? "-" : Number(sampleCpu[samples - 1]) + "%")
    .Add("CPU share", samples == 0 ? "-" : (sampleCpu[samples - 1] / 8).ToString("0.0", CultureInfo.InvariantCulture) + "% of 8 cores")
    .Add("Stray writes", (step * 4).ToString(CultureInfo.InvariantCulture)).Add("Dropped / errors", "0 / 0");
Sparkline rate = new Sparkline("rate") { Height = 3, Unit = "/s" };
rate.AddRange(sampleRates.Take(samples));
LineChart cpu = new LineChart("cpu") { Height = 6, Min = 0, Unit = "%" };
cpu.AddSeries("cpu", ThemeToken.Accent2);
foreach (double value in sampleCpu.Take(samples)) cpu.Add("cpu", value);
LogRegion output = new LogRegion { PreferredHeight = 6, ShowTime = false };
if (snapshot) for (int i = 0; i < step * 4; i++) output.Write("stray-" + i % 4 + "-" + i + ": FakeLib: " + messages[i % 5], i % 7 == 0 ? "err" : "info");
Rows body = new Rows(new Columns().Add(new Panel(lanes, "Workers"), LayoutSize.Fraction(2.2))
    .Add(new Panel(new Rows(stats, new Rule("updates/s"), rate, new Rule("CPU % (100% = one core)"), cpu), "Session")))
    .Add(new Panel(output, "Captured output"), LayoutSize.Fraction());
Dashboard dashboard = new Dashboard("[bold]Stress test[/]", body) {
    Subtitle = "32 workers · captured tool output · 6s", ShowClock = false,
    Footer = "[muted]32 workers    15 frames/s    Ctrl+C stop[/]"
};

// Behaviour: snapshots use fixed samples; live telemetry measures this run and fast handles queue worker updates.
if (snapshot) { Terminal.Write(dashboard); return; }
using LiveSession session = new LiveSession(dashboard);
session.Run(live => {
    var workerHandle = lanes.Handle(live);
    var statHandle = stats.Handle(live);
    var rateHandle = rate.Handle(live);
    var cpuHandle = cpu.Handle(live);
    using CancellationTokenSource stop = CancellationTokenSource.CreateLinkedTokenSource(live.Cancellation);
    Thread[] workers = new Thread[Workers];
    long captured = 0;
    for (int id = 0; id < Workers; id++) {
        int worker = id;
        workers[id] = new Thread(() => {
            Random random = new Random(worker * 7919);
            string key = "w" + worker;
            workerHandle.Start(key, "starting");
            int done = 0;
            while (!stop.IsCancellationRequested) {
                for (int burst = 0; burst < 40; burst++) {
                    double roll = random.NextDouble();
                    workerHandle.Advance(key, roll < 0.03 ? StatusKind.Fail : roll < 0.1 ? StatusKind.Warn : StatusKind.Pass,
                        done % 97 == 0 ? actions[done % actions.Length] + " item " + done : null);
                    done++;
                }
                if (worker < 4 && done % 400 == 0) {
                    string line = "stray-" + worker + "-" + done + ": FakeLib: " + messages[done / 400 % messages.Length];
                    if (done % 2800 == 0) Console.Error.WriteLine(line); else Console.WriteLine(line);
                    Interlocked.Increment(ref captured);
                }
                if (stop.Token.WaitHandle.WaitOne(2)) break;
            }
        }) { IsBackground = true };
        workers[id].Start();
    }
    using Process process = Process.GetCurrentProcess();
    Stopwatch wall = Stopwatch.StartNew();
    TimeSpan lastCpu = process.TotalProcessorTime;
    double lastWall = 0;
    long lastPosted = 0;
    try {
        while (wall.Elapsed.TotalSeconds < Seconds && !live.Cancellation.IsCancellationRequested) {
            if (live.Cancellation.WaitHandle.WaitOne(250)) break;
            process.Refresh();
            double now = wall.Elapsed.TotalSeconds;
            double interval = now - lastWall;
            double core = (process.TotalProcessorTime - lastCpu).TotalSeconds / interval * 100;
            long count = live.EventsPosted;
            double perSecond = (count - lastPosted) / interval;
            rateHandle.Add(perSecond);
            cpuHandle.Add(core, "cpu");
            statHandle.Set("Updates posted", Number(count));
            statHandle.Set("Updates applied", Number(live.EventsApplied));
            statHandle.Set("Update rate", Number(perSecond) + "/s");
            statHandle.Set("Frames", Number(live.FramesRendered));
            statHandle.Set("Frame rate", (live.FramesRendered / now).ToString("0.0", CultureInfo.InvariantCulture) + " fps");
            statHandle.Set("CPU", Number(core) + "%");
            statHandle.Set("CPU share", (core / Environment.ProcessorCount).ToString("0.0", CultureInfo.InvariantCulture) + "% of " + Environment.ProcessorCount + " cores");
            statHandle.Set("Stray writes", Number(Interlocked.Read(ref captured)));
            statHandle.Set("Dropped / errors", live.EventsDropped + " / " + live.ApplyErrors);
            lastCpu = process.TotalProcessorTime;
            lastWall = now;
            lastPosted = count;
        }
    } finally {
        stop.Cancel();
        foreach (Thread worker in workers) worker.Join();
    }
    for (int id = 0; id < Workers; id++) workerHandle.Complete("w" + id, StatusKind.Pass);
});
if (session.EventsDropped != 0 || session.ApplyErrors != 0 || session.EventsPosted != session.EventsApplied)
    throw new InvalidOperationException("Accepted stress updates did not drain successfully.");

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into stress.ps1.

  3. Run it

    From the folder you saved it in.

    ./stress.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n Stress; cd Stress
  2. Add ConsoleMonster

    The only package the program needs.

    dotnet add package ConsoleMonster
  3. Replace Program.cs

    Download it, or copy the program from above.

  4. Run it

    dotnet run

Or skip the copying. Every demo ships with the module and the project template.

Show-ConsoleDemo stress
Get-ConsoleDemo stress -Path ./stress.ps1
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo stress -n Stress

Make it yours

Start with these, then read the rest of the file.

  1. Replace the bounded worker loop with your queue consumers.

  2. Connect session counters and CPU samples to your own workload.

  3. Keep third-party output in the captured log while workers post updates.

More dashboards

All demos