@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.
Files changed (175) hide show
  1. package/.jsdoc.json +20 -0
  2. package/.nvmrc +1 -0
  3. package/CHANGELOG.md +7004 -0
  4. package/LICENSE +12 -0
  5. package/README.md +120 -0
  6. package/TRADEMARK +1 -0
  7. package/commitlint.config.js +4 -0
  8. package/docs/extensions.md +527 -0
  9. package/package.json +103 -0
  10. package/release.config.js +13 -0
  11. package/src/blocks/scratch3_control.js +206 -0
  12. package/src/blocks/scratch3_core_example.js +69 -0
  13. package/src/blocks/scratch3_data.js +257 -0
  14. package/src/blocks/scratch3_event.js +137 -0
  15. package/src/blocks/scratch3_looks.js +612 -0
  16. package/src/blocks/scratch3_motion.js +288 -0
  17. package/src/blocks/scratch3_operators.js +154 -0
  18. package/src/blocks/scratch3_procedures.js +79 -0
  19. package/src/blocks/scratch3_sensing.js +336 -0
  20. package/src/blocks/scratch3_sound.js +350 -0
  21. package/src/dispatch/central-dispatch.js +143 -0
  22. package/src/dispatch/shared-dispatch.js +235 -0
  23. package/src/dispatch/worker-dispatch.js +110 -0
  24. package/src/engine/adapter.js +176 -0
  25. package/src/engine/block-utility.js +242 -0
  26. package/src/engine/blocks-execute-cache.js +19 -0
  27. package/src/engine/blocks-runtime-cache.js +78 -0
  28. package/src/engine/blocks.js +1299 -0
  29. package/src/engine/comment.js +56 -0
  30. package/src/engine/execute.js +580 -0
  31. package/src/engine/monitor-record.js +23 -0
  32. package/src/engine/mutation-adapter.js +48 -0
  33. package/src/engine/profiler.js +390 -0
  34. package/src/engine/runtime.js +2686 -0
  35. package/src/engine/scratch-blocks-constants.js +27 -0
  36. package/src/engine/sequencer.js +361 -0
  37. package/src/engine/stage-layering.js +29 -0
  38. package/src/engine/target.js +799 -0
  39. package/src/engine/thread.js +404 -0
  40. package/src/engine/variable.js +70 -0
  41. package/src/extension-support/argument-type.js +47 -0
  42. package/src/extension-support/block-type.js +50 -0
  43. package/src/extension-support/define-messages.js +18 -0
  44. package/src/extension-support/extension-manager.js +440 -0
  45. package/src/extension-support/extension-metadata.js +64 -0
  46. package/src/extension-support/extension-worker.js +59 -0
  47. package/src/extension-support/reporter-scope.js +18 -0
  48. package/src/extension-support/target-type.js +17 -0
  49. package/src/extensions/scratch3_boost/index.js +2113 -0
  50. package/src/extensions/scratch3_ev3/index.js +1355 -0
  51. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  52. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  53. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  54. package/src/extensions/scratch3_microbit/index.js +984 -0
  55. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  56. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  57. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  58. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  59. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  116. package/src/extensions/scratch3_music/index.js +1333 -0
  117. package/src/extensions/scratch3_music/manifest.js +63 -0
  118. package/src/extensions/scratch3_pen/index.js +770 -0
  119. package/src/extensions/scratch3_speech2text/index.js +700 -0
  120. package/src/extensions/scratch3_text2speech/index.js +766 -0
  121. package/src/extensions/scratch3_translate/index.js +286 -0
  122. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  123. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  124. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  125. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  126. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  127. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  128. package/src/import/load-costume.js +416 -0
  129. package/src/import/load-sound.js +116 -0
  130. package/src/index.js +10 -0
  131. package/src/io/ble.js +256 -0
  132. package/src/io/bt.js +202 -0
  133. package/src/io/clock.js +39 -0
  134. package/src/io/cloud.js +169 -0
  135. package/src/io/keyboard.js +146 -0
  136. package/src/io/mouse.js +145 -0
  137. package/src/io/mouseWheel.js +28 -0
  138. package/src/io/userData.js +24 -0
  139. package/src/io/video.js +210 -0
  140. package/src/playground/benchmark.css +83 -0
  141. package/src/playground/benchmark.js +753 -0
  142. package/src/playground/index.html +111 -0
  143. package/src/playground/suite.css +78 -0
  144. package/src/playground/suite.html +25 -0
  145. package/src/playground/suite.js +544 -0
  146. package/src/playground/video-sensing.html +18 -0
  147. package/src/playground/video-sensing.js +133 -0
  148. package/src/serialization/deserialize-assets.js +178 -0
  149. package/src/serialization/sb2.js +1295 -0
  150. package/src/serialization/sb2_specmap.js +1818 -0
  151. package/src/serialization/sb3.js +1320 -0
  152. package/src/serialization/serialize-assets.js +60 -0
  153. package/src/sprites/rendered-target.js +1115 -0
  154. package/src/sprites/sprite.js +177 -0
  155. package/src/util/base64-util.js +48 -0
  156. package/src/util/cast.js +218 -0
  157. package/src/util/clone.js +17 -0
  158. package/src/util/color.js +204 -0
  159. package/src/util/fetch-with-timeout.js +53 -0
  160. package/src/util/get-monitor-id.js +33 -0
  161. package/src/util/jsonrpc.js +114 -0
  162. package/src/util/log.js +4 -0
  163. package/src/util/math-util.js +122 -0
  164. package/src/util/maybe-format-message.js +18 -0
  165. package/src/util/new-block-ids.js +33 -0
  166. package/src/util/rateLimiter.js +73 -0
  167. package/src/util/scratch-link-websocket.js +133 -0
  168. package/src/util/string-util.js +94 -0
  169. package/src/util/task-queue.js +203 -0
  170. package/src/util/timer.js +103 -0
  171. package/src/util/uid.js +29 -0
  172. package/src/util/variable-util.js +48 -0
  173. package/src/util/xml-escape.js +33 -0
  174. package/src/virtual-machine.js +1586 -0
  175. 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();