scratch-vm/src/engine/sequencer.js

253 lines
10 KiB
JavaScript
Raw Normal View History

2017-04-17 15:10:04 -04:00
const Timer = require('../util/timer');
const Thread = require('./thread');
const execute = require('./execute.js');
2017-04-17 19:42:48 -04:00
class Sequencer {
constructor (runtime) {
/**
* A utility timer for timing thread sequencing.
* @type {!Timer}
*/
this.timer = new Timer();
/**
* Reference to the runtime owning this sequencer.
* @type {!Runtime}
*/
this.runtime = runtime;
}
/**
2017-04-17 19:42:48 -04:00
* Time to run a warp-mode thread, in ms.
* @type {number}
*/
2017-04-17 19:42:48 -04:00
static get WARP_TIME () {
return 500;
}
/**
2017-04-17 19:42:48 -04:00
* Step through all threads in `this.runtime.threads`, running them in order.
* @return {Array.<!Thread>} List of inactive threads after stepping.
*/
2017-04-17 19:42:48 -04:00
stepThreads () {
// Work time is 75% of the thread stepping interval.
const WORK_TIME = 0.75 * this.runtime.currentStepTime;
// Start counting toward WORK_TIME.
this.timer.start();
// Count of active threads.
let numActiveThreads = Infinity;
// Whether `stepThreads` has run through a full single tick.
let ranFirstTick = false;
const doneThreads = [];
// Conditions for continuing to stepping threads:
// 1. We must have threads in the list, and some must be active.
// 2. Time elapsed must be less than WORK_TIME.
// 3. Either turbo mode, or no redraw has been requested by a primitive.
while (this.runtime.threads.length > 0 &&
numActiveThreads > 0 &&
this.timer.timeElapsed() < WORK_TIME &&
(this.runtime.turboMode || !this.runtime.redrawRequested)) {
numActiveThreads = 0;
// Attempt to run each thread one time.
for (let i = 0; i < this.runtime.threads.length; i++) {
const activeThread = this.runtime.threads[i];
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread.
if (doneThreads.indexOf(activeThread) < 0) {
doneThreads.push(activeThread);
}
continue;
}
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
!ranFirstTick) {
// Clear single-tick yield from the last call of `stepThreads`.
activeThread.status = Thread.STATUS_RUNNING;
}
if (activeThread.status === Thread.STATUS_RUNNING ||
activeThread.status === Thread.STATUS_YIELD) {
// Normal-mode thread: step.
this.stepThread(activeThread);
activeThread.warpTimer = null;
if (activeThread.isKilled) {
i--; // if the thread is removed from the list (killed), do not increase index
}
2017-04-17 19:42:48 -04:00
}
if (activeThread.status === Thread.STATUS_RUNNING) {
numActiveThreads++;
}
}
2017-04-17 19:42:48 -04:00
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
// threads on the next tick.
ranFirstTick = true;
}
2017-04-17 19:42:48 -04:00
// Filter inactive threads from `this.runtime.threads`.
this.runtime.threads = this.runtime.threads.filter(thread => {
if (doneThreads.indexOf(thread) > -1) {
return false;
}
return true;
});
return doneThreads;
}
2017-04-17 19:42:48 -04:00
/**
* Step the requested thread for as long as necessary.
* @param {!Thread} thread Thread object to step.
*/
stepThread (thread) {
let currentBlockId = thread.peekStack();
if (!currentBlockId) {
// A "null block" - empty branch.
thread.popStack();
}
2017-04-17 19:42:48 -04:00
while (thread.peekStack()) {
let isWarpMode = thread.peekStackFrame().warpMode;
if (isWarpMode && !thread.warpTimer) {
// Initialize warp-mode timer if it hasn't been already.
// This will start counting the thread toward `Sequencer.WARP_TIME`.
thread.warpTimer = new Timer();
thread.warpTimer.start();
}
2017-04-17 19:42:48 -04:00
// Execute the current block.
// Save the current block ID to notice if we did control flow.
currentBlockId = thread.peekStack();
execute(this, thread);
thread.blockGlowInFrame = currentBlockId;
// If the thread has yielded or is waiting, yield to other threads.
if (thread.status === Thread.STATUS_YIELD) {
// Mark as running for next iteration.
thread.status = Thread.STATUS_RUNNING;
// In warp mode, yielded blocks are re-executed immediately.
if (isWarpMode &&
thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) {
continue;
}
return;
} else if (thread.status === Thread.STATUS_PROMISE_WAIT) {
// A promise was returned by the primitive. Yield the thread
// until the promise resolves. Promise resolution should reset
// thread.status to Thread.STATUS_RUNNING.
return;
}
2017-04-17 19:42:48 -04:00
// If no control flow has happened, switch to next block.
if (thread.peekStack() === currentBlockId) {
thread.goToNextBlock();
}
// If no next block has been found at this point, look on the stack.
while (!thread.peekStack()) {
thread.popStack();
if (thread.stack.length === 0) {
// No more stack to run!
thread.status = Thread.STATUS_DONE;
return;
}
2017-01-28 09:11:48 -05:00
2017-04-17 19:42:48 -04:00
const stackFrame = thread.peekStackFrame();
isWarpMode = stackFrame.warpMode;
2017-04-17 19:42:48 -04:00
if (stackFrame.isLoop) {
// The current level of the stack is marked as a loop.
// Return to yield for the frame/tick in general.
// Unless we're in warp mode - then only return if the
// warp timer is up.
if (!isWarpMode ||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
// Don't do anything to the stack, since loops need
// to be re-executed.
return;
}
// Don't go to the next block for this level of the stack,
// since loops need to be re-executed.
continue;
} else if (stackFrame.waitingReporter) {
// This level of the stack was waiting for a value.
// This means a reporter has just returned - so don't go
// to the next block for this level of the stack.
return;
2017-04-17 15:10:04 -04:00
}
2017-04-17 19:42:48 -04:00
// Get next block of existing block on the stack.
thread.goToNextBlock();
}
}
}
2017-04-17 19:42:48 -04:00
/**
* Step a thread into a block's branch.
* @param {!Thread} thread Thread object to step to branch.
* @param {number} branchNum Which branch to step to (i.e., 1, 2).
* @param {boolean} isLoop Whether this block is a loop.
*/
stepToBranch (thread, branchNum, isLoop) {
if (!branchNum) {
branchNum = 1;
}
const currentBlockId = thread.peekStack();
const branchId = thread.target.blocks.getBranch(
currentBlockId,
branchNum
);
thread.peekStackFrame().isLoop = isLoop;
if (branchId) {
// Push branch ID to the thread's stack.
thread.pushStack(branchId);
} else {
thread.pushStack(null);
}
}
2017-04-17 19:42:48 -04:00
/**
* Step a procedure.
* @param {!Thread} thread Thread object to step to procedure.
* @param {!string} procedureCode Procedure code of procedure to step to.
*/
stepToProcedure (thread, procedureCode) {
const definition = thread.target.blocks.getProcedureDefinition(procedureCode);
if (!definition) {
return;
}
// Check if the call is recursive.
// If so, set the thread to yield after pushing.
const isRecursive = thread.isRecursiveCall(procedureCode);
// To step to a procedure, we put its definition on the stack.
// Execution for the thread will proceed through the definition hat
// and on to the main definition of the procedure.
// When that set of blocks finishes executing, it will be popped
// from the stack by the sequencer, returning control to the caller.
thread.pushStack(definition);
// In known warp-mode threads, only yield when time is up.
if (thread.peekStackFrame().warpMode &&
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
thread.status = Thread.STATUS_YIELD;
2016-10-24 13:01:41 -04:00
} else {
2017-04-17 19:42:48 -04:00
// Look for warp-mode flag on definition, and set the thread
// to warp-mode if needed.
const definitionBlock = thread.target.blocks.getBlock(definition);
const doWarp = definitionBlock.mutation.warp;
if (doWarp) {
thread.peekStackFrame().warpMode = true;
} else {
// In normal-mode threads, yield any time we have a recursive call.
if (isRecursive) {
thread.status = Thread.STATUS_YIELD;
}
2016-10-24 13:01:41 -04:00
}
}
2016-06-30 17:12:16 -04:00
}
2017-04-17 19:42:48 -04:00
/**
* Retire a thread in the middle, without considering further blocks.
* @param {!Thread} thread Thread object to retire.
*/
retireThread (thread) {
thread.stack = [];
thread.stackFrame = [];
thread.requestScriptGlowInFrame = false;
thread.status = Thread.STATUS_DONE;
}
}
2016-08-23 15:53:34 -04:00
2016-04-18 17:20:30 -04:00
module.exports = Sequencer;