main
js 373 lines 9.48 KB
Raw
1 'use strict';
2
3 require('@babel/register')({
4 presets: [['@babel/preset-react', {runtime: 'automatic'}]],
5 plugins: ['@babel/plugin-transform-modules-commonjs'],
6 only: [/\/src\//],
7 });
8
9 const http = require('http');
10 const {Readable} = require('stream');
11 const webpack = require('webpack');
12
13 const {clientManifest, ssrManifest} = require('./webpack-mock');
14 const {
15 renderFizzNode,
16 renderFizzEdge,
17 renderFlightFizzNode,
18 renderFlightFizzEdge,
19 } = require('./render-helpers');
20 const {printGrid} = require('./print-helpers');
21 const fs = require('fs');
22 const JSON_OUT = (function () {
23 const arg = process.argv.find(function (a) {
24 return a.startsWith('--json-out=');
25 });
26 return arg ? arg.slice('--json-out='.length) : null;
27 })();
28 const jsonResults = [];
29
30 // ---------------------------------------------------------------------------
31 // Build
32 // ---------------------------------------------------------------------------
33
34 function build() {
35 const config = require('./webpack.config');
36 return new Promise(function (resolve, reject) {
37 webpack(config, function (err, stats) {
38 if (err) {
39 reject(err);
40 return;
41 }
42 if (stats.hasErrors()) {
43 reject(new Error(stats.toString({errors: true})));
44 return;
45 }
46 console.log(
47 stats.toString({colors: true, modules: false, entrypoints: false})
48 );
49 resolve();
50 });
51 });
52 }
53
54 // ---------------------------------------------------------------------------
55 // Server
56 // ---------------------------------------------------------------------------
57
58 const ITEM_COUNT = 200;
59 const PORT = 3001;
60
61 async function main() {
62 console.log('Building RSC bundle...\n');
63 await build();
64
65 const {
66 renderRSCNode,
67 renderRSCEdge,
68 App: RSCApp,
69 AppAsync: RSCAppAsync,
70 } = require('./build/rsc-bundle.js');
71 const App = require('./src/App.js').default;
72 const AppAsync = require('./src/AppAsync.js').default;
73
74 function pipeStreamToRes(stream, res) {
75 if (typeof stream.pipe === 'function') {
76 // Node Readable stream
77 stream.pipe(res);
78 } else {
79 // Web ReadableStream — convert to Node stream for HTTP response
80 Readable.fromWeb(stream).pipe(res);
81 }
82 }
83
84 function pipeToRes(streamOrPromise, res) {
85 if (typeof streamOrPromise.then === 'function') {
86 streamOrPromise.then(
87 function (stream) {
88 pipeStreamToRes(stream, res);
89 },
90 function (err) {
91 console.error(err);
92 if (!res.headersSent) res.writeHead(500);
93 res.end();
94 }
95 );
96 } else {
97 pipeStreamToRes(streamOrPromise, res);
98 }
99 }
100
101 const routes = {
102 '/fizz-node-sync': function (res) {
103 pipeToRes(renderFizzNode(App, ITEM_COUNT), res);
104 },
105 '/fizz-node-async': function (res) {
106 pipeToRes(renderFizzNode(AppAsync, ITEM_COUNT), res);
107 },
108 '/fizz-edge-sync': function (res) {
109 pipeToRes(renderFizzEdge(App, ITEM_COUNT), res);
110 },
111 '/fizz-edge-async': function (res) {
112 pipeToRes(renderFizzEdge(AppAsync, ITEM_COUNT), res);
113 },
114 '/flight-node-sync': function (res) {
115 pipeToRes(
116 renderFlightFizzNode(
117 renderRSCNode,
118 RSCApp,
119 ITEM_COUNT,
120 clientManifest,
121 ssrManifest
122 ),
123 res
124 );
125 },
126 '/flight-node-sync.rsc': function (res) {
127 pipeStreamToRes(renderRSCNode(clientManifest, RSCApp, ITEM_COUNT), res);
128 },
129 '/flight-node-async': function (res) {
130 pipeToRes(
131 renderFlightFizzNode(
132 renderRSCNode,
133 RSCAppAsync,
134 ITEM_COUNT,
135 clientManifest,
136 ssrManifest
137 ),
138 res
139 );
140 },
141 '/flight-node-async.rsc': function (res) {
142 pipeStreamToRes(
143 renderRSCNode(clientManifest, RSCAppAsync, ITEM_COUNT),
144 res
145 );
146 },
147 '/flight-edge-sync': function (res) {
148 pipeToRes(
149 renderFlightFizzEdge(
150 renderRSCEdge,
151 RSCApp,
152 ITEM_COUNT,
153 clientManifest,
154 ssrManifest
155 ),
156 res
157 );
158 },
159 '/flight-edge-sync.rsc': function (res) {
160 pipeStreamToRes(renderRSCEdge(clientManifest, RSCApp, ITEM_COUNT), res);
161 },
162 '/flight-edge-async': function (res) {
163 pipeToRes(
164 renderFlightFizzEdge(
165 renderRSCEdge,
166 RSCAppAsync,
167 ITEM_COUNT,
168 clientManifest,
169 ssrManifest
170 ),
171 res
172 );
173 },
174 '/flight-edge-async.rsc': function (res) {
175 pipeStreamToRes(
176 renderRSCEdge(clientManifest, RSCAppAsync, ITEM_COUNT),
177 res
178 );
179 },
180 };
181
182 const server = http.createServer(function (req, res) {
183 const handler = routes[req.url];
184 if (!handler) {
185 if (req.url === '/' || req.url === '') {
186 res.writeHead(200, {'Content-Type': 'text/html'});
187 res.end(
188 '<html><body><h1>Flight SSR Bench</h1><ul>' +
189 Object.keys(routes)
190 .map(function (r) {
191 return '<li><a href="' + r + '">' + r + '</a></li>';
192 })
193 .join('') +
194 '</ul></body></html>'
195 );
196 return;
197 }
198 res.writeHead(404);
199 res.end('Not found');
200 return;
201 }
202 const contentType = req.url.endsWith('.rsc')
203 ? 'text/x-component'
204 : 'text/html';
205 res.writeHead(200, {'Content-Type': contentType});
206 handler(res);
207 });
208
209 await new Promise(function (resolve) {
210 server.listen(PORT, resolve);
211 });
212
213 console.log('\nServer listening on http://localhost:%d', PORT);
214 console.log('Endpoints:');
215 for (const route of Object.keys(routes)) {
216 console.log(' http://localhost:%d%s', PORT, route);
217 }
218
219 if (!process.argv.includes('--bench')) {
220 return;
221 }
222
223 // Run autocannon against each endpoint.
224 // Use a fixed request count (amount) instead of duration so that all
225 // in-flight requests complete before autocannon closes connections.
226 const autocannon = require('autocannon');
227 const concurrencyLevels = [1, 10];
228 const WARMUP_AMOUNT = 200;
229 const BENCH_AMOUNT = 1000;
230
231 function runAutocannon(benchUrl, connections, amount) {
232 return new Promise(function (resolve, reject) {
233 const instance = autocannon({url: benchUrl, connections, amount});
234 autocannon.track(instance, {
235 renderProgressBar: false,
236 renderResultsTable: false,
237 });
238 instance.on('done', resolve);
239 instance.on('error', reject);
240 });
241 }
242
243 for (const c of concurrencyLevels) {
244 console.log(
245 '\n--- HTTP Benchmark (%d warmup, c=%d, %d requests) ---\n',
246 WARMUP_AMOUNT,
247 c,
248 BENCH_AMOUNT
249 );
250
251 const results = {};
252 const benchRoutes = Object.keys(routes).filter(function (r) {
253 return !r.endsWith('.rsc');
254 });
255 const labelWidth = Math.max(
256 ...benchRoutes.map(function (r) {
257 return r.length - 1;
258 })
259 );
260
261 const header =
262 ''.padEnd(labelWidth) +
263 ' ' +
264 'req/s'.padStart(14) +
265 ' ' +
266 'p50'.padStart(8) +
267 ' ' +
268 'p99'.padStart(8);
269 console.log(' ' + header);
270 console.log(' ' + '-'.repeat(header.length));
271
272 for (const route of benchRoutes) {
273 const label = route.slice(1);
274 const benchUrl = 'http://localhost:' + PORT + route;
275
276 // Warmup
277 await runAutocannon(benchUrl, c, WARMUP_AMOUNT);
278
279 const data = await runAutocannon(benchUrl, c, BENCH_AMOUNT);
280 const reqPerSec = (1000 / data.latency.mean) * data.connections;
281 const latencyMedian = data.latency.p50;
282 const latencyP99 = data.latency.p99;
283 const errors = data.errors + data.timeouts;
284
285 results[label] = {reqPerSec, latencyMedian, latencyP99};
286 jsonResults.push({
287 name: label,
288 concurrency: c,
289 reqPerSec,
290 latencyMedian,
291 latencyP99,
292 errors,
293 });
294
295 let line =
296 ' ' +
297 label.padEnd(labelWidth) +
298 ' ' +
299 String(reqPerSec.toFixed(1)).padStart(8) +
300 ' req/s' +
301 ' ' +
302 String(latencyMedian).padStart(5) +
303 ' ms' +
304 ' ' +
305 String(latencyP99).padStart(5) +
306 ' ms';
307 if (errors > 0) {
308 line += ' (' + errors + ' errors)';
309 }
310 console.log(line);
311 }
312
313 const rps = function (r) {
314 return r.reqPerSec;
315 };
316
317 console.log('\n--- Flight overhead (c=%d) ---\n', c);
318 printGrid(
319 ['Fizz', 'Flight+Fizz'],
320 [
321 ['Node sync', results['fizz-node-sync'], results['flight-node-sync']],
322 [
323 'Node async',
324 results['fizz-node-async'],
325 results['flight-node-async'],
326 ],
327 ['Edge sync', results['fizz-edge-sync'], results['flight-edge-sync']],
328 [
329 'Edge async',
330 results['fizz-edge-async'],
331 results['flight-edge-async'],
332 ],
333 ],
334 rps,
335 'req/s'
336 );
337
338 console.log('\n--- Edge vs Node (c=%d) ---\n', c);
339 printGrid(
340 ['Node', 'Edge'],
341 [
342 ['Fizz sync', results['fizz-node-sync'], results['fizz-edge-sync']],
343 ['Fizz async', results['fizz-node-async'], results['fizz-edge-async']],
344 [
345 'Flight+Fizz sync',
346 results['flight-node-sync'],
347 results['flight-edge-sync'],
348 ],
349 [
350 'Flight+Fizz async',
351 results['flight-node-async'],
352 results['flight-edge-async'],
353 ],
354 ],
355 rps,
356 'req/s'
357 );
358 }
359
360 if (JSON_OUT) {
361 fs.writeFileSync(
362 JSON_OUT,
363 JSON.stringify({mode: 'server', results: jsonResults})
364 );
365 }
366
367 server.close();
368 }
369
370 main().catch(function (err) {
371 console.error(err);
372 process.exit(1);
373 });