• docs/zmodem_comparison.md

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Thu Jul 23 23:03:46 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/7471d1b776cb2d7ea274a41e
    Added Files:
    docs/zmodem_comparison.md
    Log Message:
    docs: add ZMODEM implementation comparison (sexyz vs lrzsz vs rzsz)

    A performance, scalability, and robustness analysis of Synchronet's ZMODEM (sexyz + the shared zmodem.c, which SyncTERM also uses) benchmarked against lrzsz 0.12.21rc and Chuck Forsberg's final rzsz 3.73 (modern branch).

    Headline findings, all empirically measured:

    - sexyz's sender is ~25x slower than lrzsz's lsz on a fast link (~8 vs
    ~204 MB/s), from two stacked overheads isolated by linking the real
    zmodem.o behind two send paths: zmodem.c's per-byte callback + escape +
    CRC design caps any sender at ~26 MB/s (shared, so SyncTERM inherits it),
    and sexyz.c's ring-buffer + output-thread drops that a further ~3x to
    ~8 MB/s (sexyz-only; a futex storm + ~84-byte writes). SyncTERM avoids
    the second layer, so its upload runs ~26 MB/s.
    - A windowed (-w) send hangs the instant the file offset crosses 2 GiB:
    zmodem.c narrows transmit-window/ACK positions to signed int32_t. Shared
    with SyncTERM. The data path (fseeko/int64) is otherwise 64-bit clean;
    4 GiB is a hard protocol ceiling (32-bit wire position field).
    - sexyz's receiver is fine past 2 GB; Forsberg's sender runs ~92 MB/s but
    its receiver won't complete headlessly (serial-tty assumptions).

    Includes a conditions matrix (block size, CRC, latency, bandwidth
    asymmetry, injected errors) and per-finding attribution to sexyz.c vs the shared zmodem.c vs SyncTERM's term.c. Relates to the sexyz-throughput and 2-GiB-window GitLab issues.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Thu Jul 23 23:03:46 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f29244a40663ce7f99d3e4e6
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs: note the sexyz SINGLE_THREADED build is not a throughput fix

    Document that sexyz.c:84 still carries a source-level
    `#define SINGLE_THREADED FALSE` (no makefile toggle), but its
    single-threaded send_byte (sexyz.c:682) writes one byte per write()
    syscall via sendbuf() Ä trading the two-thread futex storm for a
    write-syscall storm, generally worse. Add a three-way send-architecture
    table (threaded / single-threaded / buffered) and a recommendation caveat:
    the fix is output buffering (flush per subpacket, as SyncTERM's term.c
    does), not thread count.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Thu Jul 23 23:03:46 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/5c0e802235bfc797afbdadb1
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs/zmodem: correct throughput magnitudes to steady-state; version-tag

    The initial throughput figures were measured on 32 MB transfers whose
    short duration was dominated by per-transfer startup, which deflated and distorted the rates. Re-measured at 256 MB steady-state:

    - sexyz sender was ~18x (not ~25x) slower than lsz: ~11 vs ~204 MB/s.
    - The two layers are ~7.5x sexyz.c (ring per-byte, 85->11, now fixed by
    the send-batching change -> ~66 MB/s) and ~2.4x zmodem.c (per-byte send
    design, 204->85, shared with SyncTERM), not the ~3x/~8x quoted before.
    - The SyncTERM-model (ztx_buf) sender runs ~85 MB/s, not ~26; sexyz
    receiver ~130; Forsberg sender ~107.

    The mechanism findings (futex storm, ~84-byte writes, ring-per-byte) were unaffected -- only the magnitudes changed.

    Also: tag every number to a component version (sexyz.c revision 3.3->3.4, zmodem.c rev 2.2->2.3) since Synchronet's side is actively changing, and
    mark the recommendations with status (Fix A and the 2 GB fix done, Fix B
    and the adaptive-block-length model pending).

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Thu Jul 23 23:03:46 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/9c73c787d0f036e0eff35085
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs/zmodem: record that the send-batching fixes were reverted

    Multi-run error-recovery testing showed both batching prototypes regress
    error recovery under heavy injected corruption: the shipped per-byte
    sender passes 3/3 (~50 s), the Fix A prototype (batch spans into the ring, sexyz.c) timed out 0/3, and the Fix B prototype (send_buf span callback, zmodem.c) hard-failed 1/3. Root cause: batching a whole subpacket before
    one blocking flush starves the back-channel during retransmit storms (the sender is stuck in the flush and can't service ZRPOS), stalling the
    transfer; the original per-byte-to-ring keeps the pipeline draining and
    stays responsive.

    Both prototypes were reverted. The only shipped code change remains the
    2 GB signed-position fix in zmodem.c (rev 2.3, GitLab #1196); sexyz.c is unchanged (stays 3.3). The ~66/~88 MB/s figures are retained in the doc, relabeled as reverted prototypes, because the root-cause analysis (ring per-byte -> futex storm / 84-byte writes) is still valid and the eventual
    fix must service the back-channel while sending.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sun Aug 30 03:07:35 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/218c474f6d616d63799bdac5
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs/zmodem: re-measure everything against the current releases

    The document had accumulated five measuring sessions between 2026-07-23
    and 2026-08-25, comparing components at versions that no longer ship and carrying figures taken against a baseline now known to have been the bottleneck. Re-measure the whole thing in one sitting on one host and
    rewrite around it.

    Structural change: measure the full 20-pair sender x receiver matrix
    rather than holding one end fixed. Fixing an end is what produced the
    old sender table's four-way tie -- every row was scored against lrz,
    which receives for more CPU than any sender in the table spends to send,
    so 1.74x of real difference reported as 1.8%. The matrix has no such
    blind spot, and printing both endpoints' CPU makes the bottleneck visible
    in every cell.

    All twenty pairs interoperate byte-identically and wire overhead is equal
    to two decimals at equal block size, so nothing here is a compatibility problem. zmtx/zmrx 2.04 is the cheapest implementation in both
    directions by roughly a factor of two -- 10.0 instructions per data byte sending and 12.1 receiving, against the next best of 23.5 either way.
    sexyz's receiver is second at 24.6, where the same code cost 126.8 eight
    days earlier, and its error recovery is the strongest measured: 10 of 10
    in both directions where the all-lrzsz pair manages 7 of 10.

    Two findings belong to zmtx/zmrx rather than to us. Its whole-file retry budget resets only on the ZRPOS path, so NAK- and timeout-driven recovery
    still drains it and the sender cannot complete a transfer at or above a
    5e-7 error rate. And its ESCCTL escaping omits carriage return, so no
    receiver that requests escaping can download from it -- the same defect
    our own send path carried until 2026-08-24.

    New sections cover what the old document never measured: feature and
    option support side by side, ESCCTL interoperability, and per-endpoint instruction counts for all eight endpoints.

    Method notes that cost real time to learn are written down. The
    corruption model seeds its PRNG with a constant, so repeated runs at one
    rate are not independent samples and a low enough rate injects nothing at
    all -- a rate sweep is the instrument, not repetition. Error gates are
    far more load-sensitive than throughput runs: a first pass taken while a profiler was still finishing scored the same lrzsz pair 0/3 and 2/3 in
    two identical invocations, where it passes in half a second on an idle
    host. And the first cut of this measurement used a zmtx checkout 32
    commits stale, which is why every version in section 1.1 now carries its release date.

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sun Aug 30 03:07:35 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f8442b0765fd2eab167a8257
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs/zmodem: add Forsberg's DSZ as a reference for the negotiated options

    The four implementations compared here cannot settle what ZMODEM's option
    bits were meant to do, because for ESC8 none of them agrees with another. Forsberg's commercial DSZ can: he wrote the protocol, and the binary still runs.

    Add section 7.4 covering DSZ.EXE 1997-05-25 under DOSBox, with its COM1
    bridged to TCP. It is deliberately not in the throughput tables -- 16-bit
    DOS, no source, nothing to build or profile -- and appears only for the negotiated options. The base protocol interoperates perfectly: a
    4096-byte random file sent by that 1997 binary arrives byte-identical at
    both sexyz and zmrx.

    Its ZRINIT settles ESC8. "rz -E" sets ZF0 bit 0x80, so ESC8 is the
    negotiation bit for the "8th bit quoting" that DSZ.DOC describes as half
    of ZMODEM-90's 7-bit mode; "rz -e" sets ESCCTL; "rz -P" sets neither.
    CANRLE is set unconditionally, which matches the other half of that 7-bit
    mode being present in Forsberg's free source while the quoting half never
    was.

    And the two implementations of ESC8 that exist do not interoperate. DSZ
    quotes a high-bit byte as 0x0E followed by the byte with bit 7 cleared -- Kermit-style prefixing, and it switches to frame type 0x31, which is not
    among the eight types any published zmodem.h defines -- while zmtx/zmrx
    2.04 sends ZDLE and the byte XOR 0x40. Recovered by known-plaintext
    capture: 0x0E occurs 4,991 times and is followed by a byte below 0x80 in
    100% of them, and decoding on that rule alone recovers 92 contiguous
    bytes of the known plaintext while restoring the stream to 45.6%
    high-bit.

    Neither encoding is wrong, because ZMODEM.DOC names ESC8 once and never
    says how a high-bit byte is escaped, where ESCCTL gets a paragraph. Two implementers 29 years apart read that line and built unrelated mechanisms.

    Also record the method, since the obvious route is the wrong one.
    DSZ.EXE is a plain unpacked MZ image and disassembles fine, but its
    escape table is not in the file -- it is built at runtime into BSS,
    exactly as Forsberg's free zsendline_init() does. Running the binary
    against a deliberately permissive receiver with known plaintext settled
    it in three runs.

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sun Aug 30 03:07:35 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f06bafe5ee11ad2f2056050a
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs/zmodem: record option-by-option compatibility with DSZ

    Section 7.4 had DSZ answering what the negotiated option bits mean, but
    the only transfers behind it were a pair of defaults. In particular the
    BBS download case -- sexyz sending to DSZ receiving -- had never been run
    once. Run the whole option set in both directions and record it.

    Everything interoperates. Sending to DSZ: default, -8/-4/-2, -o, -s,
    -w4096, -e, -l, -y, -p and -n all transfer an 8 KiB random file
    byte-for-byte or, where the option says not to, correctly decline to.
    Receiving from DSZ: default, -m (MobyTurbo), -Q@ and -e likewise.

    Two results are worth more than the tick in the box. The file-management options added this month produce the documented behaviour at a
    third-party receiver: -p leaves DSZ's existing file alone and -n skips an
    older source. -n only demonstrates itself once the timestamps are forced apart, because FAT's two-second granularity makes a same-minute
    comparison ambiguous -- the first run of it looked like a failure and was
    not. And ESCCTL works in both directions, which is the path that carried
    the carriage-return defect until 2026-08-24.

    DSZ's 7-bit options turn out to be receiver-driven despite their names:
    "sz -E" and "sz -P" sending to sexyz both transfer identically and leave
    46.9% of the wire carrying high-bit bytes, the same as with no option at
    all. They request the mode when DSZ receives and impose nothing when it
    sends.

    State plainly what was not tested rather than letting the table imply
    coverage: -m as a local size limit, the X/YMODEM modes, and -telnet.
    Say too what "we do not support ESC8" actually means -- sexyz no longer
    sets the bit, and it ignores a remote receiver's request rather than
    refusing it, there being no way to decline a ZRINIT capability, which is
    what lrzsz does as well.

    And note a capability we decline unnecessarily: DSZ advertises CANRLE in
    every ZRINIT and implements RLE both ways, while sexyz never advertises
    it, so the compression is never used.

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sun Aug 30 03:11:20 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/6a06088bd80f6b71f9681cba
    Modified Files:
    docs/zmodem_comparison.md
    Log Message:
    docs/zmodem: sexyz no longer advertises ESC8, so stop saying it does

    The document was written while the Escape8thBit option still existed and
    kept describing it in the present tense in four places: the feature
    table, the summary, section 7.2's analysis and section 8's findings. The option and the ZRINIT advertisement were both removed the same day the
    last of those was written.

    Put the sexyz half in the past tense and say what the current behaviour
    is -- the bit is never set, and a remote receiver's request is ignored
    rather than refused, there being no way to decline a ZRINIT capability.
    The measurements stay as they were taken; only the claims about what
    sexyz does today change.

    ---
    ■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net