Skip to content

Stripe CLI transfers across relay and direct paths; fix 10s relay deadline - #52

Open
YashasVM wants to merge 3 commits into
mainfrom
claude/app-performance-sub-agents-u3sbin
Open

YashasVM wants to merge 3 commits into
mainfrom
claude/app-performance-sub-agents-u3sbin

Conversation

@YashasVM

Copy link
Copy Markdown
Owner

Summary

CLI: use the relay and the direct path together (cmd/cdx/stripe.go)

  • Profiling showed the direct WebRTC path is CPU-bound in pion's SCTP stack, at about 30 MiB/s on loopback. The relay WebSocket alone moves about 120 MiB/s.
  • Before this change, the relay sat idle once the direct path opened.
  • Senders now keep both paths busy. Each path has a writer goroutine that takes the next data record whenever it has room.
  • A perturb-and-observe controller tunes the relay's share of the in-flight budget toward the highest acknowledged rate. The relay's share is capped at 16 MiB so it stays under the relay's 32 MiB per-peer buffer.
  • Relay records are held until acknowledged. If the relay fails, they are sent again on the direct path.
  • No protocol change. Receivers (Go recordLink and browser createRecordMerger) already merge both paths by sequence number and drop second copies. Both shipped in the same commit as the direct path.
  • CD_STRIPE=0 restores the old behavior.

CLI bug fix: relay transfers longer than ~10 s failed (link.go)

  • waitRelayEvent leaves its read deadline on the WebSocket: 10 s on a receiver, 15 min on a sender.
  • The link's relay reader inherited that deadline. Any receive that stayed on the relay for more than about 10 s died with transfer stalled.
  • This reproduces against production today; see the test results below.

Web

  • sink.js: the OPFS sink keeps one write in flight, so decrypting the next records overlaps disk I/O.
    • close drains the pending write.
    • A failed write surfaces on the next write or on close.
  • zip-bundle.js: rechunk passes pieces that are already chunk-sized through without copying them.

Bench

  • scripts/bench.mjs clicked #connect-btn, but a complete short code connects as it is typed and hides that button.
  • The click retried for 600 s, so the browser benchmarks never measured anything.
  • The bench now starts timing before the fill and clicks only if the button is visible.

Test results

Local bench (wrangler dev on loopback, 256 MiB CLI → CLI, 3 runs each):

MiB/s
before (CD_STRIPE=0) 29.7 / 29.7 / 27.5 → median 29.7
after 48.9 / 59.1 / 55.1 → median 55.1 (1.9×)

Production relay (wss://cd.yash0.in, real internet path through Cloudflare). Sender and receiver ran in the same cloud container, but all traffic left the machine. Every transfer was sha256-verified.

test old cdx (main) new cdx
500 MiB, relay + direct (2 runs) 24.4, 27.9 MiB/s 36.4, 39.8 MiB/s (~1.45×)
500 MiB, relay only (CD_DIRECT=0) FAIL: transfer stalled after ~10 s OK, 19.4 MiB/s
200 MiB new sender → old receiver — OK, 34.0 MiB/s
200 MiB old sender → new receiver — OK, 22.4 MiB/s
200 MiB CLI → production browser page (2 runs) 11.3, 10.7 MiB/s 11.5, 13.6 MiB/s

The last row confirms the deployed web receiver handles striped records: the hash matches and the sender exits 0. Its speed is limited by headless Chromium.

Other checks

  • go test -race ./... passes, including 9 new striper unit tests.
  • TestDirectPathConnectsWithTrickledCandidates was skipped. It also fails on main in this sandbox, which labels its address "direct internet path".
  • npm test (57 pass), npm run typecheck, and npm run build pass.
  • verify:cli-loop: 10/10 transfers exit 0 with exact bytes.
  • Stall check under heavy CPU contention (3 busy-loop hogs on 4 cores): the new sender completes 512 MiB.

Not tested: two physically separate machines. I couldn't start a second cloud machine from this session. Every transfer above crossed the real internet through the production relay, but both ends were in one container. Please do a quick check between two PCs on different networks, for example:

  • cdx send big.iso on one PC and cdx receive <code> on the other.
  • Compare against CD_STRIPE=0.

Notes

  • The extra throughput from striping depends on the relay path. On a home connection, the sender's upload speed usually caps the relay's contribution. The controller moves data off a slow relay, so in that case it costs little.
  • The browser receive changes don't have a clean before/after browser number. The browser bench was broken before this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_012g5D2t4GGkGV1pn3Z2EcC4


Generated by Claude Code

Profiling showed the direct WebRTC path is CPU-bound in pion's SCTP stack
(about 30 MiB/s on loopback) while the relay WebSocket alone moves about
120 MiB/s, yet once the direct path opened the relay sat idle. Senders now
keep both busy: each path's writer goroutine takes the next data record
when it has room, and a perturb-and-observe controller tunes the relay's
share of the in-flight budget toward the highest acknowledged rate. Relay
records stay held until acknowledged and go out again on the direct path
if the relay fails. Receivers already merge both paths by sequence number
and drop second copies, so no protocol change is needed. CD_STRIPE=0
restores the previous behavior.

Also clear the read deadline waitRelayEvent leaves on the WebSocket (10s
on a receiver): the link's relay reader inherited it, so a receive that
stayed on the relay for more than ~10s died with "transfer stalled".

Local bench, 256 MiB CLI -> CLI: 28.8 -> 54.4 MiB/s (median of 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g5D2t4GGkGV1pn3Z2EcC4
The OPFS sink now starts each flush without waiting for it, keeping one
write in flight so decrypting the next records overlaps disk I/O; close
drains it and a failed write surfaces on the next write or close.
rechunk yields pieces that are already chunk-sized as-is instead of
copying them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g5D2t4GGkGV1pn3Z2EcC4
A complete short code connects as it is typed, which hides the button,
so the bench's click retried until its 600s timeout and never measured
the transfer. Start timing before the fill and click only if visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g5D2t4GGkGV1pn3Z2EcC4
@coderabbitai

coderabbitai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9f510ba1-fa96-463a-90ea-3d51b7ad5028
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

2 participants