dcp-client
Version:
Core libraries for accessing DCP network
295 lines (267 loc) • 11.1 kB
JavaScript
/**
* @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 */