mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 06:23:37 -05:00
Merge pull request #1119 from mzgoddard/recycle-args
Recycle block args
This commit is contained in:
commit
22852aeab4
2 changed files with 192 additions and 127 deletions
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue