Skip to content

⚡️ Forward the messages of each peer in batches - #383

Open
zeenix wants to merge 5 commits into
z-galaxy:mainfrom
zeenix:ccr-fd1ce738-0xtuf0
Open

zeenix wants to merge 5 commits into
z-galaxy:mainfrom
zeenix:ccr-fd1ce738-0xtuf0

Conversation

@zeenix

@zeenix zeenix commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

The bus forwards the messages of each peer one at a time, and writes each of them to a client's socket with a system call of its own, however many messages of the peer are there already. This forwards them in batches instead, with zbus's new Connection::send_all (z-galaxy/zbus#2079):

  • ⚡️ The task that serves a peer takes the messages that the peer sent after the one it waits for, as far as they are there already, up to 16 of them, ending with the one that brings them to 16 KiB or more, and forwards them as a batch. It picks the recipients of all of them under one hold of the locks, and sends each recipient its messages of the batch in one go, which zbus writes with one system call. Each recipient still gets the messages of each peer in order. A recipient may now get a message of a batch before another recipient gets an earlier one, which nothing ruled out before.
  • The time limit of a send, SEND_TIMEOUT, now covers the messages that a client gets in one go, as well as the wait for those on their way to it, so a batch ends at 16 KiB: a client has to take less than 16 KiB more within it than the last of those messages alone.
  • A client that didn't negotiate passing file descriptors still gets the messages of a batch that carry none: zbus turns the batch down before it writes any of it, and the bus sends it again without the others. A new test covers it.
  • 📌 busd builds on zbus from the branch of ✨ zb: Add Connection::send_all zbus#2079, by commit, until a release of zbus has send_all. The earlier 📌 commits pinned the commits of that branch that the measurements below were made with.
  • ✅ stalled_subscribers, which fails on CI on the main branch too, times the disconnections of the stalled clients while the flood goes on, rather than once all of it is sent. On CI, where every test logs all that it does, the rest of the flood took over 300 ms, and the test 853 ms on the main branch, beyond the 800 ms that it allows.

Measurements

On a 4-vCPU Xeon VM, against busd's main branch, its queue for each client before it, dbus-broker and dbus-daemon, with dbus-bench's clients of the signals and of the latencies' fan-out running each subscriber on a thread of its own, as processes of their own would (results):

  • Against busd's main branch, the broadcast of 6000 signals to 20 subscribers takes 70.5% less time (0.283 s against 0.959 s) and a burst of signals to one subscriber 13.6% less. A signal of the latencies' fan-out reaches each of 20 subscribers 72% sooner at the median, and all of them are there 66% sooner. The bus uses 60% less CPU time on the signals, 79% less system time, and 16% less CPU time on the latencies.
  • The calls, the round trips and the connection that 100 proxies share are level: within 5%, or inconclusive. The bus forwards those a message at a time, and callgrind counts 1.4% more instructions of it on the calls than with busd's main branch, with 4f346a3, whose forwarding is the same (details).
  • Against dbus-broker, busd is now faster on the broadcast too, by 11.6%, and on the fan-out, and stays 38 to 42% faster with 20 connections or more. busd still takes 24% longer than dbus-broker on the burst to one subscriber, and uses 14 to 25% more CPU time on the calls, the latencies and the shared connection, and as much on the signals.
  • No run of any broker failed.

On a Ryzen 7 7840U, with dbus-bench's earlier clients, which ran all subscribers on one thread (results), the burst and the broadcast were 6.9% and 6.7% faster than with busd's main branch, with 31.7% less CPU time of the bus on the signals, and 48.3% less system time. A run with the threaded clients there is to come.

Costs

  • A client has to take a batch to it within the time limit, rather than a message: less than 16 KiB more than the last message of the batch alone, which can be bigger. In one run on the VM, with batches of up to 64 KiB, the bus disconnected the connection that 100 proxies share, as it started, once in 40 rounds. 200 starts of that client against each of busd with batches of up to 16 KiB, busd with batches of up to 64 KiB and busd's main branch couldn't reproduce it, so the bound of 16 KiB rests on the time limit's reasoning above.

Generated by Claude Opus 5.5.

zbus can't write several messages to a socket in one go yet, so busd
writes each message that it forwards to a client with a system call of
its own. z-galaxy/zbus#2078 asks for `Connection::send_all`, which does
that, and the `ccr-fd1ce738-0xtuf0` branch of zeenix/zbus adds it, so
build on its commit until a release has it.

As for zruntime, the commit takes the place of the release through a
patch of crates.io's zbus, so that the tests, which depend on zbus too,
build on it as well.

Assisted-by: Claude Opus 5.5 (claude-opus-5-5)
The task that serves a peer forwarded its messages one at a time, and
the send of each to a client wrote it to the client's socket with a
system call of its own, however many messages of the peer were there
already for the bus to forward.

The task now takes the messages that the peer sent after the one that
it waits for, as far as they are there already, up to 16 of them, and
ending with the one that brings them to 16 KiB, and forwards them as a
batch: it picks the recipients of all of them under one hold of the
locks, and sends each recipient its messages of the batch in one go,
with `Connection::send_all`, which writes several small messages with
each system call, and a big one on its own. It does the same with the
messages of the bus itself.

A peer that waits for each reply sends batches of one message, which
the bus still forwards for little more than before. It looks for more
messages by polling the stream of the peer with the waker of its task,
which the stream keeps for the wait for the next batch if it has none.
And it lends each recipient its messages out of the batch where they
are a run of it, as those of a batch of one message are, and only looks
recipients up once a second message comes. Where a connection calls
itself through the bus 1000 times, then 20 connections do at once, the
bus runs 1.4% more instructions than before, as callgrind counts them.

As before, the task sends to all the recipients at once, each recipient
gets the messages of a peer in the order that the peer sent them, and
the task goes on to the next batch once each recipient has taken its
messages, or been disconnected. A recipient may now get a message of a
batch before another recipient gets an earlier one, which the order of
the messages of each peer to each recipient never ruled out.

The time limit of a client now covers the messages that it gets in one
go, rather than each message. It covers the wait for the other messages
on their way to the client as well, so the bound on the bytes of a batch
is low: a client has to take less than 16 KiB more within it than the
last of those messages alone.

A client that didn't negotiate passing file descriptors still gets the
messages without any: zbus turns the messages down before it writes any
of them, and the bus sends them again without those that carry file
descriptors.

A new test has a client that takes no file descriptors get a message
with one between two without, in one batch, and checks that it gets the
two.

The measurements of it are in dbus-bench's
results/2026-10-24-busd-batched-sends-xeon-4vcpu.md.

Assisted-by: Claude Opus 5.5 (claude-opus-5-5)
The bus forwards the messages of each peer in batches, which end with
the message that brings them to 16 KiB or more, and the commit of zbus
that it built on wrote up to 16 messages with each system call, but
ended each write before the message that would take it past 16 KiB. A
batch that went past 16 KiB took two writes to a recipient that got all
of it, the last message on its own, however small: the signals of 1,172
bytes of dbus-bench went 14 to a batch, and 13 and then 1 to a write.

The commit of the ccr-fd1ce738-0xtuf0 branch of zeenix/zbus that this
builds on ends a write with the message that brings it to 16 KiB or
more instead, as the bus ends a batch, so that a recipient gets the
messages of a batch with one write, unless one of them after the first
carries file descriptors.

Assisted-by: Claude Opus 5.5 (claude-opus-5-5)
The branch of z-galaxy/zbus#2079, which adds `Connection::send_all`,
and which the bus builds on until a release of zbus has it, is now
rebased onto zbus's main branch. That brings in fixes to the arguments
that `#[interface]` takes, and to the book, which the bus does not
depend on. This builds on the new head of the branch, and names the
pull request in the comment on the patch, rather than the branch.

Assisted-by: Claude Opus 5.5 (claude-opus-5-5)
`stalled_subscribers` checks that clients that stop reading, which the
same signals go to, hold those signals up for the time limit of the bus
all together, rather than for the time limit each. It timed them from
the start of a flood of 512 signals of 4 KiB until it saw the last of
them go, which it only looked for once all of the flood was sent. So it
counted the rest of the flood as well, which the bus forwards after the
stalled clients are gone, and which took over 300 ms on CI, where every
test logs all that it does. Besides, the bus can wait twice: the sockets
of the stalled clients that connected first hold a few more of the bus's
signals from before the flood, about the clients after them, so they
stop taking it a signal or so sooner than the others, which the bus then
waits on for a time limit of their own. With both, the test took 853 and
879 ms on CI on busd's main branch, beyond the 800 ms that it allows:
four time limits of 200 ms. Locally, with trace logs and the other tests
of the file running alongside, it failed 3 times in 12.

Time the stalled clients as they go, while the flood goes on. Under the
same conditions, the test passed 50 times in 50, taking 537 to 599 ms.

Assisted-by: Claude Opus 5.5 (claude-opus-5-5)
@zeenix
zeenix force-pushed the ccr-fd1ce738-0xtuf0 branch from 01d5ed5 to c087d05 Compare October 25, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant