Spaces:
Build error
Build error
// 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(); | |