Merge pull request #5 from tmickel/feature/sequencing

Add basics of threads and sequencing
This commit is contained in:
Tim Mickel 2016-04-29 15:45:50 -04:00
commit ab5c79730c
7 changed files with 736 additions and 330 deletions

View file

@ -1,16 +1,39 @@
var EventEmitter = require('events');
var Sequencer = require('./sequencer');
var Thread = require('./thread');
var util = require('util');
/**
* A simple runtime for blocks.
* Manages blocks, stacks, and the sequencer.
*/
function Runtime () {
// Bind event emitter
EventEmitter.call(this);
// State
// State for the runtime
/**
* All blocks in the workspace.
* Keys are block IDs, values are metadata about the block.
* @type {Object.<string, Object>}
*/
this.blocks = {};
/**
* All stacks in the workspace.
* A list of block IDs that represent stacks (first block in stack).
* @type {Array.<String>}
*/
this.stacks = [];
/**
* A list of threads that are currently running in the VM.
* Threads are added when execution starts and pruned when execution ends.
* @type {Array.<Thread>}
*/
this.threads = [];
/** @type {!Sequencer} */
this.sequencer = new Sequencer(this);
}
/**
@ -18,6 +41,15 @@ function Runtime () {
*/
util.inherits(Runtime, EventEmitter);
/**
* How rapidly we try to step threads, in ms.
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
/**
* Block management: create blocks and stacks from a `create` event
* @param {!Object} block Blockly create event to be processed
*/
Runtime.prototype.createBlock = function (block) {
// Create new block
this.blocks[block.id] = block;
@ -38,6 +70,10 @@ Runtime.prototype.createBlock = function (block) {
this.stacks.push(block.id);
};
/**
* Block management: change block field values
* @param {!Object} args Blockly change event to be processed
*/
Runtime.prototype.changeBlock = function (args) {
// Validate
if (args.element !== 'field') return;
@ -48,6 +84,10 @@ Runtime.prototype.changeBlock = function (args) {
this.blocks[args.id].fields[args.name].value = args.value;
};
/**
* Block management: move blocks from parent to parent
* @param {!Object} e Blockly move event to be processed
*/
Runtime.prototype.moveBlock = function (e) {
var _this = this;
@ -82,6 +122,10 @@ Runtime.prototype.moveBlock = function (e) {
}
};
/**
* Block management: delete blocks and their associated stacks
* @param {!Object} e Blockly delete event to be processed
*/
Runtime.prototype.deleteBlock = function (e) {
// @todo Stop threads running on this stack
@ -114,16 +158,73 @@ Runtime.prototype.deleteBlock = function (e) {
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Create a thread and push it to the list of threads.
* @param {!string} id ID of block that starts the stack
*/
Runtime.prototype._pushThread = function (id) {
if (this.stacks.indexOf(id) < -1) return;
var thread = new Thread(id);
this.threads.push(thread);
};
/**
* Remove a thread from the list of threads.
* @param {!string} id ID of block that starts the stack
*/
Runtime.prototype._removeThread = function (id) {
var i = this.threads.indexOf(id);
if (i > -1) this.threads.splice(i, 1);
};
/**
* Repeatedly run `sequencer.stepThreads` and filter out
* inactive threads after each iteration.
*/
Runtime.prototype._step = function () {
var inactiveThreads = this.sequencer.stepThreads(this.threads);
for (var i = 0; i < inactiveThreads.length; i++) {
this._removeThread(inactiveThreads[i]);
}
};
/**
* Set up timers to repeatedly step in a browser
*/
Runtime.prototype.start = function () {
if (!window.setInterval) return;
window.setInterval(function() {
this._step();
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Helper to remove a stack from `this.stacks`
* @param {?string} id ID of block that starts the stack
*/
Runtime.prototype._deleteStack = function (id) {
var i = this.stacks.indexOf(id);
if (i > -1) this.stacks.splice(i, 1);
};
/**
* Helper to get the next block for a particular block
* @param {?string} id ID of block to get the next block for
* @return {?string} ID of next block in the sequence
*/
Runtime.prototype._getNextBlock = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].next;
};
/**
* Helper to get the substack for a particular C-shaped block
* @param {?string} id ID for block to get the substack for
* @return {?string} ID of block in the substack
*/
Runtime.prototype._getSubstack = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].fields['SUBSTACK'];

View file

@ -1,5 +1,67 @@
function Sequencer () {
// @todo
var Timer = require('../util/timer');
function Sequencer (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;
}
/**
* The sequencer does as much work as it can within WORK_TIME milliseconds,
* then yields. This is essentially a rate-limiter for blocks.
* In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
* @const {!number}
*/
Sequencer.WORK_TIME = 1000 / 60;
/**
* Step through all threads in `this.threads`, running them in order.
* @return {Array.<Thread>} All threads which have finished in this iteration.
*/
Sequencer.prototype.stepThreads = function (threads) {
// Start counting toward WORK_TIME
this.timer.start();
// List of threads which have been killed by this step.
var inactiveThreads = [];
// While there are still threads to run and we are within WORK_TIME,
// continue executing threads.
while (threads.length > 0 &&
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 {
inactiveThreads.push(activeThread);
}
}
// Effectively filters out threads that have stopped.
threads = newThreads;
}
return inactiveThreads;
};
/**
* Step the requested thread
* @param {!Thread} thread Thread object to step
*/
Sequencer.prototype.stepThread = function (thread) {
// @todo Actually run the blocks
// Currently goes to the next block in the sequence.
var nextBlock = this.runtime._getNextBlock(thread.nextBlock);
thread.nextBlock = nextBlock;
};
module.exports = Sequencer;

View file

@ -1,5 +1,20 @@
function Thread () {
// @todo
/**
* A thread is a running stack context and all the metadata needed.
* @param {?string} firstBlock First block to execute in the thread.
* @constructor
*/
function Thread (firstBlock) {
/**
* Next block that the thread will execute.
* @type {string}
*/
this.nextBlock = firstBlock;
/**
* Stack for the thread. When the sequencer enters a control structure,
* the block is pushed onto the stack so we know where to exit.
* @type {Array.<string>}
*/
this.stack = [];
}
module.exports = Thread;

View file

@ -13,8 +13,8 @@ Timer.prototype.start = function () {
this.startTime = this.time();
};
Timer.prototype.stop = function () {
return this.startTime - this.time();
Timer.prototype.timeElapsed = function () {
return this.time() - this.startTime;
};
module.exports = Timer;

View file

@ -10,7 +10,7 @@ test('spec', function (t) {
t.type(timer.startTime, 'number');
t.type(timer.time, 'function');
t.type(timer.start, 'function');
t.type(timer.stop, 'function');
t.type(timer.timeElapsed, 'function');
t.end();
});
@ -23,7 +23,7 @@ test('time', function (t) {
t.end();
});
test('start / stop', function (t) {
test('start / timeElapsed', function (t) {
var timer = new Timer();
var delay = 100;
var threshold = 1000 / 60; // 60 hz
@ -31,10 +31,12 @@ test('start / stop', function (t) {
// Start timer
timer.start();
// Wait and stop timer
// Wait and measure timer
setTimeout(function () {
var stop = timer.stop();
t.ok(stop >= -(delay + threshold) && stop <= -(delay - threshold));
var timeElapsed = timer.timeElapsed();
t.ok(timeElapsed >= 0);
t.ok(timeElapsed >= (delay - threshold) &&
timeElapsed <= (delay + threshold));
t.end();
}, delay);
});

850
vm.js

File diff suppressed because it is too large Load diff

10
vm.min.js vendored

File diff suppressed because one or more lines are too long