Benchmarks¶
This crate's consumer and publisher sit between the SQS client and your code, and the runtime sits
above them. Both cost time on every message: the receive pump, the delivery it hands over, the
delete behind its ack, the dispatch into your handler. This page says how much of each, measured
against the same work written by hand on aws-sdk-sqs.
One process runs the same scenario three times over, as three loops that differ in one thing each: what carries the messages.
- Raw client -
aws-sdk-sqs, driven directly by a loop in the benchmark. - This crate - the same loop, on this crate's broker, subscription, delivery,
ackand publisher; no handler, no router, no dispatch. - Full service - the service a user writes:
#[subscriber], the app, the runtime.
So the two overhead columns answer two questions. Crate over raw is what this crate's consumer and publisher cost over the client they wrap, which is the question this repository is responsible for. Service over raw is what the whole stack costs; the gap between the two columns is the runtime's share over this broker, and it is published here because a share that differs from broker to broker is a fact about how the two meet rather than about the core.
Everything else is held equal across the three - the AWS configuration, the receive parameters, the wait the receive asks for, the delete per message and where it happens, the decode into the same type, the body bytes, the tokio runtime and the build. The procedure is the framework's own and is described under Methodology.
The stand is the LocalStack emulator the live suites use, not the hosted service. Read every figure below as a statement about what a delivery costs inside this crate, and about nothing else.
The numbers¶
The best of three interleaved rounds, with the median round in parentheses. Higher is better.
The table is read in your browser from the document the last run wrote, so nothing on this page is a copy that could have gone stale.
Round trips set the pace of both rows, and that is the important thing on this page. A delivery
costs a DeleteMessage of its own, because that is what this crate's ack does, plus a tenth of the
ReceiveMessage that carried its batch over. The run measures the round trip to the stand
separately and publishes it below, and those round trips account for about half of what a delivery
took; a row where they reach half is marked broker-bound. The work of this crate and of the
framework happens inside a wait the raw client pays as well, which makes the difference between the
loops a lower bound on what that work costs rather than a measurement of it.
A row reported as indistinguishable is one whose two halves differ by less than the spread between
runs of either. A figure below the run-to-run noise would read as precision that was never measured,
so none is published.
The machine-readable form of the same run, which the framework's site reads to build its
cross-broker table, is at
benchmarks/results.json.
The crate's own code¶
The second table is what a message costs on this crate's code path, counted rather than timed:
instructions under callgrind and allocations under DHAT. Each scenario is the service a user
writes, built on SqsBroker with the constructor a service uses, pointed at the same LocalStack
stand and started on a single-threaded runtime. The queue is filled from another thread between the
start and the drain, and that thread is not counted.
What is counted is everything the service's thread runs: the framework, this crate, and the work
of aws-sdk-sqs and its HTTP stack on that thread, building and signing every request and parsing
every answer. Threads the client runs on its own are not counted, and neither is the emulator's
side of a round trip. Every delivery pays its share of a ReceiveMessage and a DeleteMessage of
its own, and a reply adds a SendMessage, so most of every row is the AWS client's work rather
than this crate's.
Instructions and allocations are per message in the steady state: the slope between a run of 1000 deliveries and a run of 2000. The last column is what starting the service and taking the first delivery cost once: loading the AWS configuration, resolving the queue and reading its visibility timeout. The numbers are absolute, the framework's own cost included; the core publishes that cost alone on its benchmarks page.
A batch holds ten messages, the most one ReceiveMessage returns, and every delivery in it is
still deleted with a call of its own.
Four runs of one binary agree within half a percent on instructions and within three allocations
in 1.7 million, which is how the emulator answers and how the socket's reads split.
just bench-code fails on an allocation above the floor a scenario declares, the highest count
observed plus a tenth of a percent, and with --baseline=main on more than two percent more
instructions. A pull request that changes the cost cites its numbers.
The machine¶
The build flags are published with the numbers because they change them: a binary built with
-C target-cpu=native produces a figure no other machine can reproduce, so the recipe clears the
variable before it builds.
What they do not mean¶
The stand is an emulator. It speaks the SQS and SNS APIs over plain HTTP on the loopback, which leaves out the TLS handshake, the credential refresh and the latency of a region. The pair stays valid, because both halves talk to the same emulator and the difference between them is what is published. The saturation profile does not: a queue in a region answers on another timescale, and none of these figures says what a service costs there.
Both halves ask for a 20-second wait, the crate's default and the protocol maximum. On a queue that always has a backlog the wait never elapses, because a receive answers as soon as it has anything to answer with - but it decides what an empty queue costs, and a pair whose halves disagreed about it would be publishing a polling policy rather than a dispatch cost.
This is one consumer, one queue, a small body and an emulator on the loopback. It measures what a delivery costs in this crate, and a row here is not comparable with a row published for another broker: the transports do different work per message.
The window a run measures opens at the first delivery and closes when the last one has been read, on both halves alike, so the delete that settles it sits outside the number on both sides. That is one delete out of the thousands a run carries.
The FIFO figure is taken on a queue whose bodies are spread over 64 message groups. A FIFO queue holds a group back while one of its deliveries is in flight, so a run over a handful of groups would be measuring that hold; with this many, a receive always has untouched groups to answer from.
The numbers are a snapshot of one machine on one day. They are re-measured by hand, on a machine given to the run alone: the difference this page is about is smaller than the noise of a shared one.
Running it yourself¶
The recipe starts the stand from docker-compose.test.yml, runs both scenarios, stops the stand
and rewrites docs/benchmarks/results.json with what it measured. It takes a few minutes and
wants the machine to itself. The message count is not fixed: a probe run sets it so that every
measured run lasts at least five seconds on whatever machine it is taken on.
The recipe starts the same stand, counts the code table under valgrind, stops the stand and
rewrites the code section of the same document. It takes a few minutes and needs valgrind and the
benchmark runner: cargo install --locked gungraun-runner --version =0.19.4.