Design notes
Module map
| module | responsibility |
|---|---|
scenario.py |
YAML to frozen dataclasses (Plan, ScenarioDef, Step, LoadSpec), validation, CSV feeders, CLI overrides |
template.py, jsonpath.py |
{{ }} rendering, JSONPath subset |
schedule.py |
arrival-time generator for piecewise-linear rates |
engine.py |
virtual users, open/closed loops, step execution, checks, extraction. Clock and transport are injected |
hist.py, stats.py |
log-bucketed histogram; per-step and per-second recorder, all mergeable |
runner.py |
one thread or N spawned processes; one-second delta messages aggregated by the parent |
slo.py |
SLO parsing and evaluation |
results.py, report_html.py, compare.py |
JSON, HTML (inline SVG), comparison |
dashboard.py, cli.py |
rich live view; click commands |
har.py |
HAR to scenario |
Histogram
HDR-style: integers are stored exactly below 2^b where b = ceil(log2(2 * 10^digits)). Above that, every power-of-two
range [2^k, 2^(k+1)) is divided into 2^(b-1) equal sub-buckets, so a bucket is at most 1/2^(b-1) <= 10^-digits of
its lower bound wide. Example with 3 digits: b = 11, 1024 sub-buckets per octave; the value 1,234,567 us lands in a bucket
whose width is 1024 us (0.08%). Counts live in a sparse dict keyed by bucket index, so an empty or narrow histogram is
tiny and merging two histograms is a dictionary sum. Latencies are recorded in microseconds. percentile(p) returns the
upper bound of the bucket holding rank ceil(p/100 * n), clamped to the observed min/max, which matches numpy's
inverted_cdf method within the precision (this is what the tests compare against).
Workers ship one-second delta recorders (JSON-friendly dicts) to the parent, which merges them. Merged results equal what a single histogram would have recorded, so multi-process runs lose no accuracy.
Arrival process
For a rate function r(t) the expected number of arrivals up to t is L(t) = integral of r. A Poisson process with
that rate is obtained by drawing unit-rate exponential gaps e_i in the L domain and mapping back: arrival i happens
at t_i = L^-1(sum e_i). For a linear segment r(t) = r0 + s t, inverting r0 t + s t^2 / 2 = x gives
t = 2x / (r0 + sqrt(r0^2 + 2 s x)), which is stable for s = 0 too. Constant arrivals use gaps of exactly 1.
Worked example: stages [10s: 0 -> 100/s]. L(10) = 500, and half the arrivals (250) occur by t = 10 / sqrt(2) = 7.07s
because the rate is low at the start. With several workers each runs the same schedule at rate / n; a superposition
of independent Poisson processes is Poisson at the summed rate.
Execution model
- One asyncio task per open-model journey, waiting on a semaphore when
max_concurrencyis set. The arrival loop sleeps until each intended time and spawns; if it is behind, it spawns immediately (intended time stays in the past, and that lateness appears in corrected latency). - Closed model: a controller task reconciles the number of running virtual users with the stage target every 100 ms. Ramp-down lets a user finish its iteration.
- Per request:
sent = clock.now(),intended(from the schedule or equal tosent), after the responseservice = end - sent,corrected = end - intended. - Errors are one type per request, by priority:
timeout|connect|network(transport),status(HTTP >= 400, or astatuscheck not satisfied),extract(an extraction found nothing),check(other checks), plustemplateandfeeder.status,extractand transport errors abort the rest of the journey (later steps depend on the data);checkfailures do not. Timeouts keep their latency sample; connect/network errors do not (a refused connection at 0 ms would distort percentiles). beforehooks run once per virtual user (each journey in the open model, each VU in the closed model),afterhooks run when the user finishes, even if the main steps failed.
Testing with a fake clock
Engine takes a clock (now, sleep) and a transport callable. tests/fakeclock.py is a virtual-time driver: it
lets all tasks run until they block, then jumps time to the earliest sleeper. The coordinated-omission tests run the real
engine, feeders, recorder and histogram against a fake server with a stall window; a 10 s simulated test finishes in milliseconds with fully
deterministic results.
Limitations
- JSONPath has no filters (
[?()]) or slices. - HTTP/2 needs
pip install 'loadsmith[http2]'; there is no HTTP/3, WebSocket or gRPC support. - Latency includes the load generator's own overhead (JSON encoding, httpx); at several thousand requests per second per
core use more
--workers.