Skip to content

Commit f2301e0

Browse files
authored
Merge pull request #2126 from mzgoddard/runtime-script-cache-fix
Runtime script cache fix
2 parents 61dcf1a + ebdf386 commit f2301e0

File tree

5 files changed

+210
-50
lines changed

5 files changed

+210
-50
lines changed

src/engine/blocks-runtime-cache.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @fileoverview
3+
* The BlocksRuntimeCache caches data about the top block of scripts so that
4+
* Runtime can iterate a targeted opcode and iterate the returned set faster.
5+
* Many top blocks need to match fields as well as opcode, since that matching
6+
* compares strings in uppercase we can go ahead and uppercase the cached value
7+
* so we don't need to in the future.
8+
*/
9+
10+
/**
11+
* A set of cached data about the top block of a script.
12+
* @param {Blocks} container - Container holding the block and related data
13+
* @param {string} blockId - Id for whose block data is cached in this instance
14+
*/
15+
class RuntimeScriptCache {
16+
constructor (container, blockId) {
17+
/**
18+
* Container with block data for blockId.
19+
* @type {Blocks}
20+
*/
21+
this.container = container;
22+
23+
/**
24+
* ID for block this instance caches.
25+
* @type {string}
26+
*/
27+
this.blockId = blockId;
28+
29+
const block = container.getBlock(blockId);
30+
const fields = container.getFields(block);
31+
32+
/**
33+
* Formatted fields or fields of input blocks ready for comparison in
34+
* runtime.
35+
*
36+
* This is a clone of parts of the targeted blocks. Changes to these
37+
* clones are limited to copies under RuntimeScriptCache and will not
38+
* appear in the original blocks in their container. This copy is
39+
* modified changing the case of strings to uppercase. These uppercase
40+
* values will be compared later by the VM.
41+
* @type {object}
42+
*/
43+
this.fieldsOfInputs = Object.assign({}, fields);
44+
if (Object.keys(fields).length === 0) {
45+
const inputs = container.getInputs(block);
46+
for (const input in inputs) {
47+
if (!inputs.hasOwnProperty(input)) continue;
48+
const id = inputs[input].block;
49+
const inputBlock = container.getBlock(id);
50+
const inputFields = container.getFields(inputBlock);
51+
Object.assign(this.fieldsOfInputs, inputFields);
52+
}
53+
}
54+
for (const key in this.fieldsOfInputs) {
55+
const field = this.fieldsOfInputs[key] = Object.assign({}, this.fieldsOfInputs[key]);
56+
if (field.value.toUpperCase) {
57+
field.value = field.value.toUpperCase();
58+
}
59+
}
60+
}
61+
}
62+
63+
/**
64+
* Get an array of scripts from a block container prefiltered to match opcode.
65+
* @param {Blocks} container - Container of blocks
66+
* @param {string} opcode - Opcode to filter top blocks by
67+
*/
68+
exports.getScripts = function () {
69+
throw new Error('blocks.js has not initialized BlocksRuntimeCache');
70+
};
71+
72+
/**
73+
* Exposed RuntimeScriptCache class used by integration in blocks.js.
74+
* @private
75+
*/
76+
exports._RuntimeScriptCache = RuntimeScriptCache;
77+
78+
require('./blocks');

src/engine/blocks.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const MonitorRecord = require('./monitor-record');
55
const Clone = require('../util/clone');
66
const {Map} = require('immutable');
77
const BlocksExecuteCache = require('./blocks-execute-cache');
8+
const BlocksRuntimeCache = require('./blocks-runtime-cache');
89
const log = require('../util/log');
910
const Variable = require('./variable');
1011
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
@@ -74,7 +75,13 @@ class Blocks {
7475
* actively monitored.
7576
* @type {Array<{blockId: string, target: Target}>}
7677
*/
77-
_monitored: null
78+
_monitored: null,
79+
80+
/**
81+
* A cache of hat opcodes to collection of theads to execute.
82+
* @type {object.<string, object>}
83+
*/
84+
scripts: {}
7885
};
7986

8087
/**
@@ -509,6 +516,7 @@ class Blocks {
509516
this._cache.procedureDefinitions = {};
510517
this._cache._executeCached = {};
511518
this._cache._monitored = null;
519+
this._cache.scripts = {};
512520
}
513521

514522
/**
@@ -1215,4 +1223,35 @@ BlocksExecuteCache.getCached = function (blocks, blockId, CacheType) {
12151223
return cached;
12161224
};
12171225

1226+
/**
1227+
* Cache class constructor for runtime. Used to consider what threads should
1228+
* start based on hat data.
1229+
* @type {function}
1230+
*/
1231+
const RuntimeScriptCache = BlocksRuntimeCache._RuntimeScriptCache;
1232+
1233+
/**
1234+
* Get an array of scripts from a block container prefiltered to match opcode.
1235+
* @param {Blocks} blocks - Container of blocks
1236+
* @param {string} opcode - Opcode to filter top blocks by
1237+
* @returns {Array.<RuntimeScriptCache>} - Array of RuntimeScriptCache cache
1238+
* objects
1239+
*/
1240+
BlocksRuntimeCache.getScripts = function (blocks, opcode) {
1241+
let scripts = blocks._cache.scripts[opcode];
1242+
if (!scripts) {
1243+
scripts = blocks._cache.scripts[opcode] = [];
1244+
1245+
const allScripts = blocks._scripts;
1246+
for (let i = 0; i < allScripts.length; i++) {
1247+
const topBlockId = allScripts[i];
1248+
const block = blocks.getBlock(topBlockId);
1249+
if (block.opcode === opcode) {
1250+
scripts.push(new RuntimeScriptCache(blocks, topBlockId));
1251+
}
1252+
}
1253+
}
1254+
return scripts;
1255+
};
1256+
12181257
module.exports = Blocks;

src/engine/runtime.js

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {OrderedMap} = require('immutable');
33

44
const ArgumentType = require('../extension-support/argument-type');
55
const Blocks = require('./blocks');
6+
const BlocksRuntimeCache = require('./blocks-runtime-cache');
67
const BlockType = require('../extension-support/block-type');
78
const Profiler = require('./profiler');
89
const Sequencer = require('./sequencer');
@@ -1434,16 +1435,11 @@ class Runtime extends EventEmitter {
14341435
* @return {!Thread} The newly created thread.
14351436
*/
14361437
_pushThread (id, target, opts) {
1437-
opts = Object.assign({
1438-
stackClick: false,
1439-
updateMonitor: false
1440-
}, opts);
1441-
14421438
const thread = new Thread(id);
14431439
thread.target = target;
1444-
thread.stackClick = opts.stackClick;
1445-
thread.updateMonitor = opts.updateMonitor;
1446-
thread.blockContainer = opts.updateMonitor ?
1440+
thread.stackClick = Boolean(opts && opts.stackClick);
1441+
thread.updateMonitor = Boolean(opts && opts.updateMonitor);
1442+
thread.blockContainer = thread.updateMonitor ?
14471443
this.monitorBlocks :
14481444
target.blocks;
14491445

@@ -1586,6 +1582,20 @@ class Runtime extends EventEmitter {
15861582
}
15871583
}
15881584

1585+
allScriptsByOpcodeDo (opcode, f, optTarget) {
1586+
let targets = this.executableTargets;
1587+
if (optTarget) {
1588+
targets = [optTarget];
1589+
}
1590+
for (let t = targets.length - 1; t >= 0; t--) {
1591+
const target = targets[t];
1592+
const scripts = BlocksRuntimeCache.getScripts(target.blocks, opcode);
1593+
for (let j = 0; j < scripts.length; j++) {
1594+
f(scripts[j], target);
1595+
}
1596+
}
1597+
}
1598+
15891599
/**
15901600
* Start all relevant hats.
15911601
* @param {!string} requestedHatOpcode Opcode of hats to start.
@@ -1610,71 +1620,52 @@ class Runtime extends EventEmitter {
16101620
}
16111621

16121622
// Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
1613-
this.allScriptsDo((topBlockId, target) => {
1614-
const blocks = target.blocks;
1615-
const block = blocks.getBlock(topBlockId);
1616-
const potentialHatOpcode = block.opcode;
1617-
if (potentialHatOpcode !== requestedHatOpcode) {
1618-
// Not the right hat.
1619-
return;
1620-
}
1623+
this.allScriptsByOpcodeDo(requestedHatOpcode, (script, target) => {
1624+
const {
1625+
blockId: topBlockId,
1626+
fieldsOfInputs: hatFields
1627+
} = script;
16211628

16221629
// Match any requested fields.
16231630
// For example: ensures that broadcasts match.
16241631
// This needs to happen before the block is evaluated
16251632
// (i.e., before the predicate can be run) because "broadcast and wait"
16261633
// needs to have a precise collection of started threads.
1627-
let hatFields = blocks.getFields(block);
1628-
1629-
// If no fields are present, check inputs (horizontal blocks)
1630-
if (Object.keys(hatFields).length === 0) {
1631-
hatFields = {}; // don't overwrite the block's actual fields list
1632-
const hatInputs = blocks.getInputs(block);
1633-
for (const input in hatInputs) {
1634-
if (!hatInputs.hasOwnProperty(input)) continue;
1635-
const id = hatInputs[input].block;
1636-
const inpBlock = blocks.getBlock(id);
1637-
const fields = blocks.getFields(inpBlock);
1638-
Object.assign(hatFields, fields);
1639-
}
1640-
}
1641-
1642-
if (optMatchFields) {
1643-
for (const matchField in optMatchFields) {
1644-
if (hatFields[matchField].value.toUpperCase() !==
1645-
optMatchFields[matchField]) {
1646-
// Field mismatch.
1647-
return;
1648-
}
1634+
for (const matchField in optMatchFields) {
1635+
if (hatFields[matchField].value !== optMatchFields[matchField]) {
1636+
// Field mismatch.
1637+
return;
16491638
}
16501639
}
16511640

16521641
if (hatMeta.restartExistingThreads) {
16531642
// If `restartExistingThreads` is true, we should stop
16541643
// any existing threads starting with the top block.
1655-
for (let i = 0; i < instance.threads.length; i++) {
1656-
if (instance.threads[i].topBlock === topBlockId &&
1657-
!instance.threads[i].stackClick && // stack click threads and hat threads can coexist
1658-
instance.threads[i].target === target) {
1659-
newThreads.push(instance._restartThread(instance.threads[i]));
1644+
for (let i = 0; i < this.threads.length; i++) {
1645+
if (this.threads[i].target === target &&
1646+
this.threads[i].topBlock === topBlockId &&
1647+
// stack click threads and hat threads can coexist
1648+
!this.threads[i].stackClick) {
1649+
newThreads.push(this._restartThread(this.threads[i]));
16601650
return;
16611651
}
16621652
}
16631653
} else {
16641654
// If `restartExistingThreads` is false, we should
16651655
// give up if any threads with the top block are running.
1666-
for (let j = 0; j < instance.threads.length; j++) {
1667-
if (instance.threads[j].topBlock === topBlockId &&
1668-
instance.threads[j].target === target &&
1669-
!instance.threads[j].stackClick && // stack click threads and hat threads can coexist
1670-
instance.threads[j].status !== Thread.STATUS_DONE) {
1656+
for (let j = 0; j < this.threads.length; j++) {
1657+
if (this.threads[j].target === target &&
1658+
this.threads[j].topBlock === topBlockId &&
1659+
// stack click threads and hat threads can coexist
1660+
!this.threads[j].stackClick &&
1661+
this.threads[j].status !== Thread.STATUS_DONE) {
16711662
// Some thread is already running.
16721663
return;
16731664
}
16741665
}
16751666
}
16761667
// Start the thread with this top block.
1677-
newThreads.push(instance._pushThread(topBlockId, target));
1668+
newThreads.push(this._pushThread(topBlockId, target));
16781669
}, optTarget);
16791670
// For compatibility with Scratch 2, edge triggered hats need to be processed before
16801671
// threads are stepped. See ScratchRuntime.as for original implementation

src/engine/sequencer.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ class Sequencer {
177177
if (!currentBlockId) {
178178
// A "null block" - empty branch.
179179
thread.popStack();
180+
181+
// Did the null follow a hat block?
182+
if (thread.stack.length === 0) {
183+
thread.status = Thread.STATUS_DONE;
184+
return;
185+
}
180186
}
181187
// Save the current block ID to notice if we did control flow.
182188
while ((currentBlockId = thread.peekStack())) {

test/integration/hat-threads-run-every-frame.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,52 @@ test('edge activated hat thread runs once every frame', t => {
6464
});
6565
});
6666

67+
/**
68+
* When a hat is added it should run in the next frame. Any block related
69+
* caching should be reset.
70+
*/
71+
test('edge activated hat thread runs after being added to previously executed target', t => {
72+
const vm = new VirtualMachine();
73+
vm.attachStorage(makeTestStorage());
74+
75+
// Start VM, load project, and run
76+
t.doesNotThrow(() => {
77+
// Note: don't run vm.start(), we handle calling _step() manually in this test
78+
vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
79+
vm.clear();
80+
vm.setCompatibilityMode(false);
81+
vm.setTurboMode(false);
82+
83+
vm.loadProject(project).then(() => {
84+
t.equal(vm.runtime.threads.length, 0);
85+
86+
vm.runtime._step();
87+
let threads = vm.runtime._lastStepDoneThreads;
88+
t.equal(vm.runtime.threads.length, 0);
89+
t.equal(threads.length, 1);
90+
checkIsHatThread(t, vm, threads[0]);
91+
t.assert(threads[0].status === Thread.STATUS_DONE);
92+
93+
// Add a second hat that should create a second thread
94+
const hatBlock = threads[0].target.blocks.getBlock(threads[0].topBlock);
95+
threads[0].target.blocks.createBlock(Object.assign(
96+
{}, hatBlock, {id: 'hatblock2', next: null}
97+
));
98+
99+
// Check that the hat thread is added again when another step is taken
100+
vm.runtime._step();
101+
threads = vm.runtime._lastStepDoneThreads;
102+
t.equal(vm.runtime.threads.length, 0);
103+
t.equal(threads.length, 2);
104+
checkIsHatThread(t, vm, threads[0]);
105+
checkIsHatThread(t, vm, threads[1]);
106+
t.assert(threads[0].status === Thread.STATUS_DONE);
107+
t.assert(threads[1].status === Thread.STATUS_DONE);
108+
t.end();
109+
});
110+
});
111+
});
112+
67113
/**
68114
* If the hat doesn't finish evaluating within one frame, it shouldn't be added again
69115
* on the next frame. (We skip execution by setting the step time to 0)

0 commit comments

Comments
 (0)