UNPKG

dcp-client

Version:

Core libraries for accessing DCP network

295 lines (267 loc) 11.1 kB
/** * @file dcp-client/libexec/sandbox/worktimes.js * Specify available worktimes, allow registering custom worktimes * The single source of authority for what Worktimes are available. * * @author Wes Garland, wes@distributive.network * Severn Lortie, severn@distributive.network * Will Pringle, will@distributive.network * Hamada Gasmallah, hamada@distributive.network * @date January 2024 * @copyright Copyright (c) 2018-2024, Distributive, Ltd. All Rights Reserved. */ 'use-strict'; /* global self, bravojs, addEventListener, postMessage */ // @ts-nocheck self.wrapScriptLoading({ scriptName: 'worktimes' }, function worktimes$$fn(protectedStorage, ring1PostMessage, wrapPostMessage) { // This file starts at ring 2, but transitions to ring 3 partway through it. const ring2PostMessage = self.postMessage; let ring3PostMessage; protectedStorage.worktimes = { registeredWorktimes: [] }; // when preparing a worktime, add it's globals to this object. // only if the job assigned to the evaluator uses that worktime, they will // be added to the allow-list protectedStorage.worktimeGlobals = {}; /** * Register a worktime with the evaluator. * @param {object} worktimeInfo Information about the worktime * @param {string} worktimeInfo.name The name of the worktime * @param {string} worktimeInfo.version The semantic version (semver) number of the worktime * @param {function} worktimeInfo.initSandbox A function which initializes the worktime. It is passed the job as its only argument * @param {function} worktimeInfo.processSlice A function which runs a slice. Must take the form processSlice(data) --> result */ function registerWorktime(worktimeInfo) { protectedStorage.worktimes.registeredWorktimes.push(worktimeInfo); } protectedStorage.worktimes.registerWorktime = registerWorktime; /** * Returns a list of worktimes in the form [{name: string, version:string}] * @returns {object[]} array of worktimeInfo objects */ function getWorktimeList() { const worktimeList = []; for (const worktime of protectedStorage.worktimes.registeredWorktimes) worktimeList.push({ name: worktime.name, version: worktime.version }); return worktimeList; } protectedStorage.worktimes.getWorktimeList = getWorktimeList; globalThis.getWorktimeList = getWorktimeList; // hook for worker-info.js, removed by access-list before sandboxes see it /** * Get a worktime given its name and version. * @note The version parameter here is not a semver range. It must be a literal version, e.g. 0.23.1 * @param {string} name The name of the worktime * @param {string} version The version of the worktime * @returns {object} The worktime */ function getWorktime(name, version) { return protectedStorage.worktimes.registeredWorktimes.find(wt => wt.name === name && wt.version === version); } protectedStorage.worktimes.getWorktime = getWorktime; //Listens for postMessage from the sandbox addEventListener('message', async function worktimes$$sandboxPostMessageHandler(event) { let message = event; switch (message.request) { /* Sandbox assigned a specific job by supervisor */ case 'assign': { try { if (typeof module.main !== 'undefined') throw new Error('Main module was provided before job assignment'); protectedStorage.sandboxConfig = message.sandboxConfig; Object.assign(self.work.job.public, message.job.public); /* override locale-specific defaults if specified */ mainModuleFactoryFactory(message.job); } catch (error) { ring2PostMessage({request: 'reject', error}); } break; /* end of assign */ } /* Supervisor has asked us to execute the work function. message.data is input datum. */ case 'main': { try { await runWorkFunction(message.data); } catch (error) { ring3PostMessage({ request: 'sandboxError', error }); } break; } default: break; } }) /* Report metrics to sandbox/supervisor */ function reportTimes (metrics) { const { total, webGL, webGPU, CPU } = metrics; ring3PostMessage({ request: 'measurement', total, webGL, webGPU, CPU }); } /* Report an error from the work function to the supervisor */ function reportError (error, metrics) { const err = new Error('initial state'); for (let prop of [ 'message', 'name', 'code', 'stack', 'lineNumber', 'columnNumber' ]) { try { if (typeof error[prop] !== 'undefined') err[prop] = error[prop]; } catch(e){}; } reportTimes(metrics); // Report metrics for both 'workReject' and 'workError'. ring3PostMessage({ request: 'workError', error: err }); } /** * Report a result from work function and metrics to the supervisor. * @param result the value that the work function returned promise resolved to */ function reportResult (result, metrics) { try { reportTimes(metrics); ring3PostMessage({ request: 'complete', result }); } catch (error) { ring3PostMessage({ request: 'sandboxError', error }); } } /** * Actual mechanics for running a work function. ** This function will never reject ** * * @param successCallback callback to invoke when the work function has finished running; * it receives as its argument the resolved promise returned from * the work function * @param errorCallback callback to invoke when the work function rejects. It receives * as its argument the error that it rejected with. * @returns unused promise */ async function runWorkFunction_inner(datum, wallDuration, successCallback, errorCallback) { /** @typedef {import("./timer-classes.js").TimeInterval} TimeInterval */ var rejection = false; var result; let metrics; protectedStorage.emitConsoleMessages = true; protectedStorage.flushConsoleBuffer(); try { result = await module.main.worktime.processSlice(datum); } catch (error) { rejection = error; } try { // reset the device states and flush all pending tasks protectedStorage.lockTimers(); // lock timers so no new timeouts will be run. if (protectedStorage.webGPU) protectedStorage.webGPU.lock(); // Let microtask queue finish before getting metrics. With all event-loop possibilities locked, // only the microtask could trigger new code, so waiting for a setTimeout guarantees everything's done await new Promise((r) => protectedStorage.realSetTimeout.call(globalThis, r, 1)); // flush any pending console events, especially in the case of a repeating message that hasn't been emitted yet // then, disable emission of any more console messages try { protectedStorage.dispatchSameConsoleMessage(); } catch(e) {}; protectedStorage.emitConsoleMessages = false; metrics = await protectedStorage.bigBrother.globalTrackers.getMetrics(); await protectedStorage.bigBrother.globalTrackers.reset(); } catch (error) { ring3PostMessage({ request: 'sandboxError', error }); } finally { // due to the nature of the micro task queue, await, our `reset()` cancels all the things that could cause new // tasks, and we wait for all pending task to finish in `reset()`, we are guaranteed to have an empty task queue // now. Hence it's ok to stop the wall clock measurement now wallDuration.stop(); // safety: wallDuration is always stopped, `length` will not throw metrics = { ...metrics, total: wallDuration.length }; } if (rejection) errorCallback(rejection, metrics); else successCallback(result, metrics); /* CPU time measurement ends when this function's return value is resolved or rejected */ } /** * Run the work function, returning a promise that resolves once the function has finished * executing. * * @param datam an element of the input set */ async function runWorkFunction(datum) { // reset the time used for feature detection protectedStorage.bigBrother.globalTrackers.resetRecordedTime(); const wallDuration = new protectedStorage.TimeInterval(); protectedStorage.bigBrother.globalTrackers.wallDuration = wallDuration; if (protectedStorage.webGPU) protectedStorage.webGPU.unlock(); await protectedStorage.unlockTimers(); /* Use setTimeout trampoline to * 1. shorten stack * 2. initialize the event loop measurement code */ protectedStorage.setTimeout(() => runWorkFunction_inner(datum, wallDuration, reportResult, reportError)); } /** * Factory function which returns the main module factory for use as the second argument in module.declare. * * @param {object} job the job property of the assign message from the supervisor. * @returns {function} mainModuleFactory */ function mainModuleFactoryFactory(job) { module.declare(job.dependencies, mainModuleFactory); /* mainModule: this is the function that is run first by BravoJS once the module system has been * loaded. It functions as the main module in a CommonJS environment, and its job is to initialize * the work function for use. Once initialized, the work function is accessible as the main module's * `job` export. The work function is eventually invoked by a message from the supervisor which * invokes runWorkFunction() above. * * This function is infallible; any exceptions or rejections caught are reported to the Supervisor. */ async function mainModuleFactory(require, exports, module) { try { if (exports.hasOwnProperty('job')) throw new Error("Tried to assign sandbox when it was already assigned"); /* Should be impossible - might happen if throw during assign? */ exports.worktime = false; job.requirePath.map(p => require.paths.push(p)); job.modulePath.map(p => module.paths.push(p)); exports.worktime = protectedStorage.worktimes.getWorktime(job.worktime.name, job.worktime.version); if (!exports.worktime) throw new Error(`Unsupported worktime: ${job.worktime.name}`); await exports.worktime.initSandbox(job); } catch(error) { reportError(error); return; } ring2PostMessage({ request: 'assigned', jobAddress: job.address, }); // Now that the evaluator is assigned, wrap post message for ring 3 wrapPostMessage(); ring3PostMessage = self.postMessage; } /* end of main module */ } }); /* end of worktimes$$wrapScriptLoading IIFE */