2026-10-12 20:48:40 +09:00
|
|
|
|
---
|
|
|
|
|
|
type: report
|
|
|
|
|
|
date: 2026-10-12
|
|
|
|
|
|
---
|
|
|
|
|
|
# Raw CAN over MQTT: bus bench
|
|
|
|
|
|
|
|
|
|
|
|
Can mosquitto carry `yukart/can/raw` at real CAN rates (project-yukart decision 4)?
|
|
|
|
|
|
Measured on the car Orin (mosquitto 2.0.11, localhost) with `bus-bench`: `bridge`
|
|
|
|
|
|
reads a `vcan0` and publishes frames, `sink` subscribes, checks sequence numbers
|
|
|
|
|
|
and measures latency from the kernel's reception timestamp to the subscriber.
|
|
|
|
|
|
The car's usual services (recorder, orin-status, svc-gw, svc-vision) kept running.
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
scripts/bus-bench.sh ./bus-bench z27ag-20261009-163736-ign.log 20 # on the Orin
|
2026-10-12 22:33:44 +09:00
|
|
|
|
scripts/bus-mix.sh ./bus-bench z27ag-20261009-163736-ign.log 20 # after bus-bench.sh
|
2026-10-12 20:48:40 +09:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Loads:
|
|
|
|
|
|
|
|
|
|
|
|
- **car replay**: the in-car capture (`z27ag-20261009-163736-ign.log`, engine at
|
|
|
|
|
|
idle) at its own timing, about 890 frames/s. This is the real bus.
|
|
|
|
|
|
- **full bus**: `cangen -L 8` at about 4,400 frames/s. 500 kbit/s carries roughly
|
|
|
|
|
|
3,700–4,500 8-byte frames/s depending on bit stuffing, so this is a saturated bus.
|
|
|
|
|
|
- **flood**: `cangen -g 0`, as fast as vcan goes, to find where it breaks.
|
|
|
|
|
|
|
|
|
|
|
|
Two encodings: one MQTT message per frame, or frames batched over a 10 ms window.
|
|
|
|
|
|
Both use the same provisional binary format (24 bytes a frame, 12-byte header;
|
|
|
|
|
|
see `src/bus-bench/main.rs`). QoS 0, not retained.
|
|
|
|
|
|
|
|
|
|
|
|
## Results (20 s runs; full bus 15 s)
|
|
|
|
|
|
|
|
|
|
|
|
With `TCP_NODELAY` on both the clients and the broker (see below):
|
|
|
|
|
|
|
|
|
|
|
|
| Load | Encoding | Frames/s delivered | Lost | Latency p50 / p99 / max (ms) | CPU: broker · bridge · sink (% of a core) |
|
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
|
|
|
| car replay | per frame | 893 | 0 | 0.31 / 0.92 / 1.3 | 4.7 · 3.5 · 3.8 |
|
|
|
|
|
|
| car replay | 10 ms batch | 893 | 0 | 8.7 / 15.3 / 17.6 | 0.9 · 1.2 · 0.6 |
|
|
|
|
|
|
| full bus | per frame | 4,440 | 0 | 0.16 / 0.30 / 3.3 | 27.5 · 21.7 · 17.0 |
|
|
|
|
|
|
| full bus | 10 ms batch | 4,562 | 0 | 5.3 / 10.4 / 10.8 | 1.1 · 3.5 · 0.8 |
|
|
|
|
|
|
| flood | per frame | 45,700 | 84 % | ~1.8 s, queues full | 85 · 86 · 81 |
|
|
|
|
|
|
| flood | 10 ms batch | 268,800 | 0 | 5.3 / 10.2 / 11.1 | 1.3 · 63 · 0.9 |
|
|
|
|
|
|
|
|
|
|
|
|
- **Real rates hold, with margin.** Per frame breaks at about 45,000 messages/s:
|
|
|
|
|
|
ten times a saturated bus and fifty times the real one.
|
|
|
|
|
|
- **Batching is the clear choice.** At a full bus it costs the broker about 1 % of a
|
|
|
|
|
|
core instead of 28 %, at a fixed ~100 messages/s whatever the bus load. Frames are
|
|
|
|
|
|
never lost: the batched flood (60 times a full bus) still lost none, and the
|
|
|
|
|
|
bridge's own CAN reads became the limit. The price is up to one window of
|
|
|
|
|
|
latency (the 10 ms here). That is no problem for a dashboard, a recorder or a
|
|
|
|
|
|
decoder.
|
|
|
|
|
|
- Per frame stays the option for a consumer that needs sub-millisecond latency. Nothing
|
|
|
|
|
|
planned does.
|
|
|
|
|
|
|
|
|
|
|
|
## Nagle: the 40 ms tail
|
|
|
|
|
|
|
|
|
|
|
|
The first runs had a p99.9 near 40 ms at every load: Nagle's algorithm holding small
|
|
|
|
|
|
writes back until the previous one is acknowledged, against delayed ACKs. Both
|
|
|
|
|
|
sides do it by default: rumqttc (`NetworkOptions::set_tcp_nodelay`, off by default)
|
|
|
|
|
|
and mosquitto (`set_tcp_nodelay`, off by default).
|
|
|
|
|
|
|
|
|
|
|
|
| Full bus (~3,000 frames/s) | per frame p99 / p99.9 / max | 10 ms batch p99 / max |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| Nagle on both sides | 23.5 / 41.7 / 45 | 65.6 / 80.9 |
|
|
|
|
|
|
| client no-delay, broker Nagle | 0.61 / 38.0 / 47 | 43.3 / 55.3 |
|
|
|
|
|
|
| no-delay on both | 0.33 / 0.67 / 2.8 | 10.5 / 10.8 |
|
|
|
|
|
|
|
|
|
|
|
|
The car's broker now has `set_tcp_nodelay true` (orin-nano-env-setup `MOSQUITTO.md`).
|
|
|
|
|
|
Clients that publish or consume raw CAN set `set_tcp_nodelay(true)` too. At once a
|
|
|
|
|
|
second (orin-status) it does not matter.
|
|
|
|
|
|
|
2026-10-12 22:33:44 +09:00
|
|
|
|
## The bus as the car will use it
|
|
|
|
|
|
|
|
|
|
|
|
`scripts/bus-mix.sh`: the raw stream (10 ms batches) to four subscribers (the decoder
|
|
|
|
|
|
plus three: svc-gw, telemetry-gw, a debug tool), and a stand-in `decoder` publishing
|
|
|
|
|
|
envelopes on one retained topic per signal, to five subscribers that each take every
|
|
|
|
|
|
signal (svc-gw, recorder, svc-nav, svc-vision, telemetry-gw). Loads keep the car's
|
|
|
|
|
|
27 CAN IDs and their rates (2–100 Hz): the capture as recorded, and played 5× faster
|
|
|
|
|
|
for a full bus. Signals per frame and a per-signal rate cap (latest value wins)
|
|
|
|
|
|
stand in for the decoder design nobody has made yet. 20 s runs.
|
|
|
|
|
|
|
|
|
|
|
|
| Load | Signals/frame | Cap | Envelopes/s | Deliveries/s (×5) | Broker CPU | Envelope latency p50 / p99 (ms) |
|
|
|
|
|
|
|---|---|---|---|---|---|---|
|
|
|
|
|
|
| car | 3 | 50 Hz | 1,600 | 8,000 | 22 % | 13 / 21 |
|
|
|
|
|
|
| car | 3 | none | 2,550 | 12,800 | 35 % | 13 / 20 |
|
|
|
|
|
|
| car | 8 | 20 Hz | 2,550 | 12,800 | 32 % | 14 / 27 |
|
|
|
|
|
|
| car | 8 | 50 Hz | 4,300 | 21,600 | 47 % | 16 / 32 |
|
|
|
|
|
|
| car | 8 | none | 6,800 | 34,000 | 51 % | 14 / 47 |
|
|
|
|
|
|
| full bus | 3 | 50 Hz | 3,100 | 15,500 | 39 % | 12 / 18 |
|
|
|
|
|
|
| full bus | 8 | 20 Hz | 3,500 | 17,300 | 44 % | 22 / 41 |
|
|
|
|
|
|
| full bus | 8 | 50 Hz | 8,100 | 40,600 | 57 % | 15 / **391** |
|
|
|
|
|
|
| full bus | 3 | none | 12,800 | 63,800 | 74 % | **636 / 1,472** |
|
|
|
|
|
|
| full bus | 8 | none | 16,300 offered | — | 75 % | **5,380 / 5,994**; decoder queue overflowing |
|
|
|
|
|
|
|
|
|
|
|
|
- **The raw stream never suffered.** All four raw subscribers got every frame in every
|
|
|
|
|
|
run, p99 ≤ 16 ms, including the runs where the envelopes collapsed.
|
|
|
|
|
|
- **Envelopes are the load.** The broker's cost follows deliveries (messages ×
|
|
|
|
|
|
subscribers). Up to ~22,000 deliveries/s it stays under half a core with p99 near
|
|
|
|
|
|
30 ms; around 40,000 the tail goes to hundreds of ms; past ~60,000 queues grow by
|
|
|
|
|
|
seconds. Mosquitto is single-threaded, so this is a ceiling, not a share.
|
|
|
|
|
|
- **Each subscriber pays too.** Parsing JSON envelopes cost each sink 14–38 % of a
|
|
|
|
|
|
core at these rates; five of them took two cores. A real subscriber that takes
|
|
|
|
|
|
everything (svc-gw) pays the same.
|
|
|
|
|
|
- Envelope loss is not measured exactly (no sequence numbers in the envelope);
|
|
|
|
|
|
the decoder's own `refused` counts what it could not even queue.
|
|
|
|
|
|
|
|
|
|
|
|
What keeps it in the comfortable range, any of which is enough at the car's rate:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Cap each signal's rate in the decoder**, latest value wins. 20–50 Hz is still
|
|
|
|
|
|
10–50× OBD-II polling, and the raw stream keeps the full rate for anything that
|
|
|
|
|
|
needs it.
|
|
|
|
|
|
2. **Subscribe narrowly.** Only a consumer that shows or stores everything (svc-gw)
|
|
|
|
|
|
needs `yukart/vehicle/+`; svc-nav and svc-vision need a handful of topics.
|
|
|
|
|
|
3. **Publish slow signals on change**, with a heartbeat; temperatures and states do
|
|
|
|
|
|
not need 50 Hz.
|
|
|
|
|
|
4. **The recorder need not record decoded signals at full rate.** It records raw
|
|
|
|
|
|
frames, and decoding them again offline gives the same signals.
|
|
|
|
|
|
|
2026-10-12 20:48:40 +09:00
|
|
|
|
## Not tested
|
|
|
|
|
|
|
|
|
|
|
|
- A real CAN adapter: vcan has no bus timing, and the PCAN-USB's own reception and
|
|
|
|
|
|
timestamps are already covered by the recorder.
|
|
|
|
|
|
- The broker under memory pressure, or with the GPU/vision load of later phases.
|
2026-10-12 22:33:44 +09:00
|
|
|
|
- The real decoder: signals per frame, rates and topic layout are stand-ins.
|