diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-15 09:19:34 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-15 09:19:34 +0200 |
| commit | 4fa546a759dc9bd2ab77f793e3f2e660acd31bdb (patch) | |
| tree | 967f46a0ef0f24086587d76231224667908c6ac1 /packages/meshbay-hub/src/meshbay_hub/static/app.js | |
| parent | 84b032c65e17267d41e04605e79eea82a6f5a59f (diff) | |
| download | meshbay-4fa546a759dc9bd2ab77f793e3f2e660acd31bdb.tar.gz | |
perf(upload): several chunks in flight, instead of one per round trip
The uploader read a 48 KB slice, sent it, and waited for the node to
acknowledge it before reading the next one. That caps throughput at one
chunk per round trip regardless of available bandwidth, and it is worse
than the arithmetic suggests: the sender is idle for almost the whole
time, so SCTP's congestion window never opens either, and the transport
stays slow even when the link is not.
Measured against the real node over a 100 ms path (netem on loopback):
48 KB chunks, one at a time 0.16 MB/s
48 KB chunks, 32 in flight 3.47 MB/s
On loopback with no latency both are ~32 MB/s, which is why nothing here
ever caught it: the local end-to-end run cannot see a round-trip problem.
transport.uploadFile() now keeps a window of chunks in flight and matches
acks by arrival, with the node's own ordering rule as the guard — a
DataChannel is ordered and reliable, and the node refuses any chunk that
is not the one it expects next. It pauses when the channel's buffered
amount gets high, so the progress bar keeps reporting what the node has
taken rather than what the browser has queued. Both callers, the Files
panel and chat attachments, go through it.
The end-to-end harness grew an opt-in benchmark behind MESHBAY_BENCH=1
that removes its own files afterwards, and it taught me something about
the harness rather than the code: it took an unsolicited index_sync push
for an upload ack, because unlike app.js it had no place to put one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 26 |
1 files changed, 7 insertions, 19 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index fee21dc..3ef0da5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -832,7 +832,6 @@ function formatDate(ts) { // ── Group Page ────────────────────────────────────────────────────────────── const CHUNK_SIZE = 1024 * 1024; -const UPLOAD_CHUNK_SIZE = 48 * 1024; const PIPELINE_WINDOW = 8; async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk, writable) { @@ -1109,15 +1108,10 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, setError(''); setUlState({ name: file.name, sent: 0, total: file.size, indexing: false }); try { - const totalChunks = Math.ceil(file.size / UPLOAD_CHUNK_SIZE); - for (let i = 0; i < totalChunks; i++) { - const slice = file.slice(i * UPLOAD_CHUNK_SIZE, (i + 1) * UPLOAD_CHUNK_SIZE); - const buf = new Uint8Array(await slice.arrayBuffer()); - await transport.uploadChunk(file.name, i, totalChunks, buf); + await transport.uploadFile(file, { // Bytes actually acknowledged by the node, not bytes read locally. - setUlState(prev => prev && { ...prev, sent: Math.min(file.size, - (i + 1) * UPLOAD_CHUNK_SIZE) }); - } + onProgress: (sent) => setUlState(prev => prev && { ...prev, sent }), + }); // The node re-indexes on a filesystem event; there is nothing to poll, so // say what is happening instead of showing a finished bar and no file. setUlState(prev => prev && { ...prev, indexing: true }); @@ -1934,16 +1928,10 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, on if (!transport || !transport.connected) return; setAttaching(true); try { - const totalChunks = Math.ceil(file.size / UPLOAD_CHUNK_SIZE); - let storedAs = file.name; - for (let i = 0; i < totalChunks; i++) { - const slice = file.slice(i * UPLOAD_CHUNK_SIZE, (i + 1) * UPLOAD_CHUNK_SIZE); - const buf = new Uint8Array(await slice.arrayBuffer()); - const ack = await transport.uploadChunk(file.name, i, totalChunks, buf); - // Two people sending IMG_1234.jpg both succeed; the node picks a free - // name and the message has to point at the one it chose. - if (ack && ack.stored_as) storedAs = ack.stored_as; - } + // Two people sending IMG_1234.jpg both succeed; the node picks a free name + // and the message has to point at the one it chose. + const ack = await transport.uploadFile(file); + const storedAs = (ack && ack.stored_as) || file.name; await new Promise(r => setTimeout(r, 2500)); if (onRefreshIndex) await onRefreshIndex(); const ext = file.name.split('.').pop().toLowerCase(); |