Reports
java -jar acemq-workload.jar -f workload.yaml --report reports/ --format html,md,jsonThree formats, defaulting to html,json. Files are
timestamped, so a series of runs accumulates rather than overwriting —
which is what you want across a week of tuning.
The terminal report
workload: orders-peak
TopologySpec{orders -> orders.new [order.created] classic}
PublisherSpec{threads=4, rate=4000/s, Payload{1024 bytes}, confirms=true}
ConsumerSpec{concurrency=8, prefetch=100, handlerTime=PT0S}
window 10s
published 40,036 (4,000/s, offered 4,000/s)
consumed 40,036 (4,000/s)
queue at end 0
end-to-end n=40036 p50=1.1ms p90=1.5ms p99=4.8ms p99.9=47.7ms max=53.2ms
publish n=40036 p50=913us p90=1.3ms p99=4.4ms p99.9=47.2ms max=53.9ms
send lag n=40036 p50=188us p90=225us p99=322us p99.9=2.4ms max=11.4ms
[INFO] throughput>=3500
observed: sustained 4,000 msg/s end-to-end over 10s
means: the objective was met
result: PASSED
Read the three histograms in this order:
- send lag — did the generator keep its schedule? If not, stop; nothing below is about the broker.
- end-to-end — from due to received. The number that matters.
- publish — the confirm round trip. Separates "the broker was slow to accept" from "the consumers were slow to drain".
And queue at end: zero means the
consumers kept up. Anything else means the queue grew and the latency
figures are a function of how long you ran.
JSON
The format nobody asks for and the one that matters most. It is what a pipeline reads to decide whether to promote a build, what a dashboard graphs over time, and what lets two runs be compared without re-running either.
{
"workloads": [
{
"name": "orders-peak",
"startedAt": "2026-09-02T19:59:34.123Z",
"durationSeconds": 10,
"valid": true,
"passed": true,
"offeredRatePerSecond": 4000,
"publishedTotal": 40036,
"consumedTotal": 40036,
"publishRatePerSecond": 4000.0,
"consumeRatePerSecond": 4000.0,
"queueDepthAtEnd": 0,
"endToEnd": {
"count": 40036, "p50Micros": 1131, "p99Micros": 4823,
"p999Micros": 47710, "maxMicros": 53210
},
"sendLag": { "count": 40036, "p50Micros": 188, "p99Micros": 322 },
"findings": [
{ "rule": "throughput>=3500", "severity": "INFO",
"observation": "sustained 4,000 msg/s end-to-end over 10s",
"implication": "the objective was met" }
]
}
]
}valid and passed are separate fields for
the same reason the exit codes are separate: a build that cannot tell
"missed the objective" from "measured nothing" will retry the one that
can never succeed.
Latencies are in microseconds — integers, so no float comparison surprises when diffing two runs.
samples — the run as
it went
Everything above is one number for the whole window, which is the right shape for "how fast is this broker" and the wrong one for "what happened at 14:02". A run that idles at 3ms and spends twelve seconds at 900ms has the same summary as one that sat at 40ms throughout, and only one of those is a broker anybody would ship.
"samples": [
{"at": "2026-09-24T18:00:01Z", "elapsedMillis": 1000, "phase": "WARMUP",
"published": 200, "confirmed": 200, "failed": 0, "consumed": 198,
"publishRatePerSecond": 200.0, "consumeRatePerSecond": 199.5,
"blocked": false, "queueDepth": 7,
"endToEndP50Micros": 1131, "endToEndP99Micros": 4823}
]One entry per sampling interval, oldest first. at is
wall-clock and elapsedMillis is measured from the start of
the run, so a reader keeping its own record of events — a deployment, an
incident timeline, a fault drill — can line the two up without having to
trust either clock alone.
phase separates WARMUP from
MEASURING. Only the measured intervals are in the aggregate
above, and anything graphing all of them should say which is which.
The key is absent, not empty, when a run was shorter than one
interval. "samples": [] reads as a run that was idle, which
is a different statement from having nothing to report.
ramps — a ramp's steps
A ramp adds a ramps array beside
workloads: the settings, the highest sustained rate, what
stopped it, and every step with its rates and percentiles. Absent when
nothing ramped.
HTML
Self-contained: no external stylesheet, no CDN, no fonts to fetch. Openable from a file path, mailable, and viewable on an air-gapped machine.
It carries a comparison table at the top, then each workload's findings and full report, and embeds the JSON in a collapsed block at the bottom — so a report forwarded to somebody is still re-analysable rather than a picture of numbers.
It respects the reader's light or dark theme, and has
@media print rules.
Markdown
For a pull request comment or a wiki page. Leads with the comparison table:
| workload | result | offered | published | consumed | p50 | p99 | p99.9 |
|---|---|---|---|---|---|---|---|
| classic-queue | PASSED | 4,000/s | 4,000/s | 4,000/s | 1.4ms | 2.2ms | 5.3ms |
| quorum-queue | PASSED | 4,000/s | 4,000/s | 4,000/s | 1.1ms | 4.8ms | 47.7ms |That single table is often the whole deliverable: same load, two configurations, and the cost of the guarantee in a number.
Not supported, deliberately. --format pdf says so rather
than failing quietly.
Producing one needs a layout engine and its fonts — a large
dependency in a tool whose entire output is a table and a list — and the
result looks worse than what a browser prints. Open the HTML and print
it: the @media print rules drop the embedded JSON block,
avoid breaking findings across pages, and switch to a palette that
survives a monochrome printer.
If a signed, archival PDF is a hard requirement for a customer deliverable, that is a wrapper around the HTML rather than a feature of this tool.
Comparing two runs
Nothing in this tool does it for you yet. With the JSON it is a few lines:
jq -r '.workloads[] | "\(.name)\t\(.consumeRatePerSecond)\t\(.endToEnd.p99Micros)"' \
reports/workload-*.json | column -tA per-run comparison mode is on the roadmap. Until then the JSON is stable enough to build on, which is most of why it is written by default.