mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 15:02:52 -05:00
753 lines
23 KiB
JavaScript
753 lines
23 KiB
JavaScript
// Track loading time with timestamps and if possible the performance api.
|
|
if (window.performance) {
|
|
// Mark with the performance API when benchmark.js and its dependecies start
|
|
// evaluation. This can tell us once measured how long the code spends time
|
|
// turning into execution code for the first time. Skipping evaluation of
|
|
// some of the code can help us make it faster.
|
|
performance.mark('Scratch.EvalStart');
|
|
}
|
|
|
|
class LoadingMiddleware {
|
|
constructor () {
|
|
this.middleware = [];
|
|
this.host = null;
|
|
this.original = null;
|
|
}
|
|
|
|
install (host, original) {
|
|
this.host = host;
|
|
this.original = original;
|
|
const {middleware} = this;
|
|
return function (...args) {
|
|
let i = 0;
|
|
const next = function (_args) {
|
|
if (i >= middleware.length) {
|
|
return original.call(host, ..._args);
|
|
}
|
|
return middleware[i++](_args, next);
|
|
};
|
|
return next(args);
|
|
};
|
|
}
|
|
|
|
push (middleware) {
|
|
this.middleware.push(middleware);
|
|
}
|
|
}
|
|
|
|
const importLoadCostume = require('../import/load-costume');
|
|
const costumeMiddleware = new LoadingMiddleware();
|
|
importLoadCostume.loadCostume = costumeMiddleware.install(importLoadCostume, importLoadCostume.loadCostume);
|
|
|
|
const importLoadSound = require('../import/load-sound');
|
|
const soundMiddleware = new LoadingMiddleware();
|
|
importLoadSound.loadSound = soundMiddleware.install(importLoadSound, importLoadSound.loadSound);
|
|
|
|
const ScratchStorage = require('scratch-storage');
|
|
const VirtualMachine = require('..');
|
|
const Runtime = require('../engine/runtime');
|
|
|
|
const ScratchRender = require('scratch-render');
|
|
const AudioEngine = require('scratch-audio');
|
|
const ScratchSVGRenderer = require('scratch-svg-renderer');
|
|
|
|
const Scratch = window.Scratch = window.Scratch || {};
|
|
|
|
const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu/';
|
|
const PROJECT_SERVER = 'https://cdn.projects.scratch.mit.edu/';
|
|
|
|
const SLOW = .1;
|
|
|
|
const projectInput = document.querySelector('input');
|
|
|
|
document.querySelector('.run')
|
|
.addEventListener('click', () => {
|
|
window.location.hash = projectInput.value;
|
|
location.reload();
|
|
}, false);
|
|
|
|
const setShareLink = function (json) {
|
|
document.querySelector('.share')
|
|
.href = `#view/${btoa(JSON.stringify(json))}`;
|
|
document.querySelectorAll('.share')[1]
|
|
.href = `suite.html`;
|
|
};
|
|
|
|
const loadProject = function () {
|
|
let id = location.hash.substring(1).split(',')[0];
|
|
if (id.length < 1 || !isFinite(id)) {
|
|
id = projectInput.value;
|
|
}
|
|
Scratch.vm.downloadProjectId(id);
|
|
return id;
|
|
};
|
|
|
|
/**
|
|
* @param {Asset} asset - calculate a URL for this asset.
|
|
* @returns {string} a URL to download a project file.
|
|
*/
|
|
const getProjectUrl = function (asset) {
|
|
const assetIdParts = asset.assetId.split('.');
|
|
const assetUrlParts = [PROJECT_SERVER, 'internalapi/project/', assetIdParts[0], '/get/'];
|
|
if (assetIdParts[1]) {
|
|
assetUrlParts.push(assetIdParts[1]);
|
|
}
|
|
return assetUrlParts.join('');
|
|
};
|
|
|
|
/**
|
|
* @param {Asset} asset - calculate a URL for this asset.
|
|
* @returns {string} a URL to download a project asset (PNG, WAV, etc.)
|
|
*/
|
|
const getAssetUrl = function (asset) {
|
|
const assetUrlParts = [
|
|
ASSET_SERVER,
|
|
'internalapi/asset/',
|
|
asset.assetId,
|
|
'.',
|
|
asset.dataFormat,
|
|
'/get/'
|
|
];
|
|
return assetUrlParts.join('');
|
|
};
|
|
|
|
class LoadingProgress {
|
|
constructor (callback) {
|
|
this.dataLoaded = 0;
|
|
this.contentTotal = 0;
|
|
this.contentComplete = 0;
|
|
this.hydrateTotal = 0;
|
|
this.hydrateComplete = 0;
|
|
this.memoryCurrent = 0;
|
|
this.memoryPeak = 0;
|
|
this.callback = callback;
|
|
}
|
|
|
|
sampleMemory () {
|
|
if (window.performance && window.performance.memory) {
|
|
this.memoryCurrent = window.performance.memory.usedJSHeapSize;
|
|
this.memoryPeak = Math.max(this.memoryCurrent, this.memoryPeak);
|
|
}
|
|
}
|
|
|
|
attachHydrateMiddleware (middleware) {
|
|
const _this = this;
|
|
middleware.push((args, next) => {
|
|
_this.hydrateTotal += 1;
|
|
_this.sampleMemory();
|
|
_this.callback(_this);
|
|
return Promise.resolve(next(args))
|
|
.then(value => {
|
|
_this.hydrateComplete += 1;
|
|
_this.sampleMemory();
|
|
_this.callback(_this);
|
|
return value;
|
|
});
|
|
});
|
|
}
|
|
|
|
on (storage, vm) {
|
|
const _this = this;
|
|
|
|
this.attachHydrateMiddleware(costumeMiddleware);
|
|
this.attachHydrateMiddleware(soundMiddleware);
|
|
|
|
const _load = storage.webHelper.load;
|
|
storage.webHelper.load = function (...args) {
|
|
if (_this.dataLoaded === 0 && window.performance) {
|
|
// Mark in browser inspectors how long it takes to load the
|
|
// projects initial data file.
|
|
performance.mark('Scratch.LoadDataStart');
|
|
}
|
|
|
|
const result = _load.call(this, ...args);
|
|
|
|
if (_this.dataLoaded) {
|
|
if (_this.contentTotal === 0 && window.performance) {
|
|
performance.mark('Scratch.DownloadStart');
|
|
}
|
|
|
|
_this.contentTotal += 1;
|
|
}
|
|
_this.sampleMemory();
|
|
_this.callback(_this);
|
|
|
|
result.then(() => {
|
|
if (_this.dataLoaded === 0) {
|
|
if (window.performance) {
|
|
// How long did loading the data file take?
|
|
performance.mark('Scratch.LoadDataEnd');
|
|
performance.measure('Scratch.LoadData', 'Scratch.LoadDataStart', 'Scratch.LoadDataEnd');
|
|
}
|
|
|
|
_this.dataLoaded = 1;
|
|
|
|
window.ScratchVMLoadDataEnd = Date.now();
|
|
} else {
|
|
_this.contentComplete += 1;
|
|
}
|
|
|
|
if (_this.contentComplete && _this.contentComplete === _this.contentTotal) {
|
|
if (window.performance) {
|
|
// How long did it take to download the html, js, and
|
|
// all the project assets?
|
|
performance.mark('Scratch.DownloadEnd');
|
|
performance.measure('Scratch.Download', 'Scratch.DownloadStart', 'Scratch.DownloadEnd');
|
|
}
|
|
|
|
window.ScratchVMDownloadEnd = Date.now();
|
|
}
|
|
|
|
_this.sampleMemory();
|
|
_this.callback(_this);
|
|
});
|
|
return result;
|
|
};
|
|
vm.runtime.on(Runtime.PROJECT_LOADED, () => {
|
|
// Currently LoadingProgress tracks when the data has been loaded
|
|
// and not when the data has been decoded. It may be difficult to
|
|
// track that but it isn't hard to track when its all been decoded.
|
|
if (window.performance) {
|
|
// How long did it take to load and hydrate the html, js, and
|
|
// all the project assets?
|
|
performance.mark('Scratch.LoadEnd');
|
|
performance.measure('Scratch.Load', 'Scratch.LoadStart', 'Scratch.LoadEnd');
|
|
}
|
|
|
|
window.ScratchVMLoadEnd = Date.now();
|
|
|
|
// With this event lets update LoadingProgress a final time so its
|
|
// displayed loading time is accurate.
|
|
_this.sampleMemory();
|
|
_this.callback(_this);
|
|
});
|
|
}
|
|
}
|
|
|
|
class StatTable {
|
|
constructor ({table, keys, viewOf, isSlow}) {
|
|
this.table = table;
|
|
if (keys) {
|
|
this.keys = keys;
|
|
}
|
|
if (viewOf) {
|
|
this.viewOf = viewOf;
|
|
}
|
|
if (isSlow) {
|
|
this.isSlow = isSlow;
|
|
}
|
|
}
|
|
|
|
render () {
|
|
const table = this.table;
|
|
Array.from(table.children)
|
|
.forEach(node => table.removeChild(node));
|
|
const keys = this.keys();
|
|
for (const key of keys) {
|
|
this.viewOf(key).render({
|
|
table,
|
|
isSlow: frame => this.isSlow(key, frame)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class StatView {
|
|
constructor (name) {
|
|
this.name = name;
|
|
this.executions = 0;
|
|
this.selfTime = 0;
|
|
this.totalTime = 0;
|
|
}
|
|
|
|
update (selfTime, totalTime, count) {
|
|
this.executions += count;
|
|
this.selfTime += selfTime;
|
|
this.totalTime += totalTime;
|
|
}
|
|
|
|
render ({table, isSlow}) {
|
|
const row = document.createElement('tr');
|
|
let cell = document.createElement('td');
|
|
cell.innerText = this.name;
|
|
row.appendChild(cell);
|
|
|
|
if (isSlow(this)) {
|
|
row.setAttribute('class', 'slow');
|
|
}
|
|
|
|
cell = document.createElement('td');
|
|
cell.style.textAlign = 'right';
|
|
cell.innerText = '---';
|
|
// Truncate selfTime. Value past the microsecond are floating point
|
|
// noise.
|
|
this.selfTime = Math.floor(this.selfTime * 1000) / 1000;
|
|
if (this.selfTime > 0) {
|
|
cell.innerText = (this.selfTime / 1000).toFixed(3);
|
|
}
|
|
row.appendChild(cell);
|
|
|
|
cell = document.createElement('td');
|
|
cell.style.textAlign = 'right';
|
|
cell.innerText = '---';
|
|
// Truncate totalTime. Value past the microsecond are floating point
|
|
// noise.
|
|
this.totalTime = Math.floor(this.totalTime * 1000) / 1000;
|
|
if (this.totalTime > 0) {
|
|
cell.innerText = (this.totalTime / 1000).toFixed(3);
|
|
}
|
|
row.appendChild(cell);
|
|
|
|
cell = document.createElement('td');
|
|
cell.style.textAlign = 'right';
|
|
cell.innerText = this.executions;
|
|
row.appendChild(cell);
|
|
|
|
table.appendChild(row);
|
|
}
|
|
}
|
|
|
|
class RunningStats {
|
|
constructor (profiler) {
|
|
this.stepThreadsInnerId = profiler.idByName('Sequencer.stepThreads#inner');
|
|
this.blockFunctionId = profiler.idByName('blockFunction');
|
|
this.stpeThreadsId = profiler.idByName('Sequencer.stepThreads');
|
|
|
|
this.recordedTime = 0;
|
|
this.executed = {
|
|
steps: 0,
|
|
blocks: 0
|
|
};
|
|
}
|
|
|
|
update (id, arg, selfTime, totalTime, count) {
|
|
if (id === this.stpeThreadsId) {
|
|
this.recordedTime += totalTime;
|
|
} else if (id === this.stepThreadsInnerId) {
|
|
this.executed.steps += count;
|
|
} else if (id === this.blockFunctionId) {
|
|
this.executed.blocks += count;
|
|
}
|
|
}
|
|
}
|
|
|
|
const WORK_TIME = 0.75;
|
|
|
|
class RunningStatsView {
|
|
constructor ({runningStats, maxRecordedTime, dom}) {
|
|
this.recordedTimeDom =
|
|
dom.getElementsByClassName('profile-count-amount-recorded')[0];
|
|
this.stepsLoopedDom =
|
|
dom.getElementsByClassName('profile-count-steps-looped')[0];
|
|
this.blocksExecutedDom =
|
|
dom.getElementsByClassName('profile-count-blocks-executed')[0];
|
|
|
|
this.maxRecordedTime = maxRecordedTime;
|
|
this.maxWorkedTime = maxRecordedTime * WORK_TIME;
|
|
this.runningStats = runningStats;
|
|
}
|
|
|
|
render () {
|
|
const {
|
|
runningStats,
|
|
recordedTimeDom,
|
|
stepsLoopedDom,
|
|
blocksExecutedDom
|
|
} = this;
|
|
const {executed} = runningStats;
|
|
const fractionWorked = runningStats.recordedTime / this.maxWorkedTime;
|
|
recordedTimeDom.innerText = `${(fractionWorked * 100).toFixed(1)} %`;
|
|
stepsLoopedDom.innerText = executed.steps;
|
|
blocksExecutedDom.innerText = executed.blocks;
|
|
}
|
|
}
|
|
|
|
class Frames {
|
|
constructor (profiler) {
|
|
this.profiler = profiler;
|
|
|
|
this.frames = [];
|
|
}
|
|
|
|
update (id, arg, selfTime, totalTime, count) {
|
|
if (id < 0) return;
|
|
if (!this.frames[id]) {
|
|
this.frames[id] = new StatView(this.profiler.nameById(id));
|
|
}
|
|
this.frames[id].update(selfTime, totalTime, count);
|
|
}
|
|
}
|
|
|
|
const frameOrder = [
|
|
'blockFunction',
|
|
'execute',
|
|
'Sequencer.stepThread',
|
|
'Sequencer.stepThreads#inner',
|
|
'Sequencer.stepThreads',
|
|
'RenderWebGL.draw',
|
|
'Runtime._step'
|
|
];
|
|
|
|
class FramesTable extends StatTable {
|
|
constructor (options) {
|
|
super(options);
|
|
|
|
this.profiler = options.profiler;
|
|
this.frames = options.frames;
|
|
}
|
|
|
|
keys () {
|
|
const keys = Object.keys(this.frames.frames)
|
|
.map(id => this.profiler.nameById(Number(id)));
|
|
keys.sort((a, b) => frameOrder.indexOf(a) - frameOrder.indexOf(b));
|
|
return keys;
|
|
}
|
|
|
|
viewOf (key) {
|
|
return this.frames.frames[this.profiler.idByName(key)];
|
|
}
|
|
|
|
isSlow () {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class Opcodes {
|
|
constructor (profiler) {
|
|
this.blockFunctionId = profiler.idByName('blockFunction');
|
|
|
|
this.opcodes = {};
|
|
}
|
|
|
|
update (id, arg, selfTime, totalTime, count) {
|
|
if (id === this.blockFunctionId) {
|
|
if (!this.opcodes[arg]) {
|
|
this.opcodes[arg] = new StatView(arg);
|
|
}
|
|
this.opcodes[arg].update(selfTime, totalTime, count);
|
|
}
|
|
}
|
|
}
|
|
|
|
class OpcodeTable extends StatTable {
|
|
constructor (options) {
|
|
super(options);
|
|
|
|
this.profiler = options.profiler;
|
|
this.opcodes = options.opcodes;
|
|
this.frames = options.frames;
|
|
}
|
|
|
|
keys () {
|
|
const keys = Object.keys(this.opcodes.opcodes);
|
|
keys.sort();
|
|
return keys;
|
|
}
|
|
|
|
viewOf (key) {
|
|
return this.opcodes.opcodes[key];
|
|
}
|
|
|
|
isSlow (key) {
|
|
const blockFunctionTotalTime = this.frames.frames[this.profiler.idByName('blockFunction')].totalTime;
|
|
const rowTotalTime = this.opcodes.opcodes[key].totalTime;
|
|
const percentOfRun = rowTotalTime / blockFunctionTotalTime;
|
|
return percentOfRun > SLOW;
|
|
}
|
|
}
|
|
|
|
class ProfilerRun {
|
|
constructor ({vm, maxRecordedTime, warmUpTime}) {
|
|
this.vm = vm;
|
|
this.maxRecordedTime = maxRecordedTime;
|
|
this.warmUpTime = warmUpTime;
|
|
|
|
vm.runtime.enableProfiling();
|
|
const profiler = this.profiler = vm.runtime.profiler;
|
|
vm.runtime.profiler = null;
|
|
|
|
const runningStats = this.runningStats = new RunningStats(profiler);
|
|
const runningStatsView = this.runningStatsView = new RunningStatsView({
|
|
dom: document.getElementsByClassName('profile-count-group')[0],
|
|
|
|
runningStats,
|
|
maxRecordedTime
|
|
});
|
|
|
|
const frames = this.frames = new Frames(profiler);
|
|
this.frameTable = new FramesTable({
|
|
table: document
|
|
.getElementsByClassName('profile-count-frame-table')[0]
|
|
.getElementsByTagName('tbody')[0],
|
|
|
|
profiler,
|
|
frames
|
|
});
|
|
|
|
const opcodes = this.opcodes = new Opcodes(profiler);
|
|
this.opcodeTable = new OpcodeTable({
|
|
table: document
|
|
.getElementsByClassName('profile-count-opcode-table')[0]
|
|
.getElementsByTagName('tbody')[0],
|
|
|
|
profiler,
|
|
opcodes,
|
|
frames
|
|
});
|
|
|
|
const stepId = profiler.idByName('Runtime._step');
|
|
profiler.onFrame = ({id, arg, selfTime, totalTime, count}) => {
|
|
if (id === stepId) {
|
|
runningStatsView.render();
|
|
}
|
|
|
|
runningStats.update(id, arg, selfTime, totalTime, count);
|
|
opcodes.update(id, arg, selfTime, totalTime, count);
|
|
frames.update(id, arg, selfTime, totalTime, count);
|
|
};
|
|
}
|
|
|
|
run () {
|
|
this.projectId = loadProject();
|
|
|
|
window.parent.postMessage({
|
|
type: 'BENCH_MESSAGE_LOADING'
|
|
}, '*');
|
|
|
|
this.vm.on('workspaceUpdate', () => {
|
|
setTimeout(() => {
|
|
window.parent.postMessage({
|
|
type: 'BENCH_MESSAGE_WARMING_UP'
|
|
}, '*');
|
|
this.vm.greenFlag();
|
|
}, 100);
|
|
setTimeout(() => {
|
|
window.parent.postMessage({
|
|
type: 'BENCH_MESSAGE_ACTIVE'
|
|
}, '*');
|
|
this.vm.runtime.profiler = this.profiler;
|
|
}, 100 + this.warmUpTime);
|
|
setTimeout(() => {
|
|
this.vm.stopAll();
|
|
clearTimeout(this.vm.runtime._steppingInterval);
|
|
this.vm.runtime.profiler = null;
|
|
|
|
this.frameTable.render();
|
|
this.opcodeTable.render();
|
|
|
|
window.parent.postMessage({
|
|
type: 'BENCH_MESSAGE_COMPLETE',
|
|
frames: this.frames.frames,
|
|
opcodes: this.opcodes.opcodes
|
|
}, '*');
|
|
|
|
setShareLink({
|
|
fixture: {
|
|
projectId: this.projectId,
|
|
warmUpTime: this.warmUpTime,
|
|
recordingTime: this.maxRecordedTime
|
|
},
|
|
frames: this.frames.frames,
|
|
opcodes: this.opcodes.opcodes
|
|
});
|
|
}, 100 + this.warmUpTime + this.maxRecordedTime);
|
|
});
|
|
}
|
|
|
|
render (json) {
|
|
const {fixture} = json;
|
|
document.querySelector('[type=text]').value = [
|
|
fixture.projectId,
|
|
fixture.warmUpTime,
|
|
fixture.recordingTime
|
|
].join(',');
|
|
|
|
this.frames.frames = json.frames.map(
|
|
frame => Object.assign(new StatView(), frame, {
|
|
name: this.profiler.nameById(this.profiler.idByName(frame.name))
|
|
})
|
|
);
|
|
|
|
this.opcodes.opcodes = {};
|
|
Object.entries(json.opcodes).forEach(([opcode, data]) => {
|
|
this.opcodes.opcodes[opcode] = Object.assign(new StatView(), data);
|
|
});
|
|
|
|
this.frameTable.render();
|
|
this.opcodeTable.render();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the benchmark with given parameters in the location's hash field or
|
|
* using defaults.
|
|
*/
|
|
const runBenchmark = function () {
|
|
// Lots of global variables to make debugging easier
|
|
// Instantiate the VM.
|
|
const vm = new VirtualMachine();
|
|
Scratch.vm = vm;
|
|
|
|
vm.setTurboMode(true);
|
|
|
|
const storage = new ScratchStorage();
|
|
const AssetType = storage.AssetType;
|
|
storage.addWebSource([AssetType.Project], getProjectUrl);
|
|
storage.addWebSource([AssetType.ImageVector, AssetType.ImageBitmap, AssetType.Sound], getAssetUrl);
|
|
vm.attachStorage(storage);
|
|
|
|
new LoadingProgress(progress => {
|
|
const setElement = (name, value) => {
|
|
document.getElementsByClassName(name)[0].innerText = value;
|
|
};
|
|
const sinceLoadStart = key => (
|
|
`(${(window[key] || Date.now()) - window.ScratchVMLoadStart}ms)`
|
|
);
|
|
|
|
setElement('loading-total', 1);
|
|
setElement('loading-complete', progress.dataLoaded);
|
|
setElement('loading-time', sinceLoadStart('ScratchVMLoadDataEnd'));
|
|
|
|
setElement('loading-content-total', progress.contentTotal);
|
|
setElement('loading-content-complete', progress.contentComplete);
|
|
setElement('loading-content-time', sinceLoadStart('ScratchVMDownloadEnd'));
|
|
|
|
setElement('loading-hydrate-total', progress.hydrateTotal);
|
|
setElement('loading-hydrate-complete', progress.hydrateComplete);
|
|
setElement('loading-hydrate-time', sinceLoadStart('ScratchVMLoadEnd'));
|
|
|
|
if (progress.memoryPeak) {
|
|
setElement('loading-memory-current',
|
|
`${(progress.memoryCurrent / 1000000).toFixed(0)}MB`
|
|
);
|
|
setElement('loading-memory-peak',
|
|
`${(progress.memoryPeak / 1000000).toFixed(0)}MB`
|
|
);
|
|
}
|
|
}).on(storage, vm);
|
|
|
|
let warmUpTime = 4000;
|
|
let maxRecordedTime = 6000;
|
|
|
|
if (location.hash) {
|
|
const split = location.hash.substring(1).split(',');
|
|
if (split[1] && split[1].length > 0) {
|
|
warmUpTime = Number(split[1]);
|
|
}
|
|
maxRecordedTime = Number(split[2] || '0') || 6000;
|
|
}
|
|
|
|
new ProfilerRun({
|
|
vm,
|
|
warmUpTime,
|
|
maxRecordedTime
|
|
}).run();
|
|
|
|
// Instantiate the renderer and connect it to the VM.
|
|
const canvas = document.getElementById('scratch-stage');
|
|
const renderer = new ScratchRender(canvas);
|
|
Scratch.renderer = renderer;
|
|
vm.attachRenderer(renderer);
|
|
const audioEngine = new AudioEngine();
|
|
vm.attachAudioEngine(audioEngine);
|
|
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
|
|
|
|
// Feed mouse events as VM I/O events.
|
|
document.addEventListener('mousemove', e => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const coordinates = {
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top,
|
|
canvasWidth: rect.width,
|
|
canvasHeight: rect.height
|
|
};
|
|
Scratch.vm.postIOData('mouse', coordinates);
|
|
});
|
|
canvas.addEventListener('mousedown', e => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const data = {
|
|
isDown: true,
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top,
|
|
canvasWidth: rect.width,
|
|
canvasHeight: rect.height
|
|
};
|
|
Scratch.vm.postIOData('mouse', data);
|
|
e.preventDefault();
|
|
});
|
|
canvas.addEventListener('mouseup', e => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const data = {
|
|
isDown: false,
|
|
x: e.clientX - rect.left,
|
|
y: e.clientY - rect.top,
|
|
canvasWidth: rect.width,
|
|
canvasHeight: rect.height
|
|
};
|
|
Scratch.vm.postIOData('mouse', data);
|
|
e.preventDefault();
|
|
});
|
|
|
|
// Feed keyboard events as VM I/O events.
|
|
document.addEventListener('keydown', e => {
|
|
// Don't capture keys intended for Blockly inputs.
|
|
if (e.target !== document && e.target !== document.body) {
|
|
return;
|
|
}
|
|
Scratch.vm.postIOData('keyboard', {
|
|
keyCode: e.keyCode,
|
|
isDown: true
|
|
});
|
|
e.preventDefault();
|
|
});
|
|
document.addEventListener('keyup', e => {
|
|
// Always capture up events,
|
|
// even those that have switched to other targets.
|
|
Scratch.vm.postIOData('keyboard', {
|
|
keyCode: e.keyCode,
|
|
isDown: false
|
|
});
|
|
// E.g., prevent scroll.
|
|
if (e.target !== document && e.target !== document.body) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Run threads
|
|
vm.start();
|
|
};
|
|
|
|
/**
|
|
* Render previously run benchmark data.
|
|
* @param {object} json data from a previous benchmark run.
|
|
*/
|
|
const renderBenchmarkData = function (json) {
|
|
const vm = new VirtualMachine();
|
|
new ProfilerRun({vm}).render(json);
|
|
setShareLink(json);
|
|
};
|
|
|
|
const onload = function () {
|
|
if (location.hash.substring(1).startsWith('view')) {
|
|
document.body.className = 'render';
|
|
const data = location.hash.substring(6);
|
|
const frozen = atob(data);
|
|
const json = JSON.parse(frozen);
|
|
renderBenchmarkData(json);
|
|
} else {
|
|
runBenchmark();
|
|
}
|
|
};
|
|
|
|
window.onhashchange = function () {
|
|
location.reload();
|
|
};
|
|
|
|
if (window.performance) {
|
|
performance.mark('Scratch.EvalEnd');
|
|
performance.measure('Scratch.Eval', 'Scratch.EvalStart', 'Scratch.EvalEnd');
|
|
}
|
|
|
|
window.ScratchVMEvalEnd = Date.now();
|
|
|
|
onload();
|