mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-22 22:12:28 -05:00
Add benchmark prototype
Build a page with webpack that loads a project and automatically runs it, collecting performance information for a set amount of time, stopping and displaying the results.
This commit is contained in:
parent
614708d48c
commit
7356546072
4 changed files with 599 additions and 0 deletions
7
src/benchmark/benchmark.css
Normal file
7
src/benchmark/benchmark.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
@media (min-width: 960px) {
|
||||
.profile-tables {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 520px;
|
||||
}
|
||||
}
|
460
src/benchmark/benchmark.js
Normal file
460
src/benchmark/benchmark.js
Normal file
|
@ -0,0 +1,460 @@
|
|||
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 loadProject = function () {
|
||||
let id = location.hash.substring(1);
|
||||
if (id.length < 1 || !isFinite(id)) {
|
||||
id = '119615668';
|
||||
}
|
||||
Scratch.vm.downloadProjectId(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.total = 0;
|
||||
this.complete = 0;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
on (storage) {
|
||||
const _this = this;
|
||||
const _load = storage.webHelper.load;
|
||||
storage.webHelper.load = function (...args) {
|
||||
const result = _load.call(this, ...args);
|
||||
_this.total += 1;
|
||||
_this.callback(_this);
|
||||
result.then(() => {
|
||||
_this.complete += 1;
|
||||
_this.callback(_this);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this.executions++;
|
||||
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.innerText = (this.selfTime / 1000).toPrecision(3);
|
||||
row.appendChild(cell);
|
||||
|
||||
cell = document.createElement('td');
|
||||
cell.innerText = (this.totalTime / 1000).toPrecision(3);
|
||||
row.appendChild(cell);
|
||||
|
||||
cell = document.createElement('td');
|
||||
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, selfTime, totalTime) {
|
||||
if (id === this.stpeThreadsId) {
|
||||
this.recordedTime += totalTime;
|
||||
} else if (id === this.stepThreadsInnerId) {
|
||||
this.executed.steps++;
|
||||
} else if (id === this.blockFunctionId) {
|
||||
this.executed.blocks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, selfTime, totalTime) {
|
||||
if (!this.frames[id]) {
|
||||
this.frames[id] = new StatView(this.profiler.nameById(id));
|
||||
}
|
||||
this.frames[id].update(selfTime, totalTime);
|
||||
}
|
||||
}
|
||||
|
||||
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 (key, frame) {
|
||||
return ([
|
||||
'Sequencer.stepThreads',
|
||||
'Sequencer.stepThreads#inner',
|
||||
'Sequencer.stepThread',
|
||||
'execute'
|
||||
].indexOf(key) > 0 &&
|
||||
frame.selfTime / frame.totalTime > SLOW);
|
||||
}
|
||||
}
|
||||
|
||||
class Opcodes {
|
||||
constructor (profiler) {
|
||||
this.blockFunctionId = profiler.idByName('blockFunction');
|
||||
|
||||
this.opcodes = {};
|
||||
}
|
||||
|
||||
update (id, selfTime, totalTime, arg) {
|
||||
if (id === this.blockFunctionId) {
|
||||
if (!this.opcodes[arg]) {
|
||||
this.opcodes[arg] = new StatView(arg);
|
||||
}
|
||||
this.opcodes[arg].update(selfTime, totalTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: 6000
|
||||
});
|
||||
|
||||
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, selfTime, totalTime, arg}) => {
|
||||
if (id === stepId) {
|
||||
runningStatsView.render();
|
||||
}
|
||||
runningStats.update(id, selfTime, totalTime, arg);
|
||||
opcodes.update(id, selfTime, totalTime, arg);
|
||||
frames.update(id, selfTime, totalTime, arg);
|
||||
};
|
||||
}
|
||||
|
||||
run () {
|
||||
loadProject();
|
||||
|
||||
this.vm.on('workspaceUpdate', () => {
|
||||
setTimeout(() => {
|
||||
this.vm.greenFlag();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
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();
|
||||
}, 100 + this.warmUpTime + this.maxRecordedTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// Lots of global variables to make debugging easier
|
||||
// Instantiate the VM.
|
||||
const vm = new window.VirtualMachine();
|
||||
Scratch.vm = vm;
|
||||
|
||||
vm.setTurboMode(true);
|
||||
|
||||
const storage = new ScratchStorage(); /* global 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 => {
|
||||
document.getElementsByClassName('loading-total')[0]
|
||||
.innerText = progress.total;
|
||||
document.getElementsByClassName('loading-complete')[0]
|
||||
.innerText = progress.complete;
|
||||
}).on(storage);
|
||||
|
||||
new ProfilerRun({
|
||||
vm,
|
||||
warmUpTime: 4000,
|
||||
maxRecordedTime: 6000
|
||||
}).run();
|
||||
|
||||
// Instantiate the renderer and connect it to the VM.
|
||||
const canvas = document.getElementById('scratch-stage');
|
||||
const renderer = new window.RenderWebGL(canvas);
|
||||
Scratch.renderer = renderer;
|
||||
vm.attachRenderer(renderer);
|
||||
const audioEngine = new window.AudioEngine();
|
||||
vm.attachAudioEngine(audioEngine);
|
||||
|
||||
// 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();
|
||||
};
|
69
src/benchmark/index.html
Normal file
69
src/benchmark/index.html
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Scratch VM Benchmark</title>
|
||||
<style>
|
||||
canvas {
|
||||
border: 5px solid black;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="./benchmark.css" type="text/css" media="screen">
|
||||
</head>
|
||||
<body>
|
||||
<h2>Scratch VM Benchmark</h2>
|
||||
<canvas id="scratch-stage" style="width: 480px; height: 360px;"></canvas><br />
|
||||
<div class="loading">
|
||||
<label>Loading:</label>
|
||||
<span class="loading-complete">0</span> / <span class="loading-total">0</span>
|
||||
</div>
|
||||
<div class="profile-count-group">
|
||||
<div class="profile-count">
|
||||
<label>Percent of time worked:</label>
|
||||
<span class="profile-count-value profile-count-amount-recorded">...</span>
|
||||
</div>
|
||||
<div class="profile-count">
|
||||
<label>Steps looped:</label>
|
||||
<span class="profile-count-value profile-count-steps-looped">...</span>
|
||||
</div>
|
||||
<div class="profile-count">
|
||||
<label>Blocks executed:</label>
|
||||
<span class="profile-count-value profile-count-blocks-executed">...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-tables">
|
||||
<table class="profile-count-frame-table" cellspacing="0">
|
||||
<thead>
|
||||
<tr class="profile-count-frame-head"><th>Frame</th><th>Self Time</th><th>Total Time</th><th>Executions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="profiler-count-running"><td colspan="4"></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="profile-count-opcode-table" cellspacing="0">
|
||||
<thead>
|
||||
<tr class="profile-count-opcode-head">
|
||||
<th>opcode</th><th>Self Time</th><th>Total Time</th><th>Executions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="profiler-count-running"><td colspan="4"></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="blocks"></div>
|
||||
|
||||
<!-- FPS counter, Syntax highlighter, Blocks, Renderer -->
|
||||
<script src="../playground/vendor.js"></script>
|
||||
<!-- Storage module -->
|
||||
<script src="../playground/scratch-storage.js"></script>
|
||||
<!-- VM -->
|
||||
<script src="../playground/scratch-vm.js"></script>
|
||||
<!-- Playground -->
|
||||
<script src="./benchmark.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -130,5 +130,68 @@ module.exports = [
|
|||
from: 'src/playground'
|
||||
}])
|
||||
])
|
||||
}),
|
||||
// Benchmark
|
||||
defaultsDeep({}, base, {
|
||||
target: 'web',
|
||||
entry: {
|
||||
'scratch-vm': './src/index.js',
|
||||
'vendor': [
|
||||
// FPS counter
|
||||
'stats.js/build/stats.min.js',
|
||||
// Syntax highlighter
|
||||
'highlightjs/highlight.pack.min.js',
|
||||
// Scratch Blocks
|
||||
'scratch-blocks/dist/vertical.js',
|
||||
// Audio
|
||||
'scratch-audio',
|
||||
// Renderer
|
||||
'scratch-render'
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'benchmark'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: base.module.rules.concat([
|
||||
{
|
||||
test: require.resolve('./src/index.js'),
|
||||
loader: 'expose-loader?VirtualMachine'
|
||||
},
|
||||
{
|
||||
test: require.resolve('stats.js/build/stats.min.js'),
|
||||
loader: 'script-loader'
|
||||
},
|
||||
{
|
||||
test: require.resolve('highlightjs/highlight.pack.min.js'),
|
||||
loader: 'script-loader'
|
||||
},
|
||||
{
|
||||
test: require.resolve('scratch-blocks/dist/vertical.js'),
|
||||
loader: 'expose-loader?Blockly'
|
||||
},
|
||||
{
|
||||
test: require.resolve('scratch-audio'),
|
||||
loader: 'expose-loader?AudioEngine'
|
||||
},
|
||||
{
|
||||
test: require.resolve('scratch-render'),
|
||||
loader: 'expose-loader?RenderWebGL'
|
||||
}
|
||||
])
|
||||
},
|
||||
plugins: base.plugins.concat([
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'node_modules/scratch-blocks/media',
|
||||
to: 'media'
|
||||
}, {
|
||||
from: 'node_modules/highlightjs/styles/zenburn.css'
|
||||
}, {
|
||||
from: 'node_modules/scratch-storage/dist/web'
|
||||
}, {
|
||||
from: 'src/benchmark'
|
||||
}])
|
||||
])
|
||||
})
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue