diff --git a/benchmark/scenarios/api-endpoint.js b/benchmark/scenarios/api-endpoint.js new file mode 100644 index 0000000..53c3aef --- /dev/null +++ b/benchmark/scenarios/api-endpoint.js @@ -0,0 +1,31 @@ +'use strict'; + +// The suite had no scenario shaped like an actual API endpoint: `grep -r "res.json\|req.params\|req.query"` +// over benchmark/scenarios returned nothing. That is the most common request shape in an Express +// application, and the one where the framework's own work is a meaningful share of the request +// rather than a rounding error next to zlib or JSON.parse. +module.exports = { + name: 'routing/api-endpoint', + path: '/api/users/42/posts?fields=id,title,author&limit=10', + setup(app, express) { + const apiRouter = express.Router(); + + apiRouter.get('/users/:userId/posts', (req, res) => { + const fields = String(req.query.fields || '').split(','); + const limit = Number(req.query.limit) || 0; + const items = []; + for (let i = 0; i < limit; i++) { + items.push({ id: i, title: `post ${i}`, author: req.params.userId }); + } + + res.json({ + userId: req.params.userId, + fields, + count: items.length, + items + }); + }); + + app.use('/api', apiRouter); + } +}; diff --git a/benchmark/scenarios/body-json-4kb.js b/benchmark/scenarios/body-json-4kb.js new file mode 100644 index 0000000..3e5aaf4 --- /dev/null +++ b/benchmark/scenarios/body-json-4kb.js @@ -0,0 +1,29 @@ +'use strict'; + +// body-json-512kb is a stress case: at half a megabyte JSON.parse dominates and the framework is +// about 1% of the request, so that row cannot move. A few KB is what an API actually receives, and +// at that size the body plumbing around the parse is visible. Both rows are kept - one measures the +// parser, this one measures getting the bytes to it. +const PAD = 4 * 1024; + +module.exports = { + name: 'middlewares/body-json-4kb', + path: '/abc', + wrk: { + script: 'post-json-4kb.lua', + connections: 200 + }, + verify: { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ n: 1, pad: 'x'.repeat(PAD) }) + }, + setup(app, express) { + app.use(express.json()); + app.post('/abc', (req, res) => { + res.send(`${req.body.pad.length}`); + }); + } +}; diff --git a/benchmark/scenarios/high-concurrency.js b/benchmark/scenarios/high-concurrency.js new file mode 100644 index 0000000..7733930 --- /dev/null +++ b/benchmark/scenarios/high-concurrency.js @@ -0,0 +1,20 @@ +'use strict'; + +// Every other scenario runs at 50-200 connections, where connection handling is free. uWS's +// advantage over node:http at high connection counts is structural - per-connection memory and +// accept handling - rather than anything in the request path, and nothing in the suite exercised it. +// The handler is deliberately trivial so the row measures connections and not work. +// +// Caveat: wrk shares the runner with the server, so at this connection count part of what is +// measured is the generator. Two wrk threads on a 4 vCPU runner leaves the server two. +module.exports = { + name: 'connections/high-concurrency', + path: '/ping', + wrk: { + threads: 2, + connections: 1000 + }, + setup(app) { + app.get('/ping', (req, res) => res.send('pong')); + } +}; diff --git a/benchmark/scenarios/realistic-stack.js b/benchmark/scenarios/realistic-stack.js new file mode 100644 index 0000000..d03c1c9 --- /dev/null +++ b/benchmark/scenarios/realistic-stack.js @@ -0,0 +1,43 @@ +'use strict'; + +const helmet = require('helmet'); +const cors = require('cors'); +const cookieParser = require('cookie-parser'); +const morgan = require('morgan'); + +// middlewares-100 measures 100 no-ops, which no application runs. This is the stack a typical +// Express service actually mounts, at the depth it actually mounts it. +// morgan writes to a sink rather than stdout so the row measures the formatting work every request +// pays, not the runner's terminal. 'combined' is used because that is the production default, and +// it includes the remote address, which both frameworks have to resolve their own way. +module.exports = { + name: 'middlewares/realistic-stack', + path: '/profile', + wrk: { + script: 'realistic-stack.lua', + connections: 200 + }, + // keep these in sync with realistic-stack.lua: the verify request and the load request are + // separate definitions, so cors would otherwise be exercised only by one of them + verify: { + method: 'GET', + headers: { + 'Cookie': 'sid=abc123; theme=dark', + 'Origin': 'https://example.com' + } + }, + setup(app, express) { + app.use(helmet()); + app.use(cors()); + app.use(cookieParser()); + app.use(express.json()); + app.use(morgan('combined', { stream: { write: () => {} } })); + + app.get('/profile', (req, res) => { + res.json({ + sid: req.cookies.sid || null, + theme: req.cookies.theme || null + }); + }); + } +}; diff --git a/benchmark/wrk-scripts/post-json-4kb.lua b/benchmark/wrk-scripts/post-json-4kb.lua new file mode 100644 index 0000000..36dda00 --- /dev/null +++ b/benchmark/wrk-scripts/post-json-4kb.lua @@ -0,0 +1,7 @@ +wrk.method = "POST" +wrk.path = "/abc" +wrk.headers["Content-Type"] = "application/json" + +local kb = 1024 +local pad = string.rep("x", 4 * kb) +wrk.body = '{"n":1,"pad":"' .. pad .. '"}' diff --git a/benchmark/wrk-scripts/realistic-stack.lua b/benchmark/wrk-scripts/realistic-stack.lua new file mode 100644 index 0000000..070835f --- /dev/null +++ b/benchmark/wrk-scripts/realistic-stack.lua @@ -0,0 +1,4 @@ +wrk.method = "GET" +wrk.path = "/profile" +wrk.headers["Cookie"] = "sid=abc123; theme=dark" +wrk.headers["Origin"] = "https://example.com"