Skip to content

[file-packager] Use fetch() for loading file packages. #22016

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

Merged
merged 56 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
32fbce8
Separating file packager fetch.
seanmorris May 30, 2024
b88154d
Tweak
seanmorris May 30, 2024
b8e2ed9
Accounting for differences between FF & Chrome error text in test.
seanmorris May 30, 2024
58fcb8f
Fixing exception detection in test_missing_data_throws_error
seanmorris May 30, 2024
3945a0b
Correctly reporting overall total of dowloads
seanmorris May 30, 2024
c31c0f8
Removing debug.
seanmorris May 30, 2024
b76122f
Merge branch 'main' into sm-worker-package-fetch
seanmorris May 31, 2024
9712210
Reverting common.py
seanmorris Sep 17, 2024
ba19ffc
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 17, 2024
30d2d8e
PR comments.
seanmorris Sep 17, 2024
0c88d25
Merge conflict res.
seanmorris Sep 17, 2024
0e769ca
Removing custom shell from test_fetch_polyfill_preload
seanmorris Sep 17, 2024
b23fffa
Restoring custom shell.
seanmorris Sep 17, 2024
c3fadbc
Removing shell correctly.
seanmorris Sep 17, 2024
4b05f80
origFetch.
seanmorris Sep 17, 2024
8a027b7
Tweaks
seanmorris Sep 17, 2024
9b415a6
Tweaks
seanmorris Sep 17, 2024
db4a31d
Spacing.
seanmorris Sep 17, 2024
3bf0964
Tweaks.
seanmorris Sep 17, 2024
03b301a
Correctly checking exit code.
seanmorris Sep 17, 2024
8ff77d0
Minimal custom shell.
seanmorris Sep 17, 2024
5b37c95
closing HTML tag.
seanmorris Sep 17, 2024
34b1afd
Extra test.
seanmorris Sep 17, 2024
bedece9
Spacing.
seanmorris Sep 17, 2024
6bffc95
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 17, 2024
e2c4ea8
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 18, 2024
efaf744
Style.
seanmorris Sep 18, 2024
027532e
Merge branch 'sm-worker-package-fetch' of github.com:seanmorris/emscr…
seanmorris Sep 18, 2024
e360fa8
Brackets.
seanmorris Sep 19, 2024
295b750
Style.
seanmorris Sep 19, 2024
27a84da
Style.
seanmorris Sep 19, 2024
206bb2b
Spacing
seanmorris Sep 19, 2024
4168338
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 19, 2024
f622a0d
Modernizing JS.
seanmorris Sep 19, 2024
e24cc12
Merge branch 'sm-worker-package-fetch' of github.com:seanmorris/emscr…
seanmorris Sep 19, 2024
489bc06
Style.
seanmorris Sep 19, 2024
071b814
Spacing
seanmorris Sep 19, 2024
ddb958e
Style
seanmorris Sep 19, 2024
c553d28
Style
seanmorris Sep 19, 2024
61dea12
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 19, 2024
661258b
Style & typo.
seanmorris Sep 19, 2024
06404fb
Merge branch 'sm-worker-package-fetch' of github.com:seanmorris/emscr…
seanmorris Sep 19, 2024
68d3d02
Removing debug.
seanmorris Sep 19, 2024
9b00c17
Simplifying package data assembly.
seanmorris Sep 20, 2024
e78ac48
Style.
seanmorris Sep 20, 2024
27a1c42
Readability.
seanmorris Sep 20, 2024
edd3a18
Readability.
seanmorris Sep 20, 2024
a7747d4
-1 diff size.
seanmorris Sep 20, 2024
78958aa
Simpler
seanmorris Sep 20, 2024
572bf74
Spacing.
seanmorris Sep 20, 2024
4386290
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 20, 2024
e68044e
You can't concat() TypedArrays...
seanmorris Sep 20, 2024
4fac7e0
Spacing.
seanmorris Sep 20, 2024
1c9d521
Merge branch 'main' into sm-worker-package-fetch
seanmorris Sep 20, 2024
aadfaec
Spacing.
seanmorris Sep 20, 2024
6fd7e01
Merge branch 'sm-worker-package-fetch' of github.com:seanmorris/emscr…
seanmorris Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5376,6 +5376,55 @@ def test_webpack(self, es6):
shutil.copyfile('webpack/src/hello.wasm', 'webpack/dist/hello.wasm')
self.run_browser('webpack/dist/index.html', '/report_result?exit:0')

def test_fetch_polyfill_preload(self):
create_file('hello.txt', 'hello, world!')
create_file('main.c', r'''
#include <stdio.h>
#include <string.h>
#include <emscripten.h>
int main() {
FILE *f = fopen("hello.txt", "r");
char buf[100];
fread(buf, 1, 20, f);
buf[20] = 0;
fclose(f);
printf("%s\n", buf);
return 0;
}''')

create_file('on_window_error_shell.html', r'''
<html>
<center><canvas id='canvas' width='256' height='256'></canvas></center>
<hr><div id='output'></div><hr>
<script type='text/javascript'>
window.addEventListener('error', event => {
const error = String(event.message);
window.disableErrorReporting = true;
window.onerror = null;
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8888/report_result?exception:' + error.substr(-23), true);
xhr.send();
setTimeout(function() { window.close() }, 1000);
});
</script>
{{{ SCRIPT }}}
</body>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than require special shell can you just rely on the fact that default reporting will try bot fetch and origFetch:

let doFetch = typeof origFetch != 'undefined' ? origFetch : fetch;

i.e. when you disable fetch you can cache the original fetch so that that browser reporting doesn't break.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbc100 I'm not able to get the exception out of the browser without the custom shell. It just hangs with the exception listed as uncaught in the browser's console.

</html>''')

def test(args, expect_fail):
self.compile_btest('main.c', ['-sEXIT_RUNTIME', '--preload-file', 'hello.txt', '--shell-file', 'on_window_error_shell.html', '-o', 'a.out.html'] + args)
if expect_fail:
js = read_file('a.out.js')
create_file('a.out.js', 'let origFetch = fetch; fetch = undefined;\n' + js)
return self.run_browser('a.out.html', '/report_result?exception:fetch is not a function')
else:
return self.run_browser('a.out.html', '/report_result?exit:0')

test([], expect_fail=False)
test([], expect_fail=True)
test(['-sLEGACY_VM_SUPPORT'], expect_fail=False)
test(['-sLEGACY_VM_SUPPORT', '-sNO_POLYFILL'], expect_fail=True)

@no_wasm64('https://github.com/llvm/llvm-project/issues/98778')
def test_fetch_polyfill_shared_lib(self):
create_file('library.c', r'''
Expand Down
112 changes: 59 additions & 53 deletions tools/file_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def generate_object_file(data_files):

def main():
if len(sys.argv) == 1:
err('''Usage: file_packager TARGET [--preload A [B..]] [--embed C [D..]] [--exclude E [F..]]] [--js-output=OUTPUT.js] [--no-force] [--use-preload-cache] [--indexedDB-name=EM_PRELOAD_CACHE] [--separate-metadata] [--lz4] [--use-preload-plugins]
err('''Usage: file_packager TARGET [--preload A [B..]] [--embed C [D..]] [--exclude E [F..]]] [--js-output=OUTPUT.js] [--no-force] [--use-preload-cache] [--indexedDB-name=EM_PRELOAD_CACHE] [--separate-metadata] [--lz4] [--use-preload-plugins] [--no-node]
See the source for more details.''')
return 1

Expand Down Expand Up @@ -952,54 +952,62 @@ def generate_js(data_target, data_files, metadata):
});
return;
}'''.strip()

ret += '''
function fetchRemotePackage(packageName, packageSize, callback, errback) {
%(node_support_code)s
var xhr = new XMLHttpRequest();
xhr.open('GET', packageName, true);
xhr.responseType = 'arraybuffer';
xhr.onprogress = (event) => {
var url = packageName;
var size = packageSize;
if (event.total) size = event.total;
if (event.loaded) {
if (!xhr.addedTotal) {
xhr.addedTotal = true;
if (!Module['dataFileDownloads']) Module['dataFileDownloads'] = {};
Module['dataFileDownloads'][url] = {
loaded: event.loaded,
total: size
};
} else {
Module['dataFileDownloads'][url].loaded = event.loaded;
Module['dataFileDownloads'] ??= {};
fetch(packageName)
.catch((cause) => Promise.reject(new Error(`Network Error: ${packageName}`, {cause}))) // If fetch fails, rewrite the error to include the failing URL & the cause.
.then((response) => {
if (!response.ok) {
return Promise.reject(new Error(`${response.status}: ${response.url}`));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should be calling errback instead here? Although it looks like the existing code doesn't so maybe we can just leave it?

}
var total = 0;
var loaded = 0;
var num = 0;
for (var download in Module['dataFileDownloads']) {
var data = Module['dataFileDownloads'][download];
total += data.total;
loaded += data.loaded;
num++;

if (!response.body && response.arrayBuffer) { // If we're using the polyfill, readers won't be available...
return response.arrayBuffer().then(callback);
}
total = Math.ceil(total * Module['expectedDataFileDownloads']/num);
Module['setStatus']?.(`Downloading data... (${loaded}/${total})`);
} else if (!Module['dataFileDownloads']) {

const reader = response.body.getReader();
const iterate = () => reader.read().then(handleChunk).catch((cause) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this read() callback occur at roughly the same frequency as the old onprogress callback?

i.e. will folks still be able to render a process bar based on this?

@juj (since I think you use the progress feature).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I recall correctly the browser will call this roughly every few kilobytes. I think it maps to the size of the chunks after the Transfer-encoding: chunked header from the server but I need to double check.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true for the XHR onprogress callback too? i.e. we are going to regress users who expect progress callback but don't use Transfer-encoding: chunked?

Copy link
Contributor Author

@seanmorris seanmorris Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a file of 13_631_488 bytes without the Transfer-Encoding header, I get the following chunk sizes (bytes):

2097152
65536
393216
720896
917504
1310720
786432
1310720
196608
65536
65536
458752
2097152
851968
1245184
1048576

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbc100 I checked out the main branch and hacked the onprogress event to tell me its chunk sizes. For the same 13_631_488 byte file, I get two events, with 1_048_576 and 12_582_912 bytes each.

return Promise.reject(new Error(`Unexpected error while handling : ${response.url} ${cause}`, {cause}));
});

const chunks = [];
const headers = response.headers;
const total = Number(headers.get('Content-Length') ?? packageSize);
let loaded = 0;

const handleChunk = ({done, value}) => {
if (!done) {
chunks.push(value);
loaded += value.length;
Module['dataFileDownloads'][packageName] = {loaded, total};

let totalLoaded = 0;
let totalSize = 0;

for (const download of Object.values(Module['dataFileDownloads'])) {
totalLoaded += download.loaded;
totalSize += download.total;
}

Module['setStatus']?.(`Downloading data... (${totalLoaded}/${totalSize})`);
return iterate();
} else {
const packageData = new Uint8Array(chunks.map((c) => c.length).reduce((a, b) => a + b, 0));
let offset = 0;
for (const chunk of chunks) {
packageData.set(chunk, offset);
offset += chunk.length;
}
callback(packageData.buffer);
}
};

Module['setStatus']?.('Downloading data...');
}
};
xhr.onerror = (event) => {
throw new Error("NetworkError for: " + packageName);
}
xhr.onload = (event) => {
if (xhr.status == 200 || xhr.status == 304 || xhr.status == 206 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0
var packageData = xhr.response;
callback(packageData);
} else {
throw new Error(xhr.statusText + " : " + xhr.responseURL);
}
};
xhr.send(null);
return iterate();
});
};

function handleError(error) {
Expand Down Expand Up @@ -1097,15 +1105,14 @@ def generate_js(data_target, data_files, metadata):
function runMetaWithFS() {
Module['addRunDependency']('%(metadata_file)s');
var REMOTE_METADATA_NAME = Module['locateFile'] ? Module['locateFile']('%(metadata_file)s', '') : '%(metadata_file)s';
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
loadPackage(JSON.parse(xhr.responseText));
}
}
xhr.open('GET', REMOTE_METADATA_NAME, true);
xhr.overrideMimeType('application/json');
xhr.send(null);
fetch(REMOTE_METADATA_NAME)
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject(new Error(`${response.status}: ${response.url}`));
})
.then(loadPackage);
}

if (Module['calledRun']) {
Expand All @@ -1114,7 +1121,6 @@ def generate_js(data_target, data_files, metadata):
if (!Module['preRun']) Module['preRun'] = [];
Module["preRun"].push(runMetaWithFS);
}\n''' % {'metadata_file': os.path.basename(options.jsoutput + '.metadata')}

else:
_metadata_template = '''
}
Expand Down
Loading