Skip to content

Commit

Permalink
Fix WASM JS script not working after minification (#4562)
Browse files Browse the repository at this point in the history
* bug fix

* bug fix batch 2

* bug fix 3

* fix

* fix
  • Loading branch information
jinjingforever authored Jan 15, 2021
1 parent 54c3f4d commit 0dccac9
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 87 deletions.
114 changes: 114 additions & 0 deletions rollup.config.helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @license
* Copyright 2018 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =============================================================================
*/

import {terser} from 'rollup-plugin-terser';

/**
* Returns a standardized list of browser package configuration options
* that we want to use in all our rollup files and ship to NPM.
*
* @param {string} fileName
* @param {string} preamble
* @param {boolean} visualize - produce bundle visualizations for certain
* bundles
* @param {boolean} ci is this a CI build
* @param {object} terserExtraOptions is any extra options passed to terser
*/
export function getBrowserBundleConfigOptions(
config, name, fileName, preamble, visualize, ci, terserExtraOptions = {}) {
const bundles = [];

const terserPlugin =
terser({output: {preamble, comments: false}, ...terserExtraOptions});
const extend = true;
const umdFormat = 'umd';
const fesmFormat = 'es';

// UMD ES5 minified
bundles.push(config({
plugins: [terserPlugin],
output: {
format: umdFormat,
name,
extend,
file: `dist/${fileName}.min.js`,
freeze: false
},
tsCompilerOptions: {target: 'es5'},
visualize
}));

if (ci) {
// In CI we do not build all the possible bundles.
return bundles;
}

// UMD ES5 unminified
bundles.push(config({
output: {
format: umdFormat,
name,
extend,
file: `dist/${fileName}.js`,
freeze: false
},
tsCompilerOptions: {target: 'es5'}
}));

// UMD ES2017
bundles.push(config({
output:
{format: umdFormat, name, extend, file: `dist/${fileName}.es2017.js`},
tsCompilerOptions: {target: 'es2017'}
}));

// UMD ES2017 minified
bundles.push(config({
plugins: [terserPlugin],
output: {
format: umdFormat,
name,
extend,
file: `dist/${fileName}.es2017.min.js`
},
tsCompilerOptions: {target: 'es2017'},
visualize
}));

// FESM ES2017
bundles.push(config({
output:
{format: fesmFormat, name, extend, file: `dist/${fileName}.fesm.js`},
tsCompilerOptions: {target: 'es2017'}
}));

// FESM ES2017 minified
bundles.push(config({
plugins: [terserPlugin],
output: {
format: fesmFormat,
name,
extend,
file: `dist/${fileName}.fesm.min.js`
},
tsCompilerOptions: {target: 'es2017'},
visualize
}));


return bundles;
}
11 changes: 11 additions & 0 deletions tfjs-backend-wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ setWasmPaths(yourCustomPathPrefix, usePlatformFetch);
tf.setBackend('wasm').then(() => {...});
```

## JS Minification

If your bundler is capable of minifying JS code, please turn off the option
that transforms ```typeof foo == "undefined"``` into ```foo === void 0```. For
example, in [terser](/~https://github.com/terser/terser), the option is called
"typeofs" (located under the
[Compress options](/~https://github.com/terser/terser#compress-options) section).
Without this feature turned off, the minified code will throw "_scriptDir is not
defined" error from web workers when running in browsers with
SIMD+multi-threading support.

## Benchmarks

The benchmarks below show inference times (ms) for two different edge-friendly
Expand Down
1 change: 1 addition & 0 deletions tfjs-backend-wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"rimraf": "~2.6.2",
"rollup": "~1.26.3",
"rollup-plugin-terser": "~5.3.0",
"rollup-plugin-visualizer": "~3.3.2",
"ts-node": "~8.8.2",
"tslint": "~5.20.0",
"tslint-no-circular-imports": "~0.7.0",
Expand Down
106 changes: 49 additions & 57 deletions tfjs-backend-wasm/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import node from '@rollup/plugin-node-resolve';
import {terser} from 'rollup-plugin-terser';
import typescript from '@rollup/plugin-typescript';
import visualizer from 'rollup-plugin-visualizer';
import {getBrowserBundleConfigOptions} from '../rollup.config.helpers';

const PREAMBLE = `/**
* @license
Expand All @@ -38,7 +39,20 @@ const PREAMBLE = `/**
* =============================================================================
*/`;

function config({plugins = [], output = {}, tsCompilerOptions = {}}) {
function config({
plugins = [],
output = {},
external = [],
visualize = false,
tsCompilerOptions = {}
}) {
if (visualize) {
const filename = output.file + '.html';
plugins.push(visualizer(
{sourcemap: true, filename, template: 'sunburst', gzipSize: true}));
console.log(`Will output a bundle visualization in ${filename}`);
}

const defaultTsOptions = {
include: ['src/**/*.ts'],
module: 'ES2015',
Expand All @@ -48,8 +62,7 @@ function config({plugins = [], output = {}, tsCompilerOptions = {}}) {
return {
input: 'src/index.ts',
plugins: [
typescript(tsoptions), resolve(),
node({preferBuiltins: true}),
typescript(tsoptions), resolve(), node({preferBuiltins: true}),
// Polyfill require() from dependencies.
commonjs({
ignore: ['crypto', 'node-fetch', 'util'],
Expand All @@ -60,10 +73,24 @@ function config({plugins = [], output = {}, tsCompilerOptions = {}}) {
output: {
banner: PREAMBLE,
sourcemap: true,
globals: {'@tensorflow/tfjs-core': 'tf', 'fs': 'fs', 'path': 'path', 'worker_threads': 'worker_threads', 'perf_hooks': 'perf_hooks'},
globals: {
'@tensorflow/tfjs-core': 'tf',
'fs': 'fs',
'path': 'path',
'worker_threads': 'worker_threads',
'perf_hooks': 'perf_hooks'
},
...output,
},
external: ['crypto', '@tensorflow/tfjs-core', 'fs', 'path', 'worker_threads', 'perf_hooks'],
external: [
'crypto',
'@tensorflow/tfjs-core',
'fs',
'path',
'worker_threads',
'perf_hooks',
...external,
],
onwarn: warning => {
let {code} = warning;
if (code === 'CIRCULAR_DEPENDENCY' || code === 'CIRCULAR' ||
Expand All @@ -78,10 +105,8 @@ function config({plugins = [], output = {}, tsCompilerOptions = {}}) {
module.exports = cmdOptions => {
const bundles = [];

const terserPlugin = terser({output: {preamble: PREAMBLE, comments: false}});
const name = 'tf.wasm';
const extend = true;
const browserFormat = 'umd';
const fileName = 'tf-backend-wasm';

// Node
Expand All @@ -96,55 +121,22 @@ module.exports = cmdOptions => {
tsCompilerOptions: {target: 'es5'}
}));

if (!cmdOptions.ci || cmdOptions.npm) {
// tf-backend-wasm.min.js
bundles.push(config({
plugins: [terserPlugin],
output: {
format: 'umd',
name,
extend,
file: `dist/${fileName}.min.js`,
},
}));

}

// Without this, the terser plugin will turn `typeof _scriptDir ==
// "undefined"` into `_scriptDir === void 0` in minified JS file which will
// cause "_scriptDir is undefined" error in web worker's inline script.
//
// For more context, see scripts/patch-threaded-simd-module.js.
const terserExtraOptions = {compress: {typeofs: false}};
if (cmdOptions.npm) {
// Browser default unminified (ES5)
bundles.push(config({
output: {
format: browserFormat,
name,
extend,
file: `dist/${fileName}.js`,
freeze: false
},
tsCompilerOptions: {target: 'es5'}
}));

// Browser ES2017
bundles.push(config({
output: {
format: browserFormat,
name,
extend,
file: `dist/${fileName}.es2017.js`
},
tsCompilerOptions: {target: 'es2017'}
}));

// Browser ES2017 minified
bundles.push(config({
plugins: [terserPlugin],
output: {
format: browserFormat,
name,
extend,
file: `dist/${fileName}.es2017.min.js`
},
tsCompilerOptions: {target: 'es2017'}
}));
const browserBundles = getBrowserBundleConfigOptions(
config, name, fileName, PREAMBLE, cmdOptions.visualize, false /* CI */,
terserExtraOptions);
bundles.push(...browserBundles);
} else {
const browserBundles = getBrowserBundleConfigOptions(
config, name, fileName, PREAMBLE, cmdOptions.visualize, true /* CI */,
terserExtraOptions);
bundles.push(...browserBundles);
}

return bundles;
Expand Down
1 change: 1 addition & 0 deletions tfjs-backend-wasm/scripts/build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ if [[ "$1" != "--dev" ]]; then
wasm-out/

node ./scripts/create-worker-module.js
node ./scripts/patch-threaded-simd-module.js
fi

mkdir -p dist
Expand Down
56 changes: 56 additions & 0 deletions tfjs-backend-wasm/scripts/patch-threaded-simd-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2021 Google LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* =============================================================================
*/

/**
* This file patches the Emscripten-generated WASM JS script so that it can be
* properly loaded in web worker.
*
* We need to pass the content of this script to WASM module's
* mainScriptUrlOrBlob field so that the web worker can correctly load the
* script "inline". The returned content of the script (after it self-executes)
* is a anonymous function object in which we have the following if block:
*
* if (_scriptDir) {
* scriptDirectory = _scriptDir;
* }
*
* It works great if the script runs in the main page, where _scriptDir is
* initialized to the path of the tf-backend-wasm.js file, outside of the
* function object. However, when the script runs in a web worker, the
* code that initializes _scriptDir won't be present since it is outside
* of the scope of the function object. As a result, a "Uncaught
* ReferenceError: _scriptDir is not defined" error will be thrown fron
* the web worker.
*
* To fix this, we will replace all the occurences of "if(_scriptDir)"
* with a better version that first checks whether _scriptDir is defined
* or not
*
* For more context, see:
* /~https://github.com/emscripten-core/emscripten/pull/12832
*/
const fs = require('fs');

const BASE_PATH = './wasm-out/';
const JS_PATH = `${BASE_PATH}tfjs-backend-wasm-threaded-simd.js`;

let content = fs.readFileSync(JS_PATH, 'utf8');
content = content.replace(
/if\s*\(\s*_scriptDir\s*\)/g,
'if(typeof _scriptDir !== "undefined" && _scriptDir)');
fs.chmodSync(JS_PATH, 0o644);
fs.writeFileSync(JS_PATH, content);
Loading

0 comments on commit 0dccac9

Please sign in to comment.