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 ;
0 commit comments