Implement thread status, YieldTimer, block glow, wait

This commit is contained in:
Tim Mickel 2016-05-02 18:09:02 -04:00
parent 3eeccf1970
commit 4de24cfc30
6 changed files with 739 additions and 318 deletions

View file

@ -1,4 +1,3 @@
function Scratch3Blocks(runtime) {
/**
* The runtime instantiating this block package.
@ -31,8 +30,12 @@ Scratch3Blocks.prototype.forever = function() {
console.log('Running: control_forever');
};
Scratch3Blocks.prototype.wait = function() {
Scratch3Blocks.prototype.wait = function(argValues, util) {
console.log('Running: control_wait');
util.yield();
util.timeout(function() {
util.done();
}, 500);
};
Scratch3Blocks.prototype.stop = function() {

View file

@ -312,6 +312,19 @@ Runtime.prototype._step = function () {
}
};
/**
* Emit feedback for block glowing (used in the sequencer).
* @param {?string} blockId ID for the block to update glow
* @param {boolean} isGlowing True to turn on glow; false to turn off.
*/
Runtime.prototype.glowBlock = function (blockId, isGlowing) {
if (isGlowing) {
this.emit(Runtime.BLOCK_GLOW_ON, blockId);
} else {
this.emit(Runtime.BLOCK_GLOW_OFF, blockId);
}
};
/**
* Set up timers to repeatedly step in a browser
*/

View file

@ -1,4 +1,6 @@
var Timer = require('../util/timer');
var Thread = require('./thread');
var YieldTimers = require('../util/yieldtimers.js');
function Sequencer (runtime) {
/**
@ -31,20 +33,37 @@ Sequencer.prototype.stepThreads = function (threads) {
this.timer.start();
// List of threads which have been killed by this step.
var inactiveThreads = [];
// If all of the threads are yielding, we should yield.
var numYieldingThreads = 0;
// While there are still threads to run and we are within WORK_TIME,
// continue executing threads.
while (threads.length > 0 &&
threads.length > numYieldingThreads &&
this.timer.timeElapsed() < Sequencer.WORK_TIME) {
// New threads at the end of the iteration.
var newThreads = [];
// Attempt to run each thread one time
for (var i = 0; i < threads.length; i++) {
var activeThread = threads[i];
this.stepThread(activeThread);
if (activeThread.nextBlock !== null) {
newThreads.push(activeThread);
} else {
if (activeThread.status === Thread.STATUS_RUNNING) {
// Normal-mode thread: step.
this.stepThread(activeThread);
} else if (activeThread.status === Thread.STATUS_YIELD) {
// Yield-mode thread: check if the time has passed.
YieldTimers.resolve(activeThread.yieldTimerId);
numYieldingThreads++;
} else if (activeThread.status === Thread.STATUS_DONE) {
// Moved to a done state - finish up
activeThread.status = Thread.STATUS_RUNNING;
// @todo Deal with the return value
}
if (activeThread.nextBlock === null &&
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread - tell the runtime to clean it up.
inactiveThreads.push(activeThread);
} else {
// Keep this thead in the loop.
newThreads.push(activeThread);
}
}
// Effectively filters out threads that have stopped.
@ -58,14 +77,42 @@ Sequencer.prototype.stepThreads = function (threads) {
* @param {!Thread} thread Thread object to step
*/
Sequencer.prototype.stepThread = function (thread) {
// Save the yield timer ID, in case a primitive makes a new one
// @todo hack - perhaps patch this to allow more than one timer per
// primitive, for example...
var oldYieldTimerId = YieldTimers.timerId;
// Save the current block and set the nextBlock.
// If the primitive would like to do control flow,
// it can overwrite nextBlock.
var currentBlock = thread.nextBlock;
thread.nextBlock = this.runtime._getNextBlock(thread.nextBlock);
thread.nextBlock = this.runtime._getNextBlock(currentBlock);
var opcode = this.runtime._getOpcode(currentBlock);
/**
* A callback for the primitive to indicate its thread should yield.
* @type {Function}
*/
var threadYieldCallback = function () {
thread.status = Thread.STATUS_YIELD;
};
/**
* A callback for the primitive to indicate its thread is finished
* @type {Function}
*/
var instance = this;
var threadDoneCallback = function () {
thread.status = Thread.STATUS_DONE;
// Refresh nextBlock in case it has changed during the yield.
thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
instance.runtime.glowBlock(currentBlock, false);
};
// @todo
var argValues = [];
if (!opcode) {
console.warn('Could not get opcode for block: ' + currentBlock);
}
@ -76,11 +123,28 @@ Sequencer.prototype.stepThread = function (thread) {
}
else {
try {
blockFunction();
this.runtime.glowBlock(currentBlock, true);
// @todo deal with the return value
blockFunction(argValues, {
yield: threadYieldCallback,
done: threadDoneCallback,
timeout: YieldTimers.timeout
});
}
catch(e) {
console.error('Exception calling block function',
{opcode: opcode, exception: e});
} finally {
// Update if the thread has set a yield timer ID
// @todo hack
if (YieldTimers.timerId > oldYieldTimerId) {
thread.yieldTimerId = YieldTimers.timerId;
}
if (thread.status === Thread.STATUS_RUNNING) {
// Thread executed without yielding - move to done
thread.status = Thread.STATUS_DONE;
this.runtime.glowBlock(currentBlock, false);
}
}
}
}

View file

@ -20,6 +20,40 @@ function Thread (firstBlock) {
* @type {Array.<string>}
*/
this.stack = [];
/**
* Status of the thread, one of three states (below)
* @type {number}
*/
this.status = 0; /* Thread.STATUS_RUNNING */
/**
* Yield timer ID (for checking when the thread should unyield).
* @type {number}
*/
this.yieldTimerId = -1;
}
/**
* Thread status for initialized or running thread.
* Threads are in this state when the primitive is called for the first time.
* @const
*/
Thread.STATUS_RUNNING = 0;
/**
* Thread status for a yielded thread.
* Threads are in this state when a primitive has yielded.
* @const
*/
Thread.STATUS_YIELD = 1;
/**
* Thread status for a finished/done thread.
* Thread is moved to this state when the interpreter
* can proceed with execution.
* @const
*/
Thread.STATUS_DONE = 2;
module.exports = Thread;

91
src/util/yieldtimers.js Normal file
View file

@ -0,0 +1,91 @@
/**
* @fileoverview Timers that are synchronized with the Scratch sequencer.
*/
var Timer = require('./timer');
function YieldTimers () {}
/**
* Shared collection of timers.
* Each timer is a [Function, number] with the callback
* and absolute time for it to run.
* @type {Object.<number,Array>}
*/
YieldTimers.timers = {};
/**
* Monotonically increasing timer ID.
* @type {number}
*/
YieldTimers.timerId = 0;
/**
* Utility for measuring time.
* @type {!Timer}
*/
YieldTimers.globalTimer = new Timer();
/**
* The timeout function is passed to primitives and is intended
* as a convenient replacement for window.setTimeout.
* The sequencer will attempt to resolve the timer every time
* the yielded thread would have been stepped.
* @param {!Function} callback To be called when the timer is done.
* @param {number} timeDelta Time to wait, in ms.
* @return {number} Timer ID to be used with other methods.
*/
YieldTimers.timeout = function (callback, timeDelta) {
var id = ++YieldTimers.timerId;
YieldTimers.timers[id] = [
callback,
YieldTimers.globalTimer.time() + timeDelta
];
return id;
};
/**
* Attempt to resolve a timeout.
* If the time has passed, call the callback.
* Otherwise, do nothing.
* @param {number} id Timer ID to resolve.
* @return {boolean} True if the timer has resolved.
*/
YieldTimers.resolve = function (id) {
var timer = YieldTimers.timers[id];
if (!timer) {
// No such timer.
return false;
}
var callback = timer[0];
var time = timer[1];
if (YieldTimers.globalTimer.time() < time) {
// Not done yet.
return false;
}
// Execute the callback and remove the timer.
callback();
delete YieldTimers.timers[id];
return true;
};
/**
* Reject a timer so the callback never executes.
* @param {number} id Timer ID to reject.
*/
YieldTimers.reject = function (id) {
if (YieldTimers.timers[id]) {
delete YieldTimers.timers[id];
}
};
/**
* Reject all timers currently stored.
* Especially useful for a Scratch "stop."
*/
YieldTimers.rejectAll = function () {
YieldTimers.timers = {};
YieldTimers.timerId = 0;
};
window.YieldTimers = YieldTimers;
module.exports = YieldTimers;

836
vm.js

File diff suppressed because it is too large Load diff