Skip to content

Commit

Permalink
Add new benchmarking
Browse files Browse the repository at this point in the history
Allows to run up-to-date parsers directly from console or browser against prebundled popular frameworks and outputs results in a nice table.

Browser version now uses a Web Worker to avoid blocking UI while benchmark is running and improve stability of numbers.

No locations versions were removed (at least for now) as most parsers don't support it and it's not really the most common use-case to optimise for (most tooling that depends on us is generating source maps and such).

Also, benchmark code now targets latest stable versions of browsers / Node.js (relying on Web Workers, Promises and fetch) - while we continue to support older ones through tests, it doesn't seem worth it to target for them performance-wise and reduce code quality or transpile benchmark itself.
  • Loading branch information
RReverser committed May 17, 2017
1 parent f1a88fe commit 18dd272
Show file tree
Hide file tree
Showing 16 changed files with 120,561 additions and 42,417 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/node_modules
node_modules
/.tern-port
/local
/bin/acorn
Expand Down
185 changes: 103 additions & 82 deletions test/bench.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,95 +2,116 @@
<head>
<meta charset="utf-8">
<title>Acorn benchmark</title>
<script src="../dist/acorn.js"></script>
<script src="compare/esprima.js"></script>
<script src="compare/traceur.js"></script>
<script src="jquery-string.js"></script>
<script src="codemirror-string.js"></script>
<style>
td { text-align: right; padding-right: 20px; }
th { text-align: left; padding-right: 40px; }
body { max-width: 50em; padding: 1em 2em; }
h1 { font-size: 150%; }
table {
border-collapse: collapse;
text-align: center;
width: 100%;
table-layout: fixed;
}

table, th, td {
border: 1px solid black;
padding: 3pt;
}

.error {
color: white;
background-color: red;
}

.slowest {
color: red;
}

.fastest {
color: green;
}
</style>
</head>

<h1>Acorn/Esprima/Traceur speed comparison</h1>
<h1>JavaScript parsers speed comparison</h1>

<p>This will run each of the three ES6 parsers on the source code of
jQuery 1.11.1 and CodeMirror 3.0b1 for five seconds, and show a table
indicating the number of lines parsed per second. Note that Traceur
always stores location data, and is thus not fairly compared by the
benchmark <em>without</em> location data.<p>

<p>Also note that having the developer tools open in Chrome, or
Firebug in Firefox <em>heavily</em> influences the numbers you get. In
Chrome, the effect even lingers (in the tab) after you close the
developer tools. Load in a fresh tab to get (halfway) stable
<p>Note that having the developer tools open in browser <em>heavily</em>
influences the numbers you get. In Chrome, the effect even lingers (in the tab)
after you close the developer tools. Load in a fresh tab to get (halfway) stable
numbers.</p>

<button onclick="run(false)">Compare <strong>without</strong> location data</button>
<button onclick="run(true)">Compare <strong>with</strong> location data</button>
<button onclick="run(true, true)">Run only Acorn</button>
<span id="running"></span>
<button id="run" disabled>Run benchmarks</button>

<pre><code id="log"></code></pre>

<table>
<thead>
<tr id="parsers">
<th></th>
</tr>
</thead>
<tbody id="inputs"></tbody>
</table>

<script>
var sourceFileName = 'source.js';

function runAcorn(code, locations) {
acorn.parse(code, {ecmaVersion: 6, locations: locations, sourceFile: sourceFileName});
}
function runEsprima(code, locations) {
esprima.parse(code, {loc: locations, source: sourceFileName});
}
function runTraceur(code) {
var file = new traceur.syntax.SourceFile(sourceFileName, code);
var parser = new traceur.syntax.Parser(file);
parser.parseScript();
}

var totalLines = codemirror30.split("\n").length + jquery111.split("\n").length;

var nowHost = (typeof performance === 'object' && 'now' in performance) ? performance : Date;

function benchmark(runner, locations) {
// Give it a chance to warm up (first runs are usually outliers)
runner(jquery111, locations);
runner(codemirror30, locations);
var t0 = nowHost.now(), t1, lines = 0;
for (;;) {
runner(jquery111, locations);
runner(codemirror30, locations);
lines += totalLines;
t1 = nowHost.now();
if (t1 - t0 > 5000) break;
}
return lines / ((t1 - t0) / 1000);
}

function showOutput(values) {
var html = "<hr><table>";
for (var i = 0; i < values.length; ++i)
html += "<tr><th>" + values[i].name + "</td><td>" + Math.round(values[i].score) + " lines per second</td><td>" +
Math.round(values[i].score * 100 / values[0].score) + "%</td></tr>";
document.body.appendChild(document.createElement("div")).innerHTML = html;
}

function run(locations, acornOnly) {
var running = document.getElementById("running");
running.innerHTML = "Running benchmark...";
var data = [{name: "Acorn", runner: runAcorn},
{name: "Esprima", runner: runEsprima},
{name: "Traceur", runner: runTraceur}];
if (acornOnly) data.length = 1;
var pos = 0;
function next() {
data[pos].score = benchmark(data[pos].runner, locations);
if (++pos == data.length) {
running.innerHTML = "";
showOutput(data);
} else setTimeout(next, 100);
}
setTimeout(next, 50);
}
(() => {
'use strict';

let runElem = document.getElementById('run');
let parsersElem = document.getElementById('parsers');
let inputsElem = document.getElementById('inputs');

let worker = new Worker('bench/worker.js');

worker.onmessage = ({ data: { parserNames, inputNames } }) => {
parserNames.forEach(parserName => {
let thElem = document.createElement('th');
thElem.textContent = parserName;
parsersElem.appendChild(thElem);
});

let rows = inputNames.map(name => {
let thElem = document.createElement('th');
thElem.textContent = name;

let trElem = document.createElement('tr');
trElem.appendChild(thElem);

inputsElem.appendChild(trElem);

return trElem;
});

runElem.addEventListener('click', () => {
runElem.disabled = true;
worker.postMessage('');
});

runElem.disabled = false;

worker.onmessage = ({ data }) => {
let row = rows[data.row];
switch (data.type) {
case 'cycle': {
let tdElem = document.createElement('td');
tdElem.textContent = data.text;
row.appendChild(tdElem);
break;
}
case 'error': {
let tdElem = document.createElement('td');
tdElem.className = 'error';
tdElem.textContent = data.text;
row.appendChild(tdElem);
break;
}
case 'complete': {
['slowest', 'fastest'].forEach(type => {
data[type].forEach(i => {
row.cells[i + 1].className = type;
});
});
break;
}
}
};
};
})();
</script>
74 changes: 74 additions & 0 deletions test/bench/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';

const isWorker = typeof importScripts !== 'undefined';

if (isWorker) {
importScripts('https://unpkg.com/esprima');
importScripts('../../dist/acorn.js');
var acornDev = acorn;
var acorn = undefined;
importScripts('https://unpkg.com/acorn');
importScripts('https://unpkg.com/traceur/bin/traceur.js');
importScripts('https://unpkg.com/typescript');
} else {
var fs = require('fs');
var esprima = require('esprima');
var acornDev = require('../../dist/acorn');
var acorn = require('acorn');
require('traceur'); // yeah, it creates a global...
var ts = require('typescript');
}

var parsers = {
[`Acorn (dev)`](s) {
acornDev.parse(s, { locations: true });
},
[`Acorn ${acorn.version}`](s) {
acorn.parse(s, { locations: true });
},
[`Esprima ${esprima.version}`](s) {
esprima.parse(s, { loc: true });
},
[`TypeScript ${ts.version}`](s) {
ts.createSourceFile('source.js', s, ts.ScriptTarget.ES6);
},
[`Traceur ${traceur.loader.TraceurLoader.prototype.version}`](s) {
var file = new traceur.syntax.SourceFile('source.js', s);
var parser = new traceur.syntax.Parser(file);
parser.parseScript();
},
};

var parserNames = Object.keys(parsers);

var inputNames = [
'angular.js',
'backbone.js',
'ember.js',
'jquery.js',
'react-dom.js',
'react.js'
];

var inputs = Promise.all(inputNames.map(name => {
name = `fixtures/${name}`;

if (isWorker) {
return fetch(name).then(response => response.text());
} else {
return new Promise((resolve, reject) => {
fs.readFile(`${__dirname}/${name}`, 'utf-8', (err, data) => {
err ? reject(err) : resolve(data);
});
});
}
}));

if (!isWorker) {
module.exports = {
parsers,
parserNames,
inputs,
inputNames
};
}
Loading

0 comments on commit 18dd272

Please sign in to comment.