Skip to content

Commit 4e31feb

Browse files
authored
Enforce a chunk size limit to streaming fetch (#26898)
Enforce a chunk size limit to streaming fetch, so that JS -> Wasm onprogress handlers for streaming fetches will not blow up the WebAssembly heap size, which would revert a streaming fetch back to a non-streaming one. Fixes browser.test_fetch_stream_file on macOS Tahoe 26 on Safari 26.0.1.
1 parent 991c0de commit 4e31feb

2 files changed

Lines changed: 41 additions & 19 deletions

File tree

ChangeLog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ See docs/process.md for more on how version tagging works.
2020

2121
5.0.8 (in development)
2222
----------------------
23+
- When performing a streaming Fetch operation, the max chunk size of downloaded
24+
bytes that is handed over to the Wasm side from JS is now capped to maximum
25+
of 8 megabytes. This ensures that a streaming Fetch stays streaming, rather
26+
than transferring the whole (potentially large) file as one huge chunk, which
27+
might not fit in the WebAssembly memory. (#26898)
2328
- The minimum versions of browser engines supported by emscripten's generated
2429
code were bumped, allowing us to remove our internal support for transpilation
2530
via babel:

src/Fetch.js

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -661,30 +661,47 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
661661
return;
662662
}
663663
var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData) ? xhr.response?.byteLength ?? 0 : 0;
664-
var ptr = 0;
665-
if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
664+
665+
// Specifies the maximum chunk size that a streaming fetch will transfer from
666+
// JS over to WebAssembly side. Used to cap a streaming fetch to avoid
667+
// overallocating WebAssembly memory needlessly.
668+
var FETCH_STREAMING_MAX_CHUNK_SIZE = 8*1024*1024;
669+
670+
for (var bytePos = 0; bytePos < ptrLen || !ptrLen;) {
671+
var sz = Math.min(ptrLen - bytePos, FETCH_STREAMING_MAX_CHUNK_SIZE);
672+
673+
var ptr = 0;
674+
if (sz > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) {
675+
// Even though we are doing a streaming fetch (i.e. in small chunks), Safari may call onprogress with a huge
676+
// chunk size. This will be a problem for Wasm applications that intend to use streaming fetch to process
677+
// an input file in small chunks (to avoid blowing up the WebAssembly heap size). Therefore apply a max
678+
// chunk size ceiling to the received chunks, and transfer the data over to WebAssembly using max sized chunks.
679+
666680
#if FETCH_DEBUG
667-
dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`);
681+
dbg(`fetch: allocating ${sz} bytes in Emscripten heap for xhr data`);
668682
#endif
669683
#if ASSERTIONS
670-
assert(onprogress, 'streaming fetch requires an onprogress handler');
684+
assert(onprogress, 'streaming fetch requires an onprogress handler');
671685
#endif
672-
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
673-
// freed when emscripten_fetch_close() is called.
674-
ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen);
675-
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr);
686+
// The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is
687+
// freed when emscripten_fetch_close() is called.
688+
ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, sz);
689+
HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response), bytePos, sz), ptr);
690+
}
691+
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
692+
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, sz);
693+
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen + bytePos);
694+
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total);
695+
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
696+
var status = xhr.status;
697+
// If loading files from a source that does not give HTTP status code, assume success if we get data bytes
698+
if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200;
699+
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}}
700+
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
701+
onprogress(fetch, e);
702+
bytePos += sz;
703+
if (!ptrLen) break;
676704
}
677-
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}
678-
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen);
679-
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen);
680-
writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total);
681-
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}}
682-
var status = xhr.status;
683-
// If loading files from a source that does not give HTTP status code, assume success if we get data bytes
684-
if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200;
685-
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}}
686-
if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64);
687-
onprogress(fetch, e);
688705
};
689706
xhr.onreadystatechange = (e) => {
690707
// check if xhr was aborted by user and don't try to call back

0 commit comments

Comments
 (0)