mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-07 02:54:38 -04: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) {
|
function Scratch3Blocks(runtime) {
|
||||||
/**
|
/**
|
||||||
* The runtime instantiating this block package.
|
* The runtime instantiating this block package.
|
||||||
|
@ -31,8 +30,12 @@ Scratch3Blocks.prototype.forever = function() {
|
||||||
console.log('Running: control_forever');
|
console.log('Running: control_forever');
|
||||||
};
|
};
|
||||||
|
|
||||||
Scratch3Blocks.prototype.wait = function() {
|
Scratch3Blocks.prototype.wait = function(argValues, util) {
|
||||||
console.log('Running: control_wait');
|
console.log('Running: control_wait');
|
||||||
|
util.yield();
|
||||||
|
util.timeout(function() {
|
||||||
|
util.done();
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
Scratch3Blocks.prototype.stop = function() {
|
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
|
* Set up timers to repeatedly step in a browser
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
var Timer = require('../util/timer');
|
var Timer = require('../util/timer');
|
||||||
|
var Thread = require('./thread');
|
||||||
|
var YieldTimers = require('../util/yieldtimers.js');
|
||||||
|
|
||||||
function Sequencer (runtime) {
|
function Sequencer (runtime) {
|
||||||
/**
|
/**
|
||||||
|
@ -31,20 +33,37 @@ Sequencer.prototype.stepThreads = function (threads) {
|
||||||
this.timer.start();
|
this.timer.start();
|
||||||
// List of threads which have been killed by this step.
|
// List of threads which have been killed by this step.
|
||||||
var inactiveThreads = [];
|
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,
|
// While there are still threads to run and we are within WORK_TIME,
|
||||||
// continue executing threads.
|
// continue executing threads.
|
||||||
while (threads.length > 0 &&
|
while (threads.length > 0 &&
|
||||||
|
threads.length > numYieldingThreads &&
|
||||||
this.timer.timeElapsed() < Sequencer.WORK_TIME) {
|
this.timer.timeElapsed() < Sequencer.WORK_TIME) {
|
||||||
// New threads at the end of the iteration.
|
// New threads at the end of the iteration.
|
||||||
var newThreads = [];
|
var newThreads = [];
|
||||||
// Attempt to run each thread one time
|
// Attempt to run each thread one time
|
||||||
for (var i = 0; i < threads.length; i++) {
|
for (var i = 0; i < threads.length; i++) {
|
||||||
var activeThread = threads[i];
|
var activeThread = threads[i];
|
||||||
this.stepThread(activeThread);
|
if (activeThread.status === Thread.STATUS_RUNNING) {
|
||||||
if (activeThread.nextBlock !== null) {
|
// Normal-mode thread: step.
|
||||||
newThreads.push(activeThread);
|
this.stepThread(activeThread);
|
||||||
} else {
|
} 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);
|
inactiveThreads.push(activeThread);
|
||||||
|
} else {
|
||||||
|
// Keep this thead in the loop.
|
||||||
|
newThreads.push(activeThread);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Effectively filters out threads that have stopped.
|
// Effectively filters out threads that have stopped.
|
||||||
|
@ -58,14 +77,42 @@ Sequencer.prototype.stepThreads = function (threads) {
|
||||||
* @param {!Thread} thread Thread object to step
|
* @param {!Thread} thread Thread object to step
|
||||||
*/
|
*/
|
||||||
Sequencer.prototype.stepThread = function (thread) {
|
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.
|
// Save the current block and set the nextBlock.
|
||||||
// If the primitive would like to do control flow,
|
// If the primitive would like to do control flow,
|
||||||
// it can overwrite nextBlock.
|
// it can overwrite nextBlock.
|
||||||
var currentBlock = thread.nextBlock;
|
var currentBlock = thread.nextBlock;
|
||||||
thread.nextBlock = this.runtime._getNextBlock(thread.nextBlock);
|
thread.nextBlock = this.runtime._getNextBlock(currentBlock);
|
||||||
|
|
||||||
var opcode = this.runtime._getOpcode(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) {
|
if (!opcode) {
|
||||||
console.warn('Could not get opcode for block: ' + currentBlock);
|
console.warn('Could not get opcode for block: ' + currentBlock);
|
||||||
}
|
}
|
||||||
|
@ -76,11 +123,28 @@ Sequencer.prototype.stepThread = function (thread) {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
blockFunction();
|
this.runtime.glowBlock(currentBlock, true);
|
||||||
|
// @todo deal with the return value
|
||||||
|
blockFunction(argValues, {
|
||||||
|
yield: threadYieldCallback,
|
||||||
|
done: threadDoneCallback,
|
||||||
|
timeout: YieldTimers.timeout
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch(e) {
|
||||||
console.error('Exception calling block function',
|
console.error('Exception calling block function',
|
||||||
{opcode: opcode, exception: e});
|
{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>}
|
* @type {Array.<string>}
|
||||||
*/
|
*/
|
||||||
this.stack = [];
|
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;
|
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…
Add table
Add a link
Reference in a new issue