const Timer = require('../util/timer'); const Thread = require('./thread'); const execute = require('./execute.js'); 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; } /** * Time to run a warp-mode thread, in ms. * @type {number} */ static get WARP_TIME () { return 500; } /** * Step through all threads in `this.runtime.threads`, running them in order. * @return {Array.} List of inactive threads after stepping. */ 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 = this.runtime.threads.map(() => null); // 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. doneThreads[i] = activeThread; continue; } // A thread was removed, added or this thread was restarted. if (doneThreads[i] !== null) { doneThreads[i] = null; } 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 } } if (activeThread.status === Thread.STATUS_RUNNING) { numActiveThreads++; } } // We successfully ticked once. Prevents running STATUS_YIELD_TICK // threads on the next tick. ranFirstTick = true; } // Filter inactive threads from `this.runtime.threads`. numActiveThreads = 0; for (let i = 0; i < this.runtime.threads.length; i++) { const thread = this.runtime.threads[i]; if (doneThreads[i] === null) { this.runtime.threads[numActiveThreads] = thread; numActiveThreads++; } } this.runtime.threads.length = numActiveThreads; // Filter undefined and null values from `doneThreads`. let numDoneThreads = 0; for (let i = 0; i < doneThreads.length; i++) { const maybeThread = doneThreads[i]; if (maybeThread !== null) { doneThreads[numDoneThreads] = maybeThread; numDoneThreads++; } } doneThreads.length = numDoneThreads; return doneThreads; } /** * 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(); } 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(); } // 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; } // 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; } const stackFrame = thread.peekStackFrame(); isWarpMode = stackFrame.warpMode; 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; } // Get next block of existing block on the stack. thread.goToNextBlock(); } } } /** * 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); } } /** * 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; } else { // Look for warp-mode flag on definition, and set the thread // to warp-mode if needed. const definitionBlock = thread.target.blocks.getBlock(definition); const innerBlock = thread.target.blocks.getBlock( definitionBlock.inputs.custom_block.block); const doWarp = innerBlock.mutation.warp; if (doWarp) { thread.peekStackFrame().warpMode = true; } else if (isRecursive) { // In normal-mode threads, yield any time we have a recursive call. thread.status = Thread.STATUS_YIELD; } } } /** * 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; } } module.exports = Sequencer;