Skip to content

Commit a8a5d0a

Browse files
metcoder95mkaufmanerronagmcollina
authored
feat: Add H2 support (#2061)
* feat: port H2 work with latest main * fix: linting errors * refactor: adjust support for headers and set testing * test: add testing for h2 * refactor: make http2 session handle shorter * feat: add support for sending body over http2 * feat: ensure support for streams over H2 * refactor: remove noisy logs * feat: support 100 continue * feat: support for iterators * feat: add support for Blobs * refactor: adapt contracts to h2 support * refactor: cleanup * feat: support for content-length * refactor: body write * test: refactor check continue test * fix: bad check for headers * fix: bad change * chore: add http2 alpn test (#34) * chore: add http2 alpn test using fastify * chore: update to test https 1 with http2 * chore: update alpn test to return server request alpn protocol and http version * chore: add alpn with body * fix: remove fastify from package json * refactor: remove leftover * test: ensure dispatch feature * feat(h2): support connect * fix: pass signal down the road * test: ensure stream works as expected * test: ensure pipeline works as expected * test: ensure upgrade fails * test: ensure destroy works as expected * feat: allow to disable H2 calls upon request * fix: linting * feat: support GOAWAY frame (server-side) * refactor; use h2 constants * feat: initial shape of concurrent stream handling * refactor: header processing * chore: http/2 benchmark (#35) Co-authored-by: Carlos Fuentes <me@metcoder.dev> * refactor: adjust accordingly to review * fix: add missing error handler for socket * refactor: headers handling * feat: initial concurrent stream support * fix: lint * refactor: adjust several pieces * fix: support h2 headers for fetch * feat: enhance h2 for fetch * refactor: apply review suggestions Co-authored-by: Robert Nagy <ronagy@icloud.com> * refactor: set allowh2 to false * fix: linting * refactor: implement kHTTPConnVersion symbol * test: adjust testing * feat: buil factory * fix: rebase * feat: enhance TS types for maxConcurrent streams * test: move fetch tests to fetch folder * feat: add experimental warning * test: refactor suite * refactor: apply several changes * test: split tests between v20 and lower --------- Co-authored-by: Michael Kaufman <2073135+mkaufmaner@users.noreply.github.com> Co-authored-by: Robert Nagy <ronagy@icloud.com> Co-authored-by: Matteo Collina <hello@matteocollina.com>
1 parent ea4f257 commit a8a5d0a

22 files changed

Lines changed: 2846 additions & 90 deletions

benchmarks/benchmark-http2.js

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

0 commit comments

Comments
 (0)