-
Notifications
You must be signed in to change notification settings - Fork 3.5k
WASMFS chunked fetch backend #23021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WASMFS chunked fetch backend #23021
Changes from 10 commits
35da69c
14d419c
9e8c98d
3dc444b
1a58604
61b660c
4a1b266
efb21e4
1eee514
c602264
e42d8cd
601d061
808d1e0
c617db2
e8b5bbf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,8 +8,10 @@ addToLibrary({ | |
| $FETCHFS__deps: ['$stringToUTF8OnStack', 'wasmfs_create_fetch_backend'], | ||
| $FETCHFS: { | ||
| createBackend(opts) { | ||
| return _wasmfs_create_fetch_backend(stringToUTF8OnStack(opts.base_url)); | ||
| } | ||
| return withStackSave(() => { | ||
| return _wasmfs_create_fetch_backend(stringToUTF8OnStack(opts.base_url ?? ""), opts.chunkSize | 0); | ||
|
||
| }); | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,28 +5,24 @@ | |
| */ | ||
|
|
||
| addToLibrary({ | ||
| $wasmFS$JSMemoryRanges: {}, | ||
|
|
||
| // Fetch backend: On first access of the file (either a read or a getSize), it | ||
| // will fetch() the data from the network asynchronously. Otherwise, after | ||
| // that fetch it behaves just like JSFile (and it reuses the code from there). | ||
|
|
||
| _wasmfs_create_fetch_backend_js__deps: [ | ||
| '$wasmFS$backends', | ||
| '$wasmFS$JSMemoryFiles', | ||
| '_wasmfs_create_js_file_backend_js', | ||
| '_wasmfs_fetch_get_file_path', | ||
| '$wasmFS$JSMemoryRanges', | ||
| '_wasmfs_fetch_get_file_url', | ||
| '_wasmfs_fetch_get_chunk_size', | ||
| ], | ||
| _wasmfs_create_fetch_backend_js: async function(backend) { | ||
| // Get a promise that fetches the data and stores it in JS memory (if it has | ||
| // not already been fetched). | ||
| async function getFile(file) { | ||
| if (wasmFS$JSMemoryFiles[file]) { | ||
| // The data is already here, so nothing to do before we continue on to | ||
| // the actual read below. | ||
| return Promise.resolve(); | ||
| } | ||
| // This is the first time we want the file's data. | ||
| async function getFileRange(file, offset, len) { | ||
| var url = ''; | ||
| var fileUrl_p = __wasmfs_fetch_get_file_path(file); | ||
| var fileUrl_p = __wasmfs_fetch_get_file_url(file); | ||
| var fileUrl = UTF8ToString(fileUrl_p); | ||
| var isAbs = fileUrl.indexOf('://') !== -1; | ||
| if (isAbs) { | ||
|
|
@@ -35,34 +31,86 @@ addToLibrary({ | |
| try { | ||
| var u = new URL(fileUrl, self.location.origin); | ||
| url = u.toString(); | ||
| } catch (e) { | ||
| } catch (_e) { | ||
| throw {status: 404}; | ||
| } | ||
| } | ||
| var response = await fetch(url); | ||
| if (response.ok) { | ||
| var buffer = await response['arrayBuffer'](); | ||
| wasmFS$JSMemoryFiles[file] = new Uint8Array(buffer); | ||
| } else { | ||
| var chunkSize = __wasmfs_fetch_get_chunk_size(file); | ||
| offset ??= 0; | ||
| len ??= chunkSize; | ||
| // In which chunk does the seeked range start? E.g., 5-14 with chunksize 8 will start in chunk 0. | ||
| var firstChunk = (offset / chunkSize) | 0; | ||
| // In which chunk does the seeked range end? E.g., 5-14 with chunksize 8 will end in chunk 1, as will 5-16 (since byte 16 isn't requested). | ||
| // This will always give us a chunk >= firstChunk since len > 0. | ||
| var lastChunk = ((offset+len-1) / chunkSize) | 0; | ||
| if (!(file in wasmFS$JSMemoryRanges)) { | ||
| var fileInfo = await fetch(url,{method:"HEAD", headers:{"Range": "bytes=0-"}}); | ||
|
||
| if (fileInfo.ok && | ||
| fileInfo.headers.has("Content-Length") && | ||
| fileInfo.headers.get("Accept-Ranges") == "bytes" && | ||
| (parseInt(fileInfo.headers.get("Content-Length"), 10) > chunkSize*2)) { | ||
|
||
| wasmFS$JSMemoryRanges[file] = { | ||
| size: parseInt(fileInfo.headers.get("Content-Length"), 10), | ||
| chunks: [], | ||
| chunkSize: chunkSize | ||
| }; | ||
| } else { | ||
| // may as well/forced to download the whole file | ||
| var wholeFileReq = await fetch(url); | ||
| if (!wholeFileReq.ok) { | ||
| throw wholeFileReq; | ||
| } | ||
| var wholeFileData = new Uint8Array(await wholeFileReq.arrayBuffer()); | ||
| var text = new TextDecoder().decode(wholeFileData); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this line should've been deleted There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that’s true, I left it in by mistake. Good catch! |
||
| wasmFS$JSMemoryRanges[file] = { | ||
| size: wholeFileData.byteLength, | ||
| chunks: [wholeFileData], | ||
| chunkSize: wholeFileData.byteLength | ||
| }; | ||
| return Promise.resolve(); | ||
| } | ||
| } | ||
| var allPresent = true; | ||
| var i; | ||
| // Do we have all the chunks already? If so, we don't need to do any fetches. | ||
| for (i = firstChunk; i <= lastChunk; i++) { | ||
| if (!wasmFS$JSMemoryRanges[file].chunks[i]) { | ||
| allPresent = false; | ||
| break; | ||
| } | ||
| } | ||
| if (allPresent) { | ||
| // The data is already here, so nothing to do before we continue on to | ||
| // the actual read. | ||
| return Promise.resolve(); | ||
| } | ||
| // This is the first time we want the chunks' data. We'll make | ||
| // one request for all the chunks we need, rather than one | ||
| // request per chunk. | ||
| var start = firstChunk * chunkSize; | ||
| // We must fetch *up to* the last byte of the last chunk. | ||
| var end = (lastChunk+1) * chunkSize; | ||
| var response = await fetch(url, {headers:{"Range": `bytes=${start}-${end-1}`}}); | ||
| if (!response.ok) { | ||
| throw response; | ||
| } | ||
| var bytes = new Uint8Array(await response['arrayBuffer']()); | ||
|
||
| for (i = firstChunk; i <= lastChunk; i++) { | ||
| wasmFS$JSMemoryRanges[file].chunks[i] = bytes.slice(i*chunkSize-start,(i+1)*chunkSize-start); | ||
| } | ||
| return Promise.resolve(); | ||
| } | ||
|
|
||
| // Start with the normal JSFile operations. This sets | ||
| // wasmFS$backends[backend] | ||
| // which we will then augment. | ||
| __wasmfs_create_js_file_backend_js(backend); | ||
|
|
||
| // Add the async operations on top. | ||
| var jsFileOps = wasmFS$backends[backend]; | ||
| wasmFS$backends[backend] = { | ||
| // alloc/free operations are not actually async. Just forward to the | ||
| // parent class, but we must return a Promise as the caller expects. | ||
| allocFile: async (file) => { | ||
| jsFileOps.allocFile(file); | ||
| // nop | ||
| return Promise.resolve(); | ||
| }, | ||
| freeFile: async (file) => { | ||
| jsFileOps.freeFile(file); | ||
| // free memory | ||
| wasmFS$JSMemoryRanges[file] = undefined; | ||
| return Promise.resolve(); | ||
| }, | ||
|
|
||
|
|
@@ -72,18 +120,38 @@ addToLibrary({ | |
|
|
||
| // read/getSize fetch the data, then forward to the parent class. | ||
| read: async (file, buffer, length, offset) => { | ||
| if (length == 0) { | ||
| return 0; | ||
| } | ||
| try { | ||
| await getFile(file); | ||
| } catch (response) { | ||
| return response.status === 404 ? -{{{ cDefs.ENOENT }}} : -{{{ cDefs.EBADF }}}; | ||
| await getFileRange(file, offset || 0, length); | ||
| } catch (failedResponse) { | ||
| return failedResponse.status === 404 ? -{{{ cDefs.ENOENT }}} : -{{{ cDefs.EBADF }}}; | ||
| } | ||
| return jsFileOps.read(file, buffer, length, offset); | ||
| var fileInfo = wasmFS$JSMemoryRanges[file]; | ||
| var chunks = fileInfo.chunks; | ||
| var chunkSize = fileInfo.chunkSize; | ||
| var firstChunk = (offset / chunkSize) | 0; | ||
| // See comments in getFileRange. | ||
| var lastChunk = ((offset+length-1) / chunkSize) | 0; | ||
| var readLength = 0; | ||
| for (var i = firstChunk; i <= lastChunk; i++) { | ||
| var chunk = chunks[i]; | ||
| var start = Math.max(i*chunkSize, offset); | ||
| var chunkStart = i*chunkSize; | ||
| var end = Math.min(chunkStart+chunkSize, offset+length); | ||
| HEAPU8.set(chunk.subarray(start-chunkStart, end-chunkStart), buffer+(start-offset)); | ||
| readLength = end - offset; | ||
| } | ||
| return readLength; | ||
| }, | ||
| getSize: async (file) => { | ||
| try { | ||
| await getFile(file); | ||
| } catch (response) {} | ||
| return jsFileOps.getSize(file); | ||
| await getFileRange(file, 0, 0); | ||
| } catch (failedResponse) { | ||
| return 0; | ||
| } | ||
| return wasmFS$JSMemoryRanges[file].size; | ||
| }, | ||
| }; | ||
| }, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.