Skip to content

Commit

Permalink
fix #1900: use WebAssembly.instantiateStreaming
Browse files Browse the repository at this point in the history
This also closes #1036.
  • Loading branch information
evanw committed Dec 4, 2022
1 parent cbc7aae commit ed5e2c3
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 54 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -19,6 +19,12 @@

TypeScript code that does this should now be parsed correctly.

* Use `WebAssembly.instantiateStreaming` if available ([#1036](https://github.com/evanw/esbuild/pull/1036), [#1900](https://github.com/evanw/esbuild/pull/1900))

Currently the WebAssembly version of esbuild uses `fetch` to download `esbuild.wasm` and then `WebAssembly.instantiate` to compile it. There is a newer API called `WebAssembly.instantiateStreaming` that both downloads and compiles at the same time, which can be a performance improvement if both downloading and compiling are slow. With this release, esbuild now attempts to use `WebAssembly.instantiateStreaming` and falls back to the original approach if that fails.

The implementation for this builds on a PR by [@lbwa](https://github.com/lbwa).

* Preserve Webpack comments inside constructor calls ([#2439](https://github.com/evanw/esbuild/issues/2439))

This improves the use of esbuild as a faster TypeScript-to-JavaScript frontend for Webpack, which has special [magic comments](https://webpack.js.org/api/module-methods/#magic-comments) inside `new Worker()` expressions that affect Webpack's behavior.
Expand Down
30 changes: 19 additions & 11 deletions lib/deno/wasm.ts
Expand Up @@ -76,15 +76,7 @@ export const initialize: typeof types.initialize = async (options) => {
await initializePromise;
}

const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<Service> => {
let wasm: WebAssembly.Module;
if (wasmModule) {
wasm = wasmModule;
} else {
if (!wasmURL) wasmURL = new URL('esbuild.wasm', import.meta.url).href
wasm = await WebAssembly.compileStreaming(fetch(wasmURL))
}

const startRunningService = async (wasmURL: string | URL, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<Service> => {
let worker: {
onmessage: ((event: any) => void) | null
postMessage: (data: Uint8Array | ArrayBuffer | WebAssembly.Module) => void
Expand All @@ -106,8 +98,21 @@ const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Modu
}
}

worker.postMessage(wasm)
worker.onmessage = ({ data }) => readFromStdout(data)
let firstMessageResolve: (value: void) => void
let firstMessageReject: (error: any) => void

const firstMessagePromise = new Promise((resolve, reject) => {
firstMessageResolve = resolve
firstMessageReject = reject
})

worker.onmessage = ({ data: error }) => {
worker.onmessage = ({ data }) => readFromStdout(data)
if (error) firstMessageReject(error)
else firstMessageResolve()
}

worker.postMessage(wasmModule || new URL(wasmURL, import.meta.url).toString())

let { readFromStdout, service } = common.createChannel({
writeToStdin(bytes) {
Expand All @@ -118,6 +123,9 @@ const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Modu
esbuild: ourselves,
})

// This will throw if WebAssembly module instantiation fails
await firstMessagePromise

stopService = () => {
worker.terminate()
initializePromise = undefined
Expand Down
31 changes: 19 additions & 12 deletions lib/npm/browser.ts
Expand Up @@ -71,16 +71,7 @@ export const initialize: typeof types.initialize = options => {
return initializePromise;
}

const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<void> => {
let wasm: ArrayBuffer | WebAssembly.Module;
if (wasmModule) {
wasm = wasmModule;
} else {
let res = await fetch(wasmURL);
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasmURL)}`);
wasm = await res.arrayBuffer();
}

const startRunningService = async (wasmURL: string | URL, wasmModule: WebAssembly.Module | undefined, useWorker: boolean): Promise<void> => {
let worker: {
onmessage: ((event: any) => void) | null
postMessage: (data: Uint8Array | ArrayBuffer | WebAssembly.Module) => void
Expand All @@ -102,8 +93,21 @@ const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Modu
}
}

worker.postMessage(wasm)
worker.onmessage = ({ data }) => readFromStdout(data)
let firstMessageResolve: (value: void) => void
let firstMessageReject: (error: any) => void

const firstMessagePromise = new Promise((resolve, reject) => {
firstMessageResolve = resolve
firstMessageReject = reject
})

worker.onmessage = ({ data: error }) => {
worker.onmessage = ({ data }) => readFromStdout(data)
if (error) firstMessageReject(error)
else firstMessageResolve()
}

worker.postMessage(wasmModule || new URL(wasmURL, location.href).toString())

let { readFromStdout, service } = common.createChannel({
writeToStdin(bytes) {
Expand All @@ -114,6 +118,9 @@ const startRunningService = async (wasmURL: string, wasmModule: WebAssembly.Modu
esbuild: ourselves,
})

// This will throw if WebAssembly module instantiation fails
await firstMessagePromise

longLivedService = {
build: (options: types.BuildOptions): Promise<any> =>
new Promise<types.BuildResult>((resolve, reject) =>
Expand Down
5 changes: 4 additions & 1 deletion lib/shared/common.ts
Expand Up @@ -59,6 +59,9 @@ let mustBeStringOrArray = (value: string | string[] | undefined): string | null
let mustBeStringOrUint8Array = (value: string | Uint8Array | undefined): string | null =>
typeof value === 'string' || value instanceof Uint8Array ? null : 'a string or a Uint8Array';

let mustBeStringOrURL = (value: string | URL | undefined): string | null =>
typeof value === 'string' || value instanceof URL ? null : 'a string or a URL';

type OptionKeys = { [key: string]: boolean };

function getFlag<T, K extends (keyof T & string)>(object: T, keys: OptionKeys, key: K, mustBeFn: (value: T[K]) => string | null): T[K] | undefined {
Expand All @@ -80,7 +83,7 @@ function checkForInvalidFlags(object: Object, keys: OptionKeys, where: string):

export function validateInitializeOptions(options: types.InitializeOptions): types.InitializeOptions {
let keys: OptionKeys = Object.create(null);
let wasmURL = getFlag(options, keys, 'wasmURL', mustBeString);
let wasmURL = getFlag(options, keys, 'wasmURL', mustBeStringOrURL);
let wasmModule = getFlag(options, keys, 'wasmModule', mustBeWebAssemblyModule);
let worker = getFlag(options, keys, 'worker', mustBeBoolean);
checkForInvalidFlags(options, keys, 'in initialize() call');
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/types.ts
Expand Up @@ -588,7 +588,7 @@ export interface InitializeOptions {
* The URL of the "esbuild.wasm" file. This must be provided when running
* esbuild in the browser.
*/
wasmURL?: string
wasmURL?: string | URL

/**
* The result of calling "new WebAssembly.Module(buffer)" where "buffer"
Expand Down
35 changes: 29 additions & 6 deletions lib/shared/worker.ts
Expand Up @@ -9,7 +9,7 @@ interface Go {
declare const ESBUILD_VERSION: string;
declare function postMessage(message: any): void;

onmessage = ({ data: wasm }: { data: ArrayBuffer | WebAssembly.Module }) => {
onmessage = ({ data: wasm }: { data: WebAssembly.Module | string }) => {
let decoder = new TextDecoder()
let fs = (globalThis as any).fs

Expand Down Expand Up @@ -66,11 +66,34 @@ onmessage = ({ data: wasm }: { data: ArrayBuffer | WebAssembly.Module }) => {
let go: Go = new (globalThis as any).Go()
go.argv = ['', `--service=${ESBUILD_VERSION}`]

// Try to instantiate the module in the worker, then report back to the main thread
tryToInstantiateModule(wasm, go).then(
instance => {
postMessage(null)
go.run(instance)
},
error => {
postMessage(error)
},
)
}

async function tryToInstantiateModule(wasm: WebAssembly.Module | string, go: Go): Promise<WebAssembly.Instance> {
if (wasm instanceof WebAssembly.Module) {
WebAssembly.instantiate(wasm, go.importObject)
.then(instance => go.run(instance))
} else {
WebAssembly.instantiate(wasm, go.importObject)
.then(({ instance }) => go.run(instance))
return WebAssembly.instantiate(wasm, go.importObject)
}

const res = await fetch(wasm)
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasm)}`);

// Attempt to use the superior "instantiateStreaming" API first
if ('instantiateStreaming' in WebAssembly && /^application\/wasm($|;)/i.test(res.headers.get('Content-Type') || '')) {
const result = await WebAssembly.instantiateStreaming(res, go.importObject)
return result.instance
}

// Otherwise, fall back to the inferior "instantiate" API
const bytes = await res.arrayBuffer()
const result = await WebAssembly.instantiate(bytes, go.importObject)
return result.instance
}
10 changes: 8 additions & 2 deletions scripts/browser/browser-tests.js
Expand Up @@ -52,6 +52,12 @@ const server = http.createServer((req, res) => {
res.end(html)
return
}

if (parsed.pathname === '/scripts/browser/esbuild.wasm.bagel') {
res.writeHead(200, { 'Content-Type': 'application/octet-stream' })
res.end(wasm)
return
}
}

res.writeHead(404)
Expand Down Expand Up @@ -81,8 +87,8 @@ async function main() {
})

page.exposeFunction('testBegin', args => {
const { esm, min, worker, url } = JSON.parse(args)
console.log(`💬 config: esm=${esm}, min=${min}, worker=${worker}, url=${url}`)
const { esm, min, worker, mime, approach } = JSON.parse(args)
console.log(`💬 config: esm=${esm}, min=${min}, worker=${worker}, mime=${mime}, approach=${approach}`)
})

page.exposeFunction('testEnd', args => {
Expand Down
1 change: 1 addition & 0 deletions scripts/browser/esbuild.wasm.bagel
49 changes: 28 additions & 21 deletions scripts/browser/index.html
Expand Up @@ -187,8 +187,8 @@

async function testStart() {
if (!window.testBegin) window.testBegin = args => {
const { esm, min, worker, url } = JSON.parse(args)
console.log(`💬 config: esm=${esm}, min=${min}, worker=${worker}, url=${url}`)
const { esm, min, worker, mime, approach } = JSON.parse(args)
console.log(`💬 config: esm=${esm}, min=${min}, worker=${worker}, mime=${mime}, approach=${approach}`)
}

if (!window.testEnd) window.testEnd = args => {
Expand All @@ -206,25 +206,32 @@
for (const esm of [false, true]) {
for (const min of [false, true]) {
for (const worker of [false, true]) {
for (const url of [false, true]) {
try {
testBegin(JSON.stringify({ esm, min, worker, url }))
const esbuild = esm
? await import('/npm/esbuild-wasm/esm/browser' + (min ? '.min' : '') + '.js?' + Math.random())
: await loadScript('/npm/esbuild-wasm/lib/browser' + (min ? '.min' : '') + '.js?' + Math.random())
const initializePromise = url
? esbuild.initialize({ wasmURL: '/npm/esbuild-wasm/esbuild.wasm', worker })
: WebAssembly.compileStreaming(fetch('/npm/esbuild-wasm/esbuild.wasm')).then(module =>
esbuild.initialize({ wasmModule: module, worker }))
await initializePromise
await runAllTests({ esbuild })
testEnd(null)
} catch (e) {
testEnd(JSON.stringify({
test: e.test || null,
stack: e.stack || null,
error: (e && e.message || e) + '',
}))
for (const mime of ['correct', 'incorrect']) {
for (const approach of ['string', 'url', 'module']) {
try {
testBegin(JSON.stringify({ esm, min, worker, mime, approach }))
const esbuild = esm
? await import('/npm/esbuild-wasm/esm/browser' + (min ? '.min' : '') + '.js?' + Math.random())
: await loadScript('/npm/esbuild-wasm/lib/browser' + (min ? '.min' : '') + '.js?' + Math.random())
const url = mime === 'correct' ? '/npm/esbuild-wasm/esbuild.wasm' : '/scripts/browser/esbuild.wasm.bagel'
const initializePromise = {
string: () => esbuild.initialize({ wasmURL: url, worker }),
url: () => esbuild.initialize({ wasmURL: new URL(url, location.href), worker }),
module: () => fetch(url)
.then(r => r.arrayBuffer())
.then(bytes => WebAssembly.compile(bytes))
.then(module => esbuild.initialize({ wasmModule: module, worker })),
}[approach]()
await initializePromise
await runAllTests({ esbuild })
testEnd(null)
} catch (e) {
testEnd(JSON.stringify({
test: e.test || null,
stack: e.stack || null,
error: (e && e.message || e) + '',
}))
}
}
}
}
Expand Down

0 comments on commit ed5e2c3

Please sign in to comment.