Load testing for Elixir and Phoenix applications.
A load test is a module that implements Stampede.Scenario. Stampede starts
virtual users (VUs), each one a process that calls the scenario's
Stampede.Scenario.run/1 over and over, moves the number of running VUs
through the stages you configured, and records how long things take.
defmodule MyApp.Load.Lobby do
use Stampede.Scenario
@impl true
def options do
[
stages: [{[second: 30], 1_000}, {[minute: 1], 1_000}, {[second: 10], 0}],
thresholds: [{"channel.join", p95: 250}, {"iteration.errors", count: 0}]
]
end
@impl true
def run(_vu) do
{:ok, socket} = Stampede.Channels.connect("ws://localhost:4000/socket")
{:ok, _reply} = Stampede.Channels.join(socket, "room:lobby")
{:ok, _reply} = Stampede.Channels.push(socket, "room:lobby", "ping")
Process.sleep(1_000)
end
endRun it with mix stampede.run, or from code:
result = Stampede.run(MyApp.Load.Lobby)
IO.puts(Stampede.Reporter.format(result))Metrics
Every metric has a name and is a timing, a counter or a check. Stampede records these on its own:
"iteration"- how long each successfulStampede.Scenario.run/1took"iteration.errors"- iterations that raised, threw or exited"iterations.dropped"- iterations a:raterun could not start because all of its:max_vuswere busy"vu.crashes"- VUs that crashed and had to be replaced- the request metrics listed in
Stampede.HTTP - the connection and channel metrics listed in
Stampede.Channels - the page, join and event metrics listed in
Stampede.LiveView
Record your own with measure/2, record/3, increment/2 and check/2.
Timings, counters and checks share one set of names.
Timings are counted in buckets that grow by 2% each, so every percentile
in a Stampede.Result is within 1% of the exact value, however many samples
the run collected.
Telemetry
Stampede emits these :telemetry events:
[:stampede, :run, :start]- with the:system_timeas measurement, and the:scenarioand its:optionsas metadata[:stampede, :run, :interval]- every second, with the number of busy:vusas measurement, and the:scenarioand the:interval, a point ofStampede.Result's timeline, as metadata[:stampede, :run, :stop]- with the:durationas measurement, and the:scenarioand its:resultas metadata[:stampede, :timing]- for every timing recorded, with its:durationas measurement and its:nameas metadata[:stampede, :counter]- for every counter incremented, with the:countadded as measurement and its:nameas metadata[:stampede, :check]- for every check, with a:countof 1 as measurement, and its:nameand whether it:passed?as metadata
Durations are in native time units, as usual for telemetry. The run events
are emitted where run/2 was called, the others on the node that recorded
them.
Summary
Functions
Records whether condition holds as the check name, and returns it as a
boolean.
Adds amount to the counter name.
Calls fun and records how long it took as the timing name.
Records duration, given in unit, as the timing name.
Runs scenario and returns a Stampede.Result.
Functions
@spec check(String.t(), as_boolean(term())) :: boolean()
Records whether condition holds as the check name, and returns it as a
boolean.
Unlike a failed match, a failed check does not end the iteration. The summary shows how often each check passed, and thresholds can limit how many fail:
Stampede.check("status is 200", response.status == 200)
thresholds: [{"status is 200", failed_rate: 0.01}]
@spec increment(String.t(), pos_integer()) :: :ok
Adds amount to the counter name.
@spec measure(String.t(), (-> result)) :: result when result: var
Calls fun and records how long it took as the timing name.
Returns what fun returns. Nothing is recorded if fun raises, or when it
is called outside of a run.
Stampede.measure("checkout", fn -> MyApp.Client.checkout(cart) end)
@spec record(String.t(), non_neg_integer(), System.time_unit()) :: :ok
Records duration, given in unit, as the timing name.
Useful when the duration comes from somewhere else, such as a timestamp sent back by the server.
@spec run(module(), keyword()) :: Stampede.Result.t()
Runs scenario and returns a Stampede.Result.
Options given here take precedence over the ones returned by the
scenario's Stampede.Scenario.options/0.
Options
:stages- a list of{duration, target}tuples. Each stage moves the number of running VUs in a straight line from the previous target, starting at zero, totargetoverduration. A stage with no duration jumps to its target right away.:vusand:duration- run a constant number of VUs, 1 by default, fordurationinstead of going through stages.vus: 100, duration: [minute: 1]is the same asstages: [{0, 100}, {[minute: 1], 100}].:rateand:duration- startrateiterations per second forduration, however long they take, instead of keeping a number of VUs busy. Unlike VUs that wait for their last iteration to finish, a rate keeps the pressure on a server that slows down, which is how real traffic behaves. Rates can be fractions, such as0.5.:rate_stages- like:stages, but each target is a rate.:max_vus- how many iterations a:raterun may have in progress at once. VUs are started as needed and reused once they finish an iteration. When all of them are busy, iterations are dropped and counted as"iterations.dropped". Defaults to1_000.:thresholds- a list of{metric, limits}tuples, such as{"channel.join", p95: 250, max: 1_000}. Each limit is the highest value the statistic may have for the run to pass. Timings have:min,:mean,:p50,:p90,:p95,:p99and:max, in milliseconds. Checks have:failedand:failed_rate, from 0 to 1. Every metric has:countand:rate, per second. Thresholds on a timing with no samples fail, while counters and checks with no samples are zero.:nodes- the nodes to run the VUs on,[node()]by default. VUs and rates are split evenly between them, and their metrics are merged. Every node must be connected and able to run Stampede and the scenario's code, for example because it was started from the same project. A scenario compiled to a.beamfile is loaded on the nodes that miss it.:progress- prints the number of VUs, iterations and errors every second. Defaults tofalse.
Durations are milliseconds or anything else Kernel.to_timeout/1 accepts,
such as [second: 30] or Duration.new!(minute: 1).