From 1787cde61ddf6359e8d4a0b298473da3719fe06a Mon Sep 17 00:00:00 2001 From: Peter Farber Date: Fri, 11 Oct 2024 15:14:54 -0400 Subject: [PATCH 1/6] chore(loader): override the WebAssembly.compileStreaming to apply the metering to the binary #1034 --- loader/src/index.cjs | 47 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/loader/src/index.cjs b/loader/src/index.cjs index f84f90687..9ee26b3c9 100644 --- a/loader/src/index.cjs +++ b/loader/src/index.cjs @@ -91,12 +91,47 @@ const metering = require('@permaweb/wasm-metering') * @param {Options} [options] * @returns {Promise} */ + +/* + * Custom WebAssembly.compileStreaming Implementation with WASM Metering + * + * This implementation overrides WebAssembly.compileStreaming to add metering + * to WebAssembly binaries. Metering enables tracking of resource usage (e.g., + * CPU or memory) within the WebAssembly runtime, which is critical for monitoring + * performance and managing resource allocation in constrained environments. + * + * Why Metering? + * The CU (Compute Unit) calls WebAssembly.compileStreaming to execute WebAssembly + * modules, but to centralize and streamline metering, we handle it here in the loader + * instead of within each CU instance. By integrating metering directly into the + * loader, we ensure that all WebAssembly binaries are metered consistently across + * different CUs, reducing redundancy and enhancing modularity. + */ + +// Override WebAssembly.compileStreaming to apply metering +WebAssembly.compileStreaming = async function (source, importObject = {}) { + // Read the response and convert it to an ArrayBuffer + const arrayBuffer = await source.arrayBuffer(); + + let meterType = importObject.format.startsWith("wasm32") ? "i32" : "i64"; + + // Convert ArrayBuffer to Uint8Array for metering compatibility + const nodeBuffer = Buffer.from(arrayBuffer); + + // Apply metering with the Uint8Array buffer + const meteredView = metering.meterWASM(nodeBuffer, { meterType }); + + // const meteredResponse = new Response(meteredBuffer, { headers: { 'Content-Type': 'application/wasm' } }); + return WebAssembly.compile(meteredView.buffer); +}; + + module.exports = async function (binary, options) { let instance = null let doHandle = null let meterType = options.format.startsWith("wasm32") ? "i32" : "i64"; - const originalInstantiate = WebAssembly.instantiate; + if (options === null) { options = { format: 'wasm32-unknown-emscripten' } @@ -110,14 +145,9 @@ module.exports = async function (binary, options) { } else { if (typeof binary === "function") { - // TODO: wasmMetering is currently disabled on - // WebAssembly.instantiate = async function (wasm, info) { - // const meteredWasm = metering.meterWASM(wasm, { meterType }); - // return originalInstantiate(wasm, info); - // }; options.instantiateWasm = binary } else { - //binary = metering.meterWASM(binary, { meterType }) + binary = metering.meterWASM(binary, { meterType }) options.wasmBinary = binary } @@ -152,9 +182,6 @@ module.exports = async function (binary, options) { doHandle = instance.cwrap('handle', 'string', ['string', 'string']) } - if (typeof binary === "function") { - WebAssembly.instantiate = originalInstantiate; - } return async (buffer, msg, env) => { From 240074089c8c0750fd38a79c9a118ba121367d64 Mon Sep 17 00:00:00 2001 From: Peter Farber Date: Fri, 11 Oct 2024 15:19:36 -0400 Subject: [PATCH 2/6] chore(cu): pass moduleOptions to compileStreaming so loader can determine target arch #1034 --- servers/cu/src/config.js | 2 +- servers/cu/src/effects/ao-module.js | 2 +- servers/cu/src/effects/wasm.js | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/servers/cu/src/config.js b/servers/cu/src/config.js index ac4dba324..44aa13458 100644 --- a/servers/cu/src/config.js +++ b/servers/cu/src/config.js @@ -24,7 +24,7 @@ const MODE = process.env.NODE_CONFIG_ENV if (!MODE) throw new Error('NODE_CONFIG_ENV must be defined') -const DEFAULT_PROCESS_WASM_MODULE_FORMATS = ['wasm32-unknown-emscripten', 'wasm32-unknown-emscripten2', 'wasm64-unknown-emscripten-draft_2024_02_15'] +const DEFAULT_PROCESS_WASM_MODULE_FORMATS = ['wasm32-unknowne-mscripten', 'wasm32-unknown-emscripten2', 'wasm32-unknown-emscripten3', 'wasm32-unknown-emscripten4', 'wasm64-unknown-emscripten-draft_2024_02_15'] /** * The server config is an extension of the config required by the domain (business logic). diff --git a/servers/cu/src/effects/ao-module.js b/servers/cu/src/effects/ao-module.js index 1bb2b86b9..a112c7ef7 100644 --- a/servers/cu/src/effects/ao-module.js +++ b/servers/cu/src/effects/ao-module.js @@ -128,7 +128,7 @@ export function evaluatorWith ({ evaluateWith, loadWasmModule }) { return (args) => Promise.resolve(!(backpressure = ++backpressure % EVAL_DEFER_BACKPRESSURE)) .then(async (defer) => { - if (!wasmModule) wasmModule = await loadWasmModule({ moduleId }) + if (!wasmModule) wasmModule = await loadWasmModule({ moduleId, moduleOptions }) return defer }) .then(async (defer) => { diff --git a/servers/cu/src/effects/wasm.js b/servers/cu/src/effects/wasm.js index 3dc98b4b6..4cf68ddf6 100644 --- a/servers/cu/src/effects/wasm.js +++ b/servers/cu/src/effects/wasm.js @@ -133,7 +133,7 @@ export function loadWasmModuleWith ({ fetch, ARWEAVE_URL, WASM_BINARY_FILE_DIREC const readWasmFile = fromPromise(readWasmFileWith({ DIR: WASM_BINARY_FILE_DIRECTORY })) const writeWasmFile = writeWasmFileWith({ DIR: WASM_BINARY_FILE_DIRECTORY }) - const toWasmResponse = fromPromise((stream) => WebAssembly.compileStreaming(wasmResponse(Readable.toWeb(stream)))) + const toWasmResponse = (moduleOptions) => fromPromise((stream) => WebAssembly.compileStreaming(wasmResponse(Readable.toWeb(stream), moduleOptions))) function maybeCachedModule (args) { const { moduleId } = args @@ -147,16 +147,16 @@ export function loadWasmModuleWith ({ fetch, ARWEAVE_URL, WASM_BINARY_FILE_DIREC } function maybeStoredBinary (args) { - const { moduleId } = args + const { moduleId, moduleOptions } = args logger('Checking for wasm file to load module "%s"...', moduleId) return of(moduleId) .chain(readWasmFile) - .chain(toWasmResponse) + .chain(toWasmResponse(moduleOptions)) .bimap(always(args), identity) } - function loadTransaction ({ moduleId }) { + function loadTransaction ({ moduleId, moduleOptions }) { logger('Loading wasm transaction "%s"...', moduleId) return of(moduleId) @@ -169,7 +169,7 @@ export function loadWasmModuleWith ({ fetch, ARWEAVE_URL, WASM_BINARY_FILE_DIREC .chain(fromPromise(([s1, s2]) => Promise.all([ writeWasmFile(moduleId, Readable.fromWeb(s1)), - WebAssembly.compileStreaming(wasmResponse(s2)) + WebAssembly.compileStreaming(wasmResponse(s2), moduleOptions) ]) )) .map(([, res]) => res) @@ -177,7 +177,7 @@ export function loadWasmModuleWith ({ fetch, ARWEAVE_URL, WASM_BINARY_FILE_DIREC const lock = new AsyncLock() - return ({ moduleId }) => { + return ({ moduleId, moduleOptions }) => { /** * Prevent multiple eval streams close together * from compiling the wasm module multiple times @@ -190,7 +190,7 @@ export function loadWasmModuleWith ({ fetch, ARWEAVE_URL, WASM_BINARY_FILE_DIREC * * then create the Wasm instance */ - () => of({ moduleId }) + () => of({ moduleId, moduleOptions }) .chain(maybeStoredBinary) .bichain(loadTransaction, Resolved) /** From 76ac46d19dcb7984c26236153e49277c290ffe61 Mon Sep 17 00:00:00 2001 From: Peter Farber Date: Fri, 11 Oct 2024 15:30:28 -0400 Subject: [PATCH 3/6] fix(loader): check to make sure its a supported format before applying metering #1034 --- loader/src/index.cjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/loader/src/index.cjs b/loader/src/index.cjs index 9ee26b3c9..5f79964de 100644 --- a/loader/src/index.cjs +++ b/loader/src/index.cjs @@ -113,11 +113,16 @@ WebAssembly.compileStreaming = async function (source, importObject = {}) { // Read the response and convert it to an ArrayBuffer const arrayBuffer = await source.arrayBuffer(); - let meterType = importObject.format.startsWith("wasm32") ? "i32" : "i64"; - // Convert ArrayBuffer to Uint8Array for metering compatibility const nodeBuffer = Buffer.from(arrayBuffer); + if(options.format === "wasm32-unknown-emscripten" || options.format === "wasm32-unknown-emscripten2" || options.format === "wasm32-unknown-emscripten3") { + return WebAssembly.compile(nodeBuffer); + } + + let meterType = importObject.format.startsWith("wasm32") ? "i32" : "i64"; + + // Apply metering with the Uint8Array buffer const meteredView = metering.meterWASM(nodeBuffer, { meterType }); From 86ec2dd1b236d0f9c1f69aa67342a497dc4358b5 Mon Sep 17 00:00:00 2001 From: Peter Farber Date: Fri, 11 Oct 2024 15:33:23 -0400 Subject: [PATCH 4/6] fix(cu): fixed typo in DEFAULT_PROCESS_WASM_MODULE_FORMATS #1034 --- servers/cu/src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servers/cu/src/config.js b/servers/cu/src/config.js index 44aa13458..510e9e063 100644 --- a/servers/cu/src/config.js +++ b/servers/cu/src/config.js @@ -24,7 +24,7 @@ const MODE = process.env.NODE_CONFIG_ENV if (!MODE) throw new Error('NODE_CONFIG_ENV must be defined') -const DEFAULT_PROCESS_WASM_MODULE_FORMATS = ['wasm32-unknowne-mscripten', 'wasm32-unknown-emscripten2', 'wasm32-unknown-emscripten3', 'wasm32-unknown-emscripten4', 'wasm64-unknown-emscripten-draft_2024_02_15'] +const DEFAULT_PROCESS_WASM_MODULE_FORMATS = ['wasm32-unknown-emscripten', 'wasm32-unknown-emscripten2', 'wasm32-unknown-emscripten3', 'wasm32-unknown-emscripten4', 'wasm64-unknown-emscripten-draft_2024_02_15'] /** * The server config is an extension of the config required by the domain (business logic). From c7632325b5ff583580f6a0248552951e1d3e7c73 Mon Sep 17 00:00:00 2001 From: Peter Farber Date: Fri, 11 Oct 2024 15:36:23 -0400 Subject: [PATCH 5/6] fix(loader): changed options to importObject inside of compileStreaming --- loader/src/index.cjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/loader/src/index.cjs b/loader/src/index.cjs index 5f79964de..7eb2e5563 100644 --- a/loader/src/index.cjs +++ b/loader/src/index.cjs @@ -116,13 +116,12 @@ WebAssembly.compileStreaming = async function (source, importObject = {}) { // Convert ArrayBuffer to Uint8Array for metering compatibility const nodeBuffer = Buffer.from(arrayBuffer); - if(options.format === "wasm32-unknown-emscripten" || options.format === "wasm32-unknown-emscripten2" || options.format === "wasm32-unknown-emscripten3") { + if(importObject.format === "wasm32-unknown-emscripten" || importObject.format === "wasm32-unknown-emscripten2" || importObject.format === "wasm32-unknown-emscripten3") { return WebAssembly.compile(nodeBuffer); } let meterType = importObject.format.startsWith("wasm32") ? "i32" : "i64"; - // Apply metering with the Uint8Array buffer const meteredView = metering.meterWASM(nodeBuffer, { meterType }); @@ -130,7 +129,6 @@ WebAssembly.compileStreaming = async function (source, importObject = {}) { return WebAssembly.compile(meteredView.buffer); }; - module.exports = async function (binary, options) { let instance = null let doHandle = null From 5d1f2e3c550aa9e8e938455de65a34d54ab27d71 Mon Sep 17 00:00:00 2001 From: Peter Farber Date: Fri, 11 Oct 2024 15:39:00 -0400 Subject: [PATCH 6/6] fix(loader): updated test cases to pass format --- loader/test/emscripten2.test.js | 3 ++- loader/test/index.test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/loader/test/emscripten2.test.js b/loader/test/emscripten2.test.js index 8aed1f638..2157be4f7 100644 --- a/loader/test/emscripten2.test.js +++ b/loader/test/emscripten2.test.js @@ -55,7 +55,8 @@ describe('loader', async () => { new Response( Readable.toWeb(createReadStream('./test/process/process.wasm')), { headers: { 'Content-Type': 'application/wasm' } } - ) + ), + { format: 'wasm32-unknown-emscripten2' } ) const handle = await AoLoader((info, receiveInstance) => { diff --git a/loader/test/index.test.js b/loader/test/index.test.js index afcb0b436..15d74eead 100644 --- a/loader/test/index.test.js +++ b/loader/test/index.test.js @@ -55,7 +55,8 @@ describe('loader', async () => { new Response( Readable.toWeb(createReadStream('./test/legacy/process.wasm')), { headers: { 'Content-Type': 'application/wasm' } } - ) + ), + { format: 'wasm32-unknown-emscripten' } ) const handle = await AoLoader((info, receiveInstance) => {