Skip to content

Commit bfc0570

Browse files
author
Trygve Lie
committed
feat: Initial commit
1 parent d104666 commit bfc0570

File tree

7 files changed

+586
-0
lines changed

7 files changed

+586
-0
lines changed

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

benchmarks/benchmark.js

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import http from 'node:http';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { table } from 'table';
5+
import { fetch } from 'undici';
6+
import { Writable } from 'node:stream';
7+
import { WritableStream } from 'stream/web';
8+
import { isMainThread } from 'node:worker_threads';
9+
10+
import { Pool, Client, Agent, setGlobalDispatcher } from 'undici';
11+
12+
import PodiumHttpClient from '../lib/http-client.js';
13+
const podium = new PodiumHttpClient();
14+
15+
const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1
16+
const errorThreshold = parseInt(process.env.ERROR_TRESHOLD, 10) || 3
17+
const connections = parseInt(process.env.CONNECTIONS, 10) || 50
18+
const pipelining = parseInt(process.env.PIPELINING, 10) || 10
19+
const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100
20+
const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0
21+
const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0
22+
23+
24+
const dest = {}
25+
26+
if (process.env.PORT) {
27+
dest.port = process.env.PORT
28+
dest.url = `http://localhost:${process.env.PORT}`
29+
} else {
30+
dest.url = 'http://localhost'
31+
dest.socketPath = path.join(os.tmpdir(), 'undici.sock')
32+
}
33+
34+
35+
const httpBaseOptions = {
36+
protocol: 'http:',
37+
hostname: 'localhost',
38+
method: 'GET',
39+
path: '/',
40+
query: {
41+
frappucino: 'muffin',
42+
goat: 'scone',
43+
pond: 'moose',
44+
foo: ['bar', 'baz', 'bal'],
45+
bool: true,
46+
numberKey: 256
47+
},
48+
...dest
49+
}
50+
51+
const httpNoKeepAliveOptions = {
52+
...httpBaseOptions,
53+
agent: new http.Agent({
54+
keepAlive: false,
55+
maxSockets: connections
56+
})
57+
}
58+
59+
60+
const httpKeepAliveOptions = {
61+
...httpBaseOptions,
62+
agent: new http.Agent({
63+
keepAlive: true,
64+
maxSockets: connections
65+
})
66+
}
67+
68+
const undiciOptions = {
69+
path: '/',
70+
method: 'GET',
71+
headersTimeout,
72+
bodyTimeout
73+
}
74+
75+
const Class = connections > 1 ? Pool : Client
76+
const dispatcher = new Class(httpBaseOptions.url, {
77+
pipelining,
78+
connections,
79+
...dest
80+
})
81+
82+
setGlobalDispatcher(new Agent({ pipelining, connections }))
83+
84+
class SimpleRequest {
85+
constructor (resolve) {
86+
this.dst = new Writable({
87+
write (chunk, encoding, callback) {
88+
callback()
89+
}
90+
}).on('finish', resolve)
91+
}
92+
93+
onConnect (abort) {}
94+
95+
onHeaders (statusCode, headers, resume) {
96+
this.dst.on('drain', resume)
97+
}
98+
99+
onData (chunk) {
100+
return this.dst.write(chunk)
101+
}
102+
103+
onComplete () {
104+
this.dst.end()
105+
}
106+
107+
onError (err) {
108+
throw err
109+
}
110+
}
111+
112+
function makeParallelRequests (cb) {
113+
return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb)))
114+
}
115+
116+
function printResults (results) {
117+
// Sort results by least performant first, then compare relative performances and also printing padding
118+
let last
119+
120+
const rows = Object.entries(results)
121+
// If any failed, put on the top of the list, otherwise order by mean, ascending
122+
.sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
123+
.map(([name, result]) => {
124+
if (!result.success) {
125+
return [name, result.size, 'Errored', 'N/A', 'N/A']
126+
}
127+
128+
// Calculate throughput and relative performance
129+
const { size, mean, standardError } = result
130+
const relative = last !== 0 ? (last / mean - 1) * 100 : 0
131+
132+
// Save the slowest for relative comparison
133+
if (typeof last === 'undefined') {
134+
last = mean
135+
}
136+
137+
return [
138+
name,
139+
size,
140+
`${((connections * 1e9) / mean).toFixed(2)} req/sec`,
141+
${((standardError / mean) * 100).toFixed(2)} %`,
142+
relative > 0 ? `+ ${relative.toFixed(2)} %` : '-'
143+
]
144+
})
145+
146+
console.log(results)
147+
148+
// Add the header row
149+
rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest'])
150+
151+
return table(rows, {
152+
columns: {
153+
0: {
154+
alignment: 'left'
155+
},
156+
1: {
157+
alignment: 'right'
158+
},
159+
2: {
160+
alignment: 'right'
161+
},
162+
3: {
163+
alignment: 'right'
164+
},
165+
4: {
166+
alignment: 'right'
167+
}
168+
},
169+
drawHorizontalLine: (index, size) => index > 0 && index < size,
170+
border: {
171+
bodyLeft: '│',
172+
bodyRight: '│',
173+
bodyJoin: '│',
174+
joinLeft: '|',
175+
joinRight: '|',
176+
joinJoin: '|'
177+
}
178+
})
179+
}
180+
181+
const experiments = {
182+
'http - no keepalive' () {
183+
return makeParallelRequests(resolve => {
184+
http.get(httpNoKeepAliveOptions, res => {
185+
res
186+
.pipe(
187+
new Writable({
188+
write (chunk, encoding, callback) {
189+
callback()
190+
}
191+
})
192+
)
193+
.on('finish', resolve)
194+
})
195+
}).catch(console.log)
196+
},
197+
198+
'http - keepalive' () {
199+
return makeParallelRequests(resolve => {
200+
http.get(httpKeepAliveOptions, res => {
201+
res
202+
.pipe(
203+
new Writable({
204+
write (chunk, encoding, callback) {
205+
callback()
206+
}
207+
})
208+
)
209+
.on('finish', resolve)
210+
})
211+
}).catch(console.log)
212+
},
213+
214+
'fetch' () {
215+
return makeParallelRequests(resolve => {
216+
fetch('http://localhost:3042').then((res) => {
217+
res.body
218+
.pipeTo(new WritableStream({ write () { }, close () { resolve() } }))
219+
}).catch(console.log)
220+
})
221+
},
222+
223+
'undici - pipeline' () {
224+
return makeParallelRequests(resolve => {
225+
dispatcher
226+
.pipeline(undiciOptions, data => {
227+
return data.body
228+
})
229+
.end()
230+
.pipe(
231+
new Writable({
232+
write (chunk, encoding, callback) {
233+
callback()
234+
}
235+
})
236+
)
237+
.on('finish', resolve)
238+
}).catch(console.log)
239+
},
240+
241+
'undici - request' () {
242+
return makeParallelRequests(resolve => {
243+
dispatcher.request(undiciOptions).then(({ body }) => {
244+
body
245+
.pipe(
246+
new Writable({
247+
write (chunk, encoding, callback) {
248+
callback()
249+
}
250+
})
251+
)
252+
.on('finish', resolve)
253+
}).catch(console.log)
254+
})
255+
},
256+
257+
'podium-http - request' () {
258+
return makeParallelRequests(resolve => {
259+
podium.request('http://localhost:3042').then(({ body }) => {
260+
body
261+
.pipe(
262+
new Writable({
263+
write (chunk, encoding, callback) {
264+
callback()
265+
}
266+
})
267+
)
268+
.on('finish', resolve)
269+
})
270+
}).catch(console.log)
271+
},
272+
273+
}
274+
275+
276+
async function main () {
277+
const { cronometro } = await import('cronometro')
278+
279+
cronometro(
280+
experiments,
281+
{
282+
iterations,
283+
errorThreshold,
284+
print: false
285+
},
286+
(err, results) => {
287+
if (err) {
288+
throw err
289+
}
290+
291+
console.log(printResults(results))
292+
// dispatcher.destroy()
293+
}
294+
)
295+
}
296+
297+
let foolMain;
298+
299+
if (isMainThread) {
300+
// console.log('I am in main thread');
301+
main()
302+
//return;
303+
} else {
304+
foolMain = main;
305+
// console.log('I am NOT in main thread');
306+
}
307+
export default foolMain;

benchmarks/server.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { unlinkSync } from 'node:fs';
2+
import { createServer } from 'node:http';
3+
import cluster from 'node:cluster';
4+
import path from 'node:path';
5+
import os from 'node:os';
6+
7+
const socketPath = path.join(os.tmpdir(), 'undici.sock')
8+
9+
const port = process.env.PORT || socketPath
10+
const timeout = parseInt(process.env.TIMEOUT, 10) || 1
11+
const workers = parseInt(process.env.WORKERS) || os.cpus().length
12+
13+
if (cluster.isPrimary) {
14+
try {
15+
unlinkSync(socketPath)
16+
} catch (_) {
17+
// Do nothing if the socket does not exist
18+
}
19+
20+
for (let i = 0; i < workers; i++) {
21+
cluster.fork()
22+
}
23+
} else {
24+
const buf = Buffer.alloc(64 * 1024, '_')
25+
const server = createServer((req, res) => {
26+
setTimeout(function () {
27+
res.end(buf)
28+
}, timeout)
29+
}).listen(port)
30+
server.keepAliveTimeout = 600e3
31+
}

benchmarks/wait.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import waitOn from 'wait-on';
2+
import path from 'path';
3+
import os from 'os';
4+
5+
const socketPath = path.join(os.tmpdir(), 'undici.sock')
6+
7+
let resources
8+
if (process.env.PORT) {
9+
resources = [`http-get://localhost:${process.env.PORT}/`]
10+
} else {
11+
resources = [`http-get://unix:${socketPath}:/`]
12+
}
13+
14+
waitOn({
15+
resources,
16+
timeout: 5000
17+
}).catch((err) => {
18+
console.error(err)
19+
process.exit(1)
20+
});

0 commit comments

Comments
 (0)