Skip to content
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

feat(wasm): use instantiateStreaming as much as possible #1900

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 12 additions & 7 deletions lib/npm/browser.ts
Expand Up @@ -60,7 +60,10 @@ export const initialize: typeof types.initialize = options => {
let wasmURL = options.wasmURL;
let useWorker = options.worker !== false;
if (!wasmURL) throw new Error('Must provide the "wasmURL" option');
wasmURL += '';
wasmURL = new URL(
wasmURL + "",
`${location.origin}${wasmURL.startsWith('.') ? location.pathname : "/"}`
).href
if (initializePromise) throw new Error('Cannot call "initialize" more than once');
initializePromise = startRunningService(wasmURL, useWorker);
initializePromise.catch(() => {
Expand All @@ -71,11 +74,8 @@ export const initialize: typeof types.initialize = options => {
}

const startRunningService = async (wasmURL: string, useWorker: boolean): Promise<void> => {
let res = await fetch(wasmURL);
if (!res.ok) throw new Error(`Failed to download ${JSON.stringify(wasmURL)}`);
let wasm = await res.arrayBuffer();
let code = `{` +
`let global={};` +
`let global={WASM_URL:"${wasmURL}"};` +
`for(let o=self;o;o=Object.getPrototypeOf(o))` +
`for(let k of Object.getOwnPropertyNames(o))` +
`if(!(k in global))` +
Expand Down Expand Up @@ -104,8 +104,13 @@ const startRunningService = async (wasmURL: string, useWorker: boolean): Promise
}
}

worker.postMessage(wasm)
worker.onmessage = ({ data }) => readFromStdout(data)
worker.onmessage = ({ data }) => {
if (data.type === 'done') {
worker.onmessage = ({ data }) => readFromStdout(data)
return
}
throw new Error(data?.error ?? 'Failed to compile wasm code')
}

let { readFromStdout, service } = common.createChannel({
writeToStdin(bytes) {
Expand Down
118 changes: 69 additions & 49 deletions lib/npm/worker.ts
@@ -1,65 +1,85 @@
// This file is part of the web worker source code

declare const ESBUILD_VERSION: string;
declare function postMessage(message: any): void;

onmessage = ({ data: wasm }) => {
let decoder = new TextDecoder()
let fs = (global as any).fs
async function loadWasm(
url: string,
importObject?: WebAssembly.Imports
): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
const response = await fetch(url)
if (!response.ok)
throw new Error(`Failed to download ${JSON.stringify(url)}`)

let stderr = ''
fs.writeSync = (fd: number, buffer: Uint8Array) => {
if (fd === 1) {
postMessage(buffer)
} else if (fd === 2) {
stderr += decoder.decode(buffer)
let parts = stderr.split('\n')
if (parts.length > 1) console.log(parts.slice(0, -1).join('\n'))
stderr = parts[parts.length - 1]
} else {
throw new Error('Bad write')
}
return buffer.length
const mimeType = response.headers.get("Content-Type") ?? ""; // case-insensitive
// wasm mime type header is required for instantiateStreaming api
// https://www.w3.org/TR/wasm-web-api-1/#streaming-modules
const isWasmMime = mimeType.includes("application/wasm")
if (typeof WebAssembly.instantiateStreaming === "function" && isWasmMime) {
return WebAssembly.instantiateStreaming(response, importObject)
} else {
return WebAssembly.instantiate(await response.arrayBuffer(), importObject)
}
}

let stdin: Uint8Array[] = []
let resumeStdin: () => void
let stdinPos = 0
let decoder = new TextDecoder()
let fs = (global as any).fs

onmessage = ({ data }) => {
if (data.length > 0) {
stdin.push(data)
if (resumeStdin) resumeStdin()
}
let stderr = ''
fs.writeSync = (fd: number, buffer: Uint8Array) => {
if (fd === 1) {
postMessage(buffer)
} else if (fd === 2) {
stderr += decoder.decode(buffer)
let parts = stderr.split('\n')
if (parts.length > 1) console.log(parts.slice(0, -1).join('\n'))
stderr = parts[parts.length - 1]
} else {
throw new Error('Bad write')
}
return buffer.length
}

fs.read = (
fd: number, buffer: Uint8Array, offset: number, length: number,
position: null, callback: (err: Error | null, count?: number) => void,
) => {
if (fd !== 0 || offset !== 0 || length !== buffer.length || position !== null) {
throw new Error('Bad read')
}
let stdin: Uint8Array[] = []
let resumeStdin: () => void
let stdinPos = 0

if (stdin.length === 0) {
resumeStdin = () => fs.read(fd, buffer, offset, length, position, callback)
return
}
onmessage = ({ data }) => {
if (data.length > 0) {
stdin.push(data)
if (resumeStdin) resumeStdin()
}
}

let first = stdin[0]
let count = Math.max(0, Math.min(length, first.length - stdinPos))
buffer.set(first.subarray(stdinPos, stdinPos + count), offset)
stdinPos += count
if (stdinPos === first.length) {
stdin.shift()
stdinPos = 0
}
callback(null, count)
fs.read = (
fd: number, buffer: Uint8Array, offset: number, length: number,
position: null, callback: (err: Error | null, count?: number) => void,
) => {
if (fd !== 0 || offset !== 0 || length !== buffer.length || position !== null) {
throw new Error('Bad read')
}

let go = new (global as any).Go()
go.argv = ['', `--service=${ESBUILD_VERSION}`]
if (stdin.length === 0) {
resumeStdin = () => fs.read(fd, buffer, offset, length, position, callback)
return
}

WebAssembly.instantiate(wasm, go.importObject)
.then(({ instance }) => go.run(instance))
let first = stdin[0]
let count = Math.max(0, Math.min(length, first.length - stdinPos))
buffer.set(first.subarray(stdinPos, stdinPos + count), offset)
stdinPos += count
if (stdinPos === first.length) {
stdin.shift()
stdinPos = 0
}
callback(null, count)
}

let go = new (global as any).Go()
go.argv = ['', `--service=${ESBUILD_VERSION}`]

loadWasm((global as any).WASM_URL, go.importObject)
.then(async ({ instance }) => {
postMessage({ type: 'done' })
return go.run(instance)
})
.catch(err => postMessage({ type: 'error', error: err?.message ?? err }))
4 changes: 4 additions & 0 deletions scripts/browser/browser-tests.js
Expand Up @@ -284,6 +284,10 @@ async function main() {
async function runPage(key) {
try {
const page = await browser.newPage()
page.on('pageerror', error => {
console.error(`❌ page error: ${error?.message ?? error}`)
allTestsPassed = false
})
page.on('console', obj => console.log(`[console.${obj.type()}] ${obj.text()}`))
page.exposeFunction('testFail', error => {
console.log(`❌ ${error}`)
Expand Down