Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

benchmark: support for multiple http benchmarkers #8140

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -620,13 +620,6 @@ ifeq ($(XZ), 0)
ssh $(STAGINGSERVER) "touch nodejs/$(DISTTYPEDIR)/$(FULLVERSION)/node-$(FULLVERSION)-$(OSTYPE)-$(ARCH).tar.xz.done"
endif

haswrk=$(shell which wrk > /dev/null 2>&1; echo $$?)
wrk:
ifneq ($(haswrk), 0)
@echo "please install wrk before proceeding. More information can be found in benchmark/README.md." >&2
@exit 1
endif

bench-net: all
@$(NODE) benchmark/run.js net

Expand All @@ -636,7 +629,7 @@ bench-crypto: all
bench-tls: all
@$(NODE) benchmark/run.js tls

bench-http: wrk all
bench-http: all
@$(NODE) benchmark/run.js http

bench-fs: all
Expand Down
26 changes: 23 additions & 3 deletions benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,28 @@ This folder contains benchmarks to measure the performance of the Node.js APIs.

## Prerequisites

Most of the http benchmarks require [`wrk`][wrk] to be installed. It may be
available through your preferred package manager. If not, `wrk` can be built
[from source][wrk] via `make`.
Most of the HTTP benchmarks require a benchmarker to be installed, this can be
either [`wrk`][wrk] or [`autocannon`][autocannon].

`Autocannon` is a Node script that can be installed using
`npm install -g autocannon`. It will use the Node executable that is in the
path, hence if you want to compare two HTTP benchmark runs make sure that the
Node version in the path is not altered.

`wrk` may be available through your preferred package manger. If not, you can
easily build it [from source][wrk] via `make`.

To select which tool will be used to run your HTTP benchmark you can:
* When running the benchmakrs, set `NODE_HTTP_BENCHMARKER` environment variable
to desired benchmarker.
* To select the default benchmarker for a particular benchmark, specify it as
`benchmarker` key (e.g. `benchmarker: 'wrk'`) in configuration passed to
Copy link
Member

@AndreasMadsen AndreasMadsen Aug 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good idea. The options to createBenchmark should just be the benchmark parameters. If you want this feature, I think it would be much better to add an option object to bench.http.

var bench = common.createBenchmark(main, {
  num: [1, 4, 8, 16],
  size: [1, 64, 256],
  c: [100],
  benchmarker: ['wrk']
});

function main(conf) {
  bench.http({
    url: '/', 
    duration: 10,
    connections: conf.c,
    benchmarker: conf.benchmarker
  }, function () { ... });
}

`createBenchmark`. This can be overridden by `NODE_HTTP_BENCHMARKER` in run
time.

If you do not specify which benchmarker to use, all of the installed tools will
be used to run the benchmarks. This will also happen if you pass `all` as the
desired benchmark tool.

To analyze the results `R` should be installed. Check you package manager or
download it from https://www.r-project.org/.
Expand Down Expand Up @@ -287,5 +306,6 @@ function main(conf) {
}
```

[autocannon]: https://github.com/mcollina/autocannon
[wrk]: https://github.com/wg/wrk
[t-test]: https://en.wikipedia.org/wiki/Student%27s_t-test#Equal_or_unequal_sample_sizes.2C_unequal_variances
179 changes: 142 additions & 37 deletions benchmark/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,52 +88,157 @@ Benchmark.prototype._queue = function(options) {
return queue;
};

function hasWrk() {
const result = child_process.spawnSync('wrk', ['-h']);
if (result.error && result.error.code === 'ENOENT') {
console.error('Couldn\'t locate `wrk` which is needed for running ' +
'benchmarks. Check benchmark/README.md for further instructions.');
process.exit(1);
}
function AutocannonBenchmarker() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps these abstractions should be moved to a separate file. I find this hard to read.

const autocannon_exe = process.platform === 'win32'
? 'autocannon.cmd'
: 'autocannon';
this.present = function() {
var result = child_process.spawnSync(autocannon_exe, ['-h']);
if (result.error && result.error.code === 'ENOENT')
return false;
else
return true;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps simplify this to just:

  return !(result.error && result.error.code === 'ENOENT');

this.create = function(path, duration, connections) {
const args = ['-d', duration, '-c', connections, '-j', '-n',
'http://127.0.0.1:' + exports.PORT + path ];
var child = child_process.spawn(autocannon_exe, args);
child.stdout.setEncoding('utf8');
return child;
};
this.processResults = function(output) {
let result;
try {
result = JSON.parse(output);
} catch (err) {
// Do nothing, let next line handle this
}
if (!result || !result.requests || !result.requests.average) {
return undefined;
} else {
return result.requests.average;
}
};
}

// benchmark an http server.
const WRK_REGEXP = /Requests\/sec:[ \t]+([0-9\.]+)/;
Benchmark.prototype.http = function(urlPath, args, cb) {
hasWrk();
const self = this;

const urlFull = 'http://127.0.0.1:' + exports.PORT + urlPath;
args = args.concat(urlFull);

const childStart = process.hrtime();
const child = child_process.spawn('wrk', args);
child.stderr.pipe(process.stderr);
function WrkBenchmarker() {
this.present = function() {
var result = child_process.spawnSync('wrk', ['-h']);
if (result.error && result.error.code === 'ENOENT')
return false;
else
return true;
};
this.create = function(path, duration, connections) {
const args = ['-d', duration, '-c', connections, '-t', 8,
'http://127.0.0.1:' + exports.PORT + path ];
var child = child_process.spawn('wrk', args);
child.stdout.setEncoding('utf8');
child.stderr.pipe(process.stderr);
return child;
};
const regexp = /Requests\/sec:[ \t]+([0-9\.]+)/;
this.processResults = function(output) {
const match = output.match(regexp);
const result = match && +match[1];
if (!result)
return undefined;
else
return result;
};
}

// Collect stdout
let stdout = '';
child.stdout.on('data', (chunk) => stdout += chunk.toString());
const HTTPBenchmarkers = {
autocannon: new AutocannonBenchmarker(),
wrk: new WrkBenchmarker()
};

child.once('close', function(code) {
const elapsed = process.hrtime(childStart);
if (cb) cb(code);
// Benchmark an http server.
Benchmark.prototype.http = function(urlPath, duration, connections, cb) {
const self = this;
duration = 1;

const picked_benchmarker = process.env.NODE_HTTP_BENCHMARKER ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason to run all benchmarks by default? It takes quite a long time to run the http benchmarks, I don't think we need to add more to that by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have both tools installed, then I assume you want to use both. As for the time - each HTTP benchmark run by those tools takes 10s. On my box all of the HTTP benchmarks take 14 minutes with 1 tool, and only 3 minutes more with 2 both of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like something is wrong then. If each benchmarker takes 10 sec and it takes 14 min with one benchmarker, then it should take 28 min with two benchmarkers?

I also disagree with the premise. The benchmarkers should be functionallity equivalent, and should thus give similar results. If they give very different results (in a non-linear propertional way) it's sounds like something is wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AndreasMadsen:

I also disagree with the premise. The benchmarkers should be functionallity equivalent, and should thus give similar results. If they give very different results (in a non-linear propertional way) it's sounds like something is wrong.

Just to clarify the issue here (summing up from #7180):

a) we want to be able to run the http benchmarks on Windows too, and it seem extremely hard to get wrk on Windows
b) @bzoz proposed to use ab, but ab is significantly different from wrk
c) I proposed to use autocannon, which is based on Node, and so it works on all platforms equally
d) @mscdex and @jbergstroem argued that we are introducing a dependency on an installed version of Node, which might influence benchmarks (currently it is not #7180 (comment))
e) @Fishrock123 proposes to support both runners

@AndreasMadsen would you mind reviewing if autocannon and wrk can be functionally equivalent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: not all benchmarks in benchmark/http/ use external tool. There are things like check_invalid_header etc., which take most of the time. In any way - running node benchmark/run.js http with 2 tools does not take significantly more time than with 1.

Copy link
Member

@AndreasMadsen AndreasMadsen Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bzoz please see #8139, the issue is that http/simple.js takes too long time, using two benchmarkers would make it take twice as long.

@mcollina

a) we want to be able to run the http benchmarks on Windows too, and it seem extremely hard to get wrk on Windows

I'm all for adding autocannon for getting Windows support.

d) @mscdex and @jbergstroem argued that we are introducing a dependency on an installed version of Node, which might influence benchmarks (currently it is not #7180 (comment))

That is mostly an issue in when continuously monitoring performance. When just comparing a benchmark between master and a PR, that shouldn't be a problem.

@AndreasMadsen would you mind reviewing if autocannon and wrk can be functionally equivalent?

It's actually impossible from a philosophy of science perspective to show this, however we can validate it.

First I run the http/simple.js using wrk and then using autocannon. (raw data https://gist.github.com/AndreasMadsen/619e4a447b8df7043f32771e64b7693f). To compare wrk with autocannon we need to compensate for the parameters, as there are few of them and we have many observations we can do that by using factors (the settings becomes a set of binary numbers) and then use a linear regression. Simultaneously we can also check how much the benchmarker affects the results by also making it a factor.

Call:
lm(formula = rate ~ ., data = dat)

Residuals:
    Min      1Q  Median      3Q     Max 
-3749.9 -1446.7   110.9  1376.7  5350.7 

Coefficients:
                Estimate Std. Error t value Pr(>|t|)    
(Intercept)     14003.64     121.29 115.456  < 2e-16 ***
c500               60.51      85.76   0.706    0.481    
chunks1          -823.67     105.04  -7.841 6.95e-15 ***
chunks4         -2715.50     105.04 -25.852  < 2e-16 ***
length1024      -1689.93     105.04 -16.088  < 2e-16 ***
length102400    -7251.31     105.04 -69.034  < 2e-16 ***
type bytes      -2110.44      85.76 -24.607  < 2e-16 ***
benchmarker wrk    46.03      85.76   0.537    0.592    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 1993 on 2152 degrees of freedom
Multiple R-squared:  0.7521,    Adjusted R-squared:  0.7513 
F-statistic: 932.5 on 7 and 2152 DF,  p-value: < 2.2e-16
Raw

script: https://gist.github.com/AndreasMadsen/619e4a447b8df7043f32771e64b7693f

From this result we can see that the benchmarker does not have a statistical significant effect on the output. We can see this because there are no stars right to benchmarker wrk. Where is benchmarker autocannon some tends to ask. What happens is that autocannon is set to the default and we are simply measuring the difference in performance caused by wrk. From the results we see that wrk is 46.03 ops/sec faster, but it is not significant.

I guess one could do a more detailed analysis of the interactions between the benchmarker and the individual parameters, but those results are tricky to interpret because of paired correlation. I would say that since we can't observe a statistical significant effect we should assume that the benchmarker doesn't matter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just pick the first that is available on the system, if it is not specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that, pick first one available as default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should prefer wrk and fallback to autocannon, just because autocannon depends on Node. Yes it shouldn't really matter, but its better to be safe.

Copy link
Member

@AndreasMadsen AndreasMadsen Aug 19, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record: I have done a crude interaction analysis, it turns out that the benchmarker does affect performances when looking at a specific set of parameters:

  c chunks length    type improvement significant      p.value
  50      0      4  buffer     -7.91 %         *** 1.322680e-09
  50      0      4   bytes     17.84 %         *** 4.045045e-21
  50      0   1024  buffer    -20.69 %         *** 9.271728e-27
  50      0   1024   bytes    -22.11 %         *** 1.515380e-20
  50      0 102400  buffer    -30.34 %         *** 5.798797e-49
  50      0 102400   bytes      3.83 %         *** 2.321257e-11
  50      1      4  buffer    -12.34 %         *** 3.224916e-16
  50      1      4   bytes    -10.43 %         *** 5.706192e-13
  50      1   1024  buffer    -15.65 %         *** 2.850410e-07
  50      1   1024   bytes     22.22 %         *** 1.248468e-39
  50      1 102400  buffer    -31.47 %         *** 4.988408e-46
  50      1 102400   bytes     -0.63 %          ** 6.046574e-03
  50      4      4  buffer     38.50 %         *** 2.366325e-52
  50      4      4   bytes     29.70 %         *** 2.854201e-27
  50      4   1024  buffer     27.75 %         *** 6.241389e-36
  50      4   1024   bytes     64.73 %         *** 4.793425e-22
  50      4 102400  buffer     -8.18 %         *** 7.693781e-26
  50      4 102400   bytes      3.94 %         *** 4.965380e-06
 500      0      4  buffer      9.04 %         *** 7.695347e-18
 500      0      4   bytes     17.01 %         *** 3.182603e-34
 500      0   1024  buffer    -14.02 %         *** 2.095410e-33
 500      0   1024   bytes    -19.36 %         *** 1.450384e-32
 500      0 102400  buffer    -38.56 %         *** 2.547833e-67
 500      0 102400   bytes      4.35 %         *** 7.185082e-20
 500      1      4  buffer      8.00 %         *** 1.383958e-21
 500      1      4   bytes      5.21 %         *** 7.024325e-16
 500      1   1024  buffer     -9.36 %         *** 2.184297e-20
 500      1   1024   bytes      6.08 %         *** 7.288844e-14
 500      1 102400  buffer    -36.23 %         *** 1.578285e-65
 500      1 102400   bytes      1.00 %         *** 2.875669e-04
 500      4      4  buffer     19.72 %         *** 7.854005e-49
 500      4      4   bytes     44.20 %         *** 1.691548e-35
 500      4   1024  buffer     19.78 %         *** 1.023951e-38
 500      4   1024   bytes     34.85 %         *** 3.279581e-39
 500      4 102400  buffer     -9.27 %         *** 1.401027e-28
 500      4 102400   bytes      6.78 %         *** 1.708493e-32

table: relative performances improvement of using autocannon.

however this doesn't say anything about the benchmarks ability to benchmark an optimization proposal. It just means that the benchmarkers aren't equally performant in all aspects. Really it just optimization suggests for the benchmarker implementers :)

Anyway it was just for the record.

edit: perhaps it is possible to make an equal variances tests instead of an equal mean test, that should express the benchmark ability of the benchmarkers. It is not something I normally do, so I will have to look it up.

this.config.benchmarker || 'all';
const benchmarkers = picked_benchmarker === 'all'
? Object.keys(HTTPBenchmarkers)
: [picked_benchmarker];

// See if any benchmarker is available. Also test if all used benchmarkers
// are defined
var any_available = false;
for (var i = 0; i < benchmarkers.length; ++i) {
const benchmarker = benchmarkers[i];
const http_benchmarker = HTTPBenchmarkers[benchmarker];
if (http_benchmarker === undefined) {
console.error('Unknown http benchmarker: ', benchmarker);
process.exit(1);
}
if (http_benchmarker.present()) {
any_available = true;
}
}
if (!any_available) {
console.error('Couldn\'t locate any of the required http benchmarkers ' +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/Couldn't/Could not

'(' + benchmarkers.join(', ') + '). Check ' +
'benchmark/README.md for further instructions.');
process.exit(1);
}

if (code) {
console.error('wrk failed with ' + code);
process.exit(code);
function runHttpBenchmarker(index, collected_code) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v8 optimizations is going to affect the next results if you don't restart node.js. I think it would be much better not to support this at all on the bench.http level, and just add an option to bench.http and let the configuration queue handle this. That should also reduce the code quite a bit.

var bench = common.createBenchmark(main, {
  num: [1, 4, 8, 16],
  size: [1, 64, 256],
  c: [100],
  benchmarker: ['wrk', 'autocannon'] /* will run both */
});

function main(conf) {
  bench.http({
    url: '/', 
    duration: 10,
    connections: conf.c,
    benchmarker: conf.benchmarker
  }, function () { ... });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Just tested it, and second run is up to 10% faster.

// All benchmarkers executed
if (index === benchmarkers.length) {
if (cb)
cb(collected_code);
if (collected_code !== 0)
process.exit(1);
return;
}

// Extract requests pr second and check for odd results
const match = stdout.match(WRK_REGEXP);
if (!match || match.length <= 1) {
console.error('wrk produced strange output:');
console.error(stdout);
process.exit(1);
// Run next benchmarker
const benchmarker = benchmarkers[index];
self.config.benchmarker = benchmarker;

const http_benchmarker = HTTPBenchmarkers[benchmarker];
if (http_benchmarker.present()) {
const child_start = process.hrtime();
var child = http_benchmarker.create(urlPath, duration, connections);

// Collect stdout
let stdout = '';
child.stdout.on('data', (chunk) => stdout += chunk.toString());

child.once('close', function(code) {
const elapsed = process.hrtime(child_start);
if (code) {
if (stdout === '') {
console.error(benchmarker + ' failed with ' + code);
Copy link
Member

@jasnell jasnell Aug 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

`${benchmarker} failed with ${code}`

(that is, using template strings here and elsewhere throughout)

} else {
console.error(benchmarker + ' failed with ' + code + '. Output: ');
console.error(stdout);
}
runHttpBenchmarker(index + 1, code);
return;
}

var result = http_benchmarker.processResults(stdout);
if (!result) {
console.error(benchmarker + ' produced strange output');
console.error(stdout);
runHttpBenchmarker(index + 1, 1);
return;
}

self.report(result, elapsed);
runHttpBenchmarker(index + 1, collected_code);
});
} else {
runHttpBenchmarker(index + 1, collected_code);
}
}

// Report rate
self.report(+match[1], elapsed);
});
// Run with all benchmarkers
runHttpBenchmarker(0, 0);
};

Benchmark.prototype._run = function() {
Expand Down
4 changes: 1 addition & 3 deletions benchmark/http/chunked.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ function main(conf) {
const http = require('http');
var chunk = Buffer.alloc(conf.size, '8');

var args = ['-d', '10s', '-t', 8, '-c', conf.c];

var server = http.createServer(function(req, res) {
function send(left) {
if (left === 0) return res.end();
Expand All @@ -34,7 +32,7 @@ function main(conf) {
});

server.listen(common.PORT, function() {
bench.http('/', args, function() {
bench.http('/', 10, conf.c, function() {
server.close();
});
});
Expand Down
3 changes: 1 addition & 2 deletions benchmark/http/cluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ function main(conf) {

setTimeout(function() {
var path = '/' + conf.type + '/' + conf.length;
var args = ['-d', '10s', '-t', 8, '-c', conf.c];

bench.http(path, args, function() {
bench.http(path, 10, conf.c, function() {
w1.destroy();
w2.destroy();
});
Expand Down
3 changes: 1 addition & 2 deletions benchmark/http/end-vs-write-end.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,13 @@ function main(conf) {
}

var method = conf.method === 'write' ? write : end;
var args = ['-d', '10s', '-t', 8, '-c', conf.c];

var server = http.createServer(function(req, res) {
method(res);
});

server.listen(common.PORT, function() {
bench.http('/', args, function() {
bench.http('/', 10, conf.c, function() {
server.close();
});
});
Expand Down
3 changes: 1 addition & 2 deletions benchmark/http/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ function main(conf) {
var server = require('./_http_simple.js');
setTimeout(function() {
var path = '/' + conf.type + '/' + conf.length + '/' + conf.chunks;
var args = ['-d', '10s', '-t', 8, '-c', conf.c];

bench.http(path, args, function() {
bench.http(path, 10, conf.c, function() {
server.close();
});
}, 2000);
Expand Down