From f3a1aebb3385a446acf6239a1350e162dfaa1ccd Mon Sep 17 00:00:00 2001 From: Yulong Wang Date: Thu, 5 Aug 2021 18:01:03 -0700 Subject: [PATCH] [js/web] support override wasm file path (#8610) --- js/common/lib/env.ts | 12 ++++ js/web/lib/wasm/wasm-factory.ts | 59 +++++++++++-------- ...rowser-test-wasm-path-override-filename.js | 15 +++++ .../browser-test-wasm-path-override-prefix.js | 14 +++++ js/web/test/e2e/karma.conf.js | 4 +- js/web/test/e2e/node-test-main-no-threads.js | 2 +- js/web/test/e2e/node-test-main.js | 2 +- .../node-test-wasm-path-override-filename.js | 19 ++++++ .../node-test-wasm-path-override-prefix.js | 17 ++++++ js/web/test/e2e/run.js | 15 +++++ js/web/test/e2e/simple-http-server.js | 6 +- 11 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 js/web/test/e2e/browser-test-wasm-path-override-filename.js create mode 100644 js/web/test/e2e/browser-test-wasm-path-override-prefix.js create mode 100644 js/web/test/e2e/node-test-wasm-path-override-filename.js create mode 100644 js/web/test/e2e/node-test-wasm-path-override-prefix.js diff --git a/js/common/lib/env.ts b/js/common/lib/env.ts index e9fcaec149..49f49294b9 100644 --- a/js/common/lib/env.ts +++ b/js/common/lib/env.ts @@ -3,6 +3,12 @@ import {EnvImpl} from './env-impl'; export declare namespace Env { + export type WasmPrefixOrFilePaths = string|{ + 'ort-wasm.wasm'?: string; + 'ort-wasm-threaded.wasm'?: string; + 'ort-wasm-simd.wasm'?: string; + 'ort-wasm-simd-threaded.wasm'?: string; + }; export interface WebAssemblyFlags { /** * set or get number of thread(s). If omitted or set to 0, number of thread(s) will be determined by system. If set @@ -24,6 +30,12 @@ export declare namespace Env { * value indicates no timeout is set. (default is 0) */ initTimeout?: number; + + /** + * Set a custom URL prefix to the .wasm files or a set of overrides for each .wasm file. The override path should be + * an absolute path. + */ + wasmPaths?: WasmPrefixOrFilePaths; } export interface WebGLFlags { diff --git a/js/web/lib/wasm/wasm-factory.ts b/js/web/lib/wasm/wasm-factory.ts index 6d718666f3..f21ba84e09 100644 --- a/js/web/lib/wasm/wasm-factory.ts +++ b/js/web/lib/wasm/wasm-factory.ts @@ -44,6 +44,14 @@ const isSimdSupported = (): boolean => { } }; +const getWasmFileName = (useSimd: boolean, useThreads: boolean) => { + if (useThreads) { + return useSimd ? 'ort-wasm-simd-threaded.wasm' : 'ort-wasm-threaded.wasm'; + } else { + return useSimd ? 'ort-wasm-simd.wasm' : 'ort-wasm.wasm'; + } +}; + export const initializeWebAssembly = async(): Promise => { if (initialized) { return Promise.resolve(); @@ -64,6 +72,13 @@ export const initializeWebAssembly = async(): Promise => { const useThreads = numThreads > 1 && isMultiThreadSupported(); const useSimd = simd && isSimdSupported(); + + const wasmPrefixOverride = typeof env.wasm.wasmPaths === 'string' ? env.wasm.wasmPaths : undefined; + const wasmFileName = getWasmFileName(false, useThreads); + const wasmOverrideFileName = getWasmFileName(useSimd, useThreads); + const wasmPathOverride = + typeof env.wasm.wasmPaths === 'object' ? env.wasm.wasmPaths[wasmOverrideFileName] : undefined; + let isTimeout = false; const tasks: Array> = []; @@ -81,38 +96,34 @@ export const initializeWebAssembly = async(): Promise => { // promise for module initialization tasks.push(new Promise((resolve, reject) => { const factory = useThreads ? ortWasmFactoryThreaded : ortWasmFactory; - const config: Partial = {}; - - if (!useThreads) { - config.locateFile = (fileName: string, scriptDirectory: string) => { - if (useSimd && fileName === 'ort-wasm.wasm') { - return scriptDirectory + 'ort-wasm-simd.wasm'; + const config: Partial = { + locateFile: (fileName: string, scriptDirectory: string) => { + if (fileName.endsWith('.worker.js') && typeof Blob !== 'undefined') { + return URL.createObjectURL(new Blob( + [ + // This require() function is handled by webpack to load file content of the corresponding .worker.js + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('./binding/ort-wasm-threaded.worker.js') + ], + {type: 'text/javascript'})); } + + if (fileName === wasmFileName) { + const prefix: string = wasmPrefixOverride ?? scriptDirectory; + return wasmPathOverride ?? prefix + wasmOverrideFileName; + } + return scriptDirectory + fileName; - }; - } else { + } + }; + + if (useThreads) { if (typeof Blob === 'undefined') { config.mainScriptUrlOrBlob = path.join(__dirname, 'ort-wasm-threaded.js'); } else { const scriptSourceCode = `var ortWasmThreaded=(function(){var _scriptDir;return ${ortWasmFactoryThreaded.toString()}})();`; config.mainScriptUrlOrBlob = new Blob([scriptSourceCode], {type: 'text/javascript'}); - config.locateFile = (fileName: string, scriptDirectory: string) => { - if (fileName.endsWith('.worker.js')) { - return URL.createObjectURL(new Blob( - [ - // This require() function is handled by webpack to load file content of the corresponding .worker.js - // eslint-disable-next-line @typescript-eslint/no-require-imports - require('./binding/ort-wasm-threaded.worker.js') - ], - {type: 'text/javascript'})); - } - - if (useSimd && fileName === 'ort-wasm-threaded.wasm') { - return scriptDirectory + 'ort-wasm-simd-threaded.wasm'; - } - return scriptDirectory + fileName; - }; } } diff --git a/js/web/test/e2e/browser-test-wasm-path-override-filename.js b/js/web/test/e2e/browser-test-wasm-path-override-filename.js new file mode 100644 index 0000000000..5d99eb389b --- /dev/null +++ b/js/web/test/e2e/browser-test-wasm-path-override-filename.js @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +it('Browser E2E testing - WebAssembly backend (path override filename)', async function () { + // disable SIMD and multi-thread + ort.env.wasm.numThreads = 1; + ort.env.wasm.simd = false; + + // override .wasm file path for 'ort-wasm.wasm' + ort.env.wasm.wasmPaths = { + 'ort-wasm.wasm': new URL('./test-wasm-path-override/renamed.wasm', document.baseURI).href + }; + + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/browser-test-wasm-path-override-prefix.js b/js/web/test/e2e/browser-test-wasm-path-override-prefix.js new file mode 100644 index 0000000000..ff5d05f8c4 --- /dev/null +++ b/js/web/test/e2e/browser-test-wasm-path-override-prefix.js @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +it('Browser E2E testing - WebAssembly backend (path override prefix)', async function () { + + // disable SIMD and multi-thread + ort.env.wasm.numThreads = 1; + ort.env.wasm.simd = false; + + // override .wasm file path prefix + ort.env.wasm.wasmPaths = new URL('./test-wasm-path-override/', document.baseURI).href; + + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/karma.conf.js b/js/web/test/e2e/karma.conf.js index a0552c0f09..5c0e4740e6 100644 --- a/js/web/test/e2e/karma.conf.js +++ b/js/web/test/e2e/karma.conf.js @@ -20,11 +20,13 @@ module.exports = function (config) { { pattern: distPrefix + 'ort.js' }, { pattern: './common.js' }, { pattern: TEST_MAIN }, - { pattern: './node_modules/onnxruntime-web/dist/**/*', included: false, nocache: true }, + { pattern: './node_modules/onnxruntime-web/dist/*.wasm', included: false, nocache: true }, { pattern: './model.onnx', included: false } ], proxies: { '/model.onnx': '/base/model.onnx', + '/test-wasm-path-override/ort-wasm.wasm': '/base/node_modules/onnxruntime-web/dist/ort-wasm.wasm', + '/test-wasm-path-override/renamed.wasm': '/base/node_modules/onnxruntime-web/dist/ort-wasm.wasm', }, client: { captureConsole: true, mocha: { expose: ['body'], timeout: 60000 } }, reporters: ['mocha'], diff --git a/js/web/test/e2e/node-test-main-no-threads.js b/js/web/test/e2e/node-test-main-no-threads.js index 2eb7ee25a3..55204ff730 100644 --- a/js/web/test/e2e/node-test-main-no-threads.js +++ b/js/web/test/e2e/node-test-main-no-threads.js @@ -4,7 +4,7 @@ const ort = require('onnxruntime-web'); const testFunction = require('./common'); -it('Browser E2E testing - WebAssembly backend', async function () { +it('Node.js E2E testing - WebAssembly backend (no threads)', async function () { ort.env.wasm.numThreads = 1; await testFunction(ort, { executionProviders: ['wasm'] }); }); diff --git a/js/web/test/e2e/node-test-main.js b/js/web/test/e2e/node-test-main.js index bbac8e478b..29f4dfbdc0 100644 --- a/js/web/test/e2e/node-test-main.js +++ b/js/web/test/e2e/node-test-main.js @@ -4,7 +4,7 @@ const ort = require('onnxruntime-web'); const testFunction = require('./common'); -it('Browser E2E testing - WebAssembly backend', async function () { +it('Node.js E2E testing - WebAssembly backend', async function () { await testFunction(ort, { executionProviders: ['wasm'] }); process.exit(); diff --git a/js/web/test/e2e/node-test-wasm-path-override-filename.js b/js/web/test/e2e/node-test-wasm-path-override-filename.js new file mode 100644 index 0000000000..f9a4a7d246 --- /dev/null +++ b/js/web/test/e2e/node-test-wasm-path-override-filename.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const path = require('path'); +const ort = require('onnxruntime-web'); +const testFunction = require('./common'); + +it('Node.js E2E testing - WebAssembly backend (path override filename)', async function () { + // disable SIMD and multi-thread + ort.env.wasm.numThreads = 1; + ort.env.wasm.simd = false; + + // override .wasm file path for 'ort-wasm.wasm' + ort.env.wasm.wasmPaths = { + 'ort-wasm.wasm': path.join(__dirname, 'test-wasm-path-override/renamed.wasm') + }; + + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/node-test-wasm-path-override-prefix.js b/js/web/test/e2e/node-test-wasm-path-override-prefix.js new file mode 100644 index 0000000000..479c0859f3 --- /dev/null +++ b/js/web/test/e2e/node-test-wasm-path-override-prefix.js @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const path = require('path'); +const ort = require('onnxruntime-web'); +const testFunction = require('./common'); + +it('Node.js E2E testing - WebAssembly backend (path override prefix)', async function () { + // disable SIMD and multi-thread + ort.env.wasm.numThreads = 1; + ort.env.wasm.simd = false; + + // override .wasm file path prefix + ort.env.wasm.wasmPaths = path.join(__dirname, 'test-wasm-path-override/'); + + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/run.js b/js/web/test/e2e/run.js index 02cb3a3326..a8f5727fad 100644 --- a/js/web/test/e2e/run.js +++ b/js/web/test/e2e/run.js @@ -46,6 +46,9 @@ async function main() { // npm install with "--cache" to install packed packages with an empty cache folder await runInShell(`npm install --cache "${NPM_CACHE_FOLDER}" "${ORT_COMMON_PACKED_FILEPATH}" "${ORT_WEB_PACKED_FILEPATH}"`); + // prepare .wasm files for path override testing + prepareWasmPathOverrideFiles(); + // test case run in Node.js await testAllNodejsCases(); @@ -60,17 +63,29 @@ async function main() { process.exit(0); } +function prepareWasmPathOverrideFiles() { + const folder = path.join(TEST_E2E_RUN_FOLDER, 'test-wasm-path-override'); + const sourceFile = path.join(TEST_E2E_RUN_FOLDER, 'node_modules', 'onnxruntime-web', 'dist', 'ort-wasm.wasm'); + fs.emptyDirSync(folder); + fs.copyFileSync(sourceFile, path.join(folder, 'ort-wasm.wasm')); + fs.copyFileSync(sourceFile, path.join(folder, 'renamed.wasm')); +} + async function testAllNodejsCases() { await runInShell('node ./node_modules/mocha/bin/mocha ./node-test-main-no-threads.js'); await runInShell('node ./node_modules/mocha/bin/mocha ./node-test-main.js'); await runInShell('node --experimental-wasm-threads --experimental-wasm-bulk-memory ./node_modules/mocha/bin/mocha ./node-test-main-no-threads.js'); await runInShell('node --experimental-wasm-threads --experimental-wasm-bulk-memory ./node_modules/mocha/bin/mocha ./node-test-main.js'); + await runInShell('node ./node_modules/mocha/bin/mocha ./node-test-wasm-path-override-filename.js'); + await runInShell('node ./node_modules/mocha/bin/mocha ./node-test-wasm-path-override-prefix.js'); } async function testAllBrowserCases({ hostInKarma }) { await runKarma({ hostInKarma, main: './browser-test-webgl.js', browser: 'Chrome_default' }); await runKarma({ hostInKarma, main: './browser-test-wasm.js', browser: 'Chrome_default' }); await runKarma({ hostInKarma, main: './browser-test-wasm-no-threads.js', browser: 'Chrome_default' }); + await runKarma({ hostInKarma, main: './browser-test-wasm-path-override-filename.js', browser: 'Chrome_default' }); + await runKarma({ hostInKarma, main: './browser-test-wasm-path-override-prefix.js', browser: 'Chrome_default' }); } async function runKarma({ hostInKarma, main, browser }) { diff --git a/js/web/test/e2e/simple-http-server.js b/js/web/test/e2e/simple-http-server.js index f5e96e8aa0..7df59216c2 100644 --- a/js/web/test/e2e/simple-http-server.js +++ b/js/web/test/e2e/simple-http-server.js @@ -8,11 +8,15 @@ var http = require('http'); var fs = require('fs'); var path = require('path'); +var simpleProxies = { + './ort-wasm.wasm': './ort-wasm.wasm' +}; + module.exports = function (dir) { http.createServer(function (request, response) { console.log('request ', request.url); - var filePath = '.' + request.url; + var filePath = '.' + (simpleProxies[request.url] ?? request.url); var extname = String(path.extname(filePath)).toLowerCase(); var mimeTypes = {