mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-22 22:12:28 -05:00
Implement thread status, YieldTimer, block glow, wait
This commit is contained in:
parent
3eeccf1970
commit
4de24cfc30
6 changed files with 739 additions and 318 deletions
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
91
src/util/yieldtimers.js
Normal 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;
|
Loading…
Reference in a new issue