'use strict'; require('@babel/register')({ presets: [['@babel/preset-react', {runtime: 'automatic'}]], plugins: ['@babel/plugin-transform-modules-commonjs'], only: [/\/src\//], }); const http = require('http'); const {Readable} = require('stream'); const webpack = require('webpack'); const {clientManifest, ssrManifest} = require('./webpack-mock'); const { renderFizzNode, renderFizzEdge, renderFlightFizzNode, renderFlightFizzEdge, } = require('./render-helpers'); const {printGrid} = require('./print-helpers'); const fs = require('fs'); const JSON_OUT = (function () { const arg = process.argv.find(function (a) { return a.startsWith('--json-out='); }); return arg ? arg.slice('--json-out='.length) : null; })(); const jsonResults = []; // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- function build() { const config = require('./webpack.config'); return new Promise(function (resolve, reject) { webpack(config, function (err, stats) { if (err) { reject(err); return; } if (stats.hasErrors()) { reject(new Error(stats.toString({errors: true}))); return; } console.log( stats.toString({colors: true, modules: false, entrypoints: false}) ); resolve(); }); }); } // --------------------------------------------------------------------------- // Server // --------------------------------------------------------------------------- const ITEM_COUNT = 200; const PORT = 3001; async function main() { console.log('Building RSC bundle...\n'); await build(); const { renderRSCNode, renderRSCEdge, App: RSCApp, AppAsync: RSCAppAsync, } = require('./build/rsc-bundle.js'); const App = require('./src/App.js').default; const AppAsync = require('./src/AppAsync.js').default; function pipeStreamToRes(stream, res) { if (typeof stream.pipe === 'function') { // Node Readable stream stream.pipe(res); } else { // Web ReadableStream — convert to Node stream for HTTP response Readable.fromWeb(stream).pipe(res); } } function pipeToRes(streamOrPromise, res) { if (typeof streamOrPromise.then === 'function') { streamOrPromise.then( function (stream) { pipeStreamToRes(stream, res); }, function (err) { console.error(err); if (!res.headersSent) res.writeHead(500); res.end(); } ); } else { pipeStreamToRes(streamOrPromise, res); } } const routes = { '/fizz-node-sync': function (res) { pipeToRes(renderFizzNode(App, ITEM_COUNT), res); }, '/fizz-node-async': function (res) { pipeToRes(renderFizzNode(AppAsync, ITEM_COUNT), res); }, '/fizz-edge-sync': function (res) { pipeToRes(renderFizzEdge(App, ITEM_COUNT), res); }, '/fizz-edge-async': function (res) { pipeToRes(renderFizzEdge(AppAsync, ITEM_COUNT), res); }, '/flight-node-sync': function (res) { pipeToRes( renderFlightFizzNode( renderRSCNode, RSCApp, ITEM_COUNT, clientManifest, ssrManifest ), res ); }, '/flight-node-sync.rsc': function (res) { pipeStreamToRes(renderRSCNode(clientManifest, RSCApp, ITEM_COUNT), res); }, '/flight-node-async': function (res) { pipeToRes( renderFlightFizzNode( renderRSCNode, RSCAppAsync, ITEM_COUNT, clientManifest, ssrManifest ), res ); }, '/flight-node-async.rsc': function (res) { pipeStreamToRes( renderRSCNode(clientManifest, RSCAppAsync, ITEM_COUNT), res ); }, '/flight-edge-sync': function (res) { pipeToRes( renderFlightFizzEdge( renderRSCEdge, RSCApp, ITEM_COUNT, clientManifest, ssrManifest ), res ); }, '/flight-edge-sync.rsc': function (res) { pipeStreamToRes(renderRSCEdge(clientManifest, RSCApp, ITEM_COUNT), res); }, '/flight-edge-async': function (res) { pipeToRes( renderFlightFizzEdge( renderRSCEdge, RSCAppAsync, ITEM_COUNT, clientManifest, ssrManifest ), res ); }, '/flight-edge-async.rsc': function (res) { pipeStreamToRes( renderRSCEdge(clientManifest, RSCAppAsync, ITEM_COUNT), res ); }, }; const server = http.createServer(function (req, res) { const handler = routes[req.url]; if (!handler) { if (req.url === '/' || req.url === '') { res.writeHead(200, {'Content-Type': 'text/html'}); res.end( '

Flight SSR Bench

' ); return; } res.writeHead(404); res.end('Not found'); return; } const contentType = req.url.endsWith('.rsc') ? 'text/x-component' : 'text/html'; res.writeHead(200, {'Content-Type': contentType}); handler(res); }); await new Promise(function (resolve) { server.listen(PORT, resolve); }); console.log('\nServer listening on http://localhost:%d', PORT); console.log('Endpoints:'); for (const route of Object.keys(routes)) { console.log(' http://localhost:%d%s', PORT, route); } if (!process.argv.includes('--bench')) { return; } // Run autocannon against each endpoint. // Use a fixed request count (amount) instead of duration so that all // in-flight requests complete before autocannon closes connections. const autocannon = require('autocannon'); const concurrencyLevels = [1, 10]; const WARMUP_AMOUNT = 200; const BENCH_AMOUNT = 1000; function runAutocannon(benchUrl, connections, amount) { return new Promise(function (resolve, reject) { const instance = autocannon({url: benchUrl, connections, amount}); autocannon.track(instance, { renderProgressBar: false, renderResultsTable: false, }); instance.on('done', resolve); instance.on('error', reject); }); } for (const c of concurrencyLevels) { console.log( '\n--- HTTP Benchmark (%d warmup, c=%d, %d requests) ---\n', WARMUP_AMOUNT, c, BENCH_AMOUNT ); const results = {}; const benchRoutes = Object.keys(routes).filter(function (r) { return !r.endsWith('.rsc'); }); const labelWidth = Math.max( ...benchRoutes.map(function (r) { return r.length - 1; }) ); const header = ''.padEnd(labelWidth) + ' ' + 'req/s'.padStart(14) + ' ' + 'p50'.padStart(8) + ' ' + 'p99'.padStart(8); console.log(' ' + header); console.log(' ' + '-'.repeat(header.length)); for (const route of benchRoutes) { const label = route.slice(1); const benchUrl = 'http://localhost:' + PORT + route; // Warmup await runAutocannon(benchUrl, c, WARMUP_AMOUNT); const data = await runAutocannon(benchUrl, c, BENCH_AMOUNT); const reqPerSec = (1000 / data.latency.mean) * data.connections; const latencyMedian = data.latency.p50; const latencyP99 = data.latency.p99; const errors = data.errors + data.timeouts; results[label] = {reqPerSec, latencyMedian, latencyP99}; jsonResults.push({ name: label, concurrency: c, reqPerSec, latencyMedian, latencyP99, errors, }); let line = ' ' + label.padEnd(labelWidth) + ' ' + String(reqPerSec.toFixed(1)).padStart(8) + ' req/s' + ' ' + String(latencyMedian).padStart(5) + ' ms' + ' ' + String(latencyP99).padStart(5) + ' ms'; if (errors > 0) { line += ' (' + errors + ' errors)'; } console.log(line); } const rps = function (r) { return r.reqPerSec; }; console.log('\n--- Flight overhead (c=%d) ---\n', c); printGrid( ['Fizz', 'Flight+Fizz'], [ ['Node sync', results['fizz-node-sync'], results['flight-node-sync']], [ 'Node async', results['fizz-node-async'], results['flight-node-async'], ], ['Edge sync', results['fizz-edge-sync'], results['flight-edge-sync']], [ 'Edge async', results['fizz-edge-async'], results['flight-edge-async'], ], ], rps, 'req/s' ); console.log('\n--- Edge vs Node (c=%d) ---\n', c); printGrid( ['Node', 'Edge'], [ ['Fizz sync', results['fizz-node-sync'], results['fizz-edge-sync']], ['Fizz async', results['fizz-node-async'], results['fizz-edge-async']], [ 'Flight+Fizz sync', results['flight-node-sync'], results['flight-edge-sync'], ], [ 'Flight+Fizz async', results['flight-node-async'], results['flight-edge-async'], ], ], rps, 'req/s' ); } if (JSON_OUT) { fs.writeFileSync( JSON_OUT, JSON.stringify({mode: 'server', results: jsonResults}) ); } server.close(); } main().catch(function (err) { console.error(err); process.exit(1); });