Companion to
performance-expectations.md(the design-shaped "where the time goes" doc) andprotocol.md(wire-level framing constraints that dictate the ceiling).This file is the operational view: what to expect at runtime, recommended buffer sizes, and an explicit inventory of what we do not measure today.
There is no benchmark harness in this repo. No kotlinx-benchmark,
no jmh, no Android Macrobenchmark. CI does not produce performance
numbers, and a perf regression would only surface if it crossed into
correctness territory (a test timing out, a flow buffer overflowing).
This is a deliberate scope cut for 0.x:
- The radio link tops out at ~1 KB/s; SDK CPU is irrelevant against it.
- Adding a benchmark suite costs more in CI flakiness than it returns while we're pre-1.0 and changing internal shapes weekly.
- Memory and connect-time are observable today via the
cli probesub-command (see "Measuring" inperformance-expectations.md).
A kotlinx-benchmark-based micro-benchmark module is on the post-1.0
roadmap. When it lands, it will live at :benchmarks/ and run in a
nightly job (not on every PR).
| Path | Ceiling | Bottleneck |
|---|---|---|
Inbound frame → engine → events emission |
well above wire-rate (engine processes a frame in ≪ 1 ms on a 2020-era laptop) | wire / firmware |
Outbound client.sendText(...) → bytes on transport |
~5 ms bookkeeping; rest is firmware | wire / firmware |
| BLE write throughput | governed by Kable / CoreBluetooth pacing — sustained writes are paced per-MTU per connection interval | OS BLE stack |
| TCP throughput | LAN ≫ wire; the radio is the limit | wire |
| Serial throughput | UART baud (typically 115 200 bps); wire-format adds ~12 % framing overhead | UART + wire |
The radio link is the ceiling for every end-to-end scenario. SDK overhead would have to be catastrophic to be visible at all.
The engine's resident footprint is kilobytes, not megabytes:
- One actor coroutine + a single mailbox
Channel. - One
MutableSharedFlowper public surface (events, packets, …) with bounded replay (replay = 0for events;replay = 1for state flows). - Node-DB cache keyed by
nodeNum, typically 10–100 entries on a real mesh, bounded by firmware limits. - Pending-request map for outstanding admin/routing RPCs, typically empty.
Subscribers that block in their collect { } are the real risk —
treat hot-flow collectors as if they were on a UI thread.
These are the values that work today; tune only with measurement.
| Surface | Buffer | Rationale |
|---|---|---|
Transport incoming channel (BLE/serial/TCP) |
Channel.UNLIMITED (unbounded) for in-process fakes; bounded ring per-transport in production |
The engine drains fast; the bound exists to detect a stuck reader, not to gate steady state. |
events MutableSharedFlow |
replay = 0, extraBufferCapacity = 64, onBufferOverflow = DROP_OLDEST |
Slow consumers get MeshEvent.PacketsDropped rather than back-pressuring the engine. |
packets MutableSharedFlow |
same shape as events |
same rationale; the firmware will resend on rebroadcast if the mesh ACKs. |
| BLE outbound write | one MTU per write (Kable handles segmentation) | Larger writes get re-segmented anyway. |
| Serial frame assembler | 4 KiB scratch buffer per stream | Larger than any valid Meshtastic frame (max_packet_size ≤ 256 B); leaves room for resync after partial frames. |
| TCP read buffer | 4 KiB per connection | Aligned with default ktor-network buffering. |
Two paths matter for perceived responsiveness; see
performance-expectations.md
for the design rationale and budgets.
- Send → ACK round-trip —
client.send(...)to first byte on the transport: < 5 ms. Rest is firmware + radio. - Inbound frame →
eventsemission — < 1 ms on the engine dispatcher; subscribers run on their own dispatchers.
Neither path holds locks; neither blocks on I/O.
See protocol.md for:
- Sync-byte framing overhead (4 bytes per frame).
max_packet_sizeper device (typically 237 B post-overhead).- Heartbeat cadence (30 s default for TCP/serial; opt-in for BLE).
- Two-stage handshake (
config_id69420/69421) and why we send Stage 2 before draining Stage 1's NodeInfo storm.
Until the benchmark harness lands:
- Connect time / handshake time:
cli probe N tcp <host>over N cycles, diffpayload.duration_msbetween SDK versions. - Engine memory: attach a JVM profiler to a long-running CLI
session; the engine's retained set should plateau within seconds of
Connected. - Subscriber back-pressure: enable
LogLevel.Debugand watch forPacketsDroppedevents under a synthetic load (B3 inmanual-tests.md).
Carried over from performance-expectations.md; restated here so
operators have one page to land on:
- Frame logging at
Debugallocates a string per envelope. Off by default (ADR-011). - Many subscribers on hot flows —
events/packetsuseMutableSharedFlowwith replay; subscribers that block pile up buffers. - BLE rate-limiting — Kable / CoreBluetooth pace per-packet writes; bursts block the transport coroutine, never the engine actor.
performance-expectations.md— design rationale and end-to-end timing budgets.protocol.md— wire framing, MTU, handshake cadence.threading-model.md— what runs where.observability.md— measuring in production viaLogSink.- ADR-002 — single-writer actor.