UNPKG

dcp-client

Version:

Core libraries for accessing DCP network

411 lines (373 loc) 15.4 kB
/** * @file event-loop-virtualization.js * * File that takes control of our regular evaluator event loops. * This gives DCP introspection capability to see how long a job * should take, and how we can pay DCCs accordingly. * * All evaluators have their own implementation of the event loop at this * point, with corresponding timeout functions for their loop. This file will * create a wrapper for each of the timeouts, with a virtual event loop * to control code execution. * * * How does this guarantee the timing is correct? * * After the first run of the work function, more JavaScript can be run with only two kinds of events: A macro task * becomes ready or the stack is empty and a micro task is put onto the stack. If we time each function that executes * on the macro task and micro task queue, then we will know how much CPU resource they utilized. * * Timing macrotasks are easy, within the environment of web workers, we have explicit control over the macro tasks that * can occur, webGPU and the Timeout functions being the two macrotask sources. We can time the callback functions to these * directly for accurate measurements of macrotasks. * * A microtask is created via call to the Promise constructor, and while we can time most microtasks, those created using * async functions cannot be wrapped (without running babel on all work functions to convert async functions to Promises). * In order to accurately measure all microtasks, we recognize that all microtasks must run directly after a macrotask, queue * for directly after the macrotask finishes (see https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model). * With this, to time a macrotasks and all microtasks it creates, we can: * 1. start a timer * 2. Add an event onto the macrotask queue, to run after this current macrotask * 3. Run the macrotask * 4. <All microtasks will run> * 5. When the event we put onto the macrotask queue is executed, stop our timer * At this point, we have an accurate measurement of the total CPU time of our computation for that pass of the event loop. The next * macrotask can be run, repeating this cycle until the work function resolves. * * Since no IO is allowed in work function as of Feb 2024, the only source of this error is Timeouts have no CPU workloads * at the same time, causing the JS thread to wait. Which means the error should be small. Once the IO is allowed, as long * as IO are tracked well, the error should remain small. * * Ryan Saweczko, ryansaweczko@kingsds.network * Liang Wang, liang@distributive.network * @date May 2023 * */ /* globals self */ self.wrapScriptLoading({ scriptName: 'event-loop-virtualization' }, function eventLoopVirtualization$$fn(protectedStorage, ring0PostMessage) { (function privateScope(realSetTimeout, realSetInterval, realSetImmediate, realClearTimeout, realClearInterval, realClearImmediate, realQueueMicrotask, protectedStorage) { /** @typedef {import("./timer-classes.js").TimeThing} TimeThing */ const TimeThing = protectedStorage.TimeThing; /** * @class GlobalTrackers * @property {WebGPUQueueRegistry} webGPUQueueRegistry * @property {TimeThing} webGPUIntervals * @property {TimeThing} cpuIntervals * @property {TimeThing} webGLIntervals * @function getMetrics * @function reset * @function resetRecordedTime */ class GlobalTrackers { /** * @constructor * @returns {GlobalTrackers} */ constructor() { this.webGPUIntervals = new TimeThing(); this.cpuIntervals = new TimeThing(); this.webGLIntervals = new TimeThing(); } /** * Reset all the tracked time intervals. Unfinished intervals are simply dropped without a concern why they * were not finished * * SAFETY: * You must only call this *after* the work function has completed, because this will invalidate all gpu resources. * @async * @function reset */ async reset() { // it's *very* important that we delegate the work of resetting to the intervals rather than just assigning each // of them with new instances. These `TimeThing`s are being shared to different modules, if we just re-assign, // they all end up with stales copies and the entire state becomes corrupted /** @todo it is probably a design smell such that we have this pit of subtle bug to easily fall into */ this.webGPUIntervals.reset(); this.cpuIntervals.reset(); this.webGLIntervals.reset(); } /** * Only reset the recorded time intervals but do not remove any recources from tracking. This function pretty much * only exists for resetting the time used for feature detection. Try not to abuse it, it's not a good API. * @function resetRecordedTime */ resetRecordedTime() { this.webGPUIntervals.reset(); this.cpuIntervals.reset(); this.webGLIntervals.reset(); } /** @typedef {Object} ResourceUsageMetric * @property {number} webGPU - time spent in both device and queue timeline * @property {number} CPU - time spent in "user time" of the CPU * @property {number} webGL - time spent in webGL logic */ /** * Obtain the current metrics of our tracked resources, mostly about timings. * @async * @function getMetrics * @returns {ResourceUsageMetric} */ async getMetrics() { // flush all commands that were already enqueued if (protectedStorage.webGPU) await protectedStorage.webGPU.waitAllCommandToFinish(); const webGPUTime = this.webGPUIntervals.duration(); const webGLTime = this.webGLIntervals.duration(); const cpuTime = this.cpuIntervals.duration(); return { webGPU: webGPUTime, CPU: cpuTime, webGL: webGLTime, }; } /** * Obtain the current metrics of our tracked resources, mostly about timings. * Return metrics synchronously, so will not wait for webgpu commands to finish * @function getMetricsSync * @returns {ResourceUsageMetric} */ getMetricsSync() { const webGPUTime = this.webGPUIntervals.duration(); const webGLTime = this.webGLIntervals.duration(); const cpuTime = this.cpuIntervals.duration(); return { webGPU: webGPUTime, CPU: cpuTime, webGL: webGLTime, }; } } protectedStorage.bigBrother = { ...protectedStorage.bigBrother, globalTrackers: new GlobalTrackers() }; const cpuTimer = protectedStorage.bigBrother.globalTrackers.cpuIntervals; const events = []; events.serial = 0; const lockedEvents = []; let timersLocked = false; protectedStorage.realSetTimeout = realSetTimeout; protectedStorage.lockTimers = function lockTimers() { timersLocked = true; // Do not run any more timeouts, hold them for when the next slice arrives lockedEvents.push(...events); events.length = 0; } protectedStorage.unlockTimers = function unlockTimers() { timersLocked = false; if (lockedEvents.length === 0) return; let resolveService; function serviceLockedEvents() { let interval = new protectedStorage.TimeInterval(); cpuTimer.push(interval); const event = lockedEvents.shift(); if (event.recur) { event.when = Date.now() + event.recur; events.push(event); } // Run function then get end measurement for time realSetTimeout(event.fn, 0); realSetTimeout(endOfRealEventCycle,1); function endOfRealEventCycle() { interval.stop(); if (lockedEvents.length) realSetTimeout(serviceLockedEvents, lockedEvents[0].when - Date.now()); else resolveService(); } } const p$lockedEventsServiced = new Promise((resolve) => { resolveService = resolve; serviceLockedEvents(); }); return p$lockedEventsServiced; } function sortEvents(ev) { ev.sort(function (a, b) { return a.when - b.when; }); } /* * Assumption: serviceEvents must only be triggered if there is an event waiting to * be run. If there are no pending events (or the last one is removed), the trigger * to call serviceEvents next should be removed. */ function serviceEvents() { if (events.length === 0) return; serviceEvents.timeout = null; serviceEvents.nextTimeout = null; serviceEvents.servicing = true; serviceEvents.interval = new protectedStorage.TimeInterval(); cpuTimer.push(serviceEvents.interval); sortEvents(events); const event = events.shift(); if (event.eventType === 'timer') { serviceEvents.executingTimeout = realSetTimeout(event.fn, 0); if (event.recur) { event.when = Date.now() + event.recur; events.push(event); sortEvents(events); } } // Can add handles for events to the event loop as needed (ie messages) // Measure the time on the event loop after everything has executed serviceEvents.measurerTimeout = realSetTimeout(endOfRealEventCycle,1); function endOfRealEventCycle() { serviceEvents.servicing = false; serviceEvents.interval.stop(); if (events.length) { serviceEvents.nextTimeout = events[0].when serviceEvents.timeout = realSetTimeout(serviceEvents, events[0].when - Date.now()); } } } /** Execute callback after at least timeout ms. * * @param callback {function} Callback function to fire after a minimum callback time * @param timeout {int} integer containing the minimum time to fire callback in ms * @param arg array of arguments to be applied to the callback function * @returns {object} A value which may be used as the timeoutId parameter of clearTimeout() */ setTimeout = function eventLoop$$Worker$setTimeout(callback, timeout, arg) { timeout = timeout || 0; let timer, args; if (typeof callback === 'string') { let code = callback; callback = function eventLoop$$Worker$setTimeout$wrapper() { let indirectEval = eval; return indirectEval(code); } } args = Array.prototype.slice.call(arguments); // get a plain array from function arguments args = args.slice(2); // slice the first two elements (callback & timeout), leaving an array of user arguments let fn = callback; callback = () => fn.apply(fn, args); // apply the arguments to the callback function events.serial = +events.serial + 1; timer = { eventType: 'timer', fn: callback, when: Date.now() + (+timeout || 0), serial: events.serial, valueOf: function () { return this.serial; } } // Work function has resolved, Don't let client init any new timeouts. if (timersLocked) { lockedEvents.push(timer); return timer; } events.push(timer); sortEvents(events); if (!serviceEvents.servicing) { if (!serviceEvents.nextTimeout) { serviceEvents.nextTimeout = events[0].when; realSetTimeout(serviceEvents, events[0].when - Date.now()); } else { if (serviceEvents.nextTimeout > events[0].when) { realClearTimeout(serviceEvents.timeout); realSetTimeout(serviceEvents, events[0].when - Date.now()) } } } return timer; } /** Ensure our trampoline setTimeout in bravojs-env will have the proper setTimeout, don't allow clients to see or overwrite to prevent measuring time */ protectedStorage.setTimeout = setTimeout; /** Remove a timeout from the list of pending timeouts, regardless of its current * status. * * @param timeoutId {object} The value, returned from setTimeout(), identifying the timer. */ clearTimeout = function eventLoop$$Worker$clearTimeout(timeoutId) { function checkService() { if (!serviceEvents.servicing) { if (events.length) { realClearTimeout(serviceEvents.timeout); realSetTimeout(serviceEvents, events[0].when - Date.now()) } else realClearTimeout(serviceEvents.timeout); } } if (typeof timeoutId === "object") { let i = events.indexOf(timeoutId); if (i !== -1) events.splice(i, 1); if (i === 0) checkService() } else if (typeof timeoutId === "number") { /* slow path - object has been reinterpreted in terms of valueOf() */ for (let i = 0; i < events.length; i++) { if (events[i].serial === timeoutId) { events.splice(i, 1); if (i === 0) checkService() break; } } } } /** Execute callback after at least interval ms, regularly, at least interval ms apart. * * @param callback {function} Callback function to fire after a minimum callback time * @param timeout {int} integer containing the minimum time to fire callback in ms * @param arg array of arguments to be applied to the callback function * @returns {object} A value which may be used as the intervalId paramter of clearInterval() */ setInterval = function eventLoop$$Worker$setInterval(callback, interval, arg) { let timer = setTimeout(callback, +interval || 0, arg); timer.recur = interval; return timer; } /** Execute callback after 0 ms, immediately when the event loop allows. * * @param callback {function} Callback function to fire after a minimum callback time * @param arg array of arguments to be applied to the callback function * @returns {object} A value which may be used as the intervalId paramter of clearImmediate() */ setImmediate = function eventLoop$$Worker$setImmediate(callback, arg) { let timer = setTimeout(callback, 0, arg); return timer; } /** queues a microtask to be executed at a safe time prior to control returning to the event loop * * @param callback {function} Callback function to fire */ self.queueMicrotask = function eventLoop$$Worker$queueMicrotask(callback) { Promise.resolve().then(callback); }; protectedStorage.timedQueueMicrotask = queueMicrotask; })(self.setTimeout, self.setInterval, self.setImmediate, self.clearTimeout, self.clearInterval, self.clearImmediate, self.queueMicrotask, protectedStorage); });