Interpreter fixes, enhancements, features ()

* Thread stepping rework; interp.redraw equivalent

* Add turbo mode and pause mode

* Yielding behavior to match Scratch 2.0

* Implement warp-mode procedure threads

* Add check for recursive call

* Inline wait block timer

* Revert to setInterval and always drawing

* Restore yielding in glide

* 30TPS compatibility mode

* 5-call count recursion limit

* Removing dead primitive code

* To simplify, access runtime.threads inline in `stepThreads`.

* Warp mode/timer fixes; recursive check fixes; clean-up

* Add basic single-stepping

* Add single-stepping speed slider

* Allow yielding threads to run in single-stepping

* Restore inactive threads tracking for block glows

* Add clock pausing during pause mode

* Documentation and clean-up throughout

* Don't look for block glows in `thread.topBlock`.

* Add null check for block glows; rename `_updateScriptGlows` to reflect block glowing

* Use the current executed block for glow, instead of stack

* Add more comments to `stepToProcedure`, and re-arrange to match 2.0

* Tweak to Blocks.prototype.getTopLevelScript

* Revert previous

* Fix threads array to be resilient to changes during `stepThreads`

* Restore inactive threads filtering

* Fix typo in "procedure"

* !! instead of == true
This commit is contained in:
Tim Mickel 2016-10-17 23:23:16 -04:00 committed by GitHub
parent 060d1ab2a5
commit e49f076fa1
14 changed files with 705 additions and 281 deletions

View file

@ -15,6 +15,19 @@
<button id="greenflag">Green flag</button>
<button id="stopall">Stop</button>
</div>
<div>
Turbo: <input id='turbomode' type='checkbox' />
</div>
<div>
Pause: <input id='pausemode' type='checkbox' />
</div>
<div>
Compatibility (30 TPS): <input id='compatmode' type='checkbox' />
</div>
<div>
Single stepping: <input id='singlestepmode' type='checkbox' />
<input id='singlestepspeed' type='range' min='1' max='20' value='10' />
</div>
<br />
<ul id="playgroundLinks">
<li><a id="renderexplorer-link" href="#">Renderer</a></li>

View file

@ -245,6 +245,29 @@ window.onload = function() {
document.getElementById('stopall').addEventListener('click', function() {
vm.stopAll();
});
document.getElementById('turbomode').addEventListener('change', function() {
var turboOn = document.getElementById('turbomode').checked;
vm.setTurboMode(turboOn);
});
document.getElementById('pausemode').addEventListener('change', function() {
var pauseOn = document.getElementById('pausemode').checked;
vm.setPauseMode(pauseOn);
});
document.getElementById('compatmode').addEventListener('change',
function() {
var compatibilityMode = document.getElementById('compatmode').checked;
vm.setCompatibilityMode(compatibilityMode);
});
document.getElementById('singlestepmode').addEventListener('change',
function() {
var singleStep = document.getElementById('singlestepmode').checked;
vm.setSingleSteppingMode(singleStep);
});
document.getElementById('singlestepspeed').addEventListener('input',
function() {
var speed = document.getElementById('singlestepspeed').value;
vm.setSingleSteppingSpeed(speed);
});
var tabBlockExplorer = document.getElementById('tab-blockexplorer');
var tabThreadExplorer = document.getElementById('tab-threadexplorer');

View file

@ -1,5 +1,5 @@
var Cast = require('../util/cast');
var Promise = require('promise');
var Timer = require('../util/timer');
function Scratch3ControlBlocks(runtime) {
/**
@ -23,7 +23,6 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() {
'control_if': this.if,
'control_if_else': this.ifElse,
'control_stop': this.stop,
'control_create_clone_of_menu': this.createCloneMenu,
'control_create_clone_of': this.createClone,
'control_delete_this_clone': this.deleteClone
};
@ -46,90 +45,60 @@ Scratch3ControlBlocks.prototype.repeat = function(args, util) {
// Only execute once per frame.
// When the branch finishes, `repeat` will be executed again and
// the second branch will be taken, yielding for the rest of the frame.
if (!util.stackFrame.executedInFrame) {
util.stackFrame.executedInFrame = true;
// Decrease counter
util.stackFrame.loopCounter--;
// If we still have some left, start the branch.
if (util.stackFrame.loopCounter >= 0) {
util.startBranch();
}
} else {
util.stackFrame.executedInFrame = false;
util.yieldFrame();
// Decrease counter
util.stackFrame.loopCounter--;
// If we still have some left, start the branch.
if (util.stackFrame.loopCounter >= 0) {
util.startBranch(1, true);
}
};
Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute once per frame.
// When the branch finishes, `repeat` will be executed again and
// the second branch will be taken, yielding for the rest of the frame.
if (!util.stackFrame.executedInFrame) {
util.stackFrame.executedInFrame = true;
// If the condition is true, start the branch.
if (!condition) {
util.startBranch();
}
} else {
util.stackFrame.executedInFrame = false;
util.yieldFrame();
// If the condition is true, start the branch.
if (!condition) {
util.startBranch(1, true);
}
};
Scratch3ControlBlocks.prototype.waitUntil = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute once per frame.
if (!condition) {
util.yieldFrame();
util.yield();
}
};
Scratch3ControlBlocks.prototype.forever = function(args, util) {
// Only execute once per frame.
// When the branch finishes, `forever` will be executed again and
// the second branch will be taken, yielding for the rest of the frame.
if (!util.stackFrame.executedInFrame) {
util.stackFrame.executedInFrame = true;
util.startBranch();
} else {
util.stackFrame.executedInFrame = false;
util.yieldFrame();
}
util.startBranch(1, true);
};
Scratch3ControlBlocks.prototype.wait = function(args) {
var duration = Cast.toNumber(args.DURATION);
return new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, 1000 * duration);
});
Scratch3ControlBlocks.prototype.wait = function(args, util) {
if (!util.stackFrame.timer) {
util.stackFrame.timer = new Timer();
util.stackFrame.timer.start();
util.yield();
this.runtime.requestRedraw();
} else {
var duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
if (util.stackFrame.timer.timeElapsed() < duration) {
util.yield();
}
}
};
Scratch3ControlBlocks.prototype.if = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute one time. `if` will be returned to
// when the branch finishes, but it shouldn't execute again.
if (util.stackFrame.executedInFrame === undefined) {
util.stackFrame.executedInFrame = true;
if (condition) {
util.startBranch();
}
if (condition) {
util.startBranch(1, false);
}
};
Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute one time. `ifElse` will be returned to
// when the branch finishes, but it shouldn't execute again.
if (util.stackFrame.executedInFrame === undefined) {
util.stackFrame.executedInFrame = true;
if (condition) {
util.startBranch(1);
} else {
util.startBranch(2);
}
if (condition) {
util.startBranch(1, false);
} else {
util.startBranch(2, false);
}
};
@ -145,11 +114,6 @@ Scratch3ControlBlocks.prototype.stop = function(args, util) {
}
};
// @todo (GH-146): remove.
Scratch3ControlBlocks.prototype.createCloneMenu = function (args) {
return args.CLONE_OPTION;
};
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
var cloneTarget;
if (args.CLONE_OPTION == '_myself_') {

View file

@ -82,7 +82,7 @@ Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yieldFrame();
util.yield();
}
};

View file

@ -153,7 +153,7 @@ Scratch3LooksBlocks.prototype.switchBackdropAndWait = function (args, util) {
return instance.runtime.isActiveThread(thread);
});
if (waiting) {
util.yieldFrame();
util.yield();
}
};

View file

@ -119,7 +119,7 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
return;
}
util.yieldFrame();
util.yield();
} else {
var timeElapsed = util.stackFrame.timer.timeElapsed();
if (timeElapsed < util.stackFrame.duration * 1000) {
@ -131,7 +131,7 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
util.stackFrame.startX + dx,
util.stackFrame.startY + dy
);
util.yieldFrame();
util.yield();
} else {
// Finished: move to final position.
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);

View file

@ -24,23 +24,20 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
if (!util.stackFrame.executed) {
var procedureName = args.mutation.proccode;
var paramNames = util.getProcedureParamNames(procedureName);
var procedureCode = args.mutation.proccode;
var paramNames = util.getProcedureParamNames(procedureCode);
for (var i = 0; i < paramNames.length; i++) {
if (args.hasOwnProperty('input' + i)) {
util.pushParam(paramNames[i], args['input' + i]);
}
}
util.stackFrame.executed = true;
util.startProcedure(procedureName);
util.startProcedure(procedureCode);
}
};
Scratch3ProcedureBlocks.prototype.param = function (args, util) {
var value = util.getParam(args.mutation.paramname);
if (!value) {
return 0;
}
return value;
};

View file

@ -90,7 +90,7 @@ var execute = function (sequencer, thread) {
runtime.visualReport(currentBlockId, resolvedValue);
}
// Finished any yields.
thread.setStatus(Thread.STATUS_RUNNING);
thread.status = Thread.STATUS_RUNNING;
}
};
@ -133,15 +133,22 @@ var execute = function (sequencer, thread) {
var input = inputs[inputName];
var inputBlockId = input.block;
// Is there no value for this input waiting in the stack frame?
if (typeof currentStackFrame.reported[inputName] === 'undefined') {
if (typeof currentStackFrame.reported[inputName] === 'undefined'
&& inputBlockId) {
// If there's not, we need to evaluate the block.
var reporterYielded = (
sequencer.stepToReporter(thread, inputBlockId, inputName)
);
// If the reporter yielded, return immediately;
// it needs time to finish and report its value.
if (reporterYielded) {
// Push to the stack to evaluate the reporter block.
thread.pushStack(inputBlockId);
// Save name of input for `Thread.pushReportedValue`.
currentStackFrame.waitingReporter = inputName;
// Actually execute the block.
execute(sequencer, thread);
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
return;
} else {
// Execution returned immediately,
// and presumably a value was reported, so pop the stack.
currentStackFrame.waitingReporter = null;
thread.popStack();
}
}
argValues[inputName] = currentStackFrame.reported[inputName];
@ -164,17 +171,10 @@ var execute = function (sequencer, thread) {
stackFrame: currentStackFrame.executionContext,
target: target,
yield: function() {
thread.setStatus(Thread.STATUS_YIELD);
thread.status = Thread.STATUS_YIELD;
},
yieldFrame: function() {
thread.setStatus(Thread.STATUS_YIELD_FRAME);
},
done: function() {
thread.setStatus(Thread.STATUS_RUNNING);
sequencer.proceedThread(thread);
},
startBranch: function (branchNum) {
sequencer.stepToBranch(thread, branchNum);
startBranch: function (branchNum, isLoop) {
sequencer.stepToBranch(thread, branchNum, isLoop);
},
stopAll: function () {
runtime.stopAll();
@ -185,11 +185,11 @@ var execute = function (sequencer, thread) {
stopThread: function() {
sequencer.retireThread(thread);
},
startProcedure: function (procedureName) {
sequencer.stepToProcedure(thread, procedureName);
startProcedure: function (procedureCode) {
sequencer.stepToProcedure(thread, procedureCode);
},
getProcedureParamNames: function (procedureName) {
return blockContainer.getProcedureParamNames(procedureName);
getProcedureParamNames: function (procedureCode) {
return blockContainer.getProcedureParamNames(procedureCode);
},
pushParam: function (paramName, paramValue) {
thread.pushParam(paramName, paramValue);
@ -221,18 +221,24 @@ var execute = function (sequencer, thread) {
if (isPromise(primitiveReportedValue)) {
if (thread.status === Thread.STATUS_RUNNING) {
// Primitive returned a promise; automatically yield thread.
thread.setStatus(Thread.STATUS_YIELD);
thread.status = Thread.STATUS_PROMISE_WAIT;
}
// Promise handlers
primitiveReportedValue.then(function(resolvedValue) {
handleReport(resolvedValue);
sequencer.proceedThread(thread);
if (typeof resolvedValue !== 'undefined') {
thread.popStack();
} else {
var popped = thread.popStack();
var nextBlockId = thread.target.blocks.getNextBlock(popped);
thread.pushStack(nextBlockId);
}
}, function(rejectionReason) {
// Promise rejected: the primitive had some error.
// Log it and proceed.
console.warn('Primitive rejected promise: ', rejectionReason);
thread.setStatus(Thread.STATUS_RUNNING);
sequencer.proceedThread(thread);
thread.status = Thread.STATUS_RUNNING;
thread.popStack();
});
} else if (thread.status === Thread.STATUS_RUNNING) {
handleReport(primitiveReportedValue);

View file

@ -27,8 +27,6 @@ function Runtime () {
// Bind event emitter
EventEmitter.call(this);
// State for the runtime
/**
* Target management and storage.
* @type {Array.<!Target>}
@ -45,33 +43,131 @@ function Runtime () {
/** @type {!Sequencer} */
this.sequencer = new Sequencer(this);
/**
* Storage container for flyout blocks.
* These will execute on `_editingTarget.`
* @type {!Blocks}
*/
this.flyoutBlocks = new Blocks();
/**
* Currently known editing target for the VM.
* @type {?Target}
*/
this._editingTarget = null;
/**
* Map to look up a block primitive's implementation function by its opcode.
* This is a two-step lookup: package name first, then primitive name.
* @type {Object.<string, Function>}
*/
this._primitives = {};
/**
* Map to look up hat blocks' metadata.
* Keys are opcode for hat, values are metadata objects.
* @type {Object.<string, Object>}
*/
this._hats = {};
/**
* Currently known values for edge-activated hats.
* Keys are block ID for the hat; values are the currently known values.
* @type {Object.<string, *>}
*/
this._edgeActivatedHatValues = {};
/**
* A list of script block IDs that were glowing during the previous frame.
* @type {!Array.<!string>}
*/
this._scriptGlowsPreviousFrame = [];
/**
* A list of block IDs that were glowing during the previous frame.
* @type {!Array.<!string>}
*/
this._blockGlowsPreviousFrame = [];
/**
* Currently known number of clones, used to enforce clone limit.
* @type {number}
*/
this._cloneCounter = 0;
/**
* Whether the project is in "turbo mode."
* @type {Boolean}
*/
this.turboMode = false;
/**
* Whether the project is in "pause mode."
* @type {Boolean}
*/
this.pauseMode = false;
/**
* Whether the project is in "compatibility mode" (30 TPS).
* @type {Boolean}
*/
this.compatibilityMode = false;
/**
* Whether the project is in "single stepping mode."
* @type {Boolean}
*/
this.singleStepping = false;
/**
* How fast in ms "single stepping mode" should run, in ms.
* Can be updated dynamically.
* @type {!number}
*/
this.singleStepInterval = 1000 / 10;
/**
* A reference to the current runtime stepping interval, set
* by a `setInterval`.
* @type {!number}
*/
this._steppingInterval = null;
/**
* Current length of a step.
* Changes as mode switches, and used by the sequencer to calculate
* WORK_TIME.
* @type {!number}
*/
this.currentStepTime = null;
/**
* Whether any primitive has requested a redraw.
* Affects whether `Sequencer.stepThreads` will yield
* after stepping each thread.
* Reset on every frame.
* @type {boolean}
*/
this.redrawRequested = false;
// Register all given block packages.
this._registerBlockPackages();
// Register and initialize "IO devices", containers for processing
// I/O related data.
/** @type {Object.<string, Object>} */
this.ioDevices = {
'clock': new Clock(),
'keyboard': new Keyboard(this),
'mouse': new Mouse(this)
};
this._scriptGlowsPreviousFrame = [];
this._editingTarget = null;
/**
* Currently known number of clones.
* @type {number}
*/
this._cloneCounter = 0;
}
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* Width of the stage, in pixels.
* @const {number}
@ -115,15 +211,15 @@ Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
Runtime.VISUAL_REPORT = 'VISUAL_REPORT';
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* How rapidly we try to step threads, in ms.
* How rapidly we try to step threads by default, in ms.
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
/**
* In compatibility mode, how rapidly we try to step threads, in ms.
*/
Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY = 1000 / 30;
/**
* How many clones can be created at a time.
* @const {number}
@ -175,9 +271,6 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
return this._primitives[opcode];
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Return whether an opcode represents a hat block.
* @param {!string} opcode The opcode to look up.
@ -235,7 +328,7 @@ Runtime.prototype.attachRenderer = function (renderer) {
*/
Runtime.prototype._pushThread = function (id, target) {
var thread = new Thread(id);
thread.setTarget(target);
thread.target = target;
thread.pushStack(id);
this.threads.push(thread);
return thread;
@ -449,6 +542,10 @@ Runtime.prototype.stopAll = function () {
* inactive threads after each iteration.
*/
Runtime.prototype._step = function () {
if (this.pauseMode) {
// Don't do any execution while in pause mode.
return;
}
// Find all edge-activated hats, and add them to threads to be evaluated.
for (var hatType in this._hats) {
var hat = this._hats[hatType];
@ -456,37 +553,113 @@ Runtime.prototype._step = function () {
this.startHats(hatType);
}
}
var inactiveThreads = this.sequencer.stepThreads(this.threads);
this._updateScriptGlows();
for (var i = 0; i < inactiveThreads.length; i++) {
this._removeThread(inactiveThreads[i]);
this.redrawRequested = false;
var inactiveThreads = this.sequencer.stepThreads();
this._updateGlows(inactiveThreads);
};
/**
* Set the current editing target known by the runtime.
* @param {!Target} editingTarget New editing target.
*/
Runtime.prototype.setEditingTarget = function (editingTarget) {
this._editingTarget = editingTarget;
// Script glows must be cleared.
this._scriptGlowsPreviousFrame = [];
this._updateGlows();
};
/**
* Set whether we are in pause mode.
* @param {boolean} pauseModeOn True iff in pause mode.
*/
Runtime.prototype.setPauseMode = function (pauseModeOn) {
// Inform the project clock/timer to pause/resume its time.
if (this.ioDevices.clock) {
if (pauseModeOn && !this.pauseMode) {
this.ioDevices.clock.pause();
}
if (!pauseModeOn && this.pauseMode) {
this.ioDevices.clock.resume();
}
}
this.pauseMode = pauseModeOn;
};
/**
* Set whether we are in 30 TPS compatibility mode.
* @param {boolean} compatibilityModeOn True iff in compatibility mode.
*/
Runtime.prototype.setCompatibilityMode = function (compatibilityModeOn) {
this.compatibilityMode = compatibilityModeOn;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
Runtime.prototype.setEditingTarget = function (editingTarget) {
this._scriptGlowsPreviousFrame = [];
this._editingTarget = editingTarget;
this._updateScriptGlows();
/**
* Set whether we are in single-stepping mode.
* @param {boolean} singleSteppingOn True iff in single-stepping mode.
*/
Runtime.prototype.setSingleSteppingMode = function (singleSteppingOn) {
this.singleStepping = singleSteppingOn;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
Runtime.prototype._updateScriptGlows = function () {
/**
* Set the speed during single-stepping mode.
* @param {number} speed Interval length to step threads, in ms.
*/
Runtime.prototype.setSingleSteppingSpeed = function (speed) {
this.singleStepInterval = 1000 / speed;
if (this._steppingInterval) {
self.clearInterval(this._steppingInterval);
this.start();
}
};
/**
* Emit glows/glow clears for blocks and scripts after a single tick.
* Looks at `this.threads` and notices which have turned on/off new glows.
* @param {Array.<Thread>=} opt_extraThreads Optional list of inactive threads.
*/
Runtime.prototype._updateGlows = function (opt_extraThreads) {
var searchThreads = [];
searchThreads.push.apply(searchThreads, this.threads);
if (opt_extraThreads) {
searchThreads.push.apply(searchThreads, opt_extraThreads);
}
// Set of scripts that request a glow this frame.
var requestedGlowsThisFrame = [];
var requestedBlockGlowsThisFrame = [];
// Final set of scripts glowing during this frame.
var finalScriptGlows = [];
var finalBlockGlows = [];
// Find all scripts that should be glowing.
for (var i = 0; i < this.threads.length; i++) {
var thread = this.threads[i];
for (var i = 0; i < searchThreads.length; i++) {
var thread = searchThreads[i];
var target = thread.target;
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
var blockForThread = thread.peekStack() || thread.topBlock;
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(blockForThread);
if (target == this._editingTarget) {
var blockForThread = thread.blockGlowInFrame;
if (thread.requestScriptGlowInFrame) {
var script = target.blocks.getTopLevelScript(blockForThread);
if (!script) {
// Attempt to find in flyout blocks.
script = this.flyoutBlocks.getTopLevelScript(
blockForThread
);
}
if (script) {
requestedGlowsThisFrame.push(script);
}
}
if (script) {
requestedGlowsThisFrame.push(script);
// Only show block glows in single-stepping mode.
if (this.singleStepping && blockForThread) {
requestedBlockGlowsThisFrame.push(blockForThread);
}
}
}
@ -509,7 +682,30 @@ Runtime.prototype._updateScriptGlows = function () {
finalScriptGlows.push(currentFrameGlow);
}
}
for (var m = 0; m < this._blockGlowsPreviousFrame.length; m++) {
var previousBlockGlow = this._blockGlowsPreviousFrame[m];
if (requestedBlockGlowsThisFrame.indexOf(previousBlockGlow) < 0) {
// Glow turned off.
try {
this.glowBlock(previousBlockGlow, false);
} catch (e) {
// Block has been removed.
}
} else {
// Still glowing.
finalBlockGlows.push(previousBlockGlow);
}
}
for (var p = 0; p < requestedBlockGlowsThisFrame.length; p++) {
var currentBlockFrameGlow = requestedBlockGlowsThisFrame[p];
if (this._blockGlowsPreviousFrame.indexOf(currentBlockFrameGlow) < 0) {
// Glow turned on.
this.glowBlock(currentBlockFrameGlow, true);
finalBlockGlows.push(currentBlockFrameGlow);
}
}
this._scriptGlowsPreviousFrame = finalScriptGlows;
this._blockGlowsPreviousFrame = finalBlockGlows;
};
/**
@ -617,22 +813,38 @@ Runtime.prototype.getTargetForStage = function () {
}
};
/**
* Tell the runtime to request a redraw.
* Use after a clone/sprite has completed some visible operation on the stage.
*/
Runtime.prototype.requestRedraw = function () {
this.redrawRequested = true;
};
/**
* Handle an animation frame from the main thread.
*/
Runtime.prototype.animationFrame = function () {
if (this.renderer) {
// @todo: Only render when this.redrawRequested or clones rendered.
this.renderer.draw();
}
};
/**
* Set up timers to repeatedly step in a browser
* Set up timers to repeatedly step in a browser.
*/
Runtime.prototype.start = function () {
self.setInterval(function() {
var interval = Runtime.THREAD_STEP_INTERVAL;
if (this.singleStepping) {
interval = this.singleStepInterval;
} else if (this.compatibilityMode) {
interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
}
this.currentStepTime = interval;
this._steppingInterval = self.setInterval(function() {
this._step();
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
}.bind(this), interval);
};
module.exports = Runtime;

View file

@ -17,87 +17,163 @@ function Sequencer (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}
* Time to run a warp-mode thread, in ms.
* @type {number}
*/
Sequencer.WORK_TIME = 10;
Sequencer.WARP_TIME = 500;
/**
* Step through all threads in `this.threads`, running them in order.
* @param {Array.<Thread>} threads List of which threads to step.
* @return {Array.<Thread>} All threads which have finished in this iteration.
* Step through all threads in `this.runtime.threads`, running them in order.
* @return {Array.<!Thread>} List of inactive threads after stepping.
*/
Sequencer.prototype.stepThreads = function (threads) {
// Start counting toward WORK_TIME
Sequencer.prototype.stepThreads = function () {
// Work time is 75% of the thread stepping interval.
var WORK_TIME = 0.75 * this.runtime.currentStepTime;
// Start counting toward WORK_TIME.
this.timer.start();
// List of threads which have been killed by this step.
// Count of active threads.
var numActiveThreads = Infinity;
// Whether `stepThreads` has run through a full single tick.
var ranFirstTick = false;
var inactiveThreads = [];
// If all of the threads are yielding, we should yield.
var numYieldingThreads = 0;
// Clear all yield statuses that were for the previous frame.
for (var t = 0; t < threads.length; t++) {
if (threads[t].status === Thread.STATUS_YIELD_FRAME) {
threads[t].setStatus(Thread.STATUS_RUNNING);
}
}
// 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 = [];
// Reset yielding thread count.
numYieldingThreads = 0;
// Attempt to run each thread one time
for (var i = 0; i < threads.length; i++) {
var activeThread = threads[i];
if (activeThread.status === Thread.STATUS_RUNNING) {
// Normal-mode thread: step.
this.startThread(activeThread);
} else if (activeThread.status === Thread.STATUS_YIELD ||
activeThread.status === Thread.STATUS_YIELD_FRAME) {
// Yielding thread: do nothing for this step.
numYieldingThreads++;
}
if (activeThread.stack.length === 0 &&
// 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;
// Inline copy of the threads, updated on each step.
var threadsCopy = this.runtime.threads.slice();
// Attempt to run each thread one time.
for (var i = 0; i < threadsCopy.length; i++) {
var activeThread = threadsCopy[i];
if (activeThread.stack.length === 0 ||
activeThread.status === Thread.STATUS_DONE) {
// Finished with this thread - tell runtime to clean it up.
inactiveThreads.push(activeThread);
} else {
// Keep this thead in the loop.
newThreads.push(activeThread);
// Finished with this thread.
if (inactiveThreads.indexOf(activeThread) < 0) {
inactiveThreads.push(activeThread);
}
continue;
}
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.status === Thread.STATUS_RUNNING) {
// After stepping, status is still running.
// If we're in single-stepping mode, mark the thread as
// a single-tick yield so it doesn't re-execute
// until the next frame.
if (this.runtime.singleStepping) {
activeThread.status = Thread.STATUS_YIELD_TICK;
}
numActiveThreads++;
}
}
// Effectively filters out threads that have stopped.
threads = newThreads;
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
// threads on the next tick.
ranFirstTick = true;
}
// Filter inactive threads from `this.runtime.threads`.
this.runtime.threads = this.runtime.threads.filter(function(thread) {
if (inactiveThreads.indexOf(thread) > -1) {
return false;
}
return true;
});
return inactiveThreads;
};
/**
* Step the requested thread
* @param {!Thread} thread Thread object to step
* Step the requested thread for as long as necessary.
* @param {!Thread} thread Thread object to step.
*/
Sequencer.prototype.startThread = function (thread) {
Sequencer.prototype.stepThread = function (thread) {
var currentBlockId = thread.peekStack();
if (!currentBlockId) {
// A "null block" - empty branch.
// Yield for the frame.
thread.popStack();
thread.setStatus(Thread.STATUS_YIELD_FRAME);
return;
}
// Execute the current block
execute(this, thread);
// If the block executed without yielding and without doing control flow,
// move to done.
if (thread.status === Thread.STATUS_RUNNING &&
thread.peekStack() === currentBlockId) {
this.proceedThread(thread);
while (thread.peekStack()) {
var 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;
}
if (thread.peekStackFrame().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;
} else {
// Don't go to the next block for this level of the stack,
// since loops need to be re-executed.
continue;
}
} else if (thread.peekStackFrame().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();
}
// In single-stepping mode, force `stepThread` to only run one block
// at a time.
if (this.runtime.singleStepping) {
return;
}
}
};
@ -105,8 +181,9 @@ Sequencer.prototype.startThread = function (thread) {
* 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.
*/
Sequencer.prototype.stepToBranch = function (thread, branchNum) {
Sequencer.prototype.stepToBranch = function (thread, branchNum, isLoop) {
if (!branchNum) {
branchNum = 1;
}
@ -115,11 +192,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
currentBlockId,
branchNum
);
thread.peekStackFrame().isLoop = isLoop;
if (branchId) {
// Push branch ID to the thread's stack.
thread.pushStack(branchId);
} else {
// Push null, so we come back to the current block.
thread.pushStack(null);
}
};
@ -127,56 +204,39 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
/**
* Step a procedure.
* @param {!Thread} thread Thread object to step to procedure.
* @param {!string} procedureName Name of procedure defined in this target.
* @param {!string} procedureCode Procedure code of procedure to step to.
*/
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
var definition = thread.target.blocks.getProcedureDefinition(procedureName);
Sequencer.prototype.stepToProcedure = function (thread, procedureCode) {
var definition = thread.target.blocks.getProcedureDefinition(procedureCode);
if (!definition) {
return;
}
// Check if the call is recursive.
// If so, set the thread to yield after pushing.
var 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);
// Check if the call is recursive. If so, yield.
// @todo: Have behavior match Scratch 2.0.
if (thread.stack.indexOf(definition) > -1) {
thread.setStatus(Thread.STATUS_YIELD_FRAME);
}
};
/**
* Step a thread into an input reporter, and manage its status appropriately.
* @param {!Thread} thread Thread object to step to reporter.
* @param {!string} blockId ID of reporter block.
* @param {!string} inputName Name of input on parent block.
* @return {boolean} True if yielded, false if it finished immediately.
*/
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
var currentStackFrame = thread.peekStackFrame();
// Push to the stack to evaluate the reporter block.
thread.pushStack(blockId);
// Save name of input for `Thread.pushReportedValue`.
currentStackFrame.waitingReporter = inputName;
// Actually execute the block.
this.startThread(thread);
// If a reporter yielded, caller must wait for it to unyield.
// The value will be populated once the reporter unyields,
// and passed up to the currentStackFrame on next execution.
return thread.status === Thread.STATUS_YIELD;
};
/**
* Finish stepping a thread and proceed it to the next block.
* @param {!Thread} thread Thread object to proceed.
*/
Sequencer.prototype.proceedThread = function (thread) {
var currentBlockId = thread.peekStack();
// Mark the status as done and proceed to the next block.
// Pop from the stack - finished this level of execution.
thread.popStack();
// Push next connected block, if there is one.
var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
if (nextBlockId) {
thread.pushStack(nextBlockId);
}
// If we can't find a next block to run, mark the thread as done.
if (!thread.peekStack()) {
thread.setStatus(Thread.STATUS_DONE);
// 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.
var definitionBlock = thread.target.blocks.getBlock(definition);
var doWarp = definitionBlock.mutation.warp;
if (doWarp) {
thread.peekStackFrame().warpMode = true;
} else {
// In normal-mode threads, yield any time we have a recursive call.
if (isRecursive) {
thread.status = Thread.STATUS_YIELD;
}
}
}
};
@ -188,7 +248,7 @@ Sequencer.prototype.retireThread = function (thread) {
thread.stack = [];
thread.stackFrame = [];
thread.requestScriptGlowInFrame = false;
thread.setStatus(Thread.STATUS_DONE);
thread.status = Thread.STATUS_DONE;
};
module.exports = Sequencer;

View file

@ -40,6 +40,19 @@ function Thread (firstBlock) {
* @type {boolean}
*/
this.requestScriptGlowInFrame = false;
/**
* Which block ID should glow during this frame, if any.
* @type {?string}
*/
this.blockGlowInFrame = null;
/**
* A timer for when the thread enters warp mode.
* Substitutes the sequencer's count toward WORK_TIME on a per-thread basis.
* @type {?Timer}
*/
this.warpTimer = null;
}
/**
@ -51,25 +64,31 @@ function Thread (firstBlock) {
Thread.STATUS_RUNNING = 0;
/**
* Thread status for a yielded thread.
* Threads are in this state when a primitive has yielded; execution is paused
* until the relevant primitive unyields.
* Threads are in this state when a primitive is waiting on a promise;
* execution is paused until the promise changes thread status.
* @const
*/
Thread.STATUS_YIELD = 1;
Thread.STATUS_PROMISE_WAIT = 1;
/**
* Thread status for a single-frame yield.
* Thread status for yield.
* @const
*/
Thread.STATUS_YIELD_FRAME = 2;
Thread.STATUS_YIELD = 2;
/**
* Thread status for a single-tick yield. This will be cleared when the
* thread is resumed.
* @const
*/
Thread.STATUS_YIELD_TICK = 3;
/**
* Thread status for a finished/done thread.
* Thread is in this state when there are no more blocks to execute.
* @const
*/
Thread.STATUS_DONE = 3;
Thread.STATUS_DONE = 4;
/**
* Push stack and update stack frames appropriately.
@ -80,7 +99,14 @@ Thread.prototype.pushStack = function (blockId) {
// Push an empty stack frame, if we need one.
// Might not, if we just popped the stack.
if (this.stack.length > this.stackFrames.length) {
// Copy warp mode from any higher level.
var warpMode = false;
if (this.stackFrames[this.stackFrames.length - 1]) {
warpMode = this.stackFrames[this.stackFrames.length - 1].warpMode;
}
this.stackFrames.push({
isLoop: false, // Whether this level of the stack is a loop.
warpMode: warpMode, // Whether this level is in warp mode.
reported: {}, // Collects reported input values.
waitingReporter: null, // Name of waiting reporter.
params: {}, // Procedure parameters.
@ -125,22 +151,32 @@ Thread.prototype.peekParentStackFrame = function () {
/**
* Push a reported value to the parent of the current stack frame.
* @param {!Any} value Reported value to push.
* @param {*} value Reported value to push.
*/
Thread.prototype.pushReportedValue = function (value) {
var parentStackFrame = this.peekParentStackFrame();
if (parentStackFrame) {
var waitingReporter = parentStackFrame.waitingReporter;
parentStackFrame.reported[waitingReporter] = value;
parentStackFrame.waitingReporter = null;
}
};
/**
* Add a parameter to the stack frame.
* Use when calling a procedure with parameter values.
* @param {!string} paramName Name of parameter.
* @param {*} value Value to set for parameter.
*/
Thread.prototype.pushParam = function (paramName, value) {
var stackFrame = this.peekStackFrame();
stackFrame.params[paramName] = value;
};
/**
* Get a parameter at the lowest possible level of the stack.
* @param {!string} paramName Name of parameter.
* @return {*} value Value for parameter.
*/
Thread.prototype.getParam = function (paramName) {
for (var i = this.stackFrames.length - 1; i >= 0; i--) {
var frame = this.stackFrames[i];
@ -159,28 +195,43 @@ Thread.prototype.atStackTop = function () {
return this.peekStack() === this.topBlock;
};
/**
* Set thread status.
* @param {number} status Enum representing thread status.
* Switch the thread to the next block at the current level of the stack.
* For example, this is used in a standard sequence of blocks,
* where execution proceeds from one block to the next.
*/
Thread.prototype.setStatus = function (status) {
this.status = status;
Thread.prototype.goToNextBlock = function () {
var nextBlockId = this.target.blocks.getNextBlock(this.peekStack());
// Copy warp mode to next block.
var warpMode = this.peekStackFrame().warpMode;
// The current block is on the stack - pop it and push the next.
// Note that this could push `null` - that is handled by the sequencer.
this.popStack();
this.pushStack(nextBlockId);
if (this.peekStackFrame()) {
this.peekStackFrame().warpMode = warpMode;
}
};
/**
* Set thread target.
* @param {?Target} target Target for this thread.
* Attempt to determine whether a procedure call is recursive,
* by examining the stack.
* @param {!string} procedureCode Procedure code of procedure being called.
* @return {boolean} True if the call appears recursive.
*/
Thread.prototype.setTarget = function (target) {
this.target = target;
};
/**
* Get thread target.
* @return {?Target} Target for this thread, if available.
*/
Thread.prototype.getTarget = function () {
return this.target;
Thread.prototype.isRecursiveCall = function (procedureCode) {
var callCount = 5; // Max number of enclosing procedure calls to examine.
var sp = this.stack.length - 1;
for (var i = sp - 1; i >= 0; i--) {
var block = this.target.blocks.getBlock(this.stack[i]);
if (block.opcode == 'procedures_callnoreturn' &&
block.mutation.proccode == procedureCode) {
return true;
}
if (--callCount < 0) return false;
}
return false;
};
module.exports = Thread;

View file

@ -66,6 +66,53 @@ VirtualMachine.prototype.greenFlag = function () {
this.runtime.greenFlag();
};
/**
* Set whether the VM is in "turbo mode."
* When true, loops don't yield to redraw.
* @param {Boolean} turboModeOn Whether turbo mode should be set.
*/
VirtualMachine.prototype.setTurboMode = function (turboModeOn) {
this.runtime.turboMode = !!turboModeOn;
};
/**
* Set whether the VM is in "pause mode."
* When true, nothing is stepped.
* @param {Boolean} pauseModeOn Whether pause mode should be set.
*/
VirtualMachine.prototype.setPauseMode = function (pauseModeOn) {
this.runtime.setPauseMode(!!pauseModeOn);
};
/**
* Set whether the VM is in 2.0 "compatibility mode."
* When true, ticks go at 2.0 speed (30 TPS).
* @param {Boolean} compatibilityModeOn Whether compatibility mode is set.
*/
VirtualMachine.prototype.setCompatibilityMode = function (compatibilityModeOn) {
this.runtime.setCompatibilityMode(!!compatibilityModeOn);
};
/**
* Set whether the VM is in "single stepping mode."
* When true, blocks execute slowly and are highlighted visually.
* @param {Boolean} singleSteppingOn Whether single-stepping mode is set.
*/
VirtualMachine.prototype.setSingleSteppingMode = function (singleSteppingOn) {
this.runtime.setSingleSteppingMode(!!singleSteppingOn);
};
/**
* Set single-stepping mode speed.
* When in single-stepping mode, adjusts the speed of execution.
* @param {Number} speed Interval length in ms.
*/
VirtualMachine.prototype.setSingleSteppingSpeed = function (speed) {
this.runtime.setSingleSteppingSpeed(speed);
};
/**
* Stop all threads and running activities.
*/

View file

@ -1,14 +1,35 @@
var Timer = require('../util/timer');
function Clock () {
function Clock (runtime) {
this._projectTimer = new Timer();
this._projectTimer.start();
this._pausedTime = null;
this._paused = false;
/**
* Reference to the owning Runtime.
* @type{!Runtime}
*/
this.runtime = runtime;
}
Clock.prototype.projectTimer = function () {
if (this._paused) {
return this._pausedTime / 1000;
}
return this._projectTimer.timeElapsed() / 1000;
};
Clock.prototype.pause = function () {
this._paused = true;
this._pausedTime = this._projectTimer.timeElapsed();
};
Clock.prototype.resume = function () {
this._paused = false;
var dt = this._projectTimer.timeElapsed() - this._pausedTime;
this._projectTimer.startTime += dt;
};
Clock.prototype.resetProjectTimer = function () {
this._projectTimer.start();
};

View file

@ -152,6 +152,9 @@ Clone.prototype.setXY = function (x, y) {
this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y]
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -191,6 +194,9 @@ Clone.prototype.setDirection = function (direction) {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -224,6 +230,9 @@ Clone.prototype.setVisible = function (visible) {
this.renderer.updateDrawableProperties(this.drawableID, {
visible: this.visible
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -243,6 +252,9 @@ Clone.prototype.setSize = function (size) {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -258,6 +270,9 @@ Clone.prototype.setEffect = function (effectName, value) {
var props = {};
props[effectName] = this.effects[effectName];
this.renderer.updateDrawableProperties(this.drawableID, props);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -270,6 +285,9 @@ Clone.prototype.clearEffects = function () {
}
if (this.renderer) {
this.renderer.updateDrawableProperties(this.drawableID, this.effects);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -287,6 +305,9 @@ Clone.prototype.setCostume = function (index) {
this.renderer.updateDrawableProperties(this.drawableID, {
skin: this.sprite.costumes[this.currentCostume].skin
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -308,6 +329,9 @@ Clone.prototype.setRotationStyle = function (rotationStyle) {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -339,6 +363,9 @@ Clone.prototype.updateAllDrawableProperties = function () {
visible: this.visible,
skin: this.sprite.costumes[this.currentCostume].skin
});
if (this.visible) {
this.runtime.requestRedraw();
}
}
};
@ -552,6 +579,9 @@ Clone.prototype.dispose = function () {
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);
if (this.visible) {
this.runtime.requestRedraw();
}
}
};