Source code

Revision control

Copy as Markdown

Other Tools

// This script runs multipe parsers from a single engine.
"use strict";
// Directory where to find the list of JavaScript sources to be used for
// benchmarking.
var dir = ".";
// Skip list cache to be used to be able to compare profiles. Without a skip
// list which ensure that only runnable test cases are used, the profile would
// not represent the actual values reported by this script.
var skipList = [], skipFile = "", skipLen = 0;
// Handle command line arguments.
for (var i = 0; i < scriptArgs.length; i++) {
switch (scriptArgs[i]) {
case "--dir":
if (++i >= scriptArgs.length) {
throw Error("--dir expects a path.");
}
dir = scriptArgs[i];
break;
case "--skip-file":
if (++i >= scriptArgs.length) {
throw Error("--skip-file expects a path.");
}
skipFile = scriptArgs[i];
try {
skipList = eval(os.file.readFile(skipFile));
} catch (e) {
// ignore errors
}
skipLen = skipList.length;
break;
}
}
// Execution mode of the parser, either "script" or "module".
var mode = "script";
// Number of times each JavaScript source is used for benchmarking.
var runs_per_script = 10;
// First parser
var name_1 = "SpiderMonkey parser";
function parse_1(path) {
var start = performance.now();
parse(path, { module: mode == "module", smoosh: false });
return performance.now() - start;
}
// Second parser
var name_2 = "SmooshMonkey parser";
function parse_2(path) {
var start = performance.now();
parse(path, { module: mode == "module", smoosh: true });
return performance.now() - start;
}
// For a given `parse` function, execute it with the content of each file in
// `dir`. This process is repeated `N` times and the results are added to the
// `result` argument using the `prefix` key for the filenames.
function for_all_files(parse, N = 1, prefix = "", result = {}) {
var path = "", content = "";
var t = 0;
var list = os.file.listDir(dir);
for (var file of list) {
try {
path = os.path.join(dir, file);
content = os.file.readRelativeToScript(path);
try {
t = 0;
for (var n = 0; n < N; n++)
t += parse(content);
result[prefix + path] = { time: t / N, bytes: content.length };
} catch (e) {
// ignore all errors for now.
result[prefix + path] = { time: null, bytes: content.length };
}
} catch (e) {
// ignore all read errors.
}
}
return result;
}
// Compare the results of 2 parser runs and compute the speed ratio between the
// 2 parsers. Results from both parsers are assuming to be comparing the same
// things if they have the same property name.
//
// The aggregated results is returned as an object, which reports the total time
// for each parser, the quantity of bytes parsed and skipped and an array of
// speed ratios for each file tested.
function compare(name1, res1, name2, res2) {
var result = {
name1: name1,
name2: name2,
time1: 0,
time2: 0,
parsed_files: 0,
parsed_bytes: 0,
skipped_files: 0,
skipped_bytes: 0,
ratios_2over1: [],
};
for (var path of Object.keys(res1)) {
if (!(path in res1 && path in res2)) {
continue;
}
var p1 = res1[path];
var p2 = res2[path];
if (p1.time !== null && p2.time !== null) {
result.time1 += p1.time;
result.time2 += p2.time;
result.parsed_files += 1;
result.parsed_bytes += p1.bytes;
result.ratios_2over1.push(p2.time / p1.time);
} else {
result.skipped_files += 1;
result.skipped_bytes += p1.bytes;
}
}
return result;
}
function print_result(result) {
print(result.name1, "\t", result.time1, "ms\t", 1e6 * result.time1 / result.parsed_bytes, 'ns/byte\t', result.parsed_bytes / (1e6 * result.time1), 'bytes/ns\t');
print(result.name2, "\t", result.time2, "ms\t", 1e6 * result.time2 / result.parsed_bytes, 'ns/byte\t', result.parsed_bytes / (1e6 * result.time2), 'bytes/ns\t');
print("Total parsed (scripts:", result.parsed_files, ", bytes:", result.parsed_bytes, ")");
print("Total skipped (scripts:", result.skipped_files, ", bytes:", result.skipped_bytes, ")");
print(result.name2, "/", result.name1, ":", result.time2 / result.time1);
print(result.name2, "/", result.name1, ":", spread(result.ratios_2over1, 0, 5, 0.05));
}
// Given a `table` of speed ratios, display a distribution chart of speed
// ratios. This is useful to check if the data is noisy, bimodal, and to easily
// eye-ball characteristics of the distribution.
function spread(table, min, max, step) {
// var chars = ["\xa0", "\u2591", "\u2592", "\u2593", "\u2588"];
var chars = ["\xa0", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
var s = ["\xa0", "\xa0", "" + min, "\xa0", "\xa0"];
var ending = ["\xa0", "\xa0", "" + max, "\xa0", "\xa0"];
var scale = "\xa0\xa0";
var scale_values = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"];
var ranges = [];
var vmax = table.length / 10;
for (var i = min; i < max; i += step) {
ranges.push(0);
var decimal = i - Math.trunc(i);
var error = Math.abs(decimal - Math.round(10 * decimal) / 10);
decimal = Math.round(decimal * 10) % 10;
if (error < step / 2)
scale += scale_values[decimal];
else
scale += "\xa0";
}
for (var x of table) {
if (x < min || max < x) continue;
var idx = ((x - min) / step)|0;
ranges[idx] += 1;
}
var max_index = chars.length * s.length;
var ratio = max_index / vmax;
for (i = 0; i < s.length; i++)
s[i] += "\xa0\u2595";
for (var v of ranges) {
var d = Math.min((v * ratio)|0, max_index - 1);
var offset = max_index;
for (i = 0; i < s.length; i++) {
offset -= chars.length;
var c = Math.max(0, Math.min(d - offset, chars.length - 1));
s[i] += chars[c];
}
}
for (i = 0; i < s.length; i++)
s[i] += "\u258f\xa0" + ending[i];
var res = "";
for (i = 0; i < s.length; i++)
res += "\n" + s[i];
res += "\n" + scale;
return res;
}
// NOTE: We have multiple strategies depending whether we want to check the
// throughput of the parser assuming the parser is cold/hot in memory, the data is
// cold/hot in the cache, and the adaptive CPU throttle is low/high.
//
// Ideally we should be comparing comparable things, but due to the adaptive
// behavior of CPU and Disk, we can only approximate it while keeping results
// comparable to what users might see.
// Compare Hot-parsers on cold data.
function strategy_1() {
var res1 = for_all_files(parse_1, runs_per_script);
var res2 = for_all_files(parse_2, runs_per_script);
return compare(name_1, res1, name_2, res2);
}
// Compare Hot-parsers on cold data, and swap parse order.
function strategy_2() {
var res2 = for_all_files(parse_2, runs_per_script);
var res1 = for_all_files(parse_1, runs_per_script);
return compare(name_1, res1, name_2, res2);
}
// Interleaves N hot-parser results. (if N=1, then strategy_3 is identical to strategy_1)
//
// At the moment, this is assumed to be the best approach which might mimic how
// a helper-thread would behave if it was saturated with content to be parsed.
function strategy_3() {
var res1 = {};
var res2 = {};
var N = runs_per_script;
for (var n = 0; n < N; n++) {
for_all_files(parse_1, 1, "" + n, res1);
for_all_files(parse_2, 1, "" + n, res2);
}
return compare(name_1, res1, name_2, res2);
}
// Compare cold parsers, with alternatetively cold/hot data.
//
// By swapping parser order of execution after each file, we expect that the
// previous parser execution would be enough to evict the other from the L2
// cache, and as such cause the other parser to hit cold instruction cache where
// the instruction have to be reloaded.
//
// At the moment, this is assumed to be the best approach which might mimic how
// parsers are effectively used on the main thread.
function strategy_0() {
var path = "", content = "";
var t_1= 0, t_2 = 0, time_1 = 0, time_2 = 0;
var count = 0, count_bytes = 0, skipped = 0, skipped_bytes = 0;
var parse1_first = false;
var list = os.file.listDir(dir);
var ratios_2over1 = [];
var parse1_first = true;
for (var file of list) {
path = os.path.join(dir, file);
if (skipList.includes(path)) {
continue;
}
content = "";
try {
// print(Math.round(100 * f / list.length), file);
content = os.file.readRelativeToScript(path);
parse1_first = !parse1_first; // Math.random() > 0.5;
for (var i = 0; i < runs_per_script; i++) {
// Randomize the order in which parsers are executed as they are
// executed in the same process and the parsed content might be
// faster to load for the second parser as it is already in memory.
if (parse1_first) {
t_1 = parse_1(content);
t_2 = parse_2(content);
} else {
t_2 = parse_2(content);
t_1 = parse_1(content);
}
time_1 += t_1;
time_2 += t_2;
ratios_2over1.push(t_2 / t_1);
}
count++;
count_bytes += content.length;
} catch (e) {
// ignore all errors for now.
skipped++;
skipped_bytes += content.length;
skipList.push(path);
}
}
return {
name1: name_1,
name2: name_2,
time1: time_1,
time2: time_2,
parsed_files: count * runs_per_script,
parsed_bytes: count_bytes * runs_per_script,
skipped_files: skipped * runs_per_script,
skipped_bytes: skipped_bytes * runs_per_script,
ratios_2over1: ratios_2over1,
};
}
var outputJSON = os.getenv("SMOOSH_BENCH_AS_JSON") !== undefined;
if (!outputJSON) {
print("Main thread comparison:");
}
var main_thread_result = strategy_0();
if (!outputJSON) {
print_result(main_thread_result);
print("");
print("Off-thread comparison:");
}
var off_thread_result = strategy_3();
if (!outputJSON) {
print_result(off_thread_result);
}
if (outputJSON) {
print(JSON.stringify({
main_thread: main_thread_result,
off_thread: main_thread_result
}));
}
if (skipFile && skipList.length > skipLen) {
var content = `[${skipList.map(s => `"${s}"`).join(",")}]`;
var data = new ArrayBuffer(content.length);
var view = new Uint8Array(data);
for (var i = 0; i < content.length; i++) {
view[i] = content.charCodeAt(i);
}
os.file.writeTypedArrayToFile(skipFile, view);
}