forked from nodejs/undici
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
- Loading branch information
1 parent
d6e0219
commit f6246f0
Showing
7 changed files
with
727 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,306 @@ | ||
'use strict' | ||
|
||
const { connect } = require('http2') | ||
const { createSecureContext } = require('tls') | ||
const os = require('os') | ||
const path = require('path') | ||
const { readFileSync } = require('fs') | ||
const { table } = require('table') | ||
const { Writable } = require('stream') | ||
const { WritableStream } = require('stream/web') | ||
const { isMainThread } = require('worker_threads') | ||
|
||
const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..') | ||
|
||
const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8') | ||
const servername = 'agent1' | ||
|
||
const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1 | ||
const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3 | ||
const connections = parseInt(process.env.CONNECTIONS, 10) || 50 | ||
const pipelining = parseInt(process.env.PIPELINING, 10) || 10 | ||
const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100 | ||
const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0 | ||
const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0 | ||
const dest = {} | ||
|
||
if (process.env.PORT) { | ||
dest.port = process.env.PORT | ||
dest.url = `https://localhost:${process.env.PORT}` | ||
} else { | ||
dest.url = 'https://localhost' | ||
dest.socketPath = path.join(os.tmpdir(), 'undici.sock') | ||
} | ||
|
||
const httpsBaseOptions = { | ||
ca, | ||
servername, | ||
protocol: 'https:', | ||
hostname: 'localhost', | ||
method: 'GET', | ||
path: '/', | ||
query: { | ||
frappucino: 'muffin', | ||
goat: 'scone', | ||
pond: 'moose', | ||
foo: ['bar', 'baz', 'bal'], | ||
bool: true, | ||
numberKey: 256 | ||
}, | ||
...dest | ||
} | ||
|
||
const http2ClientOptions = { | ||
secureContext: createSecureContext({ ca }), | ||
servername | ||
} | ||
|
||
const undiciOptions = { | ||
path: '/', | ||
method: 'GET', | ||
headersTimeout, | ||
bodyTimeout | ||
} | ||
|
||
const Class = connections > 1 ? Pool : Client | ||
const dispatcher = new Class(httpsBaseOptions.url, { | ||
allowH2: true, | ||
pipelining, | ||
connections, | ||
connect: { | ||
rejectUnauthorized: false, | ||
ca, | ||
servername | ||
}, | ||
...dest | ||
}) | ||
|
||
setGlobalDispatcher(new Agent({ | ||
allowH2: true, | ||
pipelining, | ||
connections, | ||
connect: { | ||
rejectUnauthorized: false, | ||
ca, | ||
servername | ||
} | ||
})) | ||
|
||
class SimpleRequest { | ||
constructor (resolve) { | ||
this.dst = new Writable({ | ||
write (chunk, encoding, callback) { | ||
callback() | ||
} | ||
}).on('finish', resolve) | ||
} | ||
|
||
onConnect (abort) { } | ||
|
||
onHeaders (statusCode, headers, resume) { | ||
this.dst.on('drain', resume) | ||
} | ||
|
||
onData (chunk) { | ||
return this.dst.write(chunk) | ||
} | ||
|
||
onComplete () { | ||
this.dst.end() | ||
} | ||
|
||
onError (err) { | ||
throw err | ||
} | ||
} | ||
|
||
function makeParallelRequests (cb) { | ||
return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb))) | ||
} | ||
|
||
function printResults (results) { | ||
// Sort results by least performant first, then compare relative performances and also printing padding | ||
let last | ||
|
||
const rows = Object.entries(results) | ||
// If any failed, put on the top of the list, otherwise order by mean, ascending | ||
.sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean)) | ||
.map(([name, result]) => { | ||
if (!result.success) { | ||
return [name, result.size, 'Errored', 'N/A', 'N/A'] | ||
} | ||
|
||
// Calculate throughput and relative performance | ||
const { size, mean, standardError } = result | ||
const relative = last !== 0 ? (last / mean - 1) * 100 : 0 | ||
|
||
// Save the slowest for relative comparison | ||
if (typeof last === 'undefined') { | ||
last = mean | ||
} | ||
|
||
return [ | ||
name, | ||
size, | ||
`${((connections * 1e9) / mean).toFixed(2)} req/sec`, | ||
`± ${((standardError / mean) * 100).toFixed(2)} %`, | ||
relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' | ||
] | ||
}) | ||
|
||
console.log(results) | ||
|
||
// Add the header row | ||
rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest']) | ||
|
||
return table(rows, { | ||
columns: { | ||
0: { | ||
alignment: 'left' | ||
}, | ||
1: { | ||
alignment: 'right' | ||
}, | ||
2: { | ||
alignment: 'right' | ||
}, | ||
3: { | ||
alignment: 'right' | ||
}, | ||
4: { | ||
alignment: 'right' | ||
} | ||
}, | ||
drawHorizontalLine: (index, size) => index > 0 && index < size, | ||
border: { | ||
bodyLeft: '│', | ||
bodyRight: '│', | ||
bodyJoin: '│', | ||
joinLeft: '|', | ||
joinRight: '|', | ||
joinJoin: '|' | ||
} | ||
}) | ||
} | ||
|
||
const experiments = { | ||
'http2 - request' () { | ||
return makeParallelRequests(resolve => { | ||
connect(dest.url, http2ClientOptions, (session) => { | ||
const headers = { | ||
':path': '/', | ||
':method': 'GET', | ||
':scheme': 'https', | ||
':authority': `localhost:${dest.port}` | ||
} | ||
|
||
const request = session.request(headers) | ||
|
||
request.pipe( | ||
new Writable({ | ||
write (chunk, encoding, callback) { | ||
callback() | ||
} | ||
}) | ||
).on('finish', resolve) | ||
}) | ||
}) | ||
}, | ||
'undici - pipeline' () { | ||
return makeParallelRequests(resolve => { | ||
dispatcher | ||
.pipeline(undiciOptions, data => { | ||
return data.body | ||
}) | ||
.end() | ||
.pipe( | ||
new Writable({ | ||
write (chunk, encoding, callback) { | ||
callback() | ||
} | ||
}) | ||
) | ||
.on('finish', resolve) | ||
}) | ||
}, | ||
'undici - request' () { | ||
return makeParallelRequests(resolve => { | ||
try { | ||
dispatcher.request(undiciOptions).then(({ body }) => { | ||
body | ||
.pipe( | ||
new Writable({ | ||
write (chunk, encoding, callback) { | ||
callback() | ||
} | ||
}) | ||
) | ||
.on('error', (err) => { | ||
console.log('undici - request - dispatcher.request - body - error', err) | ||
}) | ||
.on('finish', () => { | ||
resolve() | ||
}) | ||
}) | ||
} catch (err) { | ||
console.error('undici - request - dispatcher.request - requestCount', err) | ||
} | ||
}) | ||
}, | ||
'undici - stream' () { | ||
return makeParallelRequests(resolve => { | ||
return dispatcher | ||
.stream(undiciOptions, () => { | ||
return new Writable({ | ||
write (chunk, encoding, callback) { | ||
callback() | ||
} | ||
}) | ||
}) | ||
.then(resolve) | ||
}) | ||
}, | ||
'undici - dispatch' () { | ||
return makeParallelRequests(resolve => { | ||
dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) | ||
}) | ||
} | ||
} | ||
|
||
if (process.env.PORT) { | ||
// fetch does not support the socket | ||
experiments['undici - fetch'] = () => { | ||
return makeParallelRequests(resolve => { | ||
fetch(dest.url, {}).then(res => { | ||
res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } })) | ||
}).catch(console.log) | ||
}) | ||
} | ||
} | ||
|
||
async function main () { | ||
const { cronometro } = await import('cronometro') | ||
|
||
cronometro( | ||
experiments, | ||
{ | ||
iterations, | ||
errorThreshold, | ||
print: false | ||
}, | ||
(err, results) => { | ||
if (err) { | ||
throw err | ||
} | ||
|
||
console.log(printResults(results)) | ||
dispatcher.destroy() | ||
} | ||
) | ||
} | ||
|
||
if (isMainThread) { | ||
main() | ||
} else { | ||
module.exports = main | ||
} |
Oops, something went wrong.