You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(load): stepped and ramped load to find a server's knee - #2909
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-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.
--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>
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.
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.
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.
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.
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>
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.
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
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.
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.
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.
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2841
What
Stepped and ramped load for
--load(one-shot CLI) andmcpdo 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 ofsec / --load-step-durationstages (rounded, at least one), eachsec / stageslong, with levels evenly spaced fromfromtoto, 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-concurrencyand needs whole-number steps; a ramp rounds. Stepping rate runs over a pool of--load-concurrencyworkers and refuses--load-rate.--load-step-duration <sec>(default 10).--load-durationand--load-requestsare refused with stages.--load-connectionsmay not exceed the smallest concurrency stage, and the connections are opened once and serve every stage.elapsedMsis the sum of the stages,durationMsthe planned total).stages[]holds one fullLoadSummaryper stage that ran, withstage,stepByandlevel, and its own rate figures and scenariosteps.{{seq}}(--load: argument templating or a data file so calls are not identical #2840) keeps counting across stages.Load threshold exceeded at stage 3 (concurrency 50): …).--load-stop-on-breach(requires a threshold) also ends the run at the first breaching stage, withstoppedEarly.reason: "threshold_breached". In mcpdo the stages run daemon-side, so these thresholds travel insideLoadOptions.stages. Any other early stop (interrupt, lost connection, circuit breaker) also ends the run. Rate-shortfall warnings are emitted per stage.How
core/cli/handlers/load-stages.ts: flag parsers, ramp level spacing, andresolveLoadStages, which refuses each conflicting combination (load_flag_conflictin both CLIs).runLoaddispatches to a single window (the old body, nowrunWindow) or torunStages.runStagesruns one window per stage into shared accumulators (totals plus per-step stats), offsetsseq, and stops on an early stop or a breach.load-thresholds.tsgains a non-throwingloadThresholdBreaches, andcheckLoadThresholdsjudges stages one by one. The flag parsers move to a newload-parsers.ts(re-exported fromload-test.ts), soload-thresholdsno longer value-importsload-test. Otherwise this change would introduce an import cycle.Tests
clients/cli/__tests__/load-stages.test.ts: parsers, ramp spacing, every refusal, option resolution. On a fake clock: per-stage concurrency,seqcontinuity, 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-stepsrun.Docs: CLI README (flag rows plus a "Stepped and ramped load" section), mcpdo README,
skills/mcpdo/SKILL.md.npm run local:gategreen.🤖 Generated with Claude Code