mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Things I found while integrating this bench into https://github.com/vercel/next.js/pull/96234. See code. --------- Co-authored-by: Hanzo Dev <[email protected]>
374 lines
9.5 KiB
JavaScript
374 lines
9.5 KiB
JavaScript
'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(
|
|
'<html><body><h1>Flight SSR Bench</h1><ul>' +
|
|
Object.keys(routes)
|
|
.map(function (r) {
|
|
return '<li><a href="' + r + '">' + r + '</a></li>';
|
|
})
|
|
.join('') +
|
|
'</ul></body></html>'
|
|
);
|
|
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);
|
|
});
|