|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +// Measures a complete HTTP/3 exchange: establish a session, send one request |
| 4 | +// and read the whole response. Run in two modes, so the cost of a resumed |
| 5 | +// 0-RTT session can be compared against a full handshake. |
| 6 | +// |
| 7 | +// The 0-RTT mode needs a session ticket, which can only come from an earlier |
| 8 | +// connection. That first connection is made during warmup, outside the |
| 9 | +// measured region, so what is timed is only the resumed exchange. |
| 10 | + |
| 11 | +const common = require('../common.js'); |
| 12 | +const fixtures = require('../../test/common/fixtures'); |
| 13 | +const { createPrivateKey } = require('crypto'); |
| 14 | + |
| 15 | +const bench = common.createBenchmark(main, { |
| 16 | + // '0rtt' resumes from a ticket and sends the request in the very first |
| 17 | + // flight; '1rtt' is a fresh session each time. 0-RTT is listed first so |
| 18 | + // that it is the mode the benchmark CI test exercises. |
| 19 | + mode: ['0rtt', '1rtt'], |
| 20 | + n: [500], |
| 21 | +}, { flags: ['--experimental-quic', '--experimental-stream-iter', |
| 22 | + '--no-warnings'] }); |
| 23 | + |
| 24 | +async function main({ mode, n }) { |
| 25 | + const { listen, connect } = require('node:quic'); |
| 26 | + const { bytes } = require('stream/iter'); |
| 27 | + |
| 28 | + const key = createPrivateKey(fixtures.readKey('agent1-key.pem')); |
| 29 | + const cert = fixtures.readKey('agent1-cert.pem'); |
| 30 | + const body = new TextEncoder().encode('x'.repeat(256)); |
| 31 | + const decoder = new TextDecoder(); |
| 32 | + |
| 33 | + const request = { |
| 34 | + ':method': 'GET', |
| 35 | + ':path': '/', |
| 36 | + ':scheme': 'https', |
| 37 | + ':authority': 'localhost', |
| 38 | + }; |
| 39 | + |
| 40 | + const endpoint = await listen((session) => { |
| 41 | + session.opened.catch(() => {}); |
| 42 | + session.closed.catch(() => {}); |
| 43 | + session.onstream = (stream) => { stream.closed.catch(() => {}); }; |
| 44 | + }, { |
| 45 | + sni: { '*': { keys: [key], certs: [cert] } }, |
| 46 | + onheaders() { |
| 47 | + this.sendHeaders({ ':status': '200' }); |
| 48 | + this.writer.writeSync(body); |
| 49 | + this.writer.endSync(); |
| 50 | + }, |
| 51 | + endpoint: { |
| 52 | + maxConnectionsPerHost: 0xFFFF, |
| 53 | + maxConnectionsTotal: 0xFFFF, |
| 54 | + sessionCreationRate: 1_000_000, |
| 55 | + sessionCreationBurst: 1_000_000, |
| 56 | + }, |
| 57 | + }); |
| 58 | + |
| 59 | + const address = endpoint.address; |
| 60 | + let received = 0; |
| 61 | + const onheaders = () => { received++; }; |
| 62 | + |
| 63 | + // A full handshake, one request, one response. When resume is supplied the |
| 64 | + // request goes out in the first flight, before the handshake completes. |
| 65 | + async function exchange(resume) { |
| 66 | + const session = await connect(address, { |
| 67 | + servername: 'localhost', |
| 68 | + verifyPeer: 'manual', |
| 69 | + alpn: 'h3', |
| 70 | + ...resume, |
| 71 | + }); |
| 72 | + const stream = await session.createBidirectionalStream({ |
| 73 | + headers: request, |
| 74 | + onheaders, |
| 75 | + }); |
| 76 | + if (resume === undefined) await session.opened; |
| 77 | + const response = decoder.decode(await bytes(stream)); |
| 78 | + if (response.length !== body.length) { |
| 79 | + throw new Error(`short response: ${response.length}`); |
| 80 | + } |
| 81 | + session.close(); |
| 82 | + await session.closed.catch(() => {}); |
| 83 | + return session; |
| 84 | + } |
| 85 | + |
| 86 | + // Collect a ticket for the 0-RTT mode from a connection that is not timed. |
| 87 | + let resume; |
| 88 | + if (mode === '0rtt') { |
| 89 | + const { promise, resolve } = Promise.withResolvers(); |
| 90 | + let ticket; |
| 91 | + let token; |
| 92 | + const session = await connect(address, { |
| 93 | + servername: 'localhost', |
| 94 | + verifyPeer: 'manual', |
| 95 | + alpn: 'h3', |
| 96 | + onsessionticket(value) { |
| 97 | + ticket ??= value; |
| 98 | + if (token !== undefined) resolve(); |
| 99 | + }, |
| 100 | + onnewtoken(value) { |
| 101 | + token ??= value; |
| 102 | + if (ticket !== undefined) resolve(); |
| 103 | + }, |
| 104 | + }); |
| 105 | + await session.opened; |
| 106 | + await promise; |
| 107 | + session.close(); |
| 108 | + await session.closed.catch(() => {}); |
| 109 | + resume = { sessionTicket: ticket, token }; |
| 110 | + } |
| 111 | + |
| 112 | + // The timed 0-RTT exchanges deliberately never await session.opened, since |
| 113 | + // waiting for the handshake is exactly what 0-RTT avoids. That leaves no |
| 114 | + // opportunity to notice early data being refused, so check separately - |
| 115 | + // otherwise a ticket the server stopped accepting would quietly turn this |
| 116 | + // into a measurement of the 1-RTT path. |
| 117 | + async function checkEarlyDataAccepted() { |
| 118 | + const session = await connect(address, { |
| 119 | + servername: 'localhost', |
| 120 | + verifyPeer: 'manual', |
| 121 | + alpn: 'h3', |
| 122 | + ...resume, |
| 123 | + }); |
| 124 | + const stream = await session.createBidirectionalStream({ |
| 125 | + headers: request, |
| 126 | + onheaders, |
| 127 | + }); |
| 128 | + const info = await session.opened; |
| 129 | + await bytes(stream); |
| 130 | + session.close(); |
| 131 | + await session.closed.catch(() => {}); |
| 132 | + if (!info.earlyDataAccepted) { |
| 133 | + throw new Error('0-RTT was not accepted, benchmark would be invalid'); |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + for (let i = 0; i < 20; i++) await exchange(resume); |
| 138 | + if (mode === '0rtt') await checkEarlyDataAccepted(); |
| 139 | + |
| 140 | + received = 0; |
| 141 | + bench.start(); |
| 142 | + for (let i = 0; i < n; i++) await exchange(resume); |
| 143 | + bench.end(n); |
| 144 | + |
| 145 | + if (received !== n) throw new Error(`missing responses: ${received}/${n}`); |
| 146 | + // The ticket is reused for every iteration, so confirm it was still being |
| 147 | + // accepted at the end of the run and not just at the start. |
| 148 | + if (mode === '0rtt') await checkEarlyDataAccepted(); |
| 149 | + await endpoint.close(); |
| 150 | +} |
0 commit comments