diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 9af98db59..04aed2ea3 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -874,23 +874,34 @@ class Blocks { * reset. * @param {Blocks} blocks Blocks containing the expected blockId * @param {string} blockId blockId for the desired execute cache + * @param {function} CacheType constructor for cached block information * @return {object} execute cache object */ -BlocksExecuteCache.getCached = function (blocks, blockId) { - const block = blocks.getBlock(blockId); - if (typeof block === 'undefined') return null; +BlocksExecuteCache.getCached = function (blocks, blockId, CacheType) { 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) - }; + const block = blocks.getBlock(blockId); + if (typeof block === 'undefined') return null; + + if (typeof CacheType === 'undefined') { + cached = { + opcode: blocks.getOpcode(block), + fields: blocks.getFields(block), + inputs: blocks.getInputs(block), + mutation: blocks.getMutation(block) + }; + } else { + cached = new CacheType(blocks, { + opcode: blocks.getOpcode(block), + fields: blocks.getFields(block), + inputs: blocks.getInputs(block), + mutation: blocks.getMutation(block) + }); + } + blocks._cache._executeCached[blockId] = cached; return cached; }; diff --git a/src/engine/execute.js b/src/engine/execute.js index 3f891a0c3..189bb2316 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -107,16 +107,160 @@ const handleReport = function ( const RECURSIVE = true; /** - * A simple description of the kind of information held in the fields of a block. - * @enum {string} + * A execute.js internal representation of a block to reduce the time spent in + * execute as the same blocks are called the most. + * + * With the help of the Blocks class create a mutable copy of block + * information. The members of BlockCached derived values of block information + * that does not need to be reevaluated until a change in Blocks. Since Blocks + * handles where the cache instance is stored, it drops all cache versions of a + * block when any change happens to it. This way we can quickly execute blocks + * and keep perform the right action according to the current block information + * in the editor. + * + * @param {Blocks} blockContainer the related Blocks instance + * @param {object} cached default set of cached values */ -const FieldKind = { - NONE: 'NONE', - VARIABLE: 'VARIABLE', - LIST: 'LIST', - BROADCAST_OPTION: 'BROADCAST_OPTION', - DYNAMIC: 'DYNAMIC' -}; +class BlockCached { + constructor (blockContainer, cached) { + /** + * Block operation code for this block. + * @type {string} + */ + this.opcode = cached.opcode; + + /** + * Original block object containing argument values for static fields. + * @type {object} + */ + this.fields = cached.fields; + + /** + * Original block object containing argument values for executable inputs. + * @type {object} + */ + this.inputs = cached.inputs; + + /** + * Procedure mutation. + * @type {?object} + */ + this.mutation = cached.mutation; + + /** + * Is the opcode a hat (event responder) block. + * @type {boolean} + */ + this._isHat = false; + + /** + * The block opcode's implementation function. + * @type {?function} + */ + this._blockFunction = null; + + /** + * Is the block function defined for this opcode? + * @type {boolean} + */ + this._definedBlockFunction = false; + + /** + * Is this block a block with no function but a static value to return. + * @type {boolean} + */ + this._isShadowBlock = false; + + /** + * The static value of this block if it is a shadow block. + * @type {?any} + */ + this._shadowValue = null; + + /** + * A copy of the block's fields that may be modified. + * @type {object} + */ + this._fields = Object.assign({}, this.fields); + + /** + * A copy of the block's inputs that may be modified. + * @type {object} + */ + this._inputs = Object.assign({}, this.inputs); + + /** + * An arguments object for block implementations. All executions of this + * specific block will use this objecct. + * @type {object} + */ + this._argValues = { + mutation: this.mutation + }; + + const {runtime} = blockUtility.sequencer; + + const {opcode, fields, inputs} = this; + + // Assign opcode isHat and blockFunction data to avoid dynamic lookups. + this._isHat = runtime.getIsHat(opcode); + this._blockFunction = runtime.getOpcodeFunction(opcode); + this._definedBlockFunction = typeof this._blockFunction !== 'undefined'; + + // Store the current shadow value if there is a shadow value. + const fieldKeys = Object.keys(fields); + this._isShadowBlock = ( + !this._definedBlockFunction && + fieldKeys.length === 1 && + Object.keys(inputs).length === 0 + ); + this._shadowValue = this._isShadowBlock && fields[fieldKeys[0]].value; + + // Store the static fields onto _argValues. + for (const fieldName in fields) { + if ( + fieldName === 'VARIABLE' || + fieldName === 'LIST' || + fieldName === 'BROADCAST_OPTION' + ) { + this._argValues[fieldName] = { + id: fields[fieldName].id, + name: fields[fieldName].value + }; + } else { + this._argValues[fieldName] = fields[fieldName].value; + } + } + + // Remove custom_block. It is not part of block execution. + delete this._inputs.custom_block; + + if ('BROADCAST_INPUT' in this._inputs) { + // BROADCAST_INPUT is called BROADCAST_OPTION in the args and is an + // object with an unchanging shape. + this._argValues.BROADCAST_OPTION = { + id: null, + name: null + }; + + // We can go ahead and compute BROADCAST_INPUT if it is a shadow + // value. + const broadcastInput = this._inputs.BROADCAST_INPUT; + if (broadcastInput.block === broadcastInput.shadow) { + // Shadow dropdown menu is being used. + // Get the appropriate information out of it. + const shadow = blockContainer.getBlock(broadcastInput.shadow); + const broadcastField = shadow.fields.BROADCAST_OPTION; + this._argValues.BROADCAST_OPTION.id = broadcastField.id; + this._argValues.BROADCAST_OPTION.name = broadcastField.value; + + // Evaluating BROADCAST_INPUT here we do not need to do so + // later. + delete this._inputs.BROADCAST_INPUT; + } + } + } +} /** * Execute a block. @@ -127,89 +271,32 @@ const FieldKind = { const execute = function (sequencer, thread, recursiveCall) { const runtime = sequencer.runtime; + // sequencer and thread are the same objects during a recursive set of + // execute operations. + if (recursiveCall !== RECURSIVE) { + blockUtility.sequencer = sequencer; + blockUtility.thread = thread; + } + // Current block to execute is the one on the top of the stack. const currentBlockId = thread.peekStack(); const currentStackFrame = thread.peekStackFrame(); let blockContainer = thread.blockContainer; - let block = blockContainer.getBlock(currentBlockId); - if (typeof block === 'undefined') { + let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + if (blockCached === null) { blockContainer = runtime.flyoutBlocks; - block = blockContainer.getBlock(currentBlockId); + blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); // Stop if block or target no longer exists. - if (typeof block === 'undefined') { + if (blockCached === null) { // No block found: stop the thread; script no longer exists. sequencer.retireThread(thread); return; } } - // With the help of the Blocks class create a cached copy of values from - // Blocks and the derived values execute needs. These values can be produced - // one time during the first execution of a block so that later executions - // are faster by using these cached values. This helps turn most costly - // javascript operations like testing if the fields for a block has a - // certain key like VARIABLE into a test that done once is saved on the - // cache object to _isFieldVariable. This reduces the cost later in execute - // when that will determine how execute creates the argValues for the - // blockFunction. - // - // With Blocks providing this private function for execute to use, any time - // Blocks is modified in the editor these cached objects will be cleaned up - // and new cached copies can be created. This lets us optimize this critical - // path while keeping up to date with editor changes to a project. - 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'; - - 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 = blockCached._isShadowBlock && 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._fieldKind = fieldKeys.length > 0 ? FieldKind.DYNAMIC : FieldKind.NONE; - if (fieldKeys.length === 1 && fieldKeys.includes('VARIABLE')) { - blockCached._fieldKind = FieldKind.VARIABLE; - blockCached._fieldVariable = { - id: fields.VARIABLE.id, - name: fields.VARIABLE.value - }; - } else if (fieldKeys.length === 1 && fieldKeys.includes('LIST')) { - blockCached._fieldKind = FieldKind.LIST; - blockCached._fieldList = { - id: fields.LIST.id, - name: fields.LIST.value - }; - } else if (fieldKeys.length === 1 && fieldKeys.includes('BROADCAST_OPTION')) { - blockCached._fieldKind = FieldKind.BROADCAST_OPTION; - blockCached._fieldBroadcastOption = { - id: fields.BROADCAST_OPTION.id, - name: fields.BROADCAST_OPTION.value - }; - } - - // 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; @@ -239,29 +326,10 @@ const execute = function (sequencer, thread, recursiveCall) { return; } - // Generate values for arguments (inputs). - const argValues = {}; + // Update values for arguments (inputs). + const argValues = blockCached._argValues; - // Add all fields on this block to the argValues. Some known fields may - // appear by themselves and can be set to argValues quicker by setting them - // explicitly. - if (blockCached._fieldKind !== FieldKind.NONE) { - switch (blockCached._fieldKind) { - case FieldKind.VARIABLE: - argValues.VARIABLE = blockCached._fieldVariable; - break; - case FieldKind.LIST: - argValues.LIST = blockCached._fieldList; - break; - case FieldKind.BROADCAST_OPTION: - argValues.BROADCAST_OPTION = blockCached._fieldBroadcastOption; - break; - default: - for (const fieldName in fields) { - argValues[fieldName] = fields[fieldName].value; - } - } - } + // Fields are set during blockCached initialization. // Recursively evaluate input blocks. for (const inputName in inputs) { @@ -320,37 +388,23 @@ const execute = function (sequencer, thread, recursiveCall) { } 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 // if the shadow dropdown menu is being used. - if (broadcastInput.block === broadcastInput.shadow) { - // Shadow dropdown menu is being used. - // Get the appropriate information out of it. - const shadow = blockContainer.getBlock(broadcastInput.shadow); - const broadcastField = shadow.fields.BROADCAST_OPTION; - argValues.BROADCAST_OPTION = { - id: broadcastField.id, - name: broadcastField.value - }; - } else { + if (broadcastInput.block !== broadcastInput.shadow) { // Something is plugged into the broadcast input. // Cast it to a string. We don't need an id here. - argValues.BROADCAST_OPTION = { - name: cast.toString(inputValue) - }; + argValues.BROADCAST_OPTION.id = null; + argValues.BROADCAST_OPTION.name = cast.toString(inputValue); } } else { argValues[inputName] = inputValue; } } - // Add any mutation to args (e.g., for procedures). - argValues.mutation = mutation; - let primitiveReportedValue = null; - blockUtility.sequencer = sequencer; - blockUtility.thread = thread; if (runtime.profiler !== null) { if (blockFunctionProfilerId === -1) { blockFunctionProfilerId = runtime.profiler.idByName(blockFunctionProfilerFrame);