open
https://gitlab.synchro.net/main/sbbs/-/issues/1195
## Summary
On a clean link, the Synchronet `sexyz` **ZMODEM sender is ~30–36× slower than
`lrzsz`'s `lsz`** (≈6 MB/s vs ≈200 MB/s on localhost). The bottleneck is `sexyz.c`'s transmit path, **not** the shared `zmodem.c` protocol engine, so **SyncTERM is not affected** (its sender is a simple buffered path).
## Measurements (32 MB random file, localhost, `-8`, unlimited window)
| Sender → Receiver | Goodput |
|---|--:|
| lsz → lrz (lrzsz baseline) | 203.8 MB/s |
| lsz → **sexyz** (sexyz receives) | 106.5 MB/s |
| **sexyz** → lrz (sexyz sends) | **5.7 MB/s** |
| sexyz → sexyz | 6.2 MB/s |
Wire overhead is identical (+2.81%) in every case — protocol efficiency is the
same; the gap is purely the sender implementation. sexyz's **receiver** is fine (~106 MB/s); only its **transmitter** is slow.
## Root cause (syscall profile, 32 MB)
| Sender | `write()` calls | avg write | `futex` calls |
|---|--:|--:|--:|
| lsz | 12,302 | ~2,803 B | 0 (single-threaded) |
| sexyz | 409,188 | **~84 B** | **2,268,600** (872 K erroring) |
`sexyz.c` feeds protocol output one byte at a time into a `RingBuf` (`send_byte()` → `RingBufWrite`, with per-write event signaling), drained by a
separate `output_thread`. Producer and consumer ping-pong: the consumer wakes on
each trickle, drains ~84 bytes, `write()`s, and waits again — a futex storm plus
badly fragmented output. Disabling the `OutbufHighwaterMark`/`OutbufDrainTimeout`
batching via `sexyz.ini` changes nothing (6.13 → 6.18 MB/s): that batching only
engages when the ring hits *empty*, which steady production avoids.
By contrast, SyncTERM's `send_byte` (`src/syncterm/term.c`) accumulates into an 8 KB `transfer_buffer` and `flush_send()`→`conn_send()` writes it in bulk (one
send per subpacket) — single-threaded, no ring buffer, no futex. Its write pattern resembles `lsz`'s, so SyncTERM does not have this problem.
## Suggested fix
Batch escaped output into the ring in spans (or bypass the ring for the bulk data path) so `write()`s are subpacket-sized rather than ~84 B and the producer/consumer stop ping-ponging on a futex. This is a `sexyz.c`-only change.
## Related enhancement (separate)
`zmodem.c` grows/shrinks the subpacket length with a blind ×2-up / ÷2-down ramp,
whereas `lrzsz` continuously tunes block length to the measured error rate (`calc_blklen`, a cost model over bytes-on-wire). Adding an adaptive block-length
model to `zmodem.c` would help both sexyz and SyncTERM on lossy/variable links.
## Method
Benchmarked with a userspace relay harness wiring two stdio ZMODEM endpoints (relay ceiling measured at ~5 GB/s, so not the bottleneck); random data on tmpfs; SHA-256 integrity-checked every run; `sexyz` = `v3.3 master/074785210`, `lrzsz` 0.12.21rc.
— *Authored by Claude (Claude Code), on behalf of @rswindell*
--- SBBSecho 3.37-Linux
* Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)