Skip to content

Commit

Permalink
Improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak committed Feb 17, 2020
1 parent 917bcb7 commit 4f52768
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 99 deletions.
41 changes: 20 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,37 +334,36 @@ Node: v13.8.0
`auto` means `http2wrapper.auto`.

```
http2-wrapper x 11,961 ops/sec ±1.31% (82 runs sampled)
http2-wrapper - preconfigured session x 13,507 ops/sec ±1.98% (85 runs sampled)
http2-wrapper - auto x 10,798 ops/sec ±1.73% (86 runs sampled)
http2 x 17,272 ops/sec ±1.22% (85 runs sampled)
http2 - 2xPassThrough x 14,262 ops/sec ±1.07% (83 runs sampled)
https - auto - keepalive x 13,066 ops/sec ±3.14% (80 runs sampled)
https - keepalive x 13,639 ops/sec ±0.82% (86 runs sampled)
https x 1,632 ops/sec ±0.97% (83 runs sampled)
http x 6,095 ops/sec ±2.09% (80 runs sampled)
http2-wrapper x 12,417 ops/sec ±3.72% (83 runs sampled)
http2-wrapper - preconfigured session x 14,517 ops/sec ±1.39% (83 runs sampled)
http2-wrapper - auto x 11,373 ops/sec ±3.17% (84 runs sampled)
http2 x 16,172 ops/sec ±1.21% (85 runs sampled)
https - auto - keepalive x 13,251 ops/sec ±3.84% (79 runs sampled)
https - keepalive x 13,158 ops/sec ±2.88% (78 runs sampled)
https x 1,618 ops/sec ±2.07% (82 runs sampled)
http x 5,922 ops/sec ±2.87% (79 runs sampled)
Fastest is http2
```

`http2-wrapper`:
- 31% less performant than `http2` (16% compared to `http2 - 2xPassThrough`)
- 12% less performant than `https - keepalive`
- 96% more performant than `http`
- 23% less performant than `http2`
- 6% less performant than `https - keepalive`
- 110% more performant than `http`

`http2-wrapper - preconfigured session`:
- 22% less performant than `http2` (5% compared to `http2 - 2xPassThrough`)
- as performant as `https - keepalive`
- 122% more performant than `http`
- 10% less performant than `http2`
- 10% more performant than `https - keepalive`
- 145% more performant than `http`

`http2-wrapper - auto`:
- 37% less performant than `http2` (24% compared to `http2 - 2xPassThrough`)
- 21% less performant than `https - keepalive`
- 77% more performant than `http`
- 30% less performant than `http2`
- 14% less performant than `https - keepalive`
- 92% more performant than `http`

`https - auto - keepalive`:
- 24% less performant than `http2` (8% compared to `http2 - 2xPassThrough`)
- 4% less performant than `https - keepalive`
- 114% more performant than `http`
- 18% less performant than `http2`
- as performant as `https - keepalive`
- 124% more performant than `http`

## Related

Expand Down
23 changes: 0 additions & 23 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use strict';
const {PassThrough} = require('stream');
const http2 = require('http2');
const https = require('https');
const http = require('http');
Expand Down Expand Up @@ -78,28 +77,6 @@ suite.add('http2-wrapper', {
deferred.resolve();
});
}
}).add('http2 - 2xPassThrough', {
defer: true,
fn: deferred => {
const inputProxy = new PassThrough();
const outputProxy = new PassThrough();

const stream = session.request({
endStream: false
});
inputProxy.pipe(stream);
stream.pipe(outputProxy);

inputProxy.end();

outputProxy.resume();
outputProxy.once('end', () => {
deferred.resolve();
});
},
onComplete: () => {
session.close();
}
}).add('https - auto - keepalive', {
defer: true,
fn: async deferred => {
Expand Down
11 changes: 8 additions & 3 deletions source/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,12 @@ class Agent extends EventEmitter {
this.tlsSessionCache = new QuickLRU({maxSize: maxCachedTlsSessions});
}

static normalizeOrigin(url) {
static normalizeOrigin(url, servername) {
if (typeof url === 'string') {
url = new URL(url);
}

return url.origin;
return servername ? `https://${servername}:${url.port || 443}` : url.origin;
}

normalizeOptions(options) {
Expand Down Expand Up @@ -216,7 +216,12 @@ class Agent extends EventEmitter {
}

const normalizedOptions = this.normalizeOptions(options);
const normalizedOrigin = Agent.normalizeOrigin(origin);
const normalizedOrigin = Agent.normalizeOrigin(origin, options && options.servername);

if (normalizedOrigin === undefined) {
reject(new TypeError('The `origin` argument needs to be a string or an URL object'));
return;
}

if (Reflect.has(this.freeSessions, normalizedOptions)) {
// Look for all available free sessions.
Expand Down
54 changes: 32 additions & 22 deletions source/client-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,67 +213,77 @@ class ClientRequest extends Writable {
this._request = stream;

if (this.destroyed || this.aborted) {
this._request.close(NGHTTP2_CANCEL);
stream.close(NGHTTP2_CANCEL);
return;
}

// Forwards `timeout`, `continue`, `close` and `error` events to this instance.
if (!isConnectMethod) {
proxyEvents(this._request, this, ['timeout', 'continue', 'close', 'error']);
proxyEvents(stream, this, ['timeout', 'continue', 'close', 'error']);
}

// This event tells we are ready to listen for the data.
this._request.once('response', (headers, flags, rawHeaders) => {
stream.once('response', (headers, flags, rawHeaders) => {
// If we were to emit raw request stream, it would be as fast as the native approach.
// Note that wrapping the raw stream in a Proxy instance won't improve the performance (already tested it).
this.res = new IncomingMessage(this.socket, this._request.readableHighWaterMark);
this.res.req = this;
this.res.statusCode = headers[HTTP2_HEADER_STATUS];
this.res.headers = headers;
this.res.rawHeaders = rawHeaders;
const response = new IncomingMessage(this.socket, stream.readableHighWaterMark);
this.res = response;

this.res.once('end', () => {
response.req = this;
response.statusCode = headers[HTTP2_HEADER_STATUS];
response.headers = headers;
response.rawHeaders = rawHeaders;

response.once('end', () => {
if (this.aborted) {
this.res.aborted = true;
this.res.emit('aborted');
response.aborted = true;
response.emit('aborted');
} else {
this.res.complete = true;
response.complete = true;
}
});

if (isConnectMethod) {
this.res.upgrade = true;
response.upgrade = true;

// The HTTP1 API says the socket is detached here,
// but we can't do that so we pass the original HTTP2 request.
if (this.emit('connect', this.res, this._request, Buffer.alloc(0))) {
if (this.emit('connect', response, stream, Buffer.alloc(0))) {
this.emit('close');
} else {
// No listeners attached, destroy the original request.
this._request.destroy();
stream.destroy();
}
} else {
// Forwards data
this._request.pipe(this.res);
stream.on('data', chunk => {
if (!response.push(chunk)) {
this.pause();
}
});

stream.once('end', () => {
response.push(null);
});

if (!this.emit('response', this.res)) {
if (!this.emit('response', response)) {
// No listeners attached, dump the response.
this.res._dump();
response._dump();
}
}
});

// Emits `information` event
this._request.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]}));
stream.once('headers', headers => this.emit('information', {statusCode: headers[HTTP2_HEADER_STATUS]}));

this._request.once('trailers', (trailers, flags, rawTrailers) => {
stream.once('trailers', (trailers, flags, rawTrailers) => {
// Assigns trailers to the response object.
this.res.trailers = trailers;
this.res.rawTrailers = rawTrailers;
});

this.socket = this._request.session.socket;
this.connection = this._request.session.socket;
this.socket = stream.session.socket;
this.connection = stream.session.socket;

process.nextTick(() => {
this.emit('socket', this.socket);
Expand Down
10 changes: 8 additions & 2 deletions source/incoming-message.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';
const {PassThrough} = require('stream');
const {Readable} = require('stream');

class IncomingMessage extends PassThrough {
class IncomingMessage extends Readable {
constructor(socket, highWaterMark) {
super({highWaterMark});

Expand Down Expand Up @@ -44,6 +44,12 @@ class IncomingMessage extends PassThrough {
this.resume();
}
}

_read() {
if (this.req) {
this.req._request.resume();
}
}
}

module.exports = IncomingMessage;
46 changes: 18 additions & 28 deletions test/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,21 @@ const tripleRequestWrapper = createWrapper({

const message = 'Simple error';

test('passing string as `authority`', wrapper, async (t, server) => {
test('getSession() - passing string as `origin`', wrapper, async (t, server) => {
const agent = new Agent();
await t.notThrowsAsync(agent.getSession(server.url));

agent.destroy();
});

test('passing options as `authority`', wrapper, async (t, server) => {
test('getSession() - passing URL as `origin`', wrapper, async (t, server) => {
const agent = new Agent();

await t.notThrowsAsync(agent.getSession({
hostname: server.options.hostname,
port: server.options.port
}));
await t.notThrowsAsync(agent.getSession(new URL(server.url)));

await t.notThrowsAsync(agent.getSession({
host: server.options.hostname,
port: server.options.port
}));

await t.notThrowsAsync(agent.getSession({
host: server.options.hostname,
servername: server.options.hostname,
port: server.options.port
}));

await t.notThrowsAsync(agent.getSession({
port: server.options.port
}));

const error = await t.throwsAsync(agent.getSession({}));
t.is(error.port, 443);
t.is(error.address, '127.0.0.1');
await t.throwsAsync(agent.getSession({}), {
message: 'The `origin` argument needs to be a string or an URL object'
});

agent.destroy();
});
Expand Down Expand Up @@ -503,7 +485,7 @@ if (supportsTlsSessions) {
await agent.getSession(server.url);
await setImmediateAsync();

t.true(is.buffer(agent.tlsSessionCache.get(`${Agent.normalizeAuthority(server.url)}:`).session));
t.true(is.buffer(agent.tlsSessionCache.get(`${Agent.normalizeOrigin(server.url)}:`).session));

agent.destroy();
});
Expand All @@ -513,7 +495,7 @@ if (supportsTlsSessions) {
const session = await agent.getSession(server.url);
await setImmediateAsync();

const tlsSession = agent.tlsSessionCache.get(`${Agent.normalizeAuthority(server.url)}:`).session;
const tlsSession = agent.tlsSessionCache.get(`${Agent.normalizeOrigin(server.url)}:`).session;

session.close();
await pEvent(session, 'close');
Expand All @@ -533,12 +515,12 @@ if (supportsTlsSessions) {
const session = await agent.getSession(server.url);
await setImmediateAsync();

t.true(is.buffer(agent.tlsSessionCache.get(`${Agent.normalizeAuthority(server.url)}:`).session));
t.true(is.buffer(agent.tlsSessionCache.get(`${Agent.normalizeOrigin(server.url)}:`).session));

session.destroy(new Error(message));
await pEvent(session, 'close', {rejectionEvents: []});

t.true(is.undefined(agent.tlsSessionCache.get(`${Agent.normalizeAuthority(server.url)}:`)));
t.true(is.undefined(agent.tlsSessionCache.get(`${Agent.normalizeOrigin(server.url)}:`)));

agent.destroy();
});
Expand Down Expand Up @@ -1016,3 +998,11 @@ test('`session` event', wrapper, async (t, server) => {

agent.destroy();
});

test('errors on failure', async t => {
const agent = new Agent();
const error = await t.throwsAsync(agent.getSession(new URL('https://localhost')));

t.is(error.port, 443);
t.is(error.address, '127.0.0.1');
});

0 comments on commit 4f52768

Please sign in to comment.