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); diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index cb124c238..ba3e16f05 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -11,6 +11,8 @@ const uid = require('../util/uid'); * See _renderBubble for explanation of this optimization. * @property {string} text - the text of the bubble. * @property {string} type - the type of the bubble, "say" or "think" + * @property {?string} usageId - ID indicating the most recent usage of the say/think bubble. + * Used for comparison when determining whether to clear a say/think bubble. */ class Scratch3LooksBlocks { @@ -46,7 +48,7 @@ class Scratch3LooksBlocks { skinId: null, text: '', type: 'say', - usageId: null // ID for hiding a timed say/think block. + usageId: null }; } 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..f819b6df2 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,39 @@ const handleReport = function ( } }; +/** + * 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. * @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 +144,94 @@ 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); + // 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'; - 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 = 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; + // 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). + // 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}`); 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}`); } @@ -173,37 +242,55 @@ const execute = function (sequencer, thread) { // Generate values for arguments (inputs). 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 { - argValues[fieldName] = fields[fieldName].value; + // 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; + } } } // 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? - 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); // 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) { + // 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; + } else { + currentStackFrame.reported[_inputName] = argValues[_inputName]; + } + } return; } @@ -212,7 +299,24 @@ const execute = function (sequencer, thread) { currentStackFrame.waitingReporter = null; thread.popStack(); } - const inputValue = currentStackFrame.reported[inputName]; + + let inputValue; + if (currentStackFrame.waitingReporter === null) { + inputValue = currentStackFrame.justReported; + currentStackFrame.justReported = null; + } else if (currentStackFrame.waitingReporter === inputName) { + inputValue = currentStackFrame.justReported; + currentStackFrame.waitingReporter = null; + currentStackFrame.justReported = null; + // 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]; + } if (inputName === 'BROADCAST_INPUT') { const broadcastInput = inputs[inputName]; // Check if something is plugged into the broadcast block, or @@ -239,16 +343,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 +366,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 +413,13 @@ const execute = function (sequencer, thread) { thread.popStack(); }); } else if (thread.status === Thread.STATUS_RUNNING) { - handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat); + 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); + } } }; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 7e58451b7..c3d68657d 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..77444af94 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 = typeof value === 'undefined' ? null : value; } } diff --git a/src/io/video.js b/src/io/video.js index ba05ff244..c01141c1e 100644 --- a/src/io/video.js +++ b/src/io/video.js @@ -1,41 +1,16 @@ -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 + * @typedef VideoProvider + * @property {Function} enableVideo - Requests camera access from the user, and upon success, + * enables the video feed + * @property {Function} disableVideo - Turns off the video feed + * @property {Function} getFrame - Return frame data from the video feed in + * specified dimensions, format, and mirroring. */ - 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 = []; + this.provider = null; /** * Id representing a Scratch Renderer skin the video is rendered to for @@ -89,6 +64,15 @@ class Video { return 1; } + /** + * Set a video provider for this device. A default implementation of + * a video provider can be found in scratch-gui/src/lib/video/video-provider + * @param {VideoProvider} provider - Video provider to use + */ + setProvider (provider) { + this.provider = provider; + } + /** * Request video be enabled. Sets up video, creates video skin and enables preview. * @@ -97,39 +81,18 @@ class Video { * @return {Promise.