@scratch/scratch-vm 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
// Track loading time with timestamps and if possible the performance api.
|
|
2
|
+
if (window.performance) {
|
|
3
|
+
// Mark with the performance API when benchmark.js and its dependecies start
|
|
4
|
+
// evaluation. This can tell us once measured how long the code spends time
|
|
5
|
+
// turning into execution code for the first time. Skipping evaluation of
|
|
6
|
+
// some of the code can help us make it faster.
|
|
7
|
+
performance.mark('Scratch.EvalStart');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class LoadingMiddleware {
|
|
11
|
+
constructor () {
|
|
12
|
+
this.middleware = [];
|
|
13
|
+
this.host = null;
|
|
14
|
+
this.original = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
install (host, original) {
|
|
18
|
+
this.host = host;
|
|
19
|
+
this.original = original;
|
|
20
|
+
const {middleware} = this;
|
|
21
|
+
return function (...args) {
|
|
22
|
+
let i = 0;
|
|
23
|
+
const next = function (_args) {
|
|
24
|
+
if (i >= middleware.length) {
|
|
25
|
+
return original.call(host, ..._args);
|
|
26
|
+
}
|
|
27
|
+
return middleware[i++](_args, next);
|
|
28
|
+
};
|
|
29
|
+
return next(args);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
push (middleware) {
|
|
34
|
+
this.middleware.push(middleware);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const importLoadCostume = require('../import/load-costume');
|
|
39
|
+
const costumeMiddleware = new LoadingMiddleware();
|
|
40
|
+
importLoadCostume.loadCostume = costumeMiddleware.install(importLoadCostume, importLoadCostume.loadCostume);
|
|
41
|
+
|
|
42
|
+
const importLoadSound = require('../import/load-sound');
|
|
43
|
+
const soundMiddleware = new LoadingMiddleware();
|
|
44
|
+
importLoadSound.loadSound = soundMiddleware.install(importLoadSound, importLoadSound.loadSound);
|
|
45
|
+
|
|
46
|
+
const ScratchStorage = require('scratch-storage').ScratchStorage;
|
|
47
|
+
const VirtualMachine = require('..');
|
|
48
|
+
const Runtime = require('../engine/runtime');
|
|
49
|
+
|
|
50
|
+
const ScratchRender = require('@scratch/scratch-render');
|
|
51
|
+
const AudioEngine = require('scratch-audio');
|
|
52
|
+
const ScratchSVGRenderer = require('@scratch/scratch-svg-renderer');
|
|
53
|
+
|
|
54
|
+
const Scratch = window.Scratch = window.Scratch || {};
|
|
55
|
+
|
|
56
|
+
const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu/';
|
|
57
|
+
const PROJECT_SERVER = 'https://cdn.projects.scratch.mit.edu/';
|
|
58
|
+
|
|
59
|
+
const SLOW = .1;
|
|
60
|
+
|
|
61
|
+
const projectInput = document.querySelector('input');
|
|
62
|
+
|
|
63
|
+
document.querySelector('.run')
|
|
64
|
+
.addEventListener('click', () => {
|
|
65
|
+
window.location.hash = projectInput.value;
|
|
66
|
+
location.reload();
|
|
67
|
+
}, false);
|
|
68
|
+
|
|
69
|
+
const setShareLink = function (json) {
|
|
70
|
+
document.querySelector('.share')
|
|
71
|
+
.href = `#view/${btoa(JSON.stringify(json))}`;
|
|
72
|
+
document.querySelectorAll('.share')[1]
|
|
73
|
+
.href = `suite.html`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const loadProject = function () {
|
|
77
|
+
let id = location.hash.substring(1).split(',')[0];
|
|
78
|
+
if (id.length < 1 || !isFinite(id)) {
|
|
79
|
+
id = projectInput.value;
|
|
80
|
+
}
|
|
81
|
+
Scratch.vm.downloadProjectId(id);
|
|
82
|
+
return id;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {Asset} asset - calculate a URL for this asset.
|
|
87
|
+
* @returns {string} a URL to download a project file.
|
|
88
|
+
*/
|
|
89
|
+
const getProjectUrl = function (asset) {
|
|
90
|
+
const assetIdParts = asset.assetId.split('.');
|
|
91
|
+
const assetUrlParts = [PROJECT_SERVER, 'internalapi/project/', assetIdParts[0], '/get/'];
|
|
92
|
+
if (assetIdParts[1]) {
|
|
93
|
+
assetUrlParts.push(assetIdParts[1]);
|
|
94
|
+
}
|
|
95
|
+
return assetUrlParts.join('');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {Asset} asset - calculate a URL for this asset.
|
|
100
|
+
* @returns {string} a URL to download a project asset (PNG, WAV, etc.)
|
|
101
|
+
*/
|
|
102
|
+
const getAssetUrl = function (asset) {
|
|
103
|
+
const assetUrlParts = [
|
|
104
|
+
ASSET_SERVER,
|
|
105
|
+
'internalapi/asset/',
|
|
106
|
+
asset.assetId,
|
|
107
|
+
'.',
|
|
108
|
+
asset.dataFormat,
|
|
109
|
+
'/get/'
|
|
110
|
+
];
|
|
111
|
+
return assetUrlParts.join('');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
class LoadingProgress {
|
|
115
|
+
constructor (callback) {
|
|
116
|
+
this.dataLoaded = 0;
|
|
117
|
+
this.contentTotal = 0;
|
|
118
|
+
this.contentComplete = 0;
|
|
119
|
+
this.hydrateTotal = 0;
|
|
120
|
+
this.hydrateComplete = 0;
|
|
121
|
+
this.memoryCurrent = 0;
|
|
122
|
+
this.memoryPeak = 0;
|
|
123
|
+
this.callback = callback;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sampleMemory () {
|
|
127
|
+
if (window.performance && window.performance.memory) {
|
|
128
|
+
this.memoryCurrent = window.performance.memory.usedJSHeapSize;
|
|
129
|
+
this.memoryPeak = Math.max(this.memoryCurrent, this.memoryPeak);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
attachHydrateMiddleware (middleware) {
|
|
134
|
+
const _this = this;
|
|
135
|
+
middleware.push((args, next) => {
|
|
136
|
+
_this.hydrateTotal += 1;
|
|
137
|
+
_this.sampleMemory();
|
|
138
|
+
_this.callback(_this);
|
|
139
|
+
return Promise.resolve(next(args))
|
|
140
|
+
.then(value => {
|
|
141
|
+
_this.hydrateComplete += 1;
|
|
142
|
+
_this.sampleMemory();
|
|
143
|
+
_this.callback(_this);
|
|
144
|
+
return value;
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
on (storage, vm) {
|
|
150
|
+
const _this = this;
|
|
151
|
+
|
|
152
|
+
this.attachHydrateMiddleware(costumeMiddleware);
|
|
153
|
+
this.attachHydrateMiddleware(soundMiddleware);
|
|
154
|
+
|
|
155
|
+
const _load = storage.webHelper.load;
|
|
156
|
+
storage.webHelper.load = function (...args) {
|
|
157
|
+
if (_this.dataLoaded === 0 && window.performance) {
|
|
158
|
+
// Mark in browser inspectors how long it takes to load the
|
|
159
|
+
// projects initial data file.
|
|
160
|
+
performance.mark('Scratch.LoadDataStart');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = _load.call(this, ...args);
|
|
164
|
+
|
|
165
|
+
if (_this.dataLoaded) {
|
|
166
|
+
if (_this.contentTotal === 0 && window.performance) {
|
|
167
|
+
performance.mark('Scratch.DownloadStart');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_this.contentTotal += 1;
|
|
171
|
+
}
|
|
172
|
+
_this.sampleMemory();
|
|
173
|
+
_this.callback(_this);
|
|
174
|
+
|
|
175
|
+
result.then(() => {
|
|
176
|
+
if (_this.dataLoaded === 0) {
|
|
177
|
+
if (window.performance) {
|
|
178
|
+
// How long did loading the data file take?
|
|
179
|
+
performance.mark('Scratch.LoadDataEnd');
|
|
180
|
+
performance.measure('Scratch.LoadData', 'Scratch.LoadDataStart', 'Scratch.LoadDataEnd');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_this.dataLoaded = 1;
|
|
184
|
+
|
|
185
|
+
window.ScratchVMLoadDataEnd = Date.now();
|
|
186
|
+
} else {
|
|
187
|
+
_this.contentComplete += 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (_this.contentComplete && _this.contentComplete === _this.contentTotal) {
|
|
191
|
+
if (window.performance) {
|
|
192
|
+
// How long did it take to download the html, js, and
|
|
193
|
+
// all the project assets?
|
|
194
|
+
performance.mark('Scratch.DownloadEnd');
|
|
195
|
+
performance.measure('Scratch.Download', 'Scratch.DownloadStart', 'Scratch.DownloadEnd');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
window.ScratchVMDownloadEnd = Date.now();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
_this.sampleMemory();
|
|
202
|
+
_this.callback(_this);
|
|
203
|
+
});
|
|
204
|
+
return result;
|
|
205
|
+
};
|
|
206
|
+
vm.runtime.on(Runtime.PROJECT_LOADED, () => {
|
|
207
|
+
// Currently LoadingProgress tracks when the data has been loaded
|
|
208
|
+
// and not when the data has been decoded. It may be difficult to
|
|
209
|
+
// track that but it isn't hard to track when its all been decoded.
|
|
210
|
+
if (window.performance) {
|
|
211
|
+
// How long did it take to load and hydrate the html, js, and
|
|
212
|
+
// all the project assets?
|
|
213
|
+
performance.mark('Scratch.LoadEnd');
|
|
214
|
+
performance.measure('Scratch.Load', 'Scratch.LoadStart', 'Scratch.LoadEnd');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
window.ScratchVMLoadEnd = Date.now();
|
|
218
|
+
|
|
219
|
+
// With this event lets update LoadingProgress a final time so its
|
|
220
|
+
// displayed loading time is accurate.
|
|
221
|
+
_this.sampleMemory();
|
|
222
|
+
_this.callback(_this);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class StatTable {
|
|
228
|
+
constructor ({table, keys, viewOf, isSlow}) {
|
|
229
|
+
this.table = table;
|
|
230
|
+
if (keys) {
|
|
231
|
+
this.keys = keys;
|
|
232
|
+
}
|
|
233
|
+
if (viewOf) {
|
|
234
|
+
this.viewOf = viewOf;
|
|
235
|
+
}
|
|
236
|
+
if (isSlow) {
|
|
237
|
+
this.isSlow = isSlow;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
render () {
|
|
242
|
+
const table = this.table;
|
|
243
|
+
Array.from(table.children)
|
|
244
|
+
.forEach(node => table.removeChild(node));
|
|
245
|
+
const keys = this.keys();
|
|
246
|
+
for (const key of keys) {
|
|
247
|
+
this.viewOf(key).render({
|
|
248
|
+
table,
|
|
249
|
+
isSlow: frame => this.isSlow(key, frame)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class StatView {
|
|
256
|
+
constructor (name) {
|
|
257
|
+
this.name = name;
|
|
258
|
+
this.executions = 0;
|
|
259
|
+
this.selfTime = 0;
|
|
260
|
+
this.totalTime = 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
update (selfTime, totalTime, count) {
|
|
264
|
+
this.executions += count;
|
|
265
|
+
this.selfTime += selfTime;
|
|
266
|
+
this.totalTime += totalTime;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
render ({table, isSlow}) {
|
|
270
|
+
const row = document.createElement('tr');
|
|
271
|
+
let cell = document.createElement('td');
|
|
272
|
+
cell.innerText = this.name;
|
|
273
|
+
row.appendChild(cell);
|
|
274
|
+
|
|
275
|
+
if (isSlow(this)) {
|
|
276
|
+
row.setAttribute('class', 'slow');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
cell = document.createElement('td');
|
|
280
|
+
cell.style.textAlign = 'right';
|
|
281
|
+
cell.innerText = '---';
|
|
282
|
+
// Truncate selfTime. Value past the microsecond are floating point
|
|
283
|
+
// noise.
|
|
284
|
+
this.selfTime = Math.floor(this.selfTime * 1000) / 1000;
|
|
285
|
+
if (this.selfTime > 0) {
|
|
286
|
+
cell.innerText = (this.selfTime / 1000).toFixed(3);
|
|
287
|
+
}
|
|
288
|
+
row.appendChild(cell);
|
|
289
|
+
|
|
290
|
+
cell = document.createElement('td');
|
|
291
|
+
cell.style.textAlign = 'right';
|
|
292
|
+
cell.innerText = '---';
|
|
293
|
+
// Truncate totalTime. Value past the microsecond are floating point
|
|
294
|
+
// noise.
|
|
295
|
+
this.totalTime = Math.floor(this.totalTime * 1000) / 1000;
|
|
296
|
+
if (this.totalTime > 0) {
|
|
297
|
+
cell.innerText = (this.totalTime / 1000).toFixed(3);
|
|
298
|
+
}
|
|
299
|
+
row.appendChild(cell);
|
|
300
|
+
|
|
301
|
+
cell = document.createElement('td');
|
|
302
|
+
cell.style.textAlign = 'right';
|
|
303
|
+
cell.innerText = this.executions;
|
|
304
|
+
row.appendChild(cell);
|
|
305
|
+
|
|
306
|
+
table.appendChild(row);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
class RunningStats {
|
|
311
|
+
constructor (profiler) {
|
|
312
|
+
this.stepThreadsInnerId = profiler.idByName('Sequencer.stepThreads#inner');
|
|
313
|
+
this.blockFunctionId = profiler.idByName('blockFunction');
|
|
314
|
+
this.stpeThreadsId = profiler.idByName('Sequencer.stepThreads');
|
|
315
|
+
|
|
316
|
+
this.recordedTime = 0;
|
|
317
|
+
this.executed = {
|
|
318
|
+
steps: 0,
|
|
319
|
+
blocks: 0
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
update (id, arg, selfTime, totalTime, count) {
|
|
324
|
+
if (id === this.stpeThreadsId) {
|
|
325
|
+
this.recordedTime += totalTime;
|
|
326
|
+
} else if (id === this.stepThreadsInnerId) {
|
|
327
|
+
this.executed.steps += count;
|
|
328
|
+
} else if (id === this.blockFunctionId) {
|
|
329
|
+
this.executed.blocks += count;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const WORK_TIME = 0.75;
|
|
335
|
+
|
|
336
|
+
class RunningStatsView {
|
|
337
|
+
constructor ({runningStats, maxRecordedTime, dom}) {
|
|
338
|
+
this.recordedTimeDom =
|
|
339
|
+
dom.getElementsByClassName('profile-count-amount-recorded')[0];
|
|
340
|
+
this.stepsLoopedDom =
|
|
341
|
+
dom.getElementsByClassName('profile-count-steps-looped')[0];
|
|
342
|
+
this.blocksExecutedDom =
|
|
343
|
+
dom.getElementsByClassName('profile-count-blocks-executed')[0];
|
|
344
|
+
|
|
345
|
+
this.maxRecordedTime = maxRecordedTime;
|
|
346
|
+
this.maxWorkedTime = maxRecordedTime * WORK_TIME;
|
|
347
|
+
this.runningStats = runningStats;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
render () {
|
|
351
|
+
const {
|
|
352
|
+
runningStats,
|
|
353
|
+
recordedTimeDom,
|
|
354
|
+
stepsLoopedDom,
|
|
355
|
+
blocksExecutedDom
|
|
356
|
+
} = this;
|
|
357
|
+
const {executed} = runningStats;
|
|
358
|
+
const fractionWorked = runningStats.recordedTime / this.maxWorkedTime;
|
|
359
|
+
recordedTimeDom.innerText = `${(fractionWorked * 100).toFixed(1)} %`;
|
|
360
|
+
stepsLoopedDom.innerText = executed.steps;
|
|
361
|
+
blocksExecutedDom.innerText = executed.blocks;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
class Frames {
|
|
366
|
+
constructor (profiler) {
|
|
367
|
+
this.profiler = profiler;
|
|
368
|
+
|
|
369
|
+
this.frames = [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
update (id, arg, selfTime, totalTime, count) {
|
|
373
|
+
if (id < 0) return;
|
|
374
|
+
if (!this.frames[id]) {
|
|
375
|
+
this.frames[id] = new StatView(this.profiler.nameById(id));
|
|
376
|
+
}
|
|
377
|
+
this.frames[id].update(selfTime, totalTime, count);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const frameOrder = [
|
|
382
|
+
'blockFunction',
|
|
383
|
+
'execute',
|
|
384
|
+
'Sequencer.stepThread',
|
|
385
|
+
'Sequencer.stepThreads#inner',
|
|
386
|
+
'Sequencer.stepThreads',
|
|
387
|
+
'RenderWebGL.draw',
|
|
388
|
+
'Runtime._step'
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
class FramesTable extends StatTable {
|
|
392
|
+
constructor (options) {
|
|
393
|
+
super(options);
|
|
394
|
+
|
|
395
|
+
this.profiler = options.profiler;
|
|
396
|
+
this.frames = options.frames;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
keys () {
|
|
400
|
+
const keys = Object.keys(this.frames.frames)
|
|
401
|
+
.map(id => this.profiler.nameById(Number(id)));
|
|
402
|
+
keys.sort((a, b) => frameOrder.indexOf(a) - frameOrder.indexOf(b));
|
|
403
|
+
return keys;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
viewOf (key) {
|
|
407
|
+
return this.frames.frames[this.profiler.idByName(key)];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
isSlow () {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
class Opcodes {
|
|
416
|
+
constructor (profiler) {
|
|
417
|
+
this.blockFunctionId = profiler.idByName('blockFunction');
|
|
418
|
+
|
|
419
|
+
this.opcodes = {};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
update (id, arg, selfTime, totalTime, count) {
|
|
423
|
+
if (id === this.blockFunctionId) {
|
|
424
|
+
if (!this.opcodes[arg]) {
|
|
425
|
+
this.opcodes[arg] = new StatView(arg);
|
|
426
|
+
}
|
|
427
|
+
this.opcodes[arg].update(selfTime, totalTime, count);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
class OpcodeTable extends StatTable {
|
|
433
|
+
constructor (options) {
|
|
434
|
+
super(options);
|
|
435
|
+
|
|
436
|
+
this.profiler = options.profiler;
|
|
437
|
+
this.opcodes = options.opcodes;
|
|
438
|
+
this.frames = options.frames;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
keys () {
|
|
442
|
+
const keys = Object.keys(this.opcodes.opcodes);
|
|
443
|
+
keys.sort();
|
|
444
|
+
return keys;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
viewOf (key) {
|
|
448
|
+
return this.opcodes.opcodes[key];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
isSlow (key) {
|
|
452
|
+
const blockFunctionTotalTime = this.frames.frames[this.profiler.idByName('blockFunction')].totalTime;
|
|
453
|
+
const rowTotalTime = this.opcodes.opcodes[key].totalTime;
|
|
454
|
+
const percentOfRun = rowTotalTime / blockFunctionTotalTime;
|
|
455
|
+
return percentOfRun > SLOW;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
class ProfilerRun {
|
|
460
|
+
constructor ({vm, maxRecordedTime, warmUpTime}) {
|
|
461
|
+
this.vm = vm;
|
|
462
|
+
this.maxRecordedTime = maxRecordedTime;
|
|
463
|
+
this.warmUpTime = warmUpTime;
|
|
464
|
+
|
|
465
|
+
vm.runtime.enableProfiling();
|
|
466
|
+
const profiler = this.profiler = vm.runtime.profiler;
|
|
467
|
+
vm.runtime.profiler = null;
|
|
468
|
+
|
|
469
|
+
const runningStats = this.runningStats = new RunningStats(profiler);
|
|
470
|
+
const runningStatsView = this.runningStatsView = new RunningStatsView({
|
|
471
|
+
dom: document.getElementsByClassName('profile-count-group')[0],
|
|
472
|
+
|
|
473
|
+
runningStats,
|
|
474
|
+
maxRecordedTime
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const frames = this.frames = new Frames(profiler);
|
|
478
|
+
this.frameTable = new FramesTable({
|
|
479
|
+
table: document
|
|
480
|
+
.getElementsByClassName('profile-count-frame-table')[0]
|
|
481
|
+
.getElementsByTagName('tbody')[0],
|
|
482
|
+
|
|
483
|
+
profiler,
|
|
484
|
+
frames
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const opcodes = this.opcodes = new Opcodes(profiler);
|
|
488
|
+
this.opcodeTable = new OpcodeTable({
|
|
489
|
+
table: document
|
|
490
|
+
.getElementsByClassName('profile-count-opcode-table')[0]
|
|
491
|
+
.getElementsByTagName('tbody')[0],
|
|
492
|
+
|
|
493
|
+
profiler,
|
|
494
|
+
opcodes,
|
|
495
|
+
frames
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const stepId = profiler.idByName('Runtime._step');
|
|
499
|
+
profiler.onFrame = ({id, arg, selfTime, totalTime, count}) => {
|
|
500
|
+
if (id === stepId) {
|
|
501
|
+
runningStatsView.render();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
runningStats.update(id, arg, selfTime, totalTime, count);
|
|
505
|
+
opcodes.update(id, arg, selfTime, totalTime, count);
|
|
506
|
+
frames.update(id, arg, selfTime, totalTime, count);
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
run () {
|
|
511
|
+
this.projectId = loadProject();
|
|
512
|
+
|
|
513
|
+
window.parent.postMessage({
|
|
514
|
+
type: 'BENCH_MESSAGE_LOADING'
|
|
515
|
+
}, '*');
|
|
516
|
+
|
|
517
|
+
this.vm.on('workspaceUpdate', () => {
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
window.parent.postMessage({
|
|
520
|
+
type: 'BENCH_MESSAGE_WARMING_UP'
|
|
521
|
+
}, '*');
|
|
522
|
+
this.vm.greenFlag();
|
|
523
|
+
}, 100);
|
|
524
|
+
setTimeout(() => {
|
|
525
|
+
window.parent.postMessage({
|
|
526
|
+
type: 'BENCH_MESSAGE_ACTIVE'
|
|
527
|
+
}, '*');
|
|
528
|
+
this.vm.runtime.profiler = this.profiler;
|
|
529
|
+
}, 100 + this.warmUpTime);
|
|
530
|
+
setTimeout(() => {
|
|
531
|
+
this.vm.stopAll();
|
|
532
|
+
clearTimeout(this.vm.runtime._steppingInterval);
|
|
533
|
+
this.vm.runtime.profiler = null;
|
|
534
|
+
|
|
535
|
+
this.frameTable.render();
|
|
536
|
+
this.opcodeTable.render();
|
|
537
|
+
|
|
538
|
+
window.parent.postMessage({
|
|
539
|
+
type: 'BENCH_MESSAGE_COMPLETE',
|
|
540
|
+
frames: this.frames.frames,
|
|
541
|
+
opcodes: this.opcodes.opcodes
|
|
542
|
+
}, '*');
|
|
543
|
+
|
|
544
|
+
setShareLink({
|
|
545
|
+
fixture: {
|
|
546
|
+
projectId: this.projectId,
|
|
547
|
+
warmUpTime: this.warmUpTime,
|
|
548
|
+
recordingTime: this.maxRecordedTime
|
|
549
|
+
},
|
|
550
|
+
frames: this.frames.frames,
|
|
551
|
+
opcodes: this.opcodes.opcodes
|
|
552
|
+
});
|
|
553
|
+
}, 100 + this.warmUpTime + this.maxRecordedTime);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
render (json) {
|
|
558
|
+
const {fixture} = json;
|
|
559
|
+
document.querySelector('[type=text]').value = [
|
|
560
|
+
fixture.projectId,
|
|
561
|
+
fixture.warmUpTime,
|
|
562
|
+
fixture.recordingTime
|
|
563
|
+
].join(',');
|
|
564
|
+
|
|
565
|
+
this.frames.frames = json.frames.map(
|
|
566
|
+
frame => Object.assign(new StatView(), frame, {
|
|
567
|
+
name: this.profiler.nameById(this.profiler.idByName(frame.name))
|
|
568
|
+
})
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
this.opcodes.opcodes = {};
|
|
572
|
+
Object.entries(json.opcodes).forEach(([opcode, data]) => {
|
|
573
|
+
this.opcodes.opcodes[opcode] = Object.assign(new StatView(), data);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
this.frameTable.render();
|
|
577
|
+
this.opcodeTable.render();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Run the benchmark with given parameters in the location's hash field or
|
|
583
|
+
* using defaults.
|
|
584
|
+
*/
|
|
585
|
+
const runBenchmark = function () {
|
|
586
|
+
// Lots of global variables to make debugging easier
|
|
587
|
+
// Instantiate the VM.
|
|
588
|
+
const vm = new VirtualMachine();
|
|
589
|
+
Scratch.vm = vm;
|
|
590
|
+
|
|
591
|
+
vm.setTurboMode(true);
|
|
592
|
+
|
|
593
|
+
const storage = new ScratchStorage();
|
|
594
|
+
const AssetType = storage.AssetType;
|
|
595
|
+
storage.addWebSource([AssetType.Project], getProjectUrl);
|
|
596
|
+
storage.addWebSource([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
|
|
597
|
+
vm.attachStorage(storage);
|
|
598
|
+
|
|
599
|
+
new LoadingProgress(progress => {
|
|
600
|
+
const setElement = (name, value) => {
|
|
601
|
+
document.getElementsByClassName(name)[0].innerText = value;
|
|
602
|
+
};
|
|
603
|
+
const sinceLoadStart = key => (
|
|
604
|
+
`(${(window[key] || Date.now()) - window.ScratchVMLoadStart}ms)`
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
setElement('loading-total', 1);
|
|
608
|
+
setElement('loading-complete', progress.dataLoaded);
|
|
609
|
+
setElement('loading-time', sinceLoadStart('ScratchVMLoadDataEnd'));
|
|
610
|
+
|
|
611
|
+
setElement('loading-content-total', progress.contentTotal);
|
|
612
|
+
setElement('loading-content-complete', progress.contentComplete);
|
|
613
|
+
setElement('loading-content-time', sinceLoadStart('ScratchVMDownloadEnd'));
|
|
614
|
+
|
|
615
|
+
setElement('loading-hydrate-total', progress.hydrateTotal);
|
|
616
|
+
setElement('loading-hydrate-complete', progress.hydrateComplete);
|
|
617
|
+
setElement('loading-hydrate-time', sinceLoadStart('ScratchVMLoadEnd'));
|
|
618
|
+
|
|
619
|
+
if (progress.memoryPeak) {
|
|
620
|
+
setElement('loading-memory-current',
|
|
621
|
+
`${(progress.memoryCurrent / 1000000).toFixed(0)}MB`
|
|
622
|
+
);
|
|
623
|
+
setElement('loading-memory-peak',
|
|
624
|
+
`${(progress.memoryPeak / 1000000).toFixed(0)}MB`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}).on(storage, vm);
|
|
628
|
+
|
|
629
|
+
let warmUpTime = 4000;
|
|
630
|
+
let maxRecordedTime = 6000;
|
|
631
|
+
|
|
632
|
+
if (location.hash) {
|
|
633
|
+
const split = location.hash.substring(1).split(',');
|
|
634
|
+
if (split[1] && split[1].length > 0) {
|
|
635
|
+
warmUpTime = Number(split[1]);
|
|
636
|
+
}
|
|
637
|
+
maxRecordedTime = Number(split[2] || '0') || 6000;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
new ProfilerRun({
|
|
641
|
+
vm,
|
|
642
|
+
warmUpTime,
|
|
643
|
+
maxRecordedTime
|
|
644
|
+
}).run();
|
|
645
|
+
|
|
646
|
+
// Instantiate the renderer and connect it to the VM.
|
|
647
|
+
const canvas = document.getElementById('scratch-stage');
|
|
648
|
+
const renderer = new ScratchRender(canvas);
|
|
649
|
+
Scratch.renderer = renderer;
|
|
650
|
+
vm.attachRenderer(renderer);
|
|
651
|
+
const audioEngine = new AudioEngine();
|
|
652
|
+
vm.attachAudioEngine(audioEngine);
|
|
653
|
+
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
|
|
654
|
+
|
|
655
|
+
// Feed mouse events as VM I/O events.
|
|
656
|
+
document.addEventListener('mousemove', e => {
|
|
657
|
+
const rect = canvas.getBoundingClientRect();
|
|
658
|
+
const coordinates = {
|
|
659
|
+
x: e.clientX - rect.left,
|
|
660
|
+
y: e.clientY - rect.top,
|
|
661
|
+
canvasWidth: rect.width,
|
|
662
|
+
canvasHeight: rect.height
|
|
663
|
+
};
|
|
664
|
+
Scratch.vm.postIOData('mouse', coordinates);
|
|
665
|
+
});
|
|
666
|
+
canvas.addEventListener('mousedown', e => {
|
|
667
|
+
const rect = canvas.getBoundingClientRect();
|
|
668
|
+
const data = {
|
|
669
|
+
isDown: true,
|
|
670
|
+
x: e.clientX - rect.left,
|
|
671
|
+
y: e.clientY - rect.top,
|
|
672
|
+
canvasWidth: rect.width,
|
|
673
|
+
canvasHeight: rect.height
|
|
674
|
+
};
|
|
675
|
+
Scratch.vm.postIOData('mouse', data);
|
|
676
|
+
e.preventDefault();
|
|
677
|
+
});
|
|
678
|
+
canvas.addEventListener('mouseup', e => {
|
|
679
|
+
const rect = canvas.getBoundingClientRect();
|
|
680
|
+
const data = {
|
|
681
|
+
isDown: false,
|
|
682
|
+
x: e.clientX - rect.left,
|
|
683
|
+
y: e.clientY - rect.top,
|
|
684
|
+
canvasWidth: rect.width,
|
|
685
|
+
canvasHeight: rect.height
|
|
686
|
+
};
|
|
687
|
+
Scratch.vm.postIOData('mouse', data);
|
|
688
|
+
e.preventDefault();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Feed keyboard events as VM I/O events.
|
|
692
|
+
document.addEventListener('keydown', e => {
|
|
693
|
+
// Don't capture keys intended for Blockly inputs.
|
|
694
|
+
if (e.target !== document && e.target !== document.body) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
Scratch.vm.postIOData('keyboard', {
|
|
698
|
+
keyCode: e.keyCode,
|
|
699
|
+
isDown: true
|
|
700
|
+
});
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
});
|
|
703
|
+
document.addEventListener('keyup', e => {
|
|
704
|
+
// Always capture up events,
|
|
705
|
+
// even those that have switched to other targets.
|
|
706
|
+
Scratch.vm.postIOData('keyboard', {
|
|
707
|
+
keyCode: e.keyCode,
|
|
708
|
+
isDown: false
|
|
709
|
+
});
|
|
710
|
+
// E.g., prevent scroll.
|
|
711
|
+
if (e.target !== document && e.target !== document.body) {
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Run threads
|
|
717
|
+
vm.start();
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Render previously run benchmark data.
|
|
722
|
+
* @param {object} json data from a previous benchmark run.
|
|
723
|
+
*/
|
|
724
|
+
const renderBenchmarkData = function (json) {
|
|
725
|
+
const vm = new VirtualMachine();
|
|
726
|
+
new ProfilerRun({vm}).render(json);
|
|
727
|
+
setShareLink(json);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const onload = function () {
|
|
731
|
+
if (location.hash.substring(1).startsWith('view')) {
|
|
732
|
+
document.body.className = 'render';
|
|
733
|
+
const data = location.hash.substring(6);
|
|
734
|
+
const frozen = atob(data);
|
|
735
|
+
const json = JSON.parse(frozen);
|
|
736
|
+
renderBenchmarkData(json);
|
|
737
|
+
} else {
|
|
738
|
+
runBenchmark();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
window.onhashchange = function () {
|
|
743
|
+
location.reload();
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
if (window.performance) {
|
|
747
|
+
performance.mark('Scratch.EvalEnd');
|
|
748
|
+
performance.measure('Scratch.Eval', 'Scratch.EvalStart', 'Scratch.EvalEnd');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
window.ScratchVMEvalEnd = Date.now();
|
|
752
|
+
|
|
753
|
+
onload();
|