diff --git a/src/engine/blocks-execute-cache.js b/src/engine/blocks-execute-cache.js new file mode 100644 index 000000000..ffa8708b2 --- /dev/null +++ b/src/engine/blocks-execute-cache.js @@ -0,0 +1,19 @@ +/** + * @fileoverview + * Access point for private method shared between blocks.js and execute.js for + * caching execute information. + */ + +/** + * A private method shared with execute to build an object containing the block + * information execute needs and that is reset when other cached Blocks info is + * reset. + * @param {Blocks} blocks Blocks containing the expected blockId + * @param {string} blockId blockId for the desired execute cache + */ +exports.getCached = function () { + throw new Error('blocks.js has not initialized BlocksExecuteCache'); +}; + +// Call after the default throwing getCached is assigned for Blocks to replace. +require('./blocks'); diff --git a/src/engine/blocks.js b/src/engine/blocks.js index baad5e4c6..9af98db59 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -4,6 +4,7 @@ const xmlEscape = require('../util/xml-escape'); const MonitorRecord = require('./monitor-record'); const Clone = require('../util/clone'); const {Map} = require('immutable'); +const BlocksExecuteCache = require('./blocks-execute-cache'); /** * @fileoverview @@ -47,7 +48,14 @@ class Blocks { * Cache procedure definitions by block id * @type {object.<string, ?string>} */ - procedureDefinitions: {} + procedureDefinitions: {}, + + /** + * A cache for execute to use and store on. Only available to + * execute. + * @type {object.<string, object>} + */ + _executeCached: {} }; } @@ -359,6 +367,7 @@ class Blocks { this._cache.inputs = {}; this._cache.procedureParamNames = {}; this._cache.procedureDefinitions = {}; + this._cache._executeCached = {}; } /** @@ -435,7 +444,7 @@ class Blocks { const isSpriteSpecific = optRuntime.monitorBlockInfo.hasOwnProperty(block.opcode) && optRuntime.monitorBlockInfo[block.opcode].isSpriteSpecific; block.targetId = isSpriteSpecific ? optRuntime.getEditingTarget().id : null; - + if (wasMonitored && !block.isMonitored) { optRuntime.requestRemoveMonitor(block.id); } else if (!wasMonitored && block.isMonitored) { @@ -859,4 +868,31 @@ class Blocks { } } +/** + * A private method shared with execute to build an object containing the block + * information execute needs and that is reset when other cached Blocks info is + * reset. + * @param {Blocks} blocks Blocks containing the expected blockId + * @param {string} blockId blockId for the desired execute cache + * @return {object} execute cache object + */ +BlocksExecuteCache.getCached = function (blocks, blockId) { + const block = blocks.getBlock(blockId); + if (typeof block === 'undefined') return null; + let cached = blocks._cache._executeCached[blockId]; + if (typeof cached !== 'undefined') { + return cached; + } + + cached = { + _initialized: false, + opcode: blocks.getOpcode(block), + fields: blocks.getFields(block), + inputs: blocks.getInputs(block), + mutation: blocks.getMutation(block) + }; + blocks._cache._executeCached[blockId] = cached; + return cached; +}; + module.exports = Blocks; diff --git a/src/engine/execute.js b/src/engine/execute.js index 13742ec76..85af1b821 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,4 +1,5 @@ const BlockUtility = require('./block-utility'); +const BlocksExecuteCache = require('./blocks-execute-cache'); const log = require('../util/log'); const Thread = require('./thread'); const {Map} = require('immutable'); @@ -98,32 +99,27 @@ const handleReport = function ( } }; +/** + * A convenienve constant to hide that the recursiveCall argument to execute is + * a boolean trap. + * @const {boolean} + */ +const RECURSIVE = true; + /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. * @param {!Thread} thread Thread which to read and execute. + * @param {boolean} recursiveCall is execute called from another execute call? */ -const execute = function (sequencer, thread) { +const execute = function (sequencer, thread, recursiveCall) { const runtime = sequencer.runtime; - const target = thread.target; - - // Stop if block or target no longer exists. - if (target === null) { - // No block found: stop the thread; script no longer exists. - sequencer.retireThread(thread); - return; - } // Current block to execute is the one on the top of the stack. const currentBlockId = thread.peekStack(); const currentStackFrame = thread.peekStackFrame(); - let blockContainer; - if (thread.updateMonitor) { - blockContainer = runtime.monitorBlocks; - } else { - blockContainer = target.blocks; - } + let blockContainer = thread.blockContainer; let block = blockContainer.getBlock(currentBlockId); if (typeof block === 'undefined') { blockContainer = runtime.flyoutBlocks; @@ -136,33 +132,83 @@ const execute = function (sequencer, thread) { } } - const opcode = blockContainer.getOpcode(block); - const fields = blockContainer.getFields(block); - const inputs = blockContainer.getInputs(block); - const blockFunction = runtime.getOpcodeFunction(opcode); - const isHat = runtime.getIsHat(opcode); + const blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId); + if (blockCached._initialized !== true) { + const {opcode, fields, inputs} = blockCached; + // Assign opcode isHat and blockFunction data to avoid dynamic lookups. + blockCached._isHat = runtime.getIsHat(opcode); + blockCached._blockFunction = runtime.getOpcodeFunction(opcode); + blockCached._definedBlockFunction = typeof blockCached._blockFunction !== 'undefined'; - if (!opcode) { - log.warn(`Could not get opcode for block: ${currentBlockId}`); - return; + const fieldKeys = Object.keys(fields); + + // Store the current shadow value if there is a shadow value. + blockCached._isShadowBlock = fieldKeys.length === 1 && Object.keys(inputs).length === 0; + blockCached._shadowValue = fieldKeys.length === 1 && fields[fieldKeys[0]].value; + + // Store a fields copy. If fields is a VARIABLE, LIST, or + // BROADCAST_OPTION, store the created values so fields assignment to + // argValues does not iterate over fields. + blockCached._fields = Object.assign({}, blockCached.fields); + blockCached._isFieldVariable = fieldKeys.length === 1 && fieldKeys.includes('VARIABLE'); + blockCached._fieldVariable = blockCached._isFieldVariable ? + { + id: fields.VARIABLE.id, + name: fields.VARIABLE.value + } : + null; + blockCached._isFieldList = fieldKeys.length === 1 && fieldKeys.includes('LIST'); + blockCached._fieldList = blockCached._isFieldList ? + { + id: fields.LIST.id, + name: fields.LIST.value + } : + null; + blockCached._isFieldBroadcastOption = fieldKeys.length === 1 && fieldKeys.includes('BROADCAST_OPTION'); + blockCached._fieldBroadcastOption = blockCached._isFieldBroadcastOption ? + { + id: fields.BROADCAST_OPTION.id, + name: fields.BROADCAST_OPTION.value + } : + null; + blockCached._isFieldKnown = blockCached._isFieldVariable || + blockCached._isFieldList || blockCached._isFieldBroadcastOption; + + // Store a modified inputs. This assures the keys are its own properties + // and that custom_block will not be evaluated. + blockCached._inputs = Object.assign({}, blockCached.inputs); + delete blockCached._inputs.custom_block; + + blockCached._initialized = true; } + const opcode = blockCached.opcode; + const fields = blockCached._fields; + const inputs = blockCached._inputs; + const mutation = blockCached.mutation; + const blockFunction = blockCached._blockFunction; + const isHat = blockCached._isHat; + // Hats and single-field shadows are implemented slightly differently // from regular blocks. // For hats: if they have an associated block function, // it's treated as a predicate; if not, execution will proceed as a no-op. // For single-field shadows: If the block has a single field, and no inputs, // immediately return the value of the field. - if (typeof blockFunction === 'undefined') { - if (isHat) { - // Skip through the block (hat with no predicate). + if (!blockCached._definedBlockFunction) { + if (!opcode) { + log.warn(`Could not get opcode for block: ${currentBlockId}`); return; } - const keys = Object.keys(fields); - if (keys.length === 1 && Object.keys(inputs).length === 0) { + + if (recursiveCall === RECURSIVE && blockCached._isShadowBlock) { // One field and no inputs - treat as arg. - handleReport(fields[keys[0]].value, sequencer, thread, currentBlockId, opcode, isHat); + thread.pushReportedValue(blockCached._shadowValue); + thread.status = Thread.STATUS_RUNNING; + } else if (isHat) { + // Skip through the block (hat with no predicate). + return; } else { log.warn(`Could not get implementation for opcode: ${opcode}`); } @@ -174,24 +220,22 @@ const execute = function (sequencer, thread) { const argValues = {}; // Add all fields on this block to the argValues. - for (const fieldName in fields) { - if (!fields.hasOwnProperty(fieldName)) continue; - if (fieldName === 'VARIABLE' || fieldName === 'LIST' || - fieldName === 'BROADCAST_OPTION') { - argValues[fieldName] = { - id: fields[fieldName].id, - name: fields[fieldName].value - }; - } else { + if (blockCached._isFieldKnown) { + if (blockCached._isFieldVariable) { + argValues.VARIABLE = blockCached._fieldVariable; + } else if (blockCached._isFieldList) { + argValues.LIST = blockCached._fieldList; + } else if (blockCached._isFieldBroadcastOption) { + argValues.BROADCAST_OPTION = blockCached._fieldBroadcastOption; + } + } else { + for (const fieldName in fields) { argValues[fieldName] = fields[fieldName].value; } } // Recursively evaluate input blocks. for (const inputName in inputs) { - if (!inputs.hasOwnProperty(inputName)) continue; - // Do not evaluate the internal custom command block within definition - if (inputName === 'custom_block') continue; const input = inputs[inputName]; const inputBlockId = input.block; // Is there no value for this input waiting in the stack frame? @@ -202,8 +246,16 @@ const execute = function (sequencer, thread) { // Save name of input for `Thread.pushReportedValue`. currentStackFrame.waitingReporter = inputName; // Actually execute the block. - execute(sequencer, thread); + execute(sequencer, thread, RECURSIVE); if (thread.status === Thread.STATUS_PROMISE_WAIT) { + for (const _inputName in inputs) { + if (_inputName === inputName) break; + if (_inputName === 'BROADCAST_INPUT') { + currentStackFrame.reported[_inputName] = argValues[_inputName].name; + } else { + currentStackFrame.reported[_inputName] = argValues[_inputName]; + } + } return; } @@ -212,7 +264,23 @@ const execute = function (sequencer, thread) { currentStackFrame.waitingReporter = null; thread.popStack(); } - const inputValue = currentStackFrame.reported[inputName]; + let inputValue; + if ( + currentStackFrame.waitingReporter === null + ) { + inputValue = currentStackFrame.justReported; + } else if (currentStackFrame.waitingReporter === inputName) { + inputValue = currentStackFrame.justReported; + currentStackFrame.waitingReporter = null; + // If we've gotten this far, all of the input blocks are evaluated, + // and `argValues` is fully populated. So, execute the block + // primitive. First, clear `currentStackFrame.reported`, so any + // subsequent execution (e.g., on return from a branch) gets fresh + // inputs. + currentStackFrame.reported = {}; + } else if (typeof currentStackFrame.reported[inputName] !== 'undefined') { + inputValue = currentStackFrame.reported[inputName]; + } if (inputName === 'BROADCAST_INPUT') { const broadcastInput = inputs[inputName]; // Check if something is plugged into the broadcast block, or @@ -239,16 +307,7 @@ const execute = function (sequencer, thread) { } // Add any mutation to args (e.g., for procedures). - const mutation = blockContainer.getMutation(block); - if (mutation !== null) { - argValues.mutation = mutation; - } - - // If we've gotten this far, all of the input blocks are evaluated, - // and `argValues` is fully populated. So, execute the block primitive. - // First, clear `currentStackFrame.reported`, so any subsequent execution - // (e.g., on return from a branch) gets fresh inputs. - currentStackFrame.reported = {}; + argValues.mutation = mutation; let primitiveReportedValue = null; blockUtility.sequencer = sequencer; @@ -271,7 +330,7 @@ const execute = function (sequencer, thread) { runtime.profiler.records.push(runtime.profiler.STOP, performance.now()); } - if (typeof primitiveReportedValue === 'undefined') { + if (recursiveCall !== RECURSIVE && typeof primitiveReportedValue === 'undefined') { // No value reported - potentially a command block. // Edge-activated hats don't request a glow; all commands do. thread.requestScriptGlowInFrame = true; @@ -318,7 +377,11 @@ const execute = function (sequencer, thread) { thread.popStack(); }); } else if (thread.status === Thread.STATUS_RUNNING) { - handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat); + if (recursiveCall === RECURSIVE) { + thread.pushReportedValue(primitiveReportedValue); + } else { + handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat); + } } }; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 7c400e0e4..f0500061b 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -947,6 +947,9 @@ class Runtime extends EventEmitter { thread.target = target; thread.stackClick = opts.stackClick; thread.updateMonitor = opts.updateMonitor; + thread.blockContainer = opts.updateMonitor ? + this.monitorBlocks : + target.blocks; thread.pushStack(id); this.threads.push(thread); @@ -976,6 +979,7 @@ class Runtime extends EventEmitter { newThread.target = thread.target; newThread.stackClick = thread.stackClick; newThread.updateMonitor = thread.updateMonitor; + newThread.blockContainer = thread.blockContainer; newThread.pushStack(thread.topBlock); const i = this.threads.indexOf(thread); if (i > -1) { diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index ce65163d4..7cdb47247 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -196,7 +196,11 @@ class Sequencer { this.runtime.profiler.records.push( this.runtime.profiler.START, executeProfilerId, null, performance.now()); } - execute(this, thread); + if (thread.target === null) { + this.retireThread(thread); + } else { + execute(this, thread); + } if (this.runtime.profiler !== null) { // this.runtime.profiler.stop(); this.runtime.profiler.records.push(this.runtime.profiler.STOP, performance.now()); diff --git a/src/engine/thread.js b/src/engine/thread.js index def1fc1ef..a618b460b 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -42,6 +42,12 @@ class Thread { */ this.target = null; + /** + * The Blocks this thread will execute. + * @type {Blocks} + */ + this.blockContainer = null; + /** * Whether the thread requests its script to glow during this frame. * @type {boolean} @@ -124,7 +130,8 @@ class Thread { 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. + justReported: null, // Reported value from just executed block. + reported: {}, // Persists reported inputs during async block. waitingReporter: null, // Name of waiting reporter. params: {}, // Procedure parameters. executionContext: {} // A context passed to block implementations. @@ -209,9 +216,8 @@ class Thread { */ pushReportedValue (value) { const parentStackFrame = this.peekParentStackFrame(); - if (parentStackFrame) { - const waitingReporter = parentStackFrame.waitingReporter; - parentStackFrame.reported[waitingReporter] = value; + if (parentStackFrame !== null) { + parentStackFrame.justReported = value; } } diff --git a/test/unit/engine_sequencer.js b/test/unit/engine_sequencer.js index 751e9bb64..55faeb69e 100644 --- a/test/unit/engine_sequencer.js +++ b/test/unit/engine_sequencer.js @@ -91,7 +91,8 @@ const generateThread = function (runtime) { rt.blocks.createBlock(generateBlock(next)); th.pushStack(next); th.target = rt; - + th.blockContainer = rt.blocks; + runtime.threads.push(th); return th; diff --git a/test/unit/engine_thread.js b/test/unit/engine_thread.js index 15fab7c02..7667a1b2e 100644 --- a/test/unit/engine_thread.js +++ b/test/unit/engine_thread.js @@ -89,8 +89,8 @@ test('pushReportedValue', t => { th.pushStack('arbitraryString'); th.pushStack('secondString'); th.pushReportedValue('value'); - t.strictEquals(th.peekParentStackFrame().reported.null, 'value'); - + t.strictEquals(th.peekParentStackFrame().justReported, 'value'); + t.end(); });