diff --git a/src/playground/benchmark.js b/src/playground/benchmark.js
index 843ffca63..bca3e0532 100644
--- a/src/playground/benchmark.js
+++ b/src/playground/benchmark.js
@@ -348,11 +348,21 @@ class ProfilerRun {
run () {
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(() => {
@@ -362,6 +372,12 @@ class ProfilerRun {
this.frameTable.render();
this.opcodeTable.render();
+
+ window.parent.postMessage({
+ type: 'BENCH_MESSAGE_COMPLETE',
+ frames: this.frames.frames,
+ opcodes: this.opcodes.opcodes
+ }, '*');
}, 100 + this.warmUpTime + this.maxRecordedTime);
});
}
diff --git a/src/playground/suite.css b/src/playground/suite.css
new file mode 100644
index 000000000..08eb5d61e
--- /dev/null
+++ b/src/playground/suite.css
@@ -0,0 +1,45 @@
+html, body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
+
+.runner-controls {
+ position: absolute;
+ width: 30em;
+ height: 100%;
+ left: 0;
+ right: 30em;
+ top: 0;
+ bottom: 0;
+ overflow: scroll;
+ padding: 1em;
+ box-sizing: border-box;
+}
+
+.bench-frame-owner {
+ position: absolute;
+ width: calc(100% - 30em);
+ height: 100%;
+ left: 30em;
+ right: 100%;
+ top: 0;
+ bottom: 0;
+}
+
+.fixture-project {
+ display: inline-block;
+}
+
+.fixture-warm-up {
+ display: inline-block;
+}
+
+.fixture-recording {
+ display: inline-block;
+}
+
+.result-status {
+ float: right;
+}
diff --git a/src/playground/suite.html b/src/playground/suite.html
new file mode 100644
index 000000000..8acd7ca65
--- /dev/null
+++ b/src/playground/suite.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Scratch VM Benchmark Suite
+
+
+
+
+
Scratch VM Benchmark Suite
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/playground/suite.js b/src/playground/suite.js
new file mode 100644
index 000000000..114828124
--- /dev/null
+++ b/src/playground/suite.js
@@ -0,0 +1,385 @@
+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 BENCH_MESSAGE_TYPE = {
+ 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);
+ }
+
+ startBench (args) {
+ Promise.resolve()
+ .then(() => new Promise(resolve => setTimeout(resolve, 100)))
+ .then(() => {
+ this.frame.contentWindow.location.assign('about:blank');
+ })
+ .then(() => new Promise(resolve => setTimeout(resolve, 100)))
+ .then(() => {
+ this.frame.contentWindow.location.assign(
+ `/benchmark/#${args.projectId},${args.warmUpTime},${args.recordingTime}`);
+ });
+ }
+}
+
+const BENCH_STATUS = {
+ INACTIVE: 'BENCH_STATUS_INACTIVE',
+ 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.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.STARTING]: 'Starting',
+ [BENCH_STATUS.LOADING]: 'Loading',
+ [BENCH_STATUS.WARMING_UP]: 'Warming Up',
+ [BENCH_STATUS.ACTIVE]: 'Active',
+ [BENCH_STATUS.COMPLETE]: 'Complete'
+};
+
+class BenchResultView {
+ constructor ({result}) {
+ this.result = result;
+ this.dom = document.createElement('div');
+ }
+
+ update (result) {
+ soon().then(() => this.render(result));
+ }
+
+ render (newResult = this.result) {
+ const blockFunctionFrame = (newResult.frames ? newResult.frames : [])
+ .find(frame => frame.name === 'blockFunction');
+ const stepThreadsInnerFrame = (newResult.frames ? newResult.frames : [])
+ .find(frame => frame.name === 'Sequencer.stepThreads#inner');
+
+ const blocksPerSecond = blockFunctionFrame ?
+ (blockFunctionFrame.executions / (stepThreadsInnerFrame.totalTime / 1000)) | 0 :
+ 0;
+
+ this.dom.innerHTML = `
+
+ (${newResult.fixture.warmUpTime / 1000}s
+ ${newResult.fixture.recordingTime / 1000}s)
+
+ ${blockFunctionFrame ? `${blocksPerSecond} blocks/s` : viewNames[newResult.status]}
+
+ `;
+
+ this.result = newResult;
+ return this;
+ }
+}
+
+class BenchSuiteResultView {
+ constructor ({runner}) {
+ this.runner = runner;
+ this.suite = runner.suite;
+ this.views = {};
+ this.dom = document.createElement('div');
+
+ const {suite} = runner;
+ for (const fixture of suite.fixtures) {
+ this.views[fixture.id] = new BenchResultView({result: new BenchResult({fixture})});
+ this.dom.appendChild(this.views[fixture.id].render().dom);
+ }
+
+ suite.on('result', result => {
+ this.views[result.fixture.id].update(result);
+ });
+ }
+
+ render () {
+ return this;
+ }
+}
+
+window.onload = function () {
+ const suite = new BenchSuite();
+
+ suite.add(new BenchFixture({
+ projectId: 130041250,
+ warmUpTime: 0,
+ recordingTime: 2000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 130041250,
+ warmUpTime: 4000,
+ recordingTime: 6000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 14844969,
+ warmUpTime: 0,
+ recordingTime: 2000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 14844969,
+ warmUpTime: 1000,
+ recordingTime: 6000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 173918262,
+ warmUpTime: 0,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 173918262,
+ warmUpTime: 5000,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 155128646,
+ warmUpTime: 0,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 155128646,
+ warmUpTime: 5000,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 89811578,
+ warmUpTime: 0,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 89811578,
+ warmUpTime: 5000,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 139193539,
+ warmUpTime: 0,
+ recordingTime: 5000
+ }));
+
+ suite.add(new BenchFixture({
+ projectId: 139193539,
+ warmUpTime: 5000,
+ recordingTime: 5000
+ }));
+
+ const frame = document.getElementsByTagName('iframe')[0];
+ const runner = new BenchRunner({frame, suite});
+ const resultsView = new BenchSuiteResultView({runner});
+
+ document.getElementsByClassName('suite-results')[0].appendChild(resultsView.dom);
+
+ runner.run();
+};