UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

566 lines (451 loc) • 15.2 kB
import { assert } from "../../assert.js"; import Signal from "../../events/signal/Signal.js"; import { IllegalStateException } from "../../fsm/exceptions/IllegalStateException.js"; import { objectKeyByValue } from "../../model/object/objectKeyByValue.js"; import { TaskSignal } from "../task/TaskSignal.js"; import TaskState from "../task/TaskState.js"; /** * * @param {TaskGroup|Task} t * @returns {boolean} */ function isGroupTask(t) { return t.children instanceof Array; } /** * @class */ class ConcurrentExecutor { /** * How many time-slice cycles have been executed this far. This is a monotonically increasing counter * @type {number} */ #cycle_count = 0; /** * Handle of the last scheduled `setTimeout` * @type {number} */ #timeout_handle = -1; /** * * @param {number} [quietTime] in milliseconds * @param {number} [workTime] in milliseconds * @constructor */ constructor(quietTime = 1, workTime = 15) { /** * * @type {number} */ this.quietTime = quietTime; /** * * @type {number} */ this.workTime = workTime; /** * Tasks in state pending resolution or initial sate are put here * @type {Task[]} */ this.queueUnresolved = []; /** * ready tasks are those who's dependencies have all been completed * @type {Task[]} */ this.queueReady = []; this.on = { task_started: new Signal(), task_completed: new Signal(), completed: new Signal() }; this.busy = false; /** * * @type {number|SchedulingPolicy} */ this.policy = SchedulingPolicy.ROUND_ROBIN; } /** * * @param {TaskGroup} taskGroup */ runGroup(taskGroup) { assert.equal(taskGroup.isTaskGroup, true, 'taskGroup.isTaskGroup !== true'); const currentState = taskGroup.state.getValue(); if (currentState !== TaskState.INITIAL) { throw new IllegalStateException(`Expected task state INITIAL, instead got ${objectKeyByValue(TaskState, currentState)}`); } const self = this; const children = taskGroup.children; const numChildren = children.length; let pendingCount = numChildren; let resolved = false; function signalSuccess() { if (!resolved) { resolved = true; taskGroup.state.set(TaskState.SUCCEEDED); taskGroup.on.completed.send0(); self.prod(); } } function signalFailure(reason) { if (!resolved) { resolved = true; taskGroup.state.set(TaskState.FAILED); taskGroup.on.failed.send1(reason); self.prod(); } } function subTaskCompleted() { pendingCount--; if (pendingCount <= 0) { signalSuccess(); } } function subTaskFailed(reason) { signalFailure(reason); } if (numChildren > 0) { taskGroup.state.set(TaskState.RUNNING); let i = 0; for (; i < numChildren; i++) { const child = children[i]; child.on.completed.add(subTaskCompleted); child.on.failed.add(subTaskFailed); if (isGroupTask(child)) { this.runGroup(child); } else { this.run(child); } } } else { //no children, succeed immediately signalSuccess(); } } /** * * @param {TaskGroup} group */ removeGroup(group) { const children = group.children; const n = children.length; for (let i = 0; i < n; i++) { const child = children[i]; if (isGroupTask(child)) { this.removeGroup(child); } else { this.removeTask(child); } } } /** * * @param {Task} task * @return {boolean} */ removeTask(task) { //console.warn("Removing task:", task); const readyIndex = this.queueReady.indexOf(task); if (readyIndex !== -1) { //is in ready queue, remove this.queueReady.splice(readyIndex, 1); return true; } const unresolvedIndex = this.queueUnresolved.indexOf(task); if (unresolvedIndex !== -1) { //found in unresolved queue, remove this.queueUnresolved.splice(unresolvedIndex, 1); return true; } //not found return false; } /** * * @param {Task} task */ run(task) { this.queueUnresolved.push(task); this.prod(); } /** * Shortcut for {@link #run} method for scheduling multiple tasks at once * @param {Task[]} tasks */ runMany(tasks) { Array.prototype.push.apply(this.queueUnresolved, tasks); this.prod(); } /** * @private * @param {Task} task * @returns {boolean} */ startTask(task) { const timeInitializationStart = performance.now(); try { task.initialize(task, this); } catch (e) { console.error(`Task initialization failed`, task, e); task.state.set(TaskState.FAILED); task.on.failed.dispatch(e); return false; } finally { const timeInitializationEnd = performance.now(); const timeInitializationDuration = timeInitializationEnd - timeInitializationStart; task.__executedCpuTime += timeInitializationDuration; } // console.log("Starting task", task); task.state.set(TaskState.RUNNING); //dispatch start notification task.on.started.send1(this); //add to the queue this.queueReady.push(task); this.on.task_started.send1(task); return true; } /** * Go through unresolved queue and move tasks whose dependencies have been completed to ready queue or fail them */ resolveTasks() { const queueUnresolved = this.queueUnresolved; let i = 0, l = queueUnresolved.length; for (; i < l; i++) { const unresolvedTask = queueUnresolved[i]; const resolution = tryResolve(unresolvedTask); switch (resolution) { case ResolutionType.READY: //remove task from unresolved queue to prevent infinite recursion in case "resolveTasks" is attempted again inside task initializer queueUnresolved.splice(i, 1); //set state of task to READY unresolvedTask.state.set(TaskState.READY); //attempt to start the task this.startTask(unresolvedTask); //task start could have altered unresolved queue, re-initialize iteration i = 0; l = queueUnresolved.length; break; case ResolutionType.FAILED: queueUnresolved.splice(i, 1); l--; i--; break; } } } /** * * @param {Task} task * @returns {boolean} */ contains(task) { if (this.queueUnresolved.indexOf(task) !== -1) { return true; } if (this.queueReady.indexOf(task) !== -1) { return true; } return false; } /** * * @return {Task|undefined} */ #pick_next_task() { const ready = this.queueReady; switch (this.policy) { case ConcurrentExecutor.POLICY.ROUND_ROBIN: return ready[this.#cycle_count % ready.length]; default: console.warn('Unknown scheduling policy: ', this.policy, 'Defaulting to sequential'); // fallthrough case ConcurrentExecutor.POLICY.SEQUENTIAL: return ready[0]; } } /** * * @param {Task} task */ #complete_task(task) { const readyTasks = this.queueReady; const taskIndex = readyTasks.indexOf(task); if (taskIndex !== -1) { readyTasks.splice(taskIndex, 1); } else { console.error("Failed to remove ready task, not found in the ready queue", task, readyTasks.slice()); } task.state.set(TaskState.SUCCEEDED); task.on.completed.send1(this); this.resolveTasks(); this.on.task_completed.send1(task); // console.warn(`Task complete '${task.name}', cycles=${task.__executedCycleCount}, time=${task.__executedCpuTime}`); } /** * * @param {Task} task * @param {*} [reason] */ #fail_task(task, reason) { const readyTasks = this.queueReady; const taskIndex = readyTasks.indexOf(task); if (taskIndex !== -1) { readyTasks.splice(taskIndex, 1); } else { console.error("Failed to remove ready task, not found in the ready queue", task, readyTasks.slice()); } task.state.set(TaskState.FAILED); task.on.failed.send1(reason); if (!task.on.failed.hasHandlers()) { // no one is watching for task failure console.warn(`Task '${task.name}' failed.`, reason); } this.resolveTasks(); this.on.task_completed.send1(task); } /** * * @param {Task} task * @param {number} time in milliseconds * @param {function} completionCallback * @param {function} failureCallback */ #runTaskForTimeMonitored(task, time) { let cycle_count = 0; /** * * @type {function(): TaskSignal} */ const cycle = task.cycle; const startTime = performance.now(); const endTime = startTime + time; //We use tiny delta to avoid problems with timer accuracy on some systems that result in 0 measured execution time let t = startTime + 0.000001; let signal; while (t < endTime) { cycle_count++; signal = cycle(); t = performance.now(); if (signal === TaskSignal.Continue) { continue; } if (signal === TaskSignal.Yield) { //give up current quanta break; } if (signal === TaskSignal.EndSuccess) { break; } else if (signal === TaskSignal.EndFailure) { break; } else { throw new Error(`Task '${task.name}' produced unknown signal: ` + signal); } } const executionDuration = t - startTime; task.__executedCpuTime += executionDuration; task.__executedCycleCount += cycle_count; if (signal === TaskSignal.EndSuccess) { this.#complete_task(task); } else if (signal === TaskSignal.EndFailure) { this.#fail_task(task, "Task signalled failure"); } return executionDuration; } #bound_executeTimeSlice = this.#executeTimeSlice.bind(this); #executeTimeSlice() { let sliceTimeLeft = this.workTime; let executionTime = 0; const queueReady = this.queueReady; while (queueReady.length > 0) { const task = this.#pick_next_task(); if (task === undefined) { console.warn('Next task not found, likely result of removing task mid-execution'); break; } try { executionTime = this.#runTaskForTimeMonitored(task, sliceTimeLeft); } catch (e) { const error = new Error("Task threw an exception"); error.cause = e; // console.error(`Task threw an exception`, task, e); this.#fail_task(task, error); } this.#cycle_count++; //make sure that execution time that we subtract from current CPU slice is always reducing the slice sliceTimeLeft -= Math.max(executionTime, 1); if (sliceTimeLeft <= 0) { break; } } if (this.queueReady.length === 0) { this.busy = false; this.on.completed.send0(); } else { //schedule next time slice this.#timeout_handle = setTimeout(this.#bound_executeTimeSlice, this.quietTime); } } /** * kicks the scheduler into action, this is an internal method and should not be called from outside * @private */ prod() { this.resolveTasks(); if (!this.busy && this.queueReady.length > 0) { this.busy = true; this.#executeTimeSlice(); } } join(callback) { if (this.queueReady.length === 0 && this.queueUnresolved.length === 0) { callback(); } else { this.on.completed.addOne(callback); } } } /** * @readonly * @enum {number} */ const SchedulingPolicy = { ROUND_ROBIN: 0, SEQUENTIAL: 1, TIME_SLICE: 2 }; ConcurrentExecutor.POLICY = SchedulingPolicy; const ResolutionType = { READY: 0, FAILED: 1, UNRESOLVED: 2 }; /** * * @param {Task} unresolvedTask * @returns {number} */ function tryResolve(unresolvedTask) { const dependencies = unresolvedTask.dependencies; let j = 0; const jl = dependencies.length; for (; j < jl; j++) { const dependency = dependencies[j]; switch (dependency.state.getValue()) { case TaskState.INITIAL: case TaskState.RUNNING: case TaskState.READY: return ResolutionType.UNRESOLVED; case TaskState.SUCCEEDED: break; default: console.error("Unknown dependency task state", dependency.state, "dependency:", dependency, "dependant:", unresolvedTask, "Failing the task."); //intended fallthrough case TaskState.FAILED: //fail dependant task return ResolutionType.FAILED; } } return ResolutionType.READY; } export default ConcurrentExecutor;