Files
performance-tests/wasm_node_pthread.js

252 lines
10 KiB
JavaScript
Raw Permalink Normal View History

2025-11-18 12:55:09 +01:00
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import { WASI } from 'wasi';
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs/promises'; // For async file reading
import os from 'os'; // For optimal NUM_WORKERS
import { performance } from 'perf_hooks'; // For benchmarking
// Helper to get __dirname equivalent in ES Modules for this file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// --- Configuration Constants (shared by main and workers where relevant) ---
const ARRAY_SIZE = 15000576; // Must match C code
const NUM_WORKERS = os.cpus().length; // Optimal number of workers based on CPU cores
// WASM Memory configuration (consistent with Emscripten compile flags)
const INITIAL_MEMORY_BYTES = 180224000;
const MAXIMUM_MEMORY_BYTES = 268435456;
const PAGE_SIZE = 65536; // 64KB
// --- WASI Import Version (based on previous successful tests) ---
const WASI_VERSION = 'preview1'; // Or 'wasi_snapshot_preview1', 'wasi_unstable' based on your Node.js's specific requirement.
// --- Main Thread Logic ---
if (isMainThread) {
async function main() {
const wasmPath = './binaries/wasm_add.wasm'; // Path to your compiled WASM binary
try {
// Read the WebAssembly binary file once for all threads
const wasmBuffer = await fs.readFile(wasmPath);
// Call the setupWebAssembly function to initialize and run the benchmarks
await setupWebAssembly(wasmBuffer);
} catch (error) {
console.error("Error during WebAssembly execution:", error);
process.exit(1); // Exit with an error code on failure
}
}
// Call the main function to start the application
main();
} else { // --- Worker Thread Logic ---
// Data passed from the main thread when the worker was created
const { wasmBinary, wasmMemory, functionName, args } = workerData;
// Setup WASI imports for this worker thread's WASM instance
const wasi = new WASI({
version: WASI_VERSION,
args: process.argv,
env: process.env,
preopens: { '.': '.' }
});
// Minimal import object for standalone WASM in a worker
const importObject = {
wasi_snapshot_preview1: wasi.wasiImports, // Provide WASI imports
env: {
memory: wasmMemory, // The shared WebAssembly.Memory object
// Common stubs for functions WASM might try to call (if not handled by WASI)
_abort: () => { console.error("WASM called abort from worker!"); process.exit(1); },
_sbrk: (increment) => {
console.warn(`Worker: _sbrk called with increment ${increment}. Not fully implemented for custom runtime.`);
return 0; // Return dummy value
},
__setThrew: (ptr, val) => {},
__resumeException: (ptr) => {},
},
};
async function initAndRunWorker() {
let instance;
try {
// Instantiate the WASM module within this worker thread
instance = (await WebAssembly.instantiate(wasmBinary, importObject)).instance;
} catch (e) {
console.error("Worker: Failed to instantiate WASM module:", e);
parentPort.postMessage({ type: 'error', message: `WASM instantiation failed: ${e.message}` });
process.exit(1);
}
const exports = instance.exports;
// Call the specified WASM function with its arguments
if (typeof exports[functionName] === 'function') {
try {
exports[functionName](...args);
parentPort.postMessage({ type: 'done' }); // Signal successful completion
} catch (e) {
console.error(`Worker: Error executing ${functionName}:`, e);
parentPort.postMessage({ type: 'error', message: `Execution failed: ${e.message}` });
}
} else {
console.error(`Worker: Function '${functionName}' not found in WASM exports.`);
parentPort.postMessage({ type: 'error', message: `Function '${functionName}' not found.` });
}
process.exit(0); // Exit the worker process cleanly
}
// Start execution in the worker thread
initAndRunWorker();
}
// --- The setupWebAssembly Function (available to both, but called only by main) ---
// This function orchestrates the WASM module loading and parallel execution.
export async function setupWebAssembly(wasmBuffer) {
// Create the shared WebAssembly.Memory object
const memory = new WebAssembly.Memory({
initial: INITIAL_MEMORY_BYTES / PAGE_SIZE,
maximum: MAXIMUM_MEMORY_BYTES / PAGE_SIZE,
shared: true
});
// Setup WASI for the main thread's WASM instance
const wasi = new WASI({
version: WASI_VERSION,
args: process.argv,
env: process.env,
preopens: { '.': '.' }
});
// Instantiate the WASM module on the main thread
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
wasi_snapshot_preview1: wasi.wasiImports, // Provide WASI imports
env: {
memory: memory, // Pass the shared memory
emscripten_notify_memory_growth: (index) => {
console.log(`Memory grew to ${index / 65536} pages`);
},
_abort: () => { console.error("WASM called abort from main!"); process.exit(1); },
_sbrk: (increment) => {
console.warn(`Main: _sbrk called with increment ${increment}. Not fully implemented.`);
return 0;
},
__setThrew: (ptr, val) => {},
__resumeException: (ptr) => {},
}
});
const exports = instance.exports; // Original WASM exports from the main instance
const ARRAY_SIZE = 15000576; // Keep consistent with C code
// Allocate arrays using the main thread's WASM instance (malloc/free are C exports)
const a = exports._alloc_float_array(ARRAY_SIZE);
const b = exports._alloc_float_array(ARRAY_SIZE);
const c = exports._alloc_float_array(ARRAY_SIZE);
if (!a || !b || !c) throw new Error("Failed to allocate memory");
console.log(`Allocated arrays in shared memory at: a=${a}, b=${b}, c=${c}`);
// --- Worker Orchestration Helper (Internal to setupWebAssembly) ---
// This helper function handles spawning workers and waiting for their completion.
async function runRangeTaskInWorkers(wasmFunctionName, arrayPointers) {
const workers = [];
const promises = [];
const chunkSize = Math.ceil(ARRAY_SIZE / NUM_WORKERS);
for (let i = 0; i < NUM_WORKERS; i++) {
const startIdx = i * chunkSize;
const endIdx = Math.min(startIdx + chunkSize, ARRAY_SIZE);
if (startIdx >= ARRAY_SIZE) break; // Stop if no more work chunks
const worker = new Worker(__filename, { // Crucially, the worker executes *this same file*
workerData: {
wasmBinary: wasmBuffer, // Pass the WASM binary data to workers
wasmMemory: memory, // Pass the shared memory object
functionName: wasmFunctionName, // The C function to call in the worker
args: [...arrayPointers, startIdx, endIdx] // Arguments for the C function
}
});
const workerPromise = new Promise((resolve, reject) => {
worker.on('message', (msg) => {
if (msg.type === 'done') {
resolve();
} else if (msg.type === 'error') {
reject(new Error(`Worker error for ${wasmFunctionName}: ${msg.message}`));
}
});
worker.on('error', reject); // Catch errors emitted by the worker process
worker.on('exit', (code) => {
if (code !== 0 && code !== undefined) {
reject(new Error(`Worker exited with code ${code} for ${wasmFunctionName}`));
} else {
resolve(); // Resolve if exited cleanly (code 0 or undefined)
}
});
});
workers.push(worker);
promises.push(workerPromise);
}
// Wait for all workers to complete their assigned tasks
await Promise.all(promises);
}
// --- Override/Wrap original C function calls with worker orchestration ---
// This makes the subsequent benchmark calls look exactly like your original script.
const wrappedExports = {
init_arrays: async (aPtr, bPtr) => {
await runRangeTaskInWorkers('_init_arrays_range', [aPtr, bPtr]);
},
calculate: async (aPtr, bPtr, cPtr) => {
await runRangeTaskInWorkers('_calculate_range', [aPtr, bPtr, cPtr]);
},
// Keep original alloc/dealloc directly from WASM as they are not parallelized
alloc_float_array: exports._alloc_float_array,
dealloc_float_array: exports._dealloc_float_array,
alloc: exports._alloc,
dealloc: exports._dealloc,
// Expose memory for direct access like in your original code
memory: exports.memory, // This points to the same shared memory object
};
// --- Benchmark Execution (This part now perfectly matches your desired input!) ---
// Benchmark initialization
const initStart = performance.now();
await wrappedExports.init_arrays(a, b); // Calls the JS wrapper
const initTime = performance.now() - initStart;
// Benchmark calculation
const calcStart = performance.now();
await wrappedExports.calculate(a, b, c); // Calls the JS wrapper
const calcTime = performance.now() - calcStart;
console.log(`Array initialization time: ${initTime.toFixed(3)} ms`);
console.log(`Calculation time: ${calcTime.toFixed(3)} ms`);
console.log(`Total time: ${(initTime + calcTime).toFixed(3)} ms`);
// Sample output
// Access memory via the original instance's exports.memory (or the shared 'memory' var)
const mem = new Float32Array(wrappedExports.memory.buffer);
for (let i = 0; i < 10; i++) {
console.log(`c[${i}] = ${mem[c/4 + i]}`);
}
// Clean up
wrappedExports.dealloc_float_array(a);
wrappedExports.dealloc_float_array(b);
wrappedExports.dealloc_float_array(c);
}