Skip to content

feat(load): stepped and ramped load to find a server's knee - #2909

Merged
cliffhall merged 6 commits into
v2/mainfrom
v2/feat/2841-load-stages
Nov 5, 2026
Merged

cliffhall merged 6 commits into
v2/mainfrom
v2/feat/2841-load-stages

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2841

What

Stepped and ramped load for --load (one-shot CLI) and mcpdo tools/call --load. Consecutive stages run at rising levels, with one summary row per stage, so a server's knee shows in one run.

--load --load-steps 10,25,50,100 --load-step-duration 30
--load --load-ramp 50:500:120 --load-step-duration 10 --load-step-by rate --load-concurrency 200 \
       --load-max-p99 250 --load-stop-on-breach
  • --load-steps <list>: one stage per level. --load-ramp <from>:<to>:<sec>: a staircase of sec / --load-step-duration stages (rounded, at least one), each sec / stages long, with levels evenly spaced from from to to, both included. Stairs rather than a continuous ramp, so every level runs long enough to measure and gets its own row.
  • --load-step-by concurrency|rate (default concurrency). Stepping concurrency refuses --load-concurrency and needs whole-number steps; a ramp rounds. Stepping rate runs over a pool of --load-concurrency workers and refuses --load-rate.
  • --load-step-duration <sec> (default 10). --load-duration and --load-requests are refused with stages. --load-connections may not exceed the smallest concurrency stage, and the connections are opened once and serve every stage.
  • Summary: the totals stay at the top level, over every stage (elapsedMs is the sum of the stages, durationMs the planned total). stages[] holds one full LoadSummary per stage that ran, with stage, stepBy and level, and its own rate figures and scenario steps. {{seq}} (--load: argument templating or a data file so calls are not identical #2840) keeps counting across stages.
  • Thresholds judge each stage, and the breach message names the stage (Load threshold exceeded at stage 3 (concurrency 50): …). --load-stop-on-breach (requires a threshold) also ends the run at the first breaching stage, with stoppedEarly.reason: "threshold_breached". In mcpdo the stages run daemon-side, so these thresholds travel inside LoadOptions.stages. Any other early stop (interrupt, lost connection, circuit breaker) also ends the run. Rate-shortfall warnings are emitted per stage.

How

  • New core/cli/handlers/load-stages.ts: flag parsers, ramp level spacing, and resolveLoadStages, which refuses each conflicting combination (load_flag_conflict in both CLIs).
  • Engine: runLoad dispatches to a single window (the old body, now runWindow) or to runStages. runStages runs one window per stage into shared accumulators (totals plus per-step stats), offsets seq, and stops on an early stop or a breach.
  • load-thresholds.ts gains a non-throwing loadThresholdBreaches, and checkLoadThresholds judges stages one by one. The flag parsers move to a new load-parsers.ts (re-exported from load-test.ts), so load-thresholds no longer value-imports load-test. Otherwise this change would introduce an import cycle.
  • mcpdo's text summary prints one line per stage.

Tests

  • clients/cli/__tests__/load-stages.test.ts: parsers, ramp spacing, every refusal, option resolution. On a fake clock: per-stage concurrency, seq continuity, totals, rate stepping, stop-on-breach, interrupt, and scenario step totals. Also judging and warnings, and CLI end-to-end runs: steps, a rate ramp, stop-on-breach exiting 10, and the refusals.
  • clients/mcpdo/__tests__/load.test.ts: flag resolution, the stage rendering, and a live-daemon --load-steps run.

Docs: CLI README (flag rows plus a "Stepped and ramped load" section), mcpdo README, skills/mcpdo/SKILL.md. npm run local:gate green.

🤖 Generated with Claude Code

--load-steps <list> and --load-ramp <from>:<to>:<sec> run consecutive stages
at their own concurrency (or rate, with --load-step-by rate), each for
--load-step-duration, with one full summary per stage in stages[] under the
totals. Thresholds judge each stage; --load-stop-on-breach ends the run at the
first stage that breaches. The flag parsers move to load-parsers.ts so the
engine can use the thresholds' breach check without an import cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Nov 5, 2026
@cliffhall cliffhall linked an issue Nov 5, 2026 that may be closed by this pull request
2 tasks done
@cliffhall
cliffhall requested a balanced review from Copilot November 5, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Rate-limited stages advance before their actual deadlines, compressing runs and permitting boundary bursts, while rate rounding converts valid positive inputs into zero-rate stages.

2 open issues
What changed in this PR

Adds stepped and ramped load testing to the shared load engine, letting the one-shot CLI and mcpdo identify where server latency or errors begin rising in a single run.

Changes:

  • Adds stage-planning flags, validation, and sequential execution with aggregate and per-stage summaries.
  • Evaluates thresholds and rate shortfalls per stage, with optional stopping on the first breach.
  • Adds CLI integration, human-readable stage output, tests, and usage documentation.
File Description
skills/​mcpdo/​SKILL.md Documents staged load usage and threshold stops.
core/​cli/​handlers/​load-thresholds.ts Adds reusable breach checks and per-stage evaluation.
core/​cli/​handlers/​load-test.ts Executes stages and aggregates results across them.
core/​cli/​handlers/​load-stages.ts Parses stage flags and plans stepped or ramped levels.
core/​cli/​handlers/​load-parsers.ts Extracts shared numeric parsers to avoid an import cycle.
clients/​mcpdo/​src/​connection/​mcp.ts Registers the new load-stage flags.
clients/​mcpdo/​src/​connection/​load-flags.ts Resolves stages and forwards stop thresholds to the daemon.
clients/​mcpdo/​src/​connection/​format-human.ts Prints one summary line per stage.
clients/​mcpdo/​README.md Documents staged load support.
clients/​mcpdo/​__tests__/​load.test.ts Tests flag resolution, rendering, and live-daemon execution.
clients/​cli/​src/​cli.ts Wires stage flags into one-shot CLI options.
clients/​cli/​README.md Explains flags, summaries, thresholds, and examples.
clients/​cli/​__tests__/​load-stages.test.ts Tests planning, execution, thresholds, warnings, and CLI behavior.

Review effort: Balanced

Comment thread core/cli/handlers/load-stages.ts Resolved
Comment thread core/cli/handlers/load-test.ts Resolved
…ngth

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 1 (review 5783243560), both fixed in 6c5cecf:

  • Rounded rate levels could become zero: ramped rates are now kept exact.
  • Rate-limited stages advanced before their deadlines: each stage now waits out its real length (abortably) before the next starts. There are tests on the actual start times and on aborting during the wait.

local:gate green. Requesting round 2.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Aborting during the stage wait leaves elapsed time and derived throughput incorrect and omits the stop reason, causing inaccurate stage and aggregate results.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread core/cli/handlers/load-test.ts Resolved
An abort during the wait is now recorded by the stage itself, with the
time that actually passed, instead of reporting the padded window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 2 (review 5783415691), fixed in a71b3ca:

  • An abort during the stage wait now finalizes that stage with its real elapsed time, rates and stop reason. The wait moved inside the stage's window, where its abort handling is still active.

local:gate green. Requesting round 3.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The circuit breaker resets its failure streak at each stage, allowing continuous failures across short stages to bypass early stopping.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

High severity Circuit breaker resets between stages, allowing repeated failures

core/​cli/​handlers/​load-test.ts:951

Each runWindow resets errorStreak and streakSince, so short stages can bypass the circuit breaker even when every call throws. With one worker and 20 ms failures, five 400 ms stages finish all 100 failures without stoppedEarly, whereas a single two-second window stops after 50. Share the breaker state across stages, reset the streak only on a server response, and add a regression covering failures spanning stage boundaries.

1 resolved since last review

Review effort: Balanced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 3 (review 5783550962): no inline comments. The one finding was in the "previously missed issue" block, so it has no thread and is answered here. Fixed in c1c0fd2:

  • The circuit breaker resets at each stage: the streak and its start time now live in state shared by every stage of a run, so failures spanning stage boundaries keep counting. Only a server response still resets the streak. A regression test runs five 400 ms stages of 20 ms failures and asserts the breaker trips partway through; it does not trip without the fix.

local:gate green. Requesting round 4.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because resetting weighted scenario selection between stages can skew load-test coverage and leave operations untested.

No open issues 🎉

Review effort: Balanced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 4 (review 5783676104): no inline comments. The headline flagged that the weighted scenario picker reset between stages. Fixed in 47fc513:

  • One picker now serves the whole stepped run, so a scenario's weighted cycle continues across stage boundaries instead of restarting with the heaviest step. A test with weights 3:1 over four two-call stages asserts the picks run 0,0,1,0,0,0,1,0; with a fresh picker per stage the light step would never run.

local:gate green. Requesting round 5.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because the shared circuit breaker can miss repeated failures across single-call stages, allowing execution beyond the configured error limit.

No open issues 🎉

Review effort: Balanced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 5 (review 5783795045): no inline comments. The headline flagged that the breaker could miss failures across single-call stages. Fixed in 7598c98:

  • A stage with another stage after it now always counts as able to start more calls, so a failure landing after that stage's own last start still trips the breaker. Only the final stage keeps the "draining in-flight calls is not an early stop" rule. A test of 80 one-call stages against a dead server asserts consecutive_errors within the first few stages.

local:gate green. Requesting round 6.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are recorded for the staged-load testing changes.

No open issues 🎉

Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review loop closed: round 6 (review 5783898542) was clean, with no inline comments, no headline finding and no suppressed block. All findings from rounds 1–5 were fixed.

@cliffhall
cliffhall merged commit 219e605 into v2/main Nov 5, 2026
12 checks passed
@cliffhall
cliffhall deleted the v2/feat/2841-load-stages branch November 5, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--load: ramp-up and stepped load to find a server's knee

2 participants