diff --git a/codecs/oxipng/Cargo.lock b/codecs/oxipng/Cargo.lock index 49428bb2..4f331186 100644 --- a/codecs/oxipng/Cargo.lock +++ b/codecs/oxipng/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "adler" version = "0.2.3" @@ -232,6 +234,15 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9f84f9b115ce7843d60706df1422a916680bfdfcbdb0447c5614ff9d7e4d78" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -350,12 +361,6 @@ dependencies = [ "libc", ] -[[package]] -name = "once_cell" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - [[package]] name = "oxipng" version = "4.0.3" @@ -401,9 +406,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid", ] @@ -484,24 +489,28 @@ dependencies = [ "pest", ] +[[package]] +name = "spmc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8428da277a8e3a15271d79943e80ccc2ef254e78813a166a08d65e4c3ece5" + [[package]] name = "squoosh-oxipng" version = "0.1.0" dependencies = [ - "crossbeam-channel", "libdeflater", "log", - "once_cell", "oxipng", - "rayon", "wasm-bindgen", + "wasm-bindgen-rayon", ] [[package]] name = "syn" -version = "1.0.58" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" dependencies = [ "proc-macro2", "quote", @@ -522,9 +531,9 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "wasm-bindgen" -version = "0.2.69" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -532,9 +541,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.69" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" dependencies = [ "bumpalo", "lazy_static", @@ -547,9 +556,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.69" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -557,9 +566,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.69" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" dependencies = [ "proc-macro2", "quote", @@ -569,7 +578,19 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.69" +name = "wasm-bindgen-rayon" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" +checksum = "3069d2a42e7a7e3bfde668f84adb5fbc35701ca2b39b27a064cbd5ede4e78194" +dependencies = [ + "js-sys", + "rayon", + "spmc", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" diff --git a/codecs/oxipng/Cargo.toml b/codecs/oxipng/Cargo.toml index 6ab15d3a..b0b9a191 100644 --- a/codecs/oxipng/Cargo.toml +++ b/codecs/oxipng/Cargo.toml @@ -14,15 +14,13 @@ crate-type = ["cdylib"] [dependencies] oxipng = { version = "4.0.1", default-features = false, features = ["libdeflater"] } libdeflater = { version = "0.7.1", features = ["freestanding"] } -wasm-bindgen = "0.2.68" +wasm-bindgen = "0.2.73" log = { version = "0.4.11", features = ["release_max_level_off"] } -rayon = { version = "1.5.0", optional = true } -once_cell = { version = "1.5.2", optional = true } -crossbeam-channel = { version = "0.5.0", optional = true } +wasm-bindgen-rayon = { version = "1.0", optional = true } [profile.release] lto = true opt-level = "s" [features] -parallel = ["oxipng/parallel", "rayon", "crossbeam-channel", "once_cell"] +parallel = ["oxipng/parallel", "wasm-bindgen-rayon"] diff --git a/codecs/oxipng/build.sh b/codecs/oxipng/build.sh index 200481fb..ee3d2539 100755 --- a/codecs/oxipng/build.sh +++ b/codecs/oxipng/build.sh @@ -6,6 +6,4 @@ rm -rf pkg,{-parallel} export CFLAGS="${CFLAGS} -DUNALIGNED_ACCESS_IS_FAST=1" wasm-pack build -t web RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' wasm-pack build -t web -d pkg-parallel -- -Z build-std=panic_abort,std --features=parallel -# Workaround https://github.com/rustwasm/wasm-bindgen/issues/2133: -sed -i "s|maybe_memory:|maybe_memory?:|" pkg-parallel/squoosh_oxipng.d.ts rm pkg{,-parallel}/.gitignore diff --git a/codecs/oxipng/pkg-parallel/snippets/wasm-bindgen-rayon-3d2df09ebec17a22/src/workerHelpers.js b/codecs/oxipng/pkg-parallel/snippets/wasm-bindgen-rayon-3d2df09ebec17a22/src/workerHelpers.js new file mode 100644 index 00000000..da13c772 --- /dev/null +++ b/codecs/oxipng/pkg-parallel/snippets/wasm-bindgen-rayon-3d2df09ebec17a22/src/workerHelpers.js @@ -0,0 +1,98 @@ +/** + * Copyright 2021 Google Inc. 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. + */ + +// Note: we use `wasm_bindgen_worker_`-prefixed message types to make sure +// we can handle bundling into other files, which might happen to have their +// own `postMessage`/`onmessage` communication channels. +// +// If we didn't take that into the account, we could send much simpler signals +// like just `0` or whatever, but the code would be less resilient. + +function waitForMsgType(target, type) { + return new Promise(resolve => { + target.addEventListener('message', function onMsg({ data }) { + if (data == null || data.type !== type) return; + target.removeEventListener('message', onMsg); + resolve(data); + }); + }); +} + +waitForMsgType(self, 'wasm_bindgen_worker_init').then(async data => { + // # Note 1 + // Our JS should have been generated in + // `[out-dir]/snippets/wasm-bindgen-rayon-[hash]/workerHelpers.js`, + // resolve the main module via `../../..`. + // + // This might need updating if the generated structure changes on wasm-bindgen + // side ever in the future, but works well with bundlers today. The whole + // point of this crate, after all, is to abstract away unstable features + // and temporary bugs so that you don't need to deal with them in your code. + // + // # Note 2 + // This could be a regular import, but then some bundlers complain about + // circular deps. + // + // Dynamic import could be cheap if this file was inlined into the parent, + // which would require us just using `../../..` in `new Worker` below, + // but that doesn't work because wasm-pack unconditionally adds + // "sideEffects":false (see below). + // + // OTOH, even though it can't be inlined, it should be still reasonably + // cheap since the requested file is already in cache (it was loaded by + // the main thread). + const pkg = await import('../../..'); + await pkg.default(data.module, data.memory); + postMessage({ type: 'wasm_bindgen_worker_ready' }); + pkg.wbg_rayon_start_worker(data.receiver); +}); + +export async function startWorkers(module, memory, builder) { + const workerInit = { + type: 'wasm_bindgen_worker_init', + module, + memory, + receiver: builder.receiver() + }; + + try { + await Promise.all( + Array.from({ length: builder.numThreads() }, () => { + // Self-spawn into a new Worker. + // + // TODO: while `new URL('...', import.meta.url) becomes a semi-standard + // way to get asset URLs relative to the module across various bundlers + // and browser, ideally we should switch to `import.meta.resolve` + // once it becomes a standard. + // + // Note: we could use `../../..` as the URL here to inline workerHelpers.js + // into the parent entry instead of creating another split point - + // this would be preferable from optimization perspective - + // however, Webpack then eliminates all message handler code + // because wasm-pack produces "sideEffects":false in package.json + // unconditionally. + // + // The only way to work around that is to have side effect code + // in an entry point such as Worker file itself. + const worker = new Worker(new URL('./workerHelpers.js', import.meta.url), { + type: 'module' + }); + worker.postMessage(workerInit); + return waitForMsgType(worker, 'wasm_bindgen_worker_ready'); + }) + ); + builder.build(); + } finally { + builder.free(); + } +} diff --git a/codecs/oxipng/pkg-parallel/squoosh_oxipng.d.ts b/codecs/oxipng/pkg-parallel/squoosh_oxipng.d.ts index 3ed1d157..8ca403de 100644 --- a/codecs/oxipng/pkg-parallel/squoosh_oxipng.d.ts +++ b/codecs/oxipng/pkg-parallel/squoosh_oxipng.d.ts @@ -7,25 +7,43 @@ */ export function optimise(data: Uint8Array, level: number): Uint8Array; /** -* @param {number} num -* @returns {any} +* @param {number} num_threads +* @returns {Promise} */ -export function worker_initializer(num: number): any; +export function initThreadPool(num_threads: number): Promise; +/** +* @param {number} receiver +*/ +export function wbg_rayon_start_worker(receiver: number): void; /** */ -export function start_main_thread(): void; +export class wbg_rayon_PoolBuilder { + free(): void; +/** +* @returns {number} +*/ + numThreads(): number; +/** +* @returns {number} +*/ + receiver(): number; /** */ -export function start_worker_thread(): void; + build(): void; +} export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { readonly optimise: (a: number, b: number, c: number, d: number) => void; - readonly worker_initializer: (a: number) => number; - readonly start_main_thread: () => void; - readonly start_worker_thread: () => void; + readonly __wbg_wbg_rayon_poolbuilder_free: (a: number) => void; + readonly wbg_rayon_poolbuilder_numThreads: (a: number) => number; + readonly wbg_rayon_poolbuilder_receiver: (a: number) => number; + readonly wbg_rayon_poolbuilder_build: (a: number) => void; + readonly initThreadPool: (a: number) => number; + readonly wbg_rayon_start_worker: (a: number) => void; readonly __wbindgen_export_0: WebAssembly.Memory; + readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_start: () => void; @@ -41,4 +59,3 @@ export interface InitOutput { * @returns {Promise} */ export default function init (module_or_path?: InitInput | Promise, maybe_memory?: WebAssembly.Memory): Promise; - \ No newline at end of file diff --git a/codecs/oxipng/pkg-parallel/squoosh_oxipng.js b/codecs/oxipng/pkg-parallel/squoosh_oxipng.js index 1111760d..a1689a5f 100644 --- a/codecs/oxipng/pkg-parallel/squoosh_oxipng.js +++ b/codecs/oxipng/pkg-parallel/squoosh_oxipng.js @@ -1,21 +1,6 @@ +import { startWorkers } from './snippets/wasm-bindgen-rayon-3d2df09ebec17a22/src/workerHelpers.js'; let wasm; -let memory; - -const heap = new Array(32).fill(undefined); - -heap.push(undefined, null, true, false); - -let heap_next = heap.length; - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); @@ -33,6 +18,21 @@ function getStringFromWasm0(ptr, len) { return cachedTextDecoder.decode(getUint8Memory0().slice(ptr, ptr + len)); } +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + let WASM_VECTOR_LEN = 0; function passArray8ToWasm0(arg, malloc) { @@ -60,8 +60,7 @@ function getArrayU8FromWasm0(ptr, len) { */ export function optimise(data, level) { try { - const retptr = wasm.__wbindgen_export_1.value - 16; - wasm.__wbindgen_export_1.value = retptr; + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); var len0 = WASM_VECTOR_LEN; wasm.optimise(retptr, ptr0, len0, level); @@ -71,7 +70,7 @@ export function optimise(data, level) { wasm.__wbindgen_free(r0, r1 * 1); return v1; } finally { - wasm.__wbindgen_export_1.value += 16; + wasm.__wbindgen_add_to_stack_pointer(16); } } @@ -89,29 +88,66 @@ function takeObject(idx) { return ret; } /** -* @param {number} num -* @returns {any} +* @param {number} num_threads +* @returns {Promise} */ -export function worker_initializer(num) { - var ret = wasm.worker_initializer(num); +export function initThreadPool(num_threads) { + var ret = wasm.initThreadPool(num_threads); return takeObject(ret); } /** +* @param {number} receiver */ -export function start_main_thread() { - wasm.start_main_thread(); +export function wbg_rayon_start_worker(receiver) { + wasm.wbg_rayon_start_worker(receiver); } /** */ -export function start_worker_thread() { - wasm.start_worker_thread(); +export class wbg_rayon_PoolBuilder { + + static __wrap(ptr) { + const obj = Object.create(wbg_rayon_PoolBuilder.prototype); + obj.ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_wbg_rayon_poolbuilder_free(ptr); + } + /** + * @returns {number} + */ + numThreads() { + var ret = wasm.wbg_rayon_poolbuilder_numThreads(this.ptr); + return ret >>> 0; + } + /** + * @returns {number} + */ + receiver() { + var ret = wasm.wbg_rayon_poolbuilder_receiver(this.ptr); + return ret; + } + /** + */ + build() { + wasm.wbg_rayon_poolbuilder_build(this.ptr); + } } -async function load(module, imports, maybe_memory) { +async function load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { - memory = imports.wbg.memory = new WebAssembly.Memory({initial:17,maximum:16384,shared:true}); if (typeof WebAssembly.instantiateStreaming === 'function') { try { return await WebAssembly.instantiateStreaming(module, imports); @@ -130,7 +166,6 @@ async function load(module, imports, maybe_memory) { return await WebAssembly.instantiate(bytes, imports); } else { - memory = imports.wbg.memory = maybe_memory; const instance = await WebAssembly.instantiate(module, imports); if (instance instanceof WebAssembly.Instance) { @@ -144,10 +179,13 @@ async function load(module, imports, maybe_memory) { async function init(input, maybe_memory) { if (typeof input === 'undefined') { - input = import.meta.url.replace(/\.js$/, '_bg.wasm'); + input = new URL('squoosh_oxipng_bg.wasm', import.meta.url); } const imports = {}; imports.wbg = {}; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; imports.wbg.__wbindgen_module = function() { var ret = init.__wbindgen_wasm_module; return addHeapObject(ret); @@ -156,19 +194,18 @@ async function init(input, maybe_memory) { var ret = wasm.__wbindgen_export_0; return addHeapObject(ret); }; - imports.wbg.__wbg_of_6510501edc06d65e = function(arg0, arg1) { - var ret = Array.of(takeObject(arg0), takeObject(arg1)); + imports.wbg.__wbg_startWorkers_914655bb4d5bb5e1 = function(arg0, arg1, arg2) { + var ret = startWorkers(takeObject(arg0), takeObject(arg1), wbg_rayon_PoolBuilder.__wrap(arg2)); return addHeapObject(ret); }; - imports.wbg.__wbindgen_throw = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }; if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { input = fetch(input); } - const { instance, module } = await load(await input, imports, maybe_memory); + imports.wbg.memory = maybe_memory || new WebAssembly.Memory({initial:17,maximum:16384,shared:true}); + + const { instance, module } = await load(await input, imports); wasm = instance.exports; init.__wbindgen_wasm_module = module; diff --git a/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm b/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm index 49fe70f3..6c030492 100644 Binary files a/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm and b/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm differ diff --git a/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm.d.ts b/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm.d.ts index d1b8b633..760b49d9 100644 --- a/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm.d.ts +++ b/codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm.d.ts @@ -1,10 +1,14 @@ /* tslint:disable */ /* eslint-disable */ export function optimise(a: number, b: number, c: number, d: number): void; -export function worker_initializer(a: number): number; -export function start_main_thread(): void; -export function start_worker_thread(): void; +export function __wbg_wbg_rayon_poolbuilder_free(a: number): void; +export function wbg_rayon_poolbuilder_numThreads(a: number): number; +export function wbg_rayon_poolbuilder_receiver(a: number): number; +export function wbg_rayon_poolbuilder_build(a: number): void; +export function initThreadPool(a: number): number; +export function wbg_rayon_start_worker(a: number): void; export const __wbindgen_export_0: WebAssembly.Memory; +export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_malloc(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_start(): void; diff --git a/codecs/oxipng/pkg/squoosh_oxipng.d.ts b/codecs/oxipng/pkg/squoosh_oxipng.d.ts index 1c5c5b4d..759bf922 100644 --- a/codecs/oxipng/pkg/squoosh_oxipng.d.ts +++ b/codecs/oxipng/pkg/squoosh_oxipng.d.ts @@ -12,6 +12,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl export interface InitOutput { readonly memory: WebAssembly.Memory; readonly optimise: (a: number, b: number, c: number, d: number) => void; + readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; } @@ -25,4 +26,3 @@ export interface InitOutput { * @returns {Promise} */ export default function init (module_or_path?: InitInput | Promise): Promise; - \ No newline at end of file diff --git a/codecs/oxipng/pkg/squoosh_oxipng.js b/codecs/oxipng/pkg/squoosh_oxipng.js index 74c98c89..ee7a699f 100644 --- a/codecs/oxipng/pkg/squoosh_oxipng.js +++ b/codecs/oxipng/pkg/squoosh_oxipng.js @@ -44,8 +44,7 @@ function getArrayU8FromWasm0(ptr, len) { */ export function optimise(data, level) { try { - const retptr = wasm.__wbindgen_export_0.value - 16; - wasm.__wbindgen_export_0.value = retptr; + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); var len0 = WASM_VECTOR_LEN; wasm.optimise(retptr, ptr0, len0, level); @@ -55,13 +54,12 @@ export function optimise(data, level) { wasm.__wbindgen_free(r0, r1 * 1); return v1; } finally { - wasm.__wbindgen_export_0.value += 16; + wasm.__wbindgen_add_to_stack_pointer(16); } } async function load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { try { return await WebAssembly.instantiateStreaming(module, imports); @@ -80,7 +78,6 @@ async function load(module, imports) { return await WebAssembly.instantiate(bytes, imports); } else { - const instance = await WebAssembly.instantiate(module, imports); if (instance instanceof WebAssembly.Instance) { @@ -94,7 +91,7 @@ async function load(module, imports) { async function init(input) { if (typeof input === 'undefined') { - input = import.meta.url.replace(/\.js$/, '_bg.wasm'); + input = new URL('squoosh_oxipng_bg.wasm', import.meta.url); } const imports = {}; imports.wbg = {}; @@ -106,6 +103,8 @@ async function init(input) { input = fetch(input); } + + const { instance, module } = await load(await input, imports); wasm = instance.exports; diff --git a/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm b/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm index 97715f1a..97356d37 100644 Binary files a/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm and b/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm differ diff --git a/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm.d.ts b/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm.d.ts index d16fb460..b7c19ac5 100644 --- a/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm.d.ts +++ b/codecs/oxipng/pkg/squoosh_oxipng_bg.wasm.d.ts @@ -2,5 +2,6 @@ /* eslint-disable */ export const memory: WebAssembly.Memory; export function optimise(a: number, b: number, c: number, d: number): void; +export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_malloc(a: number): number; export function __wbindgen_free(a: number, b: number): void; diff --git a/codecs/oxipng/src/lib.rs b/codecs/oxipng/src/lib.rs index d9d44655..202621b1 100644 --- a/codecs/oxipng/src/lib.rs +++ b/codecs/oxipng/src/lib.rs @@ -1,9 +1,9 @@ +#[cfg(feature = "parallel")] +pub use wasm_bindgen_rayon::init_thread_pool; + use oxipng::AlphaOptim; use wasm_bindgen::prelude::*; -#[cfg(feature = "parallel")] -pub mod parallel; - #[wasm_bindgen] pub fn optimise(data: &[u8], level: u8) -> Vec { let mut options = oxipng::Options::from_preset(level); diff --git a/codecs/oxipng/src/parallel.rs b/codecs/oxipng/src/parallel.rs deleted file mode 100644 index 90046659..00000000 --- a/codecs/oxipng/src/parallel.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crossbeam_channel::{bounded, Receiver, Sender}; -use once_cell::sync::OnceCell; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsValue; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_namespace = Array, js_name = of)] - fn array_of_2(a: JsValue, b: JsValue) -> JsValue; -} - -// This is one of the parts that work around Chromium incorrectly implementing postMessage: -// https://bugs.chromium.org/p/chromium/issues/detail?id=1075645 -// -// rayon::ThreadPoolBuilder (used below) executes spawn handler to populate the worker pool, -// and then blocks the current thread until each worker unblocks its (opaque) lock. -// -// Normally, we could use postMessage directly inside the spawn handler to -// post module + memory + threadPtr to each worker, and the block the current thread. -// -// However, that bug means that postMessage is currently delayed until the next event loop, -// which will never spin since we block the current thread, and so the other workers will -// never be able to unblock us. -// -// To work around this problem, we: -// 1) Expose `worker_initializer` that returns module + memory pair (without threadPtr) -// that workers can be initialised with to become native threads. -// JavaScript can postMessage this pair in advance, and asynchronously wait for workers -// to acknowledge the receipt. -// 2) Create a global communication channel on the Rust side using crossbeam. -// It will be used to send threadPtr to the pre-initialised workers -// instead of postMessage. -// 3) Provide a separate `start_main_thread` that expects all workers to be ready, -// and just uses the provided channel to send `threadPtr`s using the -// shared memory and blocks the current thread until they're all grabbed. -// 4) Provide a `worker_initializer` that is expected to be invoked from various workers, -// reads one `threadPtr` from the shared channel and starts running it. -static CHANNEL: OnceCell<(Sender, Receiver)> = - OnceCell::new(); - -#[wasm_bindgen] -pub fn worker_initializer(num: usize) -> JsValue { - CHANNEL.get_or_init(|| bounded(num)); - array_of_2(wasm_bindgen::module(), wasm_bindgen::memory()) -} - -#[wasm_bindgen] -pub fn start_main_thread() { - let (sender, _) = CHANNEL.get().unwrap(); - - rayon::ThreadPoolBuilder::new() - .num_threads(sender.capacity().unwrap()) - .spawn_handler(|thread| Ok(sender.send(thread).unwrap_throw())) - .build_global() - .unwrap_throw() -} - -#[wasm_bindgen] -pub fn start_worker_thread() { - let (_, receiver) = CHANNEL.get().unwrap(); - receiver.recv().unwrap_throw().run() -} diff --git a/lib/client-bundle-plugin.js b/lib/client-bundle-plugin.js index c65efd53..06b69615 100644 --- a/lib/client-bundle-plugin.js +++ b/lib/client-bundle-plugin.js @@ -137,10 +137,24 @@ export default function (inputOptions, outputOptions, resolveFileUrl) { const dependencies = getDependencies(clientOutput, clientEntry); if (property.startsWith(allSrcPlaceholder)) { - const depCodes = dependencies.map( - (name) => clientOutput.find((item) => item.fileName === name).code, - ); - return JSON.stringify([clientEntry.code, ...depCodes].join(';')); + const allModules = [ + clientEntry, + ...dependencies.map((name) => + clientOutput.find((item) => item.fileName === name), + ), + ]; + + const inlineDefines = [ + ...allModules.map( + (item) => + `self.nextDefineUri=location.origin+${resolveFileUrl(item)};${ + item.code + }`, + ), + 'self.nextDefineUri=""', + ]; + + return JSON.stringify(inlineDefines.join('')); } return ( diff --git a/lib/css-plugin.js b/lib/css-plugin.js index 43e040b3..ce1a8216 100644 --- a/lib/css-plugin.js +++ b/lib/css-plugin.js @@ -48,7 +48,7 @@ const appendCssSource = ` } `; -export default function (resolveFileUrl) { +export default function () { /** @type {string[]} */ let emittedCSSIds; /** @type {Map} */ diff --git a/lib/omt.ejs b/lib/omt.ejs index 2f2741ed..fabca564 100644 --- a/lib/omt.ejs +++ b/lib/omt.ejs @@ -1,5 +1,5 @@ /** - * Copyright 2020 Google Inc. All Rights Reserved. + * Copyright 2021 Google Inc. 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 @@ -13,80 +13,64 @@ // If the loader is already loaded, just stop. if (!self.<%- amdFunctionName %>) { - const singleRequire = async name => { - if (name === 'require') return require; - let url; - if (name.startsWith(location.origin)) { - url = name.slice(location.origin.length); - } else { - url = name.slice(1) + '.js'; - } - if (!url.startsWith('/c/')) { - url = '/c' + url; - } - name = './static' + url; - if (registry[name]) return registry[name]; + let registry = {}; - if (!registry[name]) { + const singleRequire = (uri, parentUri) => { + let origURI = uri; + uri = new URL(uri + ".js", parentUri).href; + return registry[uri] || ( <% if (useEval) { %> - const text = await fetch(url).then(resp => resp.text()); - eval(text); + fetch(uri) + .then(resp => resp.text()) + .then(code => { + self.nextDefineUri = uri; + eval(code); + }) <% } else { %> - if ("document" in self) { - await new Promise(resolve => { + new Promise(resolve => { + if ("document" in self) { const script = document.createElement("script"); - script.src = url; - document.head.appendChild(script); + script.src = uri; script.onload = resolve; - }); - } else { - importScripts(url); - } + document.head.appendChild(script); + } else { + self.nextDefineUri = uri; + importScripts(uri); + resolve(); + } + }) <% } %> - } - if (!registry[name]) { - throw new Error(`Module ${name} didn’t register its module`); - } - return registry[name]; + .then(() => { + let promise = registry[uri]; + if (!promise) { + throw new Error(`Module ${uri} didn’t register its module`); + } + return promise; + }) + ); }; - const require = (names, resolve) => { - Promise.all(names.map(singleRequire)) - .then(modules => resolve(modules.length === 1 ? modules[0] : modules)); - }; - - const registry = { - require: Promise.resolve(require) - }; - - self.<%- amdFunctionName %> = (moduleName, depsNames, factory) => { - if (registry[moduleName]) { + self.<%- amdFunctionName %> = (depsNames, factory) => { + const uri = self.nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; + if (registry[uri]) { // Module is already loading or loaded. return; } - registry[moduleName] = Promise.resolve().then(() => { - let exports = {}; - const module = { - uri: location.origin + moduleName.slice(1) - }; - return Promise.all( - depsNames.map(depName => { - switch(depName) { - case "exports": - return exports; - case "module": - return module; - default: - return singleRequire(depName); - } - }) - ).then(deps => { - const facValue = factory(...deps); - if (!exports.default) { - exports.default = facValue; - } - return exports; - }); + let exports = {}; + const require = depUri => singleRequire(depUri, uri); + const specialDeps = { + module: { uri }, + exports, + require + }; + // Note: Promise.resolve() is necessary to delay loading until all the + // `define`s on the current page had a chance to execute first. + // This allows to inline some deps on the main page. + registry[uri] = Promise.resolve().then(() => Promise.all(depsNames.map( + depName => specialDeps[depName] || require(depName) + ))).then(deps => { + factory(...deps); + return exports; }); }; } diff --git a/package-lock.json b/package-lock.json index a1fa60e3..04043b68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,13 +179,25 @@ } }, "@surma/rollup-plugin-off-main-thread": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-1.4.2.tgz", - "integrity": "sha512-yBMPqmd1yEJo/280PAMkychuaALyQ9Lkb5q1ck3mjJrFuEobIfhnQ4J3mbvBoISmR3SWMWV+cGB/I0lCQee79A==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.1.tgz", + "integrity": "sha512-7OU8wfyv18YPWVmecg2/0Jh+pm3lQbvPhIWHd1YQpoxPKPW/vsDNGBaCnMKsZbz29RjgCoXKugAjyagPncgdEw==", "dev": true, "requires": { - "ejs": "^2.6.1", + "ejs": "^3.1.6", + "json5": "^2.2.0", "magic-string": "^0.25.0" + }, + "dependencies": { + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } } }, "@types/color-name": { @@ -405,6 +417,12 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1597,10 +1615,13 @@ } }, "ejs": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", - "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", - "dev": true + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", + "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "dev": true, + "requires": { + "jake": "^10.6.1" + } }, "electron-to-chromium": { "version": "1.3.538", @@ -1797,6 +1818,15 @@ "integrity": "sha512-na2cwntTVgMsR+BZ2YBr/XQk941DKDw2LJKbV7g6TRdGBQ3rx8V53oEviG8zPWoBOySwK9w/SlZ/gb/F/48I8A==", "dev": true }, + "filelist": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz", + "integrity": "sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2306,6 +2336,70 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "dev": true, + "requires": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "jest-worker": { "version": "26.3.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz", diff --git a/package.json b/package.json index ac4631a1..8d23cbc4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-node-resolve": "^11.1.0", "@rollup/plugin-replace": "^2.3.4", - "@surma/rollup-plugin-off-main-thread": "^1.4.2", + "@surma/rollup-plugin-off-main-thread": "^2.2.1", "@types/dedent": "^0.7.0", "@types/mime-types": "^2.1.0", "@types/node": "^14.14.7", diff --git a/rollup.config.js b/rollup.config.js index 9d5026a0..37d3865b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -37,13 +37,6 @@ function resolveFileUrl({ fileName }) { return JSON.stringify(fileName.replace(/^static\//, '/')); } -// With AMD output, Rollup always uses document.baseURI, which breaks in workers. -// This fixes it: -function resolveImportMeta(property, { chunkId }) { - if (property !== 'url') return; - return `new URL(${resolveFileUrl({ fileName: chunkId })}, location).href`; -} - const dir = '.tmp/build'; const staticPath = 'static/c/[name]-[hash][extname]'; const jsPath = staticPath.replace('[extname]', '.js'); @@ -62,6 +55,7 @@ export default async function ({ watch }) { path.join(__dirname, 'lib', 'omt.ejs'), 'utf-8', ); + await del('.tmp/build'); const isProduction = !watch; @@ -83,7 +77,7 @@ export default async function ({ watch }) { ]), urlPlugin(), dataURLPlugin(), - cssPlugin(resolveFileUrl), + cssPlugin(), ]; return { @@ -105,12 +99,12 @@ export default async function ({ watch }) { }, preserveModules: true, plugins: [ - { resolveFileUrl, resolveImportMeta }, + { resolveFileUrl }, clientBundlePlugin( { external: ['worker_threads'], plugins: [ - { resolveFileUrl, resolveImportMeta }, + { resolveFileUrl }, OMT({ loader: await omtLoaderPromise }), serviceWorkerPlugin({ output: 'static/serviceworker.js', diff --git a/src/features/encoders/oxiPNG/worker/oxipngEncode.ts b/src/features/encoders/oxiPNG/worker/oxipngEncode.ts index 43807b48..c936c382 100644 --- a/src/features/encoders/oxiPNG/worker/oxipngEncode.ts +++ b/src/features/encoders/oxiPNG/worker/oxipngEncode.ts @@ -14,54 +14,17 @@ import initOxiWasmST, { optimise as optimiseST, } from 'codecs/oxipng/pkg/squoosh_oxipng'; import initOxiWasmMT, { - worker_initializer, - start_main_thread, + initThreadPool, optimise as optimiseMT, } from 'codecs/oxipng/pkg-parallel/squoosh_oxipng'; import oxiWasmUrlST from 'url:codecs/oxipng/pkg/squoosh_oxipng_bg.wasm'; import oxiWasmUrlMT from 'url:codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm'; import { EncodeOptions } from '../shared/meta'; import { threads } from 'wasm-feature-detect'; -import workerURL from 'omt:./sub-worker'; -import type { WorkerInit } from './sub-worker'; - -function initWorker(worker: Worker, workerInit: WorkerInit) { - return new Promise((resolve) => { - worker.postMessage(workerInit); - worker.addEventListener('message', () => resolve(), { once: true }); - }); -} async function initMT() { - const num = navigator.hardwareConcurrency; - - // First, let browser fetch and spawn Workers for our pool in the background. - // This is fairly expensive, so we want to start it as early as possible. - const workers = Array.from({ length: num }, () => new Worker(workerURL)); - - // Meanwhile, asynchronously compile, instantiate and initialise Wasm on our main thread. await initOxiWasmMT(oxiWasmUrlMT); - - // Get module+memory from the Wasm instance. - // - // Ideally we wouldn't go via Wasm bindings here, since both are just JS variables, but memory is - // currently not exposed on the Wasm instance correctly by wasm-bindgen. - const workerInit: WorkerInit = worker_initializer(num); - - // Once done, we want to send module+memory to each Worker so that they instantiate Wasm too. - // While doing so, we need to wait for Workers to acknowledge that they have received our message. - // Ideally this shouldn't be necessary, but Chromium currently doesn't conform to the spec: - // https://bugs.chromium.org/p/chromium/issues/detail?id=1075645 - // - // If we didn't do this ping-pong game, the `start_main_thread` below would block the current - // thread on an atomic before even *sending* the `postMessage` containing memory, - // so Workers would never be able to unblock us back. - await Promise.all(workers.map((worker) => initWorker(worker, workerInit))); - - // Finally, instantiate rayon pool - this will use shared Wasm memory to send tasks to the - // Workers and then block until they're all ready. - start_main_thread(); - + await initThreadPool(navigator.hardwareConcurrency); return optimiseMT; } @@ -77,7 +40,7 @@ export default async function encode( options: EncodeOptions, ): Promise { if (!wasmReady) { - wasmReady = (await threads()) ? initMT() : initST(); + wasmReady = threads().then((hasThreads: boolean) => hasThreads ? initMT() : initST()); } const optimise = await wasmReady; diff --git a/src/features/encoders/oxiPNG/worker/sub-worker/index.ts b/src/features/encoders/oxiPNG/worker/sub-worker/index.ts deleted file mode 100644 index 212e81b9..00000000 --- a/src/features/encoders/oxiPNG/worker/sub-worker/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import initOxiPNG, { - start_worker_thread, -} from 'codecs/oxipng/pkg-parallel/squoosh_oxipng'; - -export type WorkerInit = [WebAssembly.Module, WebAssembly.Memory]; - -addEventListener( - 'message', - async (event) => { - // Tell the "main" thread that we've received the message. - // - // At this point, the "main" thread can run Wasm that - // will synchronously block waiting on other atomics. - // - // Note that we don't need to wait for Wasm instantiation here - it's - // better to start main thread as early as possible, and then it blocks - // on a shared atomic anyway until Worker is fully ready. - // @ts-ignore - postMessage(null); - - await initOxiPNG(...(event.data as WorkerInit)); - start_worker_thread(); - }, - { once: true }, -); diff --git a/src/sw/to-cache.ts b/src/sw/to-cache.ts index 72adfc2f..cbc922f5 100644 --- a/src/sw/to-cache.ts +++ b/src/sw/to-cache.ts @@ -85,7 +85,6 @@ import jxlEncWasm from 'url:codecs/jxl/enc/jxl_enc.wasm'; import * as jxlEnc from 'entry-data:codecs/jxl/enc/jxl_enc'; // OXI -import * as oxiMtWorker from 'entry-data:features/encoders/oxiPNG/worker/sub-worker'; import oxiMtWasm from 'url:codecs/oxipng/pkg-parallel/squoosh_oxipng_bg.wasm'; import oxiWasm from 'url:codecs/oxipng/pkg/squoosh_oxipng_bg.wasm'; @@ -176,7 +175,7 @@ export const theRest = (async () => { // OXI if (supportsThreads) { - items.push(oxiMtWorker.main, ...oxiMtWorker.deps, oxiMtWasm); + items.push(oxiMtWasm); } else { items.push(oxiWasm); } diff --git a/worker-tsconfig.json b/worker-tsconfig.json index 1fdcc912..b605e757 100644 --- a/worker-tsconfig.json +++ b/worker-tsconfig.json @@ -5,7 +5,6 @@ }, "include": [ "src/features/**/worker/**/*", - "src/features/**/sub-worker/**/*", "src/features/**/shared/**/*", "src/features/worker-utils/**/*", "src/features-worker/**/*",