Skip to content

Commit

Permalink
feat: support dynamic emscripten module import in web workers
Browse files Browse the repository at this point in the history
Firefox now has dynamic ESM support in web workers, and it is enabled in
the current stable release.

This means we can:

1. Allow WebPack, Vite, Rollup, to bundle the web workers as usual by
   default and they can use the dynamic import at runtime.
2. Not build the UMD Emscripten module wrapper for importScript imports

This patch sets the bundler handling of pipeline.worker.js to be the default,
and stops building the UMD wasm modules.
  • Loading branch information
thewtex committed Oct 12, 2023
1 parent db88662 commit 93cb744
Show file tree
Hide file tree
Showing 22 changed files with 478 additions and 448 deletions.
825 changes: 455 additions & 370 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@
"ts-loader": "^9.3.1",
"ts-standard": "^11.0.0",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpackbar": "^5.0.2"
},
"dependencies": {
Expand Down
9 changes: 1 addition & 8 deletions packages/dicom/dcmtk/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@ if (EMSCRIPTEN AND DEFINED WebAssemblyInterface_BINARY_DIR)
PROPERTY RUNTIME_OUTPUT_DIRECTORY
${WebAssemblyInterface_BINARY_DIR}/dicom
)
itk_module_target_label(${target}.umd)
itk_module_target_export(${target}.umd)
itk_module_target_install(${target}.umd)
set_property(TARGET ${target}.umd
PROPERTY RUNTIME_OUTPUT_DIRECTORY
${WebAssemblyInterface_BINARY_DIR}/dicom
)
endforeach()
return()
endif()
endif()
5 changes: 1 addition & 4 deletions packages/dicom/gdcm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,9 @@ if (EMSCRIPTEN)
read-dicom-tags
)
set(target_esm "${dicom_io_module}")
set(target_umd "${dicom_io_module}.umd")
set(dicom_common_link_flags " ${common_link_flags} -s SUPPORT_LONGJMP=1 -s DISABLE_EXCEPTION_CATCHING=0")
foreach(target ${target_esm} ${target_umd})
set_property(TARGET ${target} APPEND_STRING
PROPERTY LINK_FLAGS " ${dicom_common_link_flags}"
)
endforeach()
endforeach()
endif()
endif()
6 changes: 1 addition & 5 deletions packages/image-io/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,10 @@ foreach(io_module ${WebAssemblyInterface_ImageIOModules} WebAssemblyInterface)
endif()
if (EMSCRIPTEN)
set(target_esm_read "${read_binary}")
set(target_umd_read "${read_binary}.umd")
target_compile_definitions(${target_umd_read} PUBLIC -DIMAGE_IO_CLASS=${imageio_id_${imageio}} -DIMAGE_IO_KEBAB_NAME=${ioname})
if (NOT ${imageio} IN_LIST imageios_no_write)
set(target_esm_write "${write_binary}")
set(target_umd_write "${write_binary}.umd")
target_compile_definitions(${target_umd_write} PUBLIC -DIMAGE_IO_CLASS=${imageio_id_${imageio}} -DIMAGE_IO_KEBAB_NAME=${ioname})
endif()
foreach(target ${target_esm_read} ${target_umd_read} ${target_esm_write} ${target_umd_write})
foreach(target ${target_esm_read} ${target_esm_write})
set(exception_catching )
if(${io_module} STREQUAL "ITKIOGE")
set(exception_catching " -s DISABLE_EXCEPTION_CATCHING=0")
Expand Down
1 change: 0 additions & 1 deletion src/bindgen/typescript/typescript-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ function typescriptBindings (outputDir, buildDir, wasmBinaries, options, forNode
fs.copyFileSync(`${wasmBinaryRelativePath}.zst`, path.join(distPipelinesDir, `${path.basename(wasmBinaryRelativePath)}.zst`))
const prefix = wasmBinaryRelativePath.substring(0, wasmBinaryRelativePath.length-5)
fs.copyFileSync(`${prefix}.js`, path.join(distPipelinesDir, `${path.basename(prefix)}.js`))
fs.copyFileSync(`${prefix}.umd.js`, path.join(distPipelinesDir, `${path.basename(prefix)}.umd.js`))

const { interfaceJson, parsedPath } = wasmBinaryInterfaceJson(outputDir, buildDir, wasmBinaryName)

Expand Down
6 changes: 0 additions & 6 deletions src/build-emscripten.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,6 @@ if (options.copyBuildArtifacts) {
let imageIOFiles = glob.sync(path.join(buildDir, 'image-io', '*.js'))
imageIOFiles = imageIOFiles.concat(glob.sync(path.join(buildDir, 'image-io', '*.wasm')))
imageIOFiles = imageIOFiles.concat(glob.sync(path.join(buildDir, 'image-io', '*.wasm.zst')))
imageIOFiles = imageIOFiles.filter((fn) => !fn.endsWith('.umd.wasm'))
imageIOFiles = imageIOFiles.filter((fn) => !fn.endsWith('.umd.wasm.zst'))
const copyImageIOModules = function (imageIOFile, callback) {
const io = path.basename(imageIOFile)
const output = path.join('dist', 'image-io', io)
Expand All @@ -157,8 +155,6 @@ if (options.copyBuildArtifacts) {
let meshIOFiles = glob.sync(path.join(buildDir, 'mesh-io', '*.js'))
meshIOFiles = meshIOFiles.concat(glob.sync(path.join(buildDir, 'mesh-io', '*.wasm')))
meshIOFiles = meshIOFiles.concat(glob.sync(path.join(buildDir, 'mesh-io', '*.wasm.zst')))
meshIOFiles = meshIOFiles.filter((fn) => !fn.endsWith('.umd.wasm'))
meshIOFiles = meshIOFiles.filter((fn) => !fn.endsWith('.umd.wasm.zst'))
const copyMeshIOModules = function (meshIOFile, callback) {
const io = path.basename(meshIOFile)
const output = path.join('dist', 'mesh-io', io)
Expand All @@ -173,8 +169,6 @@ if (options.copyBuildArtifacts) {
let dicomFiles = glob.sync(path.join(buildDir, 'dicom', '*.js'))
dicomFiles = dicomFiles.concat(glob.sync(path.join(buildDir, 'dicom', '*.wasm')))
dicomFiles = dicomFiles.concat(glob.sync(path.join(buildDir, 'dicom', '*.wasm.zst')))
dicomFiles = dicomFiles.filter((fn) => !fn.endsWith('.umd.wasm'))
dicomFiles = dicomFiles.filter((fn) => !fn.endsWith('.umd.wasm.zst'))
const copyDICOMModules = function (dicomFile, callback) {
const io = path.basename(dicomFile)
const output = path.join('dist', 'dicom', 'public', 'pipelines', io)
Expand Down
6 changes: 1 addition & 5 deletions src/core/createWebWorkerPromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ async function createWebWorkerPromise (existingWorker: Worker | null, pipelineWo
} else if (workerUrl === null) {
// Use the version built with the bundler
//
// Bundlers, e.g. WebPack, see these paths at build time
//
// importScripts / UMD is required over dynamic ESM import until Firefox
// adds worker dynamic import support:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1540913
// Bundlers, e.g. WebPack, Vite, Rollup, see these paths at build time
worker = new Worker(new URL('../web-workers/pipeline.worker.js', import.meta.url))
} else {
if (workerUrl.startsWith('http')) {
Expand Down
12 changes: 3 additions & 9 deletions src/core/internal/loadEmscriptenModuleWebWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ async function loadEmscriptenModuleWebWorker(moduleRelativePathOrURL: string | U
if (modulePrefix.endsWith('.wasm')) {
modulePrefix = modulePrefix.substring(0, modulePrefix.length - 5)
}
// importScripts / UMD is required over dynamic ESM import until Firefox
// adds worker dynamic import support:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1540913
const wasmBinaryPath = `${modulePrefix}.wasm`
const response = await axios.get(`${wasmBinaryPath}.zst`, { responseType: 'arraybuffer' })
if (!decoderInitialized) {
Expand All @@ -37,12 +34,9 @@ async function loadEmscriptenModuleWebWorker(moduleRelativePathOrURL: string | U
}
const decompressedArray = decoder.decode(new Uint8Array(response.data))
const wasmBinary = decompressedArray.buffer
const modulePath = `${modulePrefix}.umd.js`
importScripts(modulePath)
const moduleBaseName: string = camelCase(modulePrefix.replace(/.*\//, ''))
// @ts-ignore: error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'WorkerGlobalScope & typeof globalThis'.
const wrapperModule = self[moduleBaseName] as (moduleParams: object) => object
const emscriptenModule = wrapperModule({ wasmBinary }) as ITKWasmEmscriptenModule
const modulePath = `${modulePrefix}.js`
const result = await import(/* webpackIgnore: true */ /* @vite-ignore */ modulePath)
const emscriptenModule = result.default({ wasmBinary }) as ITKWasmEmscriptenModule
return emscriptenModule
}

Expand Down
12 changes: 0 additions & 12 deletions src/docker/itk-wasm/ITKWebAssemblyInterface.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,9 @@ set(_target_link_libraries target_link_libraries)
function(target_link_libraries target)
_target_link_libraries(${target} ${ARGN})
if(EMSCRIPTEN)
if (TARGET ${target}.umd)
_target_link_libraries(${target}.umd ${ARGN})
endif()
get_target_property(target_type ${target} TYPE)
if ("${CMAKE_BUILD_TYPE}" STREQUAL Debug AND "${target_type}" STREQUAL EXECUTABLE)
target_sources(${target} PRIVATE /ITKWebAssemblyInterface/src/getExceptionMessage.cxx)
if (TARGET ${target}.umd)
target_sources(${target}.umd PRIVATE /ITKWebAssemblyInterface/src/getExceptionMessage.cxx)
endif()
endif()
else()
endif()
Expand All @@ -42,22 +36,16 @@ function(add_executable target)
set(wasm_target ${target})
_add_executable(${wasm_target} ${ARGN})
if(EMSCRIPTEN)
set(umd_target ${wasm_target}.umd)
_add_executable(${umd_target} ${ARGN})

kebab_to_camel(${target} targetCamel)
get_property(_link_flags TARGET ${target} PROPERTY LINK_FLAGS)
set(common_link_flags " -s FORCE_FILESYSTEM=1 -s
EXPORTED_RUNTIME_METHODS='[\"callMain\",\"cwrap\",\"ccall\",\"writeArrayToMemory\",\"writeAsciiToMemory\",\"AsciiToString\", \"stackSave\", \"stackRestore\"]' -flto -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=4GB -s WASM=1 -lnodefs.js -s WASM_ASYNC_COMPILATION=1 -s EXPORT_NAME=${targetCamel} -s MODULARIZE=1 -s EXIT_RUNTIME=0 -s INVOKE_RUN=0 --pre-js /ITKWebAssemblyInterface/src/emscripten-module/itkJSPipelinePre.js --post-js /ITKWebAssemblyInterface/src/emscripten-module/itkJSPost.js -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s EXPORTED_FUNCTIONS='[\"_main\"]' ${_link_flags}")
set_property(TARGET ${wasm_target} PROPERTY LINK_FLAGS "${common_link_flags} -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=1")
set_property(TARGET ${umd_target} PROPERTY LINK_FLAGS "${common_link_flags}")

get_property(_include_dirs TARGET ${target} PROPERTY INCLUDE_DIRECTORIES)
set_property(TARGET ${umd_target} PROPERTY INCLUDE_DIRECTORIES "${_include_dirs}")

get_property(_link_flags_debug TARGET ${target} PROPERTY LINK_FLAGS_DEBUG)
set_property(TARGET ${wasm_target} PROPERTY LINK_FLAGS_DEBUG " -s EXPORT_EXCEPTION_HANDLING_HELPERS=1 -fno-lto -s SAFE_HEAP=1 -s DISABLE_EXCEPTION_CATCHING=0 -lembind ${_link_flags_debug}")
set_property(TARGET ${umd_target} PROPERTY LINK_FLAGS_DEBUG " -s EXPORT_EXCEPTION_HANDLING_HELPERS=1 -fno-lto -s SAFE_HEAP=1 -s DISABLE_EXCEPTION_CATCHING=0 -lembind ${_link_flags_debug}")

get_property(_is_imported TARGET ${target} PROPERTY IMPORTED)
if (NOT ${_is_imported})
Expand Down
8 changes: 2 additions & 6 deletions src/io/internal/pipelines/image/convert-image/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,11 @@ foreach(io_module ${WebAssemblyInterface_ImageIOModules} WebAssemblyInterface)
target_compile_definitions(${write_binary} PUBLIC -DIMAGE_IO_CLASS=${imageio_id_${imageio}})
if (EMSCRIPTEN AND DEFINED WebAssemblyInterface_BINARY_DIR)
set(target_esm_read "${read_binary}")
set(target_umd_read "${read_binary}.umd")
set(target_esm_write "${write_binary}")
set(target_umd_write "${write_binary}.umd")
target_compile_definitions(${target_umd_read} PUBLIC -DIMAGE_IO_CLASS=${imageio_id_${imageio}})
target_compile_definitions(${target_umd_write} PUBLIC -DIMAGE_IO_CLASS=${imageio_id_${imageio}})
foreach(target ${target_esm_read} ${target_umd_read} ${target_esm_write} ${target_umd_write})
foreach(target ${target_esm_read} ${target_esm_write})
set(exception_catching )
if(${io_module} STREQUAL "ITKIOGE")
set(exception_catching " -s DISABLE_EXCEPTION_CATCHING=0")
set(exception_catching " -s DISABLE_EXCEPTION_CATCHING=0")
endif()
set(imageio_common_link_flags " -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s SUPPORT_LONGJMP=1")
get_property(link_flags TARGET ${target} PROPERTY LINK_FLAGS)
Expand Down
6 changes: 1 addition & 5 deletions src/io/internal/pipelines/mesh/convert-mesh/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,8 @@ foreach(io_module ${WebAssemblyInterface_MeshIOModules} WebAssemblyInterface)
target_compile_definitions(${write_binary} PUBLIC -DMESH_IO_CLASS=${meshio_id_${meshio}})
if (EMSCRIPTEN AND DEFINED WebAssemblyInterface_BINARY_DIR)
set(target_esm_read "${read_binary}")
set(target_umd_read "${read_binary}.umd")
set(target_esm_write "${write_binary}")
set(target_umd_write "${write_binary}.umd")
target_compile_definitions(${target_umd_read} PUBLIC -DMESH_IO_CLASS=${meshio_id_${meshio}})
target_compile_definitions(${target_umd_write} PUBLIC -DMESH_IO_CLASS=${meshio_id_${meshio}})
foreach(target ${target_esm_read} ${target_umd_read} ${target_esm_write} ${target_umd_write})
foreach(target ${target_esm_read} ${target_esm_write})
set(exception_catching )
set(meshio_common_link_flags " -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s SUPPORT_LONGJMP=1")
get_property(link_flags TARGET ${target} PROPERTY LINK_FLAGS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ add_executable(polydata-to-mesh polydata-to-mesh.cxx)
target_link_libraries(polydata-to-mesh PUBLIC ${ITK_LIBRARIES})

if (EMSCRIPTEN AND DEFINED WebAssemblyInterface_BINARY_DIR)
foreach(target mesh-to-polydata mesh-to-polydata.umd polydata-to-mesh polydata-to-mesh.umd)
foreach(target mesh-to-polydata polydata-to-mesh)
itk_module_target_label(${target})
itk_module_target_export(${target})
itk_module_target_install(${target})
Expand Down
2 changes: 1 addition & 1 deletion src/io/meshToPolyData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import getTransferables from '../core/getTransferables.js'

async function meshToPolyData (webWorker: Worker | null, mesh: Mesh): Promise<{ polyData: PolyData, webWorker: Worker }> {
let worker = webWorker
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker)
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker, null)
worker = usedWorker

const args = ['0', '0', '--memory-io']
Expand Down
2 changes: 1 addition & 1 deletion src/io/polyDataToMesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import getTransferables from '../core/getTransferables.js'

async function polyDataToMesh (webWorker: Worker | null, polyData: PolyData): Promise<{ mesh: Mesh, webWorker: Worker }> {
let worker = webWorker
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker)
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker, null)
worker = usedWorker

const args = ['0', '0', '--memory-io']
Expand Down
2 changes: 1 addition & 1 deletion src/io/readImageArrayBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ReadImageArrayBufferOptions from './ReadImageArrayBufferOptions.js'

async function readImageArrayBuffer (webWorker: Worker | null, arrayBuffer: ArrayBuffer, fileName: string, options?: ReadImageArrayBufferOptions | string): Promise<ReadImageResult> {
let worker = webWorker
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker)
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker, null)
worker = usedWorker

const filePath = `./${fileName}`
Expand Down
2 changes: 1 addition & 1 deletion src/io/readMeshArrayBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ReadMeshResult from './ReadMeshResult.js'

async function readMeshArrayBuffer (webWorker: Worker | null, arrayBuffer: ArrayBuffer, fileName: string, mimeType: string): Promise<ReadMeshResult> {
let worker = webWorker
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker)
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker, null)
worker = usedWorker

const filePath = `./${fileName}`
Expand Down
2 changes: 1 addition & 1 deletion src/io/writeImageArrayBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function writeImageArrayBuffer (webWorker: Worker | null, image: Image, fi
}

let worker = webWorker
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker)
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker, null)
worker = usedWorker

const filePath = `./${fileName}`
Expand Down
2 changes: 1 addition & 1 deletion src/io/writeMeshArrayBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function writeMeshArrayBuffer (webWorker: Worker | null, mesh: Mesh, fileN
}

let worker = webWorker
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker)
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(worker, null)
worker = usedWorker

const filePath = `./${fileName}`
Expand Down
5 changes: 2 additions & 3 deletions src/itk-wasm-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,8 @@ function bindgen(options) {
if (err.code !== 'EE XIST') throw err
}

// Building for emscripten can generate duplicate .umd.wasm and .wasm binaries
// Also filter libraries.
let filteredWasmBinaries = wasmBinaries.filter(binary => !binary.endsWith('.umd.wasm') && !path.basename(binary).startsWith('lib'))
// Filter libraries.
let filteredWasmBinaries = wasmBinaries.filter((binary) => !path.basename(binary).startsWith('lib'))

switch (iface) {
case 'typescript':
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/runPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async function runPipeline (
return result
}
let worker = webWorker
const pipelineWorkerUrl = options?.pipelineWorkerUrl
const pipelineWorkerUrl = options?.pipelineWorkerUrl ?? null
const pipelineWorkerUrlString = typeof pipelineWorkerUrl !== 'string' && typeof pipelineWorkerUrl?.href !== 'undefined' ? pipelineWorkerUrl.href : pipelineWorkerUrl
const { webworkerPromise, worker: usedWorker } = await createWebWorkerPromise(
worker as Worker | null, pipelineWorkerUrlString as string | undefined | null
Expand Down
3 changes: 0 additions & 3 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,4 @@ if(EMSCRIPTEN)
set_property(TARGET WebAssemblyInterfaceTestDriver APPEND_STRING
PROPERTY LINK_FLAGS " -s ERROR_ON_UNDEFINED_SYMBOLS=0"
)
set_property(TARGET WebAssemblyInterfaceTestDriver.umd APPEND_STRING
PROPERTY LINK_FLAGS " -s ERROR_ON_UNDEFINED_SYMBOLS=0"
)
endif()

0 comments on commit 93cb744

Please sign in to comment.