aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_downloads.py53
1 files changed, 53 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_downloads.py b/packages/meshbay-hub/tests/test_downloads.py
index 3716f4c..7062d39 100644
--- a/packages/meshbay-hub/tests/test_downloads.py
+++ b/packages/meshbay-hub/tests/test_downloads.py
@@ -602,3 +602,56 @@ console.log(JSON.stringify(out));
f"resume at {out['resumeFrom']} — that gap is a hole in the file")
# The resumed run covers exactly the rest, and repeats nothing.
assert out["writtenAfterResume"] == list(range(out["resumeFrom"], 10)), out
+
+
+def test_a_written_chunk_is_not_held_until_the_download_ends(tmp_path):
+ """A file streamed to disk holds one pipeline window in the page, not the
+ whole file.
+
+ `pipelinedDownload` kept every chunk's resolved promise in `inflight` for
+ the length of the call, and each promise held its ciphertext — so a file
+ written straight to disk was also held whole in memory until the last
+ chunk landed. Measured in the Android application on a 2 GB download: the
+ page's JS heap tracked the bytes already written, 905 MB at 928 MB, and the
+ renderer reached 2.1 GB before dropping to 82 MB at the end. Every target
+ that writes as it goes (a granted folder, a service worker, the desktop's
+ native save) had the same cost. The real function runs here, with each
+ chunk message weakly referenced and the collector forced between writes.
+ """
+ src = (STATIC / "file-utils.js").read_text(encoding="utf-8")
+ fn = src[src.index("async function pipelinedDownload"):]
+ fn = fn[:fn.index("\n}\n") + 2]
+
+ script = tmp_path / "retention.mjs"
+ script.write_text("""
+const PIPELINE_WINDOW = 4;
+const TOTAL = 40;
+const refs = [];
+let worst = 0;
+const _fetchChunkResilient = async (transport, fileId, i) => {
+ const msg = { ct: new Uint8Array(64 * 1024), nonce: new Uint8Array(12) };
+ refs[i] = new WeakRef(msg);
+ return msg;
+};
+const _writeOrStall = async (w, bytes, index) => {
+ // A macrotask ends the job that keeps WeakRef targets alive; then collect.
+ await new Promise((r) => setTimeout(r, 0));
+ globalThis.gc();
+ let alive = 0;
+ for (let j = 0; j < index - PIPELINE_WINDOW; j++) if (refs[j] && refs[j].deref()) alive++;
+ worst = Math.max(worst, alive);
+};
+globalThis.window = { MeshBayCrypto: {
+ decryptChunkBin: async () => ({ byteLength: 64 * 1024 }),
+} };
+""" + fn + """
+await pipelinedDownload({}, 'k', 'file', TOTAL, () => {}, {}, { aborted: false }, '', 0);
+console.log(JSON.stringify({ worst }));
+""", encoding="utf-8")
+ proc = subprocess.run(["node", "--expose-gc", str(script)], capture_output=True,
+ text=True, encoding="utf-8", timeout=60)
+ assert proc.returncode == 0, proc.stderr
+ worst = json.loads(proc.stdout)["worst"]
+ assert worst == 0, (
+ f"{worst} chunks already written were still held when a later one was — "
+ "the download keeps the whole file in memory until it ends")