Spaces:
Build error
Build error
const soon = (() => { | |
let _soon; | |
return () => { | |
if (!_soon) { | |
_soon = Promise.resolve() | |
.then(() => { | |
_soon = null; | |
}); | |
} | |
return _soon; | |
}; | |
})(); | |
class Emitter { | |
constructor () { | |
Object.defineProperty(this, '_listeners', { | |
value: {}, | |
enumerable: false | |
}); | |
} | |
on (name, listener, context) { | |
if (!this._listeners[name]) { | |
this._listeners[name] = []; | |
} | |
this._listeners[name].push(listener, context); | |
} | |
off (name, listener, context) { | |
if (this._listeners[name]) { | |
if (listener) { | |
for (let i = 0; i < this._listeners[name].length; i += 2) { | |
if ( | |
this._listeners[name][i] === listener && | |
this._listeners[name][i + 1] === context) { | |
this._listeners[name].splice(i, 2); | |
i -= 2; | |
} | |
} | |
} else { | |
for (let i = 0; i < this._listeners[name].length; i += 2) { | |
if (this._listeners[name][i + 1] === context) { | |
this._listeners[name].splice(i, 2); | |
i -= 2; | |
} | |
} | |
} | |
} | |
} | |
emit (name, ...args) { | |
if (this._listeners[name]) { | |
for (let i = 0; i < this._listeners[name].length; i += 2) { | |
this._listeners[name][i].call(this._listeners[name][i + 1] || this, ...args); | |
} | |
} | |
} | |
} | |
class BenchFrameStream extends Emitter { | |
constructor (frame) { | |
super(); | |
this.frame = frame; | |
window.addEventListener('message', message => { | |
this.emit('message', message.data); | |
}); | |
} | |
send (message) { | |
this.frame.send(message); | |
} | |
} | |
const benchmarkUrlArgs = args => ( | |
[ | |
args.projectId, | |
args.warmUpTime, | |
args.recordingTime | |
].join(',') | |
); | |
const BENCH_MESSAGE_TYPE = { | |
INACTIVE: 'BENCH_MESSAGE_INACTIVE', | |
LOAD: 'BENCH_MESSAGE_LOAD', | |
LOADING: 'BENCH_MESSAGE_LOADING', | |
WARMING_UP: 'BENCH_MESSAGE_WARMING_UP', | |
ACTIVE: 'BENCH_MESSAGE_ACTIVE', | |
COMPLETE: 'BENCH_MESSAGE_COMPLETE' | |
}; | |
class BenchUtil { | |
constructor (frame) { | |
this.frame = frame; | |
this.benchStream = new BenchFrameStream(frame); | |
} | |
setFrameLocation (url) { | |
this.frame.contentWindow.location.assign(url); | |
} | |
startBench (args) { | |
this.benchArgs = args; | |
this.setFrameLocation(`index.html#${benchmarkUrlArgs(args)}`); | |
} | |
pauseBench () { | |
new Promise(resolve => setTimeout(resolve, 1000)) | |
.then(() => { | |
this.benchStream.emit('message', { | |
type: BENCH_MESSAGE_TYPE.INACTIVE | |
}); | |
}); | |
} | |
resumeBench () { | |
this.startBench(this.benchArgs); | |
} | |
renderResults (results) { | |
this.setFrameLocation( | |
`index.html#view/${btoa(JSON.stringify(results))}` | |
); | |
} | |
} | |
const BENCH_STATUS = { | |
INACTIVE: 'BENCH_STATUS_INACTIVE', | |
RESUME: 'BENCH_STATUS_RESUME', | |
STARTING: 'BENCH_STATUS_STARTING', | |
LOADING: 'BENCH_STATUS_LOADING', | |
WARMING_UP: 'BENCH_STATUS_WARMING_UP', | |
ACTIVE: 'BENCH_STATUS_ACTIVE', | |
COMPLETE: 'BENCH_STATUS_COMPLETE' | |
}; | |
class BenchResult { | |
constructor ({fixture, status = BENCH_STATUS.INACTIVE, frames = null, opcodes = null}) { | |
this.fixture = fixture; | |
this.status = status; | |
this.frames = frames; | |
this.opcodes = opcodes; | |
} | |
} | |
class BenchFixture extends Emitter { | |
constructor ({ | |
projectId, | |
warmUpTime = 4000, | |
recordingTime = 6000 | |
}) { | |
super(); | |
this.projectId = projectId; | |
this.warmUpTime = warmUpTime; | |
this.recordingTime = recordingTime; | |
} | |
get id () { | |
return `${this.projectId}-${this.warmUpTime}-${this.recordingTime}`; | |
} | |
run (util) { | |
return new Promise(resolve => { | |
util.benchStream.on('message', message => { | |
const result = { | |
fixture: this, | |
status: BENCH_STATUS.STARTING, | |
frames: null, | |
opcodes: null | |
}; | |
if (message.type === BENCH_MESSAGE_TYPE.INACTIVE) { | |
result.status = BENCH_STATUS.RESUME; | |
} else if (message.type === BENCH_MESSAGE_TYPE.LOADING) { | |
result.status = BENCH_STATUS.LOADING; | |
} else if (message.type === BENCH_MESSAGE_TYPE.WARMING_UP) { | |
result.status = BENCH_STATUS.WARMING_UP; | |
} else if (message.type === BENCH_MESSAGE_TYPE.ACTIVE) { | |
result.status = BENCH_STATUS.ACTIVE; | |
} else if (message.type === BENCH_MESSAGE_TYPE.COMPLETE) { | |
result.status = BENCH_STATUS.COMPLETE; | |
result.frames = message.frames; | |
result.opcodes = message.opcodes; | |
resolve(new BenchResult(result)); | |
util.benchStream.off('message', null, this); | |
} | |
this.emit('result', new BenchResult(result)); | |
}, this); | |
util.startBench(this); | |
}); | |
} | |
} | |
class BenchSuiteResult extends Emitter { | |
constructor ({suite, results = []}) { | |
super(); | |
this.suite = suite; | |
this.results = results; | |
if (suite) { | |
suite.on('result', result => { | |
if (result.status === BENCH_STATUS.COMPLETE) { | |
this.results.push(results); | |
this.emit('add', this); | |
} | |
}); | |
} | |
} | |
} | |
class BenchSuite extends Emitter { | |
constructor (fixtures = []) { | |
super(); | |
this.fixtures = fixtures; | |
} | |
add (fixture) { | |
this.fixtures.push(fixture); | |
} | |
run (util) { | |
return new Promise(resolve => { | |
const fixtures = this.fixtures.slice(); | |
const results = []; | |
const push = result => { | |
result.fixture.off('result', null, this); | |
results.push(result); | |
}; | |
const emitResult = this.emit.bind(this, 'result'); | |
const pop = () => { | |
const fixture = fixtures.shift(); | |
if (fixture) { | |
fixture.on('result', emitResult, this); | |
fixture.run(util) | |
.then(push) | |
.then(pop); | |
} else { | |
resolve(new BenchSuiteResult({suite: this, results})); | |
} | |
}; | |
pop(); | |
}); | |
} | |
} | |
class BenchRunner extends Emitter { | |
constructor ({frame, suite}) { | |
super(); | |
this.frame = frame; | |
this.suite = suite; | |
this.util = new BenchUtil(frame); | |
} | |
run () { | |
return this.suite.run(this.util); | |
} | |
} | |
const viewNames = { | |
[BENCH_STATUS.INACTIVE]: 'Inactive', | |
[BENCH_STATUS.RESUME]: 'Resume', | |
[BENCH_STATUS.STARTING]: 'Starting', | |
[BENCH_STATUS.LOADING]: 'Loading', | |
[BENCH_STATUS.WARMING_UP]: 'Warming Up', | |
[BENCH_STATUS.ACTIVE]: 'Active', | |
[BENCH_STATUS.COMPLETE]: 'Complete' | |
}; | |
class BenchResultView { | |
constructor ({result, benchUtil}) { | |
this.result = result; | |
this.compare = null; | |
this.benchUtil = benchUtil; | |
this.dom = document.createElement('div'); | |
} | |
update (result) { | |
soon().then(() => this.render(result)); | |
} | |
resume () { | |
this.benchUtil.resumeBench(); | |
} | |
setFrameLocation (loc) { | |
this.benchUtil.pauseBench(); | |
this.benchUtil.setFrameLocation(loc); | |
} | |
act (ev) { | |
if ( | |
ev.type === 'click' && | |
ev.button === 0 && | |
!(ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) | |
) { | |
let target = ev.target; | |
while (target && target.tagName.toLowerCase() !== 'a') { | |
target = target.parentElement; | |
} | |
if (target && target.tagName.toLowerCase() === 'a') { | |
if (target.href) { | |
this.setFrameLocation(target.href); | |
ev.preventDefault(); | |
} | |
} else if (ev.currentTarget.classList.contains('resume')) { | |
this.resume(); | |
} | |
} | |
} | |
render (newResult = this.result, compareResult = this.compare) { | |
const newResultFrames = (newResult.frames ? newResult.frames : []).filter(i => i); | |
const blockFunctionFrame = newResultFrames | |
.find(frame => frame.name === 'blockFunction'); | |
const stepThreadsInnerFrame = newResultFrames | |
.find(frame => frame.name === 'Sequencer.stepThreads#inner'); | |
const blocksPerSecond = blockFunctionFrame ? | |
(blockFunctionFrame.executions / | |
(stepThreadsInnerFrame.totalTime / 1000)) | 0 : | |
0; | |
const stepsPerSecond = stepThreadsInnerFrame ? | |
(stepThreadsInnerFrame.executions / | |
(stepThreadsInnerFrame.totalTime / 1000)) | 0 : | |
0; | |
const compareResultFrames = ( | |
compareResult && compareResult.frames ? | |
compareResult.frames : | |
[] | |
); | |
const blockFunctionCompareFrame = compareResultFrames | |
.find(frame => frame.name === 'blockFunction'); | |
const stepThreadsInnerCompareFrame = compareResultFrames | |
.find(frame => frame.name === 'Sequencer.stepThreads#inner'); | |
const compareBlocksPerSecond = blockFunctionCompareFrame ? | |
(blockFunctionCompareFrame.executions / | |
(stepThreadsInnerCompareFrame.totalTime / 1000)) | 0 : | |
0; | |
const compareStepsPerSecond = stepThreadsInnerCompareFrame ? | |
(stepThreadsInnerCompareFrame.executions / | |
(stepThreadsInnerCompareFrame.totalTime / 1000)) | 0 : | |
0; | |
const statusName = viewNames[newResult.status]; | |
this.dom.className = `result-view ${ | |
viewNames[newResult.status].toLowerCase() | |
}`; | |
this.dom.onclick = this.act.bind(this); | |
let url = `index.html#${benchmarkUrlArgs(newResult.fixture)}`; | |
if (newResult.status === BENCH_STATUS.COMPLETE) { | |
url = `index.html#view/${btoa(JSON.stringify(newResult))}`; | |
} | |
let compareUrl = url; | |
if (compareResult && compareResult) { | |
compareUrl = | |
`index.html#view/${btoa(JSON.stringify(compareResult))}`; | |
} | |
let compareHTML = ''; | |
if (stepThreadsInnerFrame && stepThreadsInnerCompareFrame) { | |
compareHTML = `<a href="${compareUrl}" target="_blank"> | |
<div class="result-status"> | |
<div>${compareStepsPerSecond}</div> | |
<div>${compareBlocksPerSecond}</div> | |
</div> | |
</a>`; | |
} | |
this.dom.innerHTML = ` | |
<div class="fixture-project"> | |
<a href="${url}" target="bench_frame" | |
>${newResult.fixture.projectId}</a> | |
</div> | |
<div class="result-status"> | |
<div>${stepThreadsInnerFrame ? `steps/s` : ''}</div> | |
<div>${blockFunctionFrame ? `blocks/s` : statusName}</div> | |
</div> | |
<a href="${stepThreadsInnerFrame ? url : ''}" target="_blank"> | |
<div class="result-status"> | |
<div>${stepThreadsInnerFrame ? `${stepsPerSecond}` : ''}</div> | |
<div>${blockFunctionFrame ? `${blocksPerSecond}` : ''}</div> | |
</div> | |
</a> | |
${compareHTML} | |
<div class=""> | |
Run for ${newResult.fixture.recordingTime / 1000} seconds after | |
${newResult.fixture.warmUpTime / 1000} seconds | |
</div> | |
`; | |
this.result = newResult; | |
return this; | |
} | |
} | |
class BenchSuiteResultView { | |
constructor ({runner}) { | |
const {suite, util} = runner; | |
this.runner = runner; | |
this.suite = suite; | |
this.views = {}; | |
this.dom = document.createElement('div'); | |
for (const fixture of suite.fixtures) { | |
this.views[fixture.id] = new BenchResultView({ | |
result: new BenchResult({fixture}), | |
benchUtil: util | |
}); | |
} | |
suite.on('result', result => { | |
this.views[result.fixture.id].update(result); | |
}); | |
} | |
render () { | |
this.dom.innerHTML = `<div class="legend"> | |
<span>Project ID</span> | |
<div class="result-status"> | |
<div>steps per second</div> | |
<div>blocks per second</div> | |
</div> | |
<div>Description</div> | |
</div> | |
<div class="legend"> | |
<span> </span> | |
<div class="result-status"> | |
<div><a href="#" onclick="window.download(this)"> | |
Save Reports | |
</a></div> | |
</div> | |
<div class="result-status"> | |
<a href="#"><label for="compare-file">Compare Reports<input | |
id="compare-file" type="file" | |
class="compare-file" | |
accept="application/json" | |
onchange="window.upload(this)" /> | |
</label></a> | |
</div> | |
</div>`; | |
for (const fixture of this.suite.fixtures) { | |
this.dom.appendChild(this.views[fixture.id].render().dom); | |
} | |
return this; | |
} | |
} | |
let suite; | |
let suiteView; | |
window.upload = function (_this) { | |
if (!_this.files.length) { | |
return; | |
} | |
const reader = new FileReader(); | |
reader.onload = function () { | |
const report = JSON.parse(reader.result); | |
Object.values(suiteView.views) | |
.forEach(view => { | |
const sameFixture = report.results.find(result => ( | |
result.fixture.projectId === | |
view.result.fixture.projectId && | |
result.fixture.warmUpTime === | |
view.result.fixture.warmUpTime && | |
result.fixture.recordingTime === | |
view.result.fixture.recordingTime | |
)); | |
if (sameFixture) { | |
if ( | |
view.result && view.result.frames && | |
view.result.frames.length > 0 | |
) { | |
view.render(view.result, sameFixture); | |
} else { | |
view.compare = sameFixture; | |
} | |
} | |
}); | |
}; | |
reader.readAsText(_this.files[0]); | |
}; | |
window.download = function (_this) { | |
const blob = new Blob([JSON.stringify({ | |
meta: { | |
source: 'Scratch VM Benchmark Suite', | |
version: 1 | |
}, | |
results: Object.values(suiteView.views) | |
.map(view => view.result) | |
.filter(view => view.status === BENCH_STATUS.COMPLETE) | |
})], {type: 'application/json'}); | |
_this.download = 'scratch-vm-benchmark.json'; | |
_this.href = URL.createObjectURL(blob); | |
}; | |
window.onload = function () { | |
suite = new BenchSuite(); | |
const add = (projectId, warmUp = 0, recording = 5000) => { | |
suite.add(new BenchFixture({ | |
projectId, | |
warmUpTime: warmUp, | |
recordingTime: recording | |
})); | |
}; | |
const standard = projectId => { | |
add(projectId, 0, 5000); | |
add(projectId, 5000, 5000); | |
}; | |
add(130041250, 0, 2000); // floating blocks | |
add(130041250, 4000, 6000); | |
add(14844969, 0, 2000); // scratch cats | |
add(14844969, 1000, 6000); | |
standard(173918262); // bouncy heros | |
standard(155128646); // stacky build | |
standard(89811578); // solar system | |
standard(139193539); // pixel art maker | |
standard(187694931); // spiralgraph | |
standard(219313833); // sensing_touching benchmark | |
standard(236115215); // touching color benchmark | |
standard(238750909); // bob ross painting (heavy pen stamp) | |
const frame = document.getElementsByTagName('iframe')[0]; | |
const runner = new BenchRunner({frame, suite}); | |
const resultsView = suiteView = new BenchSuiteResultView({runner}).render(); | |
document.getElementsByClassName('suite-results')[0] | |
.appendChild(resultsView.dom); | |
runner.run(); | |
}; | |