Load shapes¶
apiverity regression measures every operation a fixed number of times.
apiverity regression --shape drives one operation at a declared arrival
rate for a declared duration.
apiverity regression openapi.yaml --base-url https://staging.example.com \
--shape 'ramp:60s@1..20' --operation 'GET /users'
GET /users ramp 60s at 1/s -> 20/s (even arrivals, seed 0)
target: https://staging.example.com/users
scheduled 631, sent 631, errors 0
requested 1/s, offered 10.5/s over 60.0s (60.2s including the drain)
p50 41.2ms p95 118.7ms p99 402.1ms
statuses: {'2xx': 629, '5xx': 2}
Open loop, which is the point¶
regression and regression --curve are closed loop: N workers each send
a request, wait for the answer, and send the next. Offered load falls as the
service slows. That is the right instrument for "how does it behave at
concurrency 16".
--shape is open loop: a request goes out when it is due, whether or not
earlier ones have come back. That is the only way to ask "what happens at 200
requests a second" — under a closed loop a service that stalls simply receives
less traffic, and the queue never builds.
Neither replaces the other, and a report that confused them would be describing a different experiment from the one it ran.
The shapes¶
| Spec | What it does |
|---|---|
constant:30s@10 |
Ten requests a second for thirty seconds |
ramp:60s@1..20 |
One a second climbing to twenty over a minute |
spike:60s@10x6@50% |
Ten a second, with a burst of sixty halfway through |
soak:600s@5 |
Five a second for ten minutes |
Add +poisson for exponentially distributed inter-arrival times rather than
even spacing — real traffic clusters, and a service that copes with a metronome
does not always cope with the same mean arriving in bursts. Add +seed=7 to
pin the arrival process, so a Poisson run is reproducible.
A spec that cannot be read is refused rather than defaulted. A generator that
quietly ran constant when the operator typed ramp would produce a report
describing a shape that never happened.
What the run says about itself¶
achieved_rps against the rate you asked for, and max_late_ms: how
far behind its own schedule the generator fell at worst. When it falls further
behind than one scheduling step, the run says so in as many words:
WARNING: this generator fell up to 3140ms behind its own schedule. The
offered load was not the profile above -- lower the rate, or drive it from
somewhere closer to the target.
That is not a verdict on the service. It is a statement about the run: with
requests going out three seconds after they were due, the latencies above
describe a load nobody asked for. k6 draws the same distinction with
dropped_iterations, and a load report that hid it would be quotable and
wrong.
The offered rate is measured over the dispatch window, not the total. An open-loop profile controls when requests go out; how long the last responses take to drain is the service's business, and dividing by the total would report two thirds of the rate a two-second profile actually offered when its tail took another second — which reads as a generator that fell behind and is nothing of the kind. The run reports both windows.
"Far enough behind" is one scheduling step, 50 ms, rather than a round wall-clock figure: 250 ms is five slots at twenty requests a second and a fiftieth of one at four, so a fixed number would mean something different at every rate.
Nothing is dropped here — scheduled == sent + errors always holds. A
generator that silently discarded a backlog would report a clean run of a
profile it did not execute.
What it will not do without being told¶
One operation, named. --shape requires --operation. A rate is stated
for one endpoint; applying constant:60s@50 to a forty-operation contract
means two thousand requests a second at a target somebody asked fifty of.
A concrete path. An operation declared at /users/{id} is refused with a
pointer to --path /users/42. There is nothing here that knows a real id, and
filling one in would send thousands of requests at a guess.
Reads, unless you say otherwise. A non-GET/HEAD/OPTIONS operation
needs --include-mutations, because POST /orders at fifty a second writes to
the target fifty times a second. See the safety model.
History¶
This module existed, with tests, before any of the above. Two things were wrong with it.
No command reached it — --profile on the command line is the severity
profile, and no flag anywhere took a load shape. And execute computed the
schedule and threw every offset away, firing requests back to back as fast as
the transport returned: a ramp, a spike, a soak and a Poisson arrival process
all produced the same run, differing only in how many requests it contained.
The shape, which is the entire content of a load profile, did nothing.
capacity_search was deleted at the same time. It swept concurrency levels
against a transport the caller supplied, so it never sent a request and its
numbers were whatever the caller's model said. regression --curve does that
job against a real target.