Skip to content

Commit f61c71b

Browse files
joyeecheungevanlucas
authored andcommitted
benchmark: add progress indicator to compare.js
* Print the progress bar and the current benchmark to stderr when stderr is TTY and stdout is not. * Allow cli arguments without values via setting.boolArgs * Add --no-progress option PR-URL: #10823 Fixes: #8659 Reviewed-By: Andreas Madsen <[email protected]>
1 parent c6af766 commit f61c71b

File tree

6 files changed

+187
-24
lines changed

6 files changed

+187
-24
lines changed

benchmark/_benchmark_progress.js

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use strict';
2+
3+
const readline = require('readline');
4+
5+
function pad(input, minLength, fill) {
6+
var result = input + '';
7+
return fill.repeat(Math.max(0, minLength - result.length)) + result;
8+
}
9+
10+
function fraction(numerator, denominator) {
11+
const fdenominator = denominator + '';
12+
const fnumerator = pad(numerator, fdenominator.length, ' ');
13+
return `${fnumerator}/${fdenominator}`;
14+
}
15+
16+
function getTime(diff) {
17+
const time = Math.ceil(diff[0] + diff[1] / 1e9);
18+
const seconds = pad(time % 60, 2, '0');
19+
const minutes = pad(Math.floor(time / 60) % (60 * 60), 2, '0');
20+
const hours = pad(Math.floor(time / (60 * 60)), 2, '0');
21+
return `${hours}:${minutes}:${seconds}`;
22+
}
23+
24+
// A run is an item in the job queue: { binary, filename, iter }
25+
// A config is an item in the subqueue: { binary, filename, iter, configs }
26+
class BenchmarkProgress {
27+
constructor(queue, benchmarks) {
28+
this.queue = queue; // Scheduled runs.
29+
this.benchmarks = benchmarks; // Filenames of scheduled benchmarks.
30+
this.completedRuns = 0; // Number of completed runs.
31+
this.scheduledRuns = queue.length; // Number of scheduled runs.
32+
// Time when starting to run benchmarks.
33+
this.startTime = process.hrtime();
34+
// Number of times each file will be run (roughly).
35+
this.runsPerFile = queue.length / benchmarks.length;
36+
this.currentFile = ''; // Filename of current benchmark.
37+
this.currentFileConfig; // Configurations for current file
38+
// Number of configurations already run for the current file.
39+
this.completedConfig = 0;
40+
// Total number of configurations for the current file
41+
this.scheduledConfig = 0;
42+
this.interval = 0; // result of setInterval for updating the elapsed time
43+
}
44+
45+
startQueue(index) {
46+
this.kStartOfQueue = index;
47+
this.currentFile = this.queue[index].filename;
48+
this.interval = setInterval(() => {
49+
if (this.completedRuns === this.scheduledRuns) {
50+
clearInterval(this.interval);
51+
} else {
52+
this.updateProgress();
53+
}
54+
}, 1000);
55+
}
56+
57+
startSubqueue(data, index) {
58+
// This subqueue is generated by a new benchmark
59+
if (data.name !== this.currentFile || index === this.kStartOfQueue) {
60+
this.currentFile = data.name;
61+
this.scheduledConfig = data.queueLength;
62+
}
63+
this.completedConfig = 0;
64+
this.updateProgress();
65+
}
66+
67+
completeConfig(data) {
68+
this.completedConfig++;
69+
this.updateProgress();
70+
}
71+
72+
completeRun(job) {
73+
this.completedRuns++;
74+
this.updateProgress();
75+
}
76+
77+
getProgress() {
78+
// Get time as soon as possible.
79+
const diff = process.hrtime(this.startTime);
80+
81+
const completedRuns = this.completedRuns;
82+
const scheduledRuns = this.scheduledRuns;
83+
const finished = completedRuns === scheduledRuns;
84+
85+
// Calculate numbers for fractions.
86+
const runsPerFile = this.runsPerFile;
87+
const completedFiles = Math.floor(completedRuns / runsPerFile);
88+
const scheduledFiles = this.benchmarks.length;
89+
const completedRunsForFile = finished ? runsPerFile :
90+
completedRuns % runsPerFile;
91+
const completedConfig = this.completedConfig;
92+
const scheduledConfig = this.scheduledConfig;
93+
94+
// Calculate the percentage.
95+
let runRate = 0; // Rate of current incomplete run.
96+
if (completedConfig !== scheduledConfig) {
97+
runRate = completedConfig / scheduledConfig;
98+
}
99+
const completedRate = ((completedRuns + runRate) / scheduledRuns);
100+
const percent = pad(Math.floor(completedRate * 100), 3, ' ');
101+
102+
const caption = finished ? 'Done\n' : this.currentFile;
103+
return `[${getTime(diff)}|% ${percent}` +
104+
`| ${fraction(completedFiles, scheduledFiles)} files ` +
105+
`| ${fraction(completedRunsForFile, runsPerFile)} runs ` +
106+
`| ${fraction(completedConfig, scheduledConfig)} configs]` +
107+
`: ${caption}`;
108+
}
109+
110+
updateProgress(finished) {
111+
if (!process.stderr.isTTY || process.stdout.isTTY) {
112+
return;
113+
}
114+
readline.clearLine(process.stderr);
115+
readline.cursorTo(process.stderr, 0);
116+
process.stderr.write(this.getProgress());
117+
}
118+
}
119+
120+
module.exports = BenchmarkProgress;

benchmark/_cli.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ function CLI(usage, settings) {
4545
currentOptional = arg.slice(1);
4646
}
4747

48-
// Default the value to true
49-
if (!settings.arrayArgs.includes(currentOptional)) {
48+
if (settings.boolArgs && settings.boolArgs.includes(currentOptional)) {
5049
this.optional[currentOptional] = true;
50+
mode = 'both';
51+
} else {
52+
// expect the next value to be option related (either -- or the value)
53+
mode = 'option';
5154
}
52-
53-
// expect the next value to be option related (either -- or the value)
54-
mode = 'option';
5555
} else if (mode === 'option') {
5656
// Optional arguments value
5757

benchmark/common.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ Benchmark.prototype.http = function(options, cb) {
128128

129129
Benchmark.prototype._run = function() {
130130
const self = this;
131+
// If forked, report to the parent.
132+
if (process.send) {
133+
process.send({
134+
type: 'config',
135+
name: this.name,
136+
queueLength: this.queue.length
137+
});
138+
}
131139

132140
(function recursive(queueIndex) {
133141
const config = self.queue[queueIndex];
@@ -217,7 +225,8 @@ Benchmark.prototype.report = function(rate, elapsed) {
217225
name: this.name,
218226
conf: this.config,
219227
rate: rate,
220-
time: elapsed[0] + elapsed[1] / 1e9
228+
time: elapsed[0] + elapsed[1] / 1e9,
229+
type: 'report'
221230
});
222231
};
223232

benchmark/compare.js

+45-18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const fork = require('child_process').fork;
44
const path = require('path');
55
const CLI = require('./_cli.js');
6+
const BenchmarkProgress = require('./_benchmark_progress.js');
67

78
//
89
// Parse arguments
@@ -13,13 +14,15 @@ const cli = CLI(`usage: ./node compare.js [options] [--] <category> ...
1314
The output is formatted as csv, which can be processed using for
1415
example 'compare.R'.
1516
16-
--new ./new-node-binary new node binary (required)
17-
--old ./old-node-binary old node binary (required)
18-
--runs 30 number of samples
19-
--filter pattern string to filter benchmark scripts
20-
--set variable=value set benchmark variable (can be repeated)
17+
--new ./new-node-binary new node binary (required)
18+
--old ./old-node-binary old node binary (required)
19+
--runs 30 number of samples
20+
--filter pattern string to filter benchmark scripts
21+
--set variable=value set benchmark variable (can be repeated)
22+
--no-progress don't show benchmark progress indicator
2123
`, {
22-
arrayArgs: ['set']
24+
arrayArgs: ['set'],
25+
boolArgs: ['no-progress']
2326
});
2427

2528
if (!cli.optional.new || !cli.optional.old) {
@@ -39,6 +42,9 @@ if (benchmarks.length === 0) {
3942

4043
// Create queue from the benchmarks list such both node versions are tested
4144
// `runs` amount of times each.
45+
// Note: BenchmarkProgress relies on this order to estimate
46+
// how much runs remaining for a file. All benchmarks generated from
47+
// the same file must be run consecutively.
4248
const queue = [];
4349
for (const filename of benchmarks) {
4450
for (let iter = 0; iter < runs; iter++) {
@@ -47,10 +53,20 @@ for (const filename of benchmarks) {
4753
}
4854
}
4955
}
56+
// queue.length = binary.length * runs * benchmarks.length
5057

5158
// Print csv header
5259
console.log('"binary", "filename", "configuration", "rate", "time"');
5360

61+
const kStartOfQueue = 0;
62+
63+
const showProgress = !cli.optional['no-progress'];
64+
let progress;
65+
if (showProgress) {
66+
progress = new BenchmarkProgress(queue, benchmarks);
67+
progress.startQueue(kStartOfQueue);
68+
}
69+
5470
(function recursive(i) {
5571
const job = queue[i];
5672

@@ -59,29 +75,40 @@ console.log('"binary", "filename", "configuration", "rate", "time"');
5975
});
6076

6177
child.on('message', function(data) {
62-
// Construct configuration string, " A=a, B=b, ..."
63-
let conf = '';
64-
for (const key of Object.keys(data.conf)) {
65-
conf += ' ' + key + '=' + JSON.stringify(data.conf[key]);
66-
}
67-
conf = conf.slice(1);
78+
if (data.type === 'report') {
79+
// Construct configuration string, " A=a, B=b, ..."
80+
let conf = '';
81+
for (const key of Object.keys(data.conf)) {
82+
conf += ' ' + key + '=' + JSON.stringify(data.conf[key]);
83+
}
84+
conf = conf.slice(1);
85+
// Escape quotes (") for correct csv formatting
86+
conf = conf.replace(/"/g, '""');
6887

69-
// Escape quotes (") for correct csv formatting
70-
conf = conf.replace(/"/g, '""');
71-
72-
console.log(`"${job.binary}", "${job.filename}", "${conf}", ` +
73-
`${data.rate}, ${data.time}`);
88+
console.log(`"${job.binary}", "${job.filename}", "${conf}", ` +
89+
`${data.rate}, ${data.time}`);
90+
if (showProgress) {
91+
// One item in the subqueue has been completed.
92+
progress.completeConfig(data);
93+
}
94+
} else if (showProgress && data.type === 'config') {
95+
// The child has computed the configurations, ready to run subqueue.
96+
progress.startSubqueue(data, i);
97+
}
7498
});
7599

76100
child.once('close', function(code) {
77101
if (code) {
78102
process.exit(code);
79103
return;
80104
}
105+
if (showProgress) {
106+
progress.completeRun(job);
107+
}
81108

82109
// If there are more benchmarks execute the next
83110
if (i + 1 < queue.length) {
84111
recursive(i + 1);
85112
}
86113
});
87-
})(0);
114+
})(kStartOfQueue);

benchmark/run.js

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ if (format === 'csv') {
4444
}
4545

4646
child.on('message', function(data) {
47+
if (data.type !== 'report') {
48+
return;
49+
}
4750
// Construct configuration string, " A=a, B=b, ..."
4851
let conf = '';
4952
for (const key of Object.keys(data.conf)) {

benchmark/scatter.js

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ function csvEncodeValue(value) {
4242
const child = fork(path.resolve(__dirname, filepath), cli.optional.set);
4343

4444
child.on('message', function(data) {
45+
if (data.type !== 'report') {
46+
return;
47+
}
48+
4549
// print csv header
4650
if (printHeader) {
4751
const confHeader = Object.keys(data.conf)

0 commit comments

Comments
 (0)