diff --git a/frameworks/koa/Dockerfile b/frameworks/koa/Dockerfile new file mode 100644 index 0000000..aed2515 --- /dev/null +++ b/frameworks/koa/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-slim +RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY package.json . +RUN npm install --omit=dev +COPY server.js . +ENV NODE_ENV=production +EXPOSE 8080 +CMD ["node", "server.js"] diff --git a/frameworks/koa/README.md b/frameworks/koa/README.md new file mode 100644 index 0000000..ee4a2c6 --- /dev/null +++ b/frameworks/koa/README.md @@ -0,0 +1,17 @@ +# Koa + +[Koa](https://github.com/koajs/koa) (~36k ⭐) is an expressive middleware framework for Node.js, designed by the team behind Express to leverage async/await for cleaner control flow. + +## Key Features +- Cascading async/await middleware (no callback hell) +- Lightweight core (~600 LOC, no bundled middleware) +- Context object (`ctx`) encapsulates request/response +- Built by TJ Holowaychick and the Express team + +## Implementation Details +- Koa 2.x with koa-router +- Cluster mode (one worker per CPU core) +- Pre-computed JSON and gzip buffers +- Raw stream body reading (no body-parser) +- better-sqlite3 with mmap for `/db` +- HTTP/2 via native `http2` module diff --git a/frameworks/koa/meta.json b/frameworks/koa/meta.json new file mode 100644 index 0000000..b1d1ea2 --- /dev/null +++ b/frameworks/koa/meta.json @@ -0,0 +1,21 @@ +{ + "display_name": "Koa", + "language": "JS", + "type": "framework", + "engine": "V8", + "description": "Expressive middleware framework for Node.js by the Express team, using async/await.", + "repo": "https://github.com/koajs/koa", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "mixed", + "noisy", + "baseline-h2", + "static-h2" + ] +} diff --git a/frameworks/koa/package.json b/frameworks/koa/package.json new file mode 100644 index 0000000..58fc4d0 --- /dev/null +++ b/frameworks/koa/package.json @@ -0,0 +1,10 @@ +{ + "name": "httparena-koa", + "version": "1.0.0", + "private": true, + "dependencies": { + "koa": "^2.15.0", + "koa-router": "^13.0.0", + "better-sqlite3": "^11.0.0" + } +} diff --git a/frameworks/koa/server.js b/frameworks/koa/server.js new file mode 100644 index 0000000..adb3f2a --- /dev/null +++ b/frameworks/koa/server.js @@ -0,0 +1,255 @@ +const cluster = require('cluster'); +const os = require('os'); +const fs = require('fs'); +const http2 = require('http2'); +const zlib = require('zlib'); + +const SERVER_NAME = 'koa'; + +let datasetItems; +let largeJsonBuf; +let largeJsonGzip; +let jsonBuf; +let dbStmt; +const staticFiles = {}; +const MIME_TYPES = { + '.css': 'text/css', '.js': 'application/javascript', '.html': 'text/html', + '.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.json': 'application/json' +}; + +function loadStaticFiles() { + const dir = '/data/static'; + try { + for (const name of fs.readdirSync(dir)) { + const buf = fs.readFileSync(dir + '/' + name); + const ext = name.slice(name.lastIndexOf('.')); + staticFiles[name] = { buf, ct: MIME_TYPES[ext] || 'application/octet-stream' }; + } + } catch (e) {} +} + +function loadDataset() { + const path = process.env.DATASET_PATH || '/data/dataset.json'; + try { + datasetItems = JSON.parse(fs.readFileSync(path, 'utf8')); + const items = datasetItems.map(d => ({ + id: d.id, name: d.name, category: d.category, + price: d.price, quantity: d.quantity, active: d.active, + tags: d.tags, rating: d.rating, + total: Math.round(d.price * d.quantity * 100) / 100 + })); + jsonBuf = Buffer.from(JSON.stringify({ items, count: items.length })); + } catch (e) {} +} + +function loadLargeDataset() { + try { + const raw = JSON.parse(fs.readFileSync('/data/dataset-large.json', 'utf8')); + const items = raw.map(d => ({ + id: d.id, name: d.name, category: d.category, + price: d.price, quantity: d.quantity, active: d.active, + tags: d.tags, rating: d.rating, + total: Math.round(d.price * d.quantity * 100) / 100 + })); + largeJsonBuf = Buffer.from(JSON.stringify({ items, count: items.length })); + largeJsonGzip = zlib.gzipSync(largeJsonBuf, { level: 1 }); + } catch (e) {} +} + +function loadDatabase() { + try { + const Database = require('better-sqlite3'); + const db = new Database('/data/benchmark.db', { readonly: true }); + db.pragma('mmap_size=268435456'); + dbStmt = db.prepare('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50'); + } catch (e) {} +} + +function sumQuery(query) { + let sum = 0; + if (query) { + for (const key of Object.keys(query)) { + const n = parseInt(query[key], 10); + if (n === n) sum += n; + } + } + return sum; +} + +function readBody(ctx) { + return new Promise((resolve, reject) => { + const chunks = []; + ctx.req.on('data', c => chunks.push(c)); + ctx.req.on('end', () => resolve(Buffer.concat(chunks))); + ctx.req.on('error', reject); + }); +} + +function startWorker() { + loadDataset(); + loadLargeDataset(); + loadStaticFiles(); + loadDatabase(); + + const Koa = require('koa'); + const Router = require('koa-router'); + + const app = new Koa(); + app.proxy = false; + + const router = new Router(); + + // --- /pipeline --- + router.get('/pipeline', (ctx) => { + ctx.set('server', SERVER_NAME); + ctx.type = 'text/plain'; + ctx.body = 'ok'; + }); + + // --- /baseline11 GET & POST --- + router.get('/baseline11', (ctx) => { + const s = sumQuery(ctx.query); + ctx.set('server', SERVER_NAME); + ctx.type = 'text/plain'; + ctx.body = String(s); + }); + + router.post('/baseline11', async (ctx) => { + const querySum = sumQuery(ctx.query); + const body = (await readBody(ctx)).toString(); + let total = querySum; + const n = parseInt(body.trim(), 10); + if (n === n) total += n; + ctx.set('server', SERVER_NAME); + ctx.type = 'text/plain'; + ctx.body = String(total); + }); + + // --- /baseline2 --- + router.get('/baseline2', (ctx) => { + const s = sumQuery(ctx.query); + ctx.set('server', SERVER_NAME); + ctx.type = 'text/plain'; + ctx.body = String(s); + }); + + // --- /json --- + router.get('/json', (ctx) => { + if (!jsonBuf) { + ctx.status = 500; + ctx.body = 'No dataset'; + return; + } + ctx.set('server', SERVER_NAME); + ctx.set('content-length', String(jsonBuf.length)); + ctx.type = 'application/json'; + ctx.body = jsonBuf; + }); + + // --- /compression --- + router.get('/compression', (ctx) => { + if (!largeJsonGzip) { + ctx.status = 500; + ctx.body = 'No dataset'; + return; + } + ctx.set('server', SERVER_NAME); + ctx.set('content-encoding', 'gzip'); + ctx.set('content-length', String(largeJsonGzip.length)); + ctx.type = 'application/json'; + ctx.body = largeJsonGzip; + }); + + // --- /db --- + router.get('/db', (ctx) => { + if (!dbStmt) { + ctx.set('server', SERVER_NAME); + ctx.type = 'application/json'; + ctx.body = '{"items":[],"count":0}'; + return; + } + let min = 10, max = 50; + if (ctx.query.min) min = parseFloat(ctx.query.min) || 10; + if (ctx.query.max) max = parseFloat(ctx.query.max) || 50; + const rows = dbStmt.all(min, max); + const items = rows.map(r => ({ + id: r.id, name: r.name, category: r.category, + price: r.price, quantity: r.quantity, active: r.active === 1, + tags: JSON.parse(r.tags), + rating: { score: r.rating_score, count: r.rating_count } + })); + const body = JSON.stringify({ items, count: items.length }); + ctx.set('server', SERVER_NAME); + ctx.set('content-length', String(Buffer.byteLength(body))); + ctx.type = 'application/json'; + ctx.body = body; + }); + + // --- /upload --- + router.post('/upload', async (ctx) => { + const body = await readBody(ctx); + ctx.set('server', SERVER_NAME); + ctx.type = 'text/plain'; + ctx.body = String(body.length); + }); + + app.use(router.routes()); + app.use(router.allowedMethods()); + + app.listen(8080, '0.0.0.0', () => { + startH2(); + }); +} + +function startH2() { + const certFile = process.env.TLS_CERT || '/certs/server.crt'; + const keyFile = process.env.TLS_KEY || '/certs/server.key'; + try { + const opts = { + cert: fs.readFileSync(certFile), + key: fs.readFileSync(keyFile), + allowHTTP1: false, + }; + const h2server = http2.createSecureServer(opts, (req, res) => { + const url = req.url; + const q = url.indexOf('?'); + const p = q === -1 ? url : url.slice(0, q); + if (p.startsWith('/static/')) { + const name = p.slice(8); + const sf = staticFiles[name]; + if (sf) { + res.writeHead(200, { 'content-type': sf.ct, 'content-length': sf.buf.length, 'server': SERVER_NAME }); + res.end(sf.buf); + } else { + res.writeHead(404); + res.end(); + } + } else { + let sum = 0; + if (q !== -1) { + const qs = url.slice(q + 1); + let i = 0; + while (i < qs.length) { + const eq = qs.indexOf('=', i); + if (eq === -1) break; + let amp = qs.indexOf('&', eq); + if (amp === -1) amp = qs.length; + const n = parseInt(qs.slice(eq + 1, amp), 10); + if (n === n) sum += n; + i = amp + 1; + } + } + res.writeHead(200, { 'content-type': 'text/plain', 'server': SERVER_NAME }); + res.end(String(sum)); + } + }); + h2server.listen(8443); + } catch (e) {} +} + +if (cluster.isPrimary) { + const numCPUs = os.availableParallelism ? os.availableParallelism() : os.cpus().length; + for (let i = 0; i < numCPUs; i++) cluster.fork(); +} else { + startWorker(); +}