Skip to content

feat: implement /perf/1.0.0 protocol and unified-testing perf test app - #275

Merged
adust09 merged 1 commit into
mainfrom
feat/130-perf-interop
Aug 26, 2026
Merged

feat: implement /perf/1.0.0 protocol and unified-testing perf test app#275
adust09 merged 1 commit into
mainfrom
feat/130-perf-interop

Conversation

@adust09

@adust09 adust09 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Closes #130 (implementation part; the upstream perf/images.yaml PR to libp2p/unified-testing is a follow-up once this merges, since the entry must pin a merge commit).

What

  • LibP2P.Protocol.Perf — canonical /perf/1.0.0 from libp2p/specs perf: client sends an 8-byte big-endian download size, streams its upload, half-closes; server drains to EOF, then (never concurrently with the upload) sends the requested bytes back. Registered on the Switch like Ping, exported via the LibP2P facade.
  • interop/perf — unified-testing perf contract daemon (docs/write-a-perf-test-app.md): uppercase env vars, Redis coordination via SET / polled GET on {TEST_KEY}_listener_multiaddr (the perf contract's reference apps use a plain string, unlike the transport contract's RPUSH/BLPOP), YAML results on stdout with interpolated quartiles and 1.5×IQR outlier filtering (mirrors the reference implementations' stats math).
  • interop/perf/Dockerfile — same path convention nim/python/lua use in perf/images.yaml (dockerfile: interop/perf/Dockerfile, context = repo root).
  • CIperf-interop job: hs↔hs self-test + both hs↔nim cross directions.
  • Shared cleanupsword64BE/readWord64BE added to Core.Binary; closeQuietly promoted to MultistreamSelect.Negotiation (Ping/NAT/Identify/Relay still carry local copies; migrating them is out of scope here).

Why nim as the cross-implementation partner

Inspecting libp2p/unified-testing showed the go perf image (images/go/v0.45) is a placeholder — it dials but opens no perf streams (// Placeholder: simulate transfer), and the rust image deviates from the spec (sends two u64 headers instead of one). nim-libp2p mounts its real library perf protocol and is the canonical-wire reference, so cross-tests pin nim at the exact commit perf/images.yaml registers for nim-v1.15 (single source of truth: NIM_LIBP2P_COMMIT in interop/Makefile).

Verification

  • Unit: 1199 examples, 0 failures (18 new: wire behavior incl. big-endian pinning, half-close flow, stats/YAML rendering)
  • Self-test hs↔hs: pass
  • Cross hs↔nim, both directions: pass
    • upload hs→nim: 0.49–0.80 Gbps (write path is chunked and fast)
    • download nim→hs: rounds to 0.00 Gbps — bounded by the stack's byte-at-a-time StreamIO read path; honest measurement, follow-up issue to add a bulk-read op before the upstream submission (1 GiB defaults would be impractical otherwise)
    • latency: ~0.6 ms median

Follow-ups

  1. Bulk-read op on StreamIO to fix download throughput (blocking for upstream submission with default 1 GiB sizes)
  2. Open the perf/images.yaml PR to libp2p/unified-testing pinned at this PR's merge commit

Implement the canonical libp2p perf protocol (specs/perf) as a library
module and add the perf test daemon for the libp2p/unified-testing
framework (issue #130).

- LibP2P.Protocol.Perf: /perf/1.0.0 responder and initiator. Client
  sends an 8-byte big-endian download size, streams zeros, half-closes;
  server drains to EOF, then sends the requested bytes back.
- interop/perf: contract binary reading the perf env vars, coordinating
  via Redis SET/GET on {TEST_KEY}_listener_multiaddr (the perf contract
  uses a plain string, unlike the transport contract's RPUSH/BLPOP),
  and reporting upload/download/latency statistics as YAML on stdout
  with IQR outlier filtering.
- Cross-tests run against nim-libp2p (pinned to the commit
  perf/images.yaml registers for nim-v1.15): the unified-testing go
  perf image is a placeholder that opens no perf streams, and its rust
  image deviates from the spec (two u64 headers), so nim is the partner
  that actually exercises the canonical wire format.
- CI: perf-interop job runs the hs<->hs self-test and both hs<->nim
  directions.
- Shared cleanups: word64BE/readWord64BE join Core.Binary, and
  closeQuietly moves to MultistreamSelect.Negotiation.

Verified locally: 1199 unit tests pass; hs<->hs self-test and both
hs<->nim cross directions succeed (upload hs->nim 0.49-0.80 Gbps,
download nim->hs limited by the byte-at-a-time StreamIO read path,
latency ~0.6 ms).
@adust09
adust09 merged commit 1d68e25 into main Aug 26, 2026
4 checks passed
@adust09
adust09 deleted the feat/130-perf-interop branch August 26, 2026 07:25
adust09 added a commit that referenced this pull request Aug 26, 2026
Switch perf's drainUntilEof and discardExactly from streamReadByte to
streamReadChunk, completing the bulk-reader migration issue #276 lists.
Deferred from the previous commit only because the perf app lived on
PR #275's branch; now that #275 is merged the switch happens here.
discardExactly caps each chunk request at the bytes still owed, so it
still never consumes past the requested download size.
adust09 added a commit that referenced this pull request Aug 27, 2026
…hput (#277)

* feat: add streamReadChunk to StreamIO for bulk-read throughput

Every consumer of StreamIO read one byte at a time, which bounded the
perf test app's download direction at under 5 Mbps (issue #276) while
uploads ran at 0.49-0.80 Gbps. Add a chunk-level read to StreamIO:

  streamReadChunk :: Int -> IO ByteString

returning between 1 and n bytes (whatever is buffered or arrives next),
with EOF surfacing as an IOException exactly like streamReadByte. The
max-length argument (not in the issue's sketch) is what lets
readExactBounded use it safely: with no push-back mechanism, an
unbounded chunk read would consume bytes past a message boundary.

Implemented in the yamux adapter and Noise session wrapper (hand back
the buffered chunk / decrypted frame), the TCP socket (recv n), the
in-memory test pairs, and a mkByteStreamIO helper that derives a
one-byte-per-call chunk read for byte-queue test mocks.

readExactBounded now reads chunks, which moves the whole receive path
off byte-at-a-time reads: Noise frame reads from the raw socket, the
yamux read callback, and every length-delimited protocol reader. The
relay's forwardWithLimit also forwards at chunk granularity, still
never consuming a byte beyond the circuit's limit.

Two DCUtR upgrade tests asserted that no direct connection exists 500ms
after the circuit dial; the faster relayed path now lets the automatic
DCUtR upgrade pool a direct connection inside that window (on loopback
the handler-side dial is an ordinary client dial and succeeds). Those
tests now run with the automatic upgrade disabled via zero-length
timeout windows, making their pool preconditions deterministic.

* feat: drain perf payloads at chunk granularity

Switch perf's drainUntilEof and discardExactly from streamReadByte to
streamReadChunk, completing the bulk-reader migration issue #276 lists.
Deferred from the previous commit only because the perf app lived on
PR #275's branch; now that #275 is merged the switch happens here.
discardExactly caps each chunk request at the bytes still owed, so it
still never consumes past the requested download size.
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.

interop: implement and submit perf monitoring test app to libp2p/unified-testing

1 participant