From d04d6b2c6ad1c7a79b06f9eb727170cc3068c517 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Tue, 10 Apr 2018 16:29:51 -0400 Subject: [PATCH 1/9] Revert "Revert "Push reported"" This reverts commit 107adad647e63be9de90c8d8864d34609ef0701f. --- src/engine/blocks-execute-cache.js | 19 ++++ src/engine/blocks.js | 40 ++++++- src/engine/execute.js | 173 ++++++++++++++++++++--------- src/engine/runtime.js | 4 + src/engine/sequencer.js | 6 +- src/engine/thread.js | 14 ++- test/unit/engine_sequencer.js | 3 +- test/unit/engine_thread.js | 4 +- 8 files changed, 198 insertions(+), 65 deletions(-) create mode 100644 src/engine/blocks-execute-cache.js 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.} */ - procedureDefinitions: {} + procedureDefinitions: {}, + + /** + * A cache for execute to use and store on. Only available to + * execute. + * @type {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(); }); From 02dd9a8c22b1284afa265fe2cafe565092836ff2 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 16 Apr 2018 17:35:56 -0400 Subject: [PATCH 2/9] Unset justReported after reading its value Avoid justReported values leaking to other argValues by setting back to the default null value. --- src/engine/execute.js | 79 +++++++++++++++++++++++++------------------ src/engine/thread.js | 2 +- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 85af1b821..2169ede95 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -100,12 +100,24 @@ const handleReport = function ( }; /** - * A convenienve constant to hide that the recursiveCall argument to execute is - * a boolean trap. + * A convenience constant to help make use of the recursiveCall argument easier + * to read. * @const {boolean} */ const RECURSIVE = true; +/** + * A simple description of the kind of information held in the fields of a block. + * @enum {string} + */ +const FieldKind = { + NONE: 'NONE', + VARIABLE: 'VARIABLE', + LIST: 'LIST', + BROADCAST_OPTION: 'BROADCAST_OPTION', + DYNAMIC: 'DYNAMIC' +}; + /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. @@ -145,35 +157,32 @@ const execute = function (sequencer, thread, recursiveCall) { // 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; + 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._isFieldVariable = fieldKeys.length === 1 && fieldKeys.includes('VARIABLE'); - blockCached._fieldVariable = blockCached._isFieldVariable ? - { + 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 - } : - null; - blockCached._isFieldList = fieldKeys.length === 1 && fieldKeys.includes('LIST'); - blockCached._fieldList = blockCached._isFieldList ? - { + }; + } else if (fieldKeys.length === 1 && fieldKeys.includes('LIST')) { + blockCached._fieldKind = FieldKind.LIST; + blockCached._fieldList = { id: fields.LIST.id, name: fields.LIST.value - } : - null; - blockCached._isFieldBroadcastOption = fieldKeys.length === 1 && fieldKeys.includes('BROADCAST_OPTION'); - blockCached._fieldBroadcastOption = blockCached._isFieldBroadcastOption ? - { + }; + } 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 - } : - 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. @@ -220,17 +229,21 @@ const execute = function (sequencer, thread, recursiveCall) { const argValues = {}; // Add all fields on this block to the argValues. - if (blockCached._isFieldKnown) { - if (blockCached._isFieldVariable) { + if (blockCached._fieldKind !== FieldKind.NONE) { + switch (blockCached._fieldKind) { + case FieldKind.VARIABLE: argValues.VARIABLE = blockCached._fieldVariable; - } else if (blockCached._isFieldList) { + break; + case FieldKind.LIST: argValues.LIST = blockCached._fieldList; - } else if (blockCached._isFieldBroadcastOption) { + break; + case FieldKind.BROADCAST_OPTION: argValues.BROADCAST_OPTION = blockCached._fieldBroadcastOption; - } - } else { - for (const fieldName in fields) { - argValues[fieldName] = fields[fieldName].value; + break; + default: + for (const fieldName in fields) { + argValues[fieldName] = fields[fieldName].value; + } } } @@ -239,7 +252,7 @@ const execute = function (sequencer, thread, recursiveCall) { const input = inputs[inputName]; const inputBlockId = input.block; // Is there no value for this input waiting in the stack frame? - if (inputBlockId !== null && typeof currentStackFrame.reported[inputName] === 'undefined') { + if (inputBlockId !== null && currentStackFrame.waitingReporter === null) { // If there's not, we need to evaluate the block. // Push to the stack to evaluate the reporter block. thread.pushStack(inputBlockId); @@ -251,7 +264,7 @@ const execute = function (sequencer, thread, recursiveCall) { for (const _inputName in inputs) { if (_inputName === inputName) break; if (_inputName === 'BROADCAST_INPUT') { - currentStackFrame.reported[_inputName] = argValues[_inputName].name; + currentStackFrame.reported[_inputName] = argValues.BROADCAST_OPTION.name; } else { currentStackFrame.reported[_inputName] = argValues[_inputName]; } @@ -265,13 +278,13 @@ const execute = function (sequencer, thread, recursiveCall) { thread.popStack(); } let inputValue; - if ( - currentStackFrame.waitingReporter === null - ) { + if (currentStackFrame.waitingReporter === null) { inputValue = currentStackFrame.justReported; + currentStackFrame.justReported = null; } else if (currentStackFrame.waitingReporter === inputName) { inputValue = currentStackFrame.justReported; currentStackFrame.waitingReporter = null; + currentStackFrame.justReported = 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 diff --git a/src/engine/thread.js b/src/engine/thread.js index a618b460b..77444af94 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -217,7 +217,7 @@ class Thread { pushReportedValue (value) { const parentStackFrame = this.peekParentStackFrame(); if (parentStackFrame !== null) { - parentStackFrame.justReported = value; + parentStackFrame.justReported = typeof value === 'undefined' ? null : value; } } From 1c68b543dfa726e2c199bf6c4955b85afcc94e6f Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 18 Apr 2018 12:24:39 -0400 Subject: [PATCH 3/9] Comment execute's BlocksExecuteCache usage Explain how BlocksExecuteCache helps speed up execute by storing values from Blocks and one-time derived values on an object that is released when Blocks is edited in the editor. The one time created cache object for a block then simplifies the complexity of later javascript operations to use these stored values. --- src/engine/execute.js | 45 +++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 2169ede95..f819b6df2 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -144,6 +144,20 @@ const execute = function (sequencer, thread, recursiveCall) { } } + // 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; @@ -201,10 +215,10 @@ const execute = function (sequencer, thread, recursiveCall) { // 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. + // 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 (!blockCached._definedBlockFunction) { if (!opcode) { log.warn(`Could not get opcode for block: ${currentBlockId}`); @@ -228,7 +242,9 @@ const execute = function (sequencer, thread, recursiveCall) { // Generate values for arguments (inputs). const argValues = {}; - // Add all fields on this block to the 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: @@ -261,7 +277,13 @@ const execute = function (sequencer, thread, recursiveCall) { // Actually execute the block. execute(sequencer, thread, RECURSIVE); if (thread.status === Thread.STATUS_PROMISE_WAIT) { + // Waiting for the block to resolve, store the current argValues + // onto a member of the currentStackFrame that can be used once + // the nested block resolves to rebuild argValues up to this + // point. for (const _inputName in inputs) { + // We are waiting on the nested block at inputName so we + // don't need to store any more inputs. if (_inputName === inputName) break; if (_inputName === 'BROADCAST_INPUT') { currentStackFrame.reported[_inputName] = argValues.BROADCAST_OPTION.name; @@ -277,6 +299,7 @@ const execute = function (sequencer, thread, recursiveCall) { currentStackFrame.waitingReporter = null; thread.popStack(); } + let inputValue; if (currentStackFrame.waitingReporter === null) { inputValue = currentStackFrame.justReported; @@ -285,11 +308,11 @@ const execute = function (sequencer, thread, recursiveCall) { inputValue = currentStackFrame.justReported; currentStackFrame.waitingReporter = null; currentStackFrame.justReported = 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. + // We have rebuilt argValues with all the stored values in the + // currentStackFrame from the nested block's promise resolving. + // Using the reported value from the block we waited on, reset the + // storage member of currentStackFrame so the next execute call at + // this level can use it in a clean state. currentStackFrame.reported = {}; } else if (typeof currentStackFrame.reported[inputName] !== 'undefined') { inputValue = currentStackFrame.reported[inputName]; @@ -391,6 +414,8 @@ const execute = function (sequencer, thread, recursiveCall) { }); } else if (thread.status === Thread.STATUS_RUNNING) { if (recursiveCall === RECURSIVE) { + // In recursive calls (where execute calls execute) handleReport + // simplifies to just calling thread.pushReportedValue. thread.pushReportedValue(primitiveReportedValue); } else { handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat); From 218725d7714cae85be408add1ee316b016bc43a9 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Thu, 26 Apr 2018 10:29:29 -0400 Subject: [PATCH 4/9] Cast 'create clone' argument to string. Resolves GH-974 --- src/blocks/scratch3_control.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index 3f7a8e79b..4636d282d 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -144,15 +144,21 @@ class Scratch3ControlBlocks { } createClone (args, util) { + // Cast argument to string + args.CLONE_OPTION = Cast.toString(args.CLONE_OPTION); + + // Set clone target let cloneTarget; if (args.CLONE_OPTION === '_myself_') { cloneTarget = util.target; } else { cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION); } - if (!cloneTarget) { - return; - } + + // If clone target is not found, return + if (!cloneTarget) return; + + // Create clone const newClone = cloneTarget.makeClone(); if (newClone) { this.runtime.targets.push(newClone); From 5f4139cbe4cc1fadafc4df102522d813b6682da4 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Thu, 26 Apr 2018 13:24:31 -0400 Subject: [PATCH 5/9] Use video provider instead of making call to getUserMedia directly. --- src/io/video.js | 325 ++++------------------------------------- src/virtual-machine.js | 4 + 2 files changed, 32 insertions(+), 297 deletions(-) diff --git a/src/io/video.js b/src/io/video.js index ba05ff244..70b753908 100644 --- a/src/io/video.js +++ b/src/io/video.js @@ -1,67 +1,7 @@ -const log = require('../util/log'); - class Video { constructor (runtime) { - /** - * Reference to the owning Runtime. - * @type{!Runtime} - */ this.runtime = runtime; - - /** - * Default value for mirrored frames. - * @type boolean - */ - this.mirror = true; - - /** - * Cache frames for this many ms. - * @type number - */ - this._frameCacheTimeout = 16; - - /** - * DOM Video element - * @private - */ - this._video = null; - - /** - * Usermedia stream track - * @private - */ - this._track = null; - - /** - * Stores some canvas/frame data per resolution/mirror states - */ - this._workspace = []; - - /** - * Id representing a Scratch Renderer skin the video is rendered to for - * previewing. - * @type {number} - */ - this._skinId = -1; - - /** - * The Scratch Renderer Skin object. - * @type {Skin} - */ - this._skin = null; - - /** - * Id for a drawable using the video's skin that will render as a video - * preview. - * @type {Drawable} - */ - this._drawable = -1; - - /** - * Store the last state of the video transparency ghost effect - * @type {number} - */ - this._ghost = 0; + this.provider = null; } static get FORMAT_IMAGE_DATA () { @@ -97,39 +37,17 @@ class Video { * @return {Promise.