From a8fcde54784cb2cf0e73204a9a636cfcaee18f62 Mon Sep 17 00:00:00 2001 From: Chris M Date: Thu, 29 Oct 2026 11:19:18 -0600 Subject: [PATCH 01/12] morojs: HTTP/2 listeners; compression middleware on the plain listener only Two more processes of the same file on Moro's HTTP/2 server (Node's http2 module behind the framework's request and response objects): :8443 with ALPN h2 for baseline-h2 and static-h2, :8082 cleartext prior-knowledge h2 for baseline-h2c and json-h2c. The compression middleware moves to the /json route of the plain listener alone, so json-tls stays on the router's fast path. Picks up MoroJS 1.8.15. --- frameworks/morojs/README.md | 6 ++++ frameworks/morojs/app.mjs | 58 +++++++++++++++++++++++----------- frameworks/morojs/meta.json | 8 +++-- frameworks/morojs/package.json | 2 +- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/frameworks/morojs/README.md b/frameworks/morojs/README.md index 4629694f5b..c4ca560036 100644 --- a/frameworks/morojs/README.md +++ b/frameworks/morojs/README.md @@ -57,6 +57,12 @@ MoroJS on its native HTTP engine, clustered by the framework itself. validated and swapped onto the running listener without a restart and without touching connections already established. Only started when `/certs-tls` is mounted, which validate.sh alone does. +- `baseline-h2`, `static-h2`, `baseline-h2c` and `json-h2c` run on Moro's HTTP/2 server, which is + Node's http2 module behind the framework's own request and response objects (`engine: 'node'` + with `http2: true`); the native engine speaks HTTP/1.1 only. `:8443` terminates TLS with ALPN + `h2` (HTTP/1.1 still accepted), `:8082` is cleartext prior-knowledge HTTP/2. Each is its own + process for the same one-configuration-per-process reason, clustered as node:cluster processes, + which is Moro's transport for its Node servers. - `/echo` sends back `req.rawBody`, the framework's undecoded copy of the request body, so the bytes are the bytes that arrived, `Content-Length` or chunked alike. - `/ws` is the engine's own RFC 6455 support through `app.websocket()` with `{ raw: true }`, so a diff --git a/frameworks/morojs/app.mjs b/frameworks/morojs/app.mjs index ddfdaf86cf..87298e0d4f 100644 --- a/frameworks/morojs/app.mjs +++ b/frameworks/morojs/app.mjs @@ -23,12 +23,20 @@ const STATIC_ROOT = '/data/static'; // One process per listener, because Moro locks one configuration per // process. 'plain' serves :8080; 'tls' serves json-tls, static-tls and 8gbit // on :8081 from /certs; 'tlscheck' serves the opt-in TLS hardening section on -// :9000 from /certs-tls, reloading the pair whenever the files change. +// :9000 from /certs-tls, reloading the pair whenever the files change. The +// two HTTP/2 listeners run on Moro's own HTTP/2 server (Node's http2 module, +// `engine: 'node'` with `http2: true`): the native engine speaks HTTP/1.1 +// only. 'h2' is TLS with ALPN h2 on :8443 for baseline-h2 and static-h2; +// 'h2c' is cleartext prior-knowledge HTTP/2 on :8082 for baseline-h2c and +// json-h2c. Both cluster as node:cluster processes, Moro's transport for the +// Node servers. const ROLE = process.env.MORO_ROLE || 'plain'; const LISTENERS = { plain: { port: 8080 }, tls: { port: 8081, certs: '/certs' }, tlscheck: { port: 9000, certs: '/certs-tls', watch: true }, + h2: { port: 8443, certs: '/certs', http2: true }, + h2c: { port: 8082, http2: true }, }; const pairIn = dir => ({ keyFile: join(dir, 'server.key'), certFile: join(dir, 'server.crt') }); const hasPair = dir => existsSync(pairIn(dir).keyFile) && existsSync(pairIn(dir).certFile); @@ -55,11 +63,12 @@ const workers = cpuCount(); const isWorker = !isMainThread || cluster.isWorker; const isPrimary = !isWorker; -// The harness mounts /certs for the TLS profiles and /certs-tls for the -// tls_check section only; a listener whose pair is absent is not started. +// The harness mounts /certs for the TLS and HTTP/2 profiles and /certs-tls +// for the tls_check section only; a listener whose pair is absent is not +// started. h2c needs no material and always starts. if (ROLE === 'plain' && isPrimary) { - for (const role of ['tls', 'tlscheck']) { - if (!hasPair(LISTENERS[role].certs)) continue; + for (const role of ['tls', 'tlscheck', 'h2', 'h2c']) { + if (LISTENERS[role].certs && !hasPair(LISTENERS[role].certs)) continue; const child = spawn(process.execPath, [fileURLToPath(import.meta.url)], { stdio: 'inherit', env: { ...process.env, MORO_ROLE: role }, @@ -170,8 +179,11 @@ function defineRoutes(app) { app.post('/baseline11').handler(baseline11); // Behind the gateway proxies, which terminate TLS and h2 and forward this - // over loopback h1. + // over loopback h1, and directly on the HTTP/2 listeners. The type is set + // by hand so a bare number is text/plain on every listener, whatever a + // server's content sniff would make of it. app.get('/baseline2').handler((req, res) => { + res.setHeader('Content-Type', 'text/plain'); res.send(String(sumQuery(req.query))); }); @@ -190,17 +202,23 @@ function defineRoutes(app) { // json-comp: the framework's compression middleware on this route alone, // so no other endpoint pays for the encoder. It negotiates off - // Accept-Encoding per request and sends the body as is when none is sent, - // which is what json-tls on :8081 gets. gzip is preferred over brotli: - // at the middleware's default level it costs about half the CPU of - // brotli at the default quality for a body one tenth larger, and the - // profile prices bytes squared against rate. brotli stays available for - // a client that accepts nothing else. - app.get('/json/:count') - .before(middleware.compression({ encodings: ['gzip', 'br'] })) - .handler((req, res) => { - res.json(jsonItems(req)); - }); + // Accept-Encoding per request and sends the body as is when none is sent. + // gzip is preferred over brotli: at the middleware's default level it + // costs about half the CPU of brotli at the default quality for a body one + // tenth larger, and the profile prices bytes squared against rate. brotli + // stays available for a client that accepts nothing else. The TLS listener + // (json-tls) is never asked for an encoding, so its route carries no + // middleware and stays on the router's fast path. + const json = (req, res) => { + res.json(jsonItems(req)); + }; + if (ROLE === 'plain') { + app.get('/json/:count') + .before(middleware.compression({ encodings: ['gzip', 'br'] })) + .handler(json); + } else { + app.get('/json/:count').handler(json); + } // 8gbit: the body exactly as it arrived, Content-Length or chunked, sent // back as the same bytes. req.rawBody is the framework's undecoded copy. @@ -372,6 +390,10 @@ const server = { port: listener.port }; if (listener.certs) { server.ssl = { ...pairIn(listener.certs), ...(listener.watch ? { watch: true } : {}) }; } +if (listener.http2) { + server.engine = 'node'; + server.http2 = true; +} const app = await createApp({ server: { @@ -408,7 +430,7 @@ if (ROLE === 'plain') { ); } -if (ROLE === 'tls') { +if (ROLE === 'tls' || ROLE === 'h2') { // Static files through the framework's own handler, which stats and // reads the file on every request and serves the .br or .gz sidecar on // disk when the client accepts it. Behind a path check so the JSON route diff --git a/frameworks/morojs/meta.json b/frameworks/morojs/meta.json index 0c628fc81d..a5dfdad721 100644 --- a/frameworks/morojs/meta.json +++ b/frameworks/morojs/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "@morojs/engine", - "description": "MoroJS 1.8 on its native HTTP engine (@morojs/engine), default configuration, clustered by the framework itself with one worker thread per core sharing the port through SO_REUSEPORT. Routing, path and query parameters and body parsing through the Moro request, JSON through res.json, compression, static files and the cache-aside through the built-in middleware and cache adapters, WebSockets through the engine's own RFC 6455 support.", + "description": "MoroJS 1.8 on its native HTTP engine (@morojs/engine), default configuration, clustered by the framework itself with one worker thread per core sharing the port through SO_REUSEPORT. Routing, path and query parameters and body parsing through the Moro request, JSON through res.json, compression, static files and the cache-aside through the built-in middleware and cache adapters, WebSockets through the engine's own RFC 6455 support, HTTP/2 through Moro's HTTP/2 server on Node's http2 module.", "repo": "https://github.com/Moro-JS/moro", "enabled": true, "tls_check": true, @@ -33,7 +33,11 @@ "gateway-64", "gateway-h3", "production-stack", - "fortunes" + "fortunes", + "baseline-h2", + "static-h2", + "baseline-h2c", + "json-h2c" ], "maintainers": [ "M-Chris" diff --git a/frameworks/morojs/package.json b/frameworks/morojs/package.json index 089243fb5c..7c3198c911 100644 --- a/frameworks/morojs/package.json +++ b/frameworks/morojs/package.json @@ -2,7 +2,7 @@ "name": "httparena-morojs", "private": true, "dependencies": { - "@morojs/moro": "^1.8.13", + "@morojs/moro": "^1.8.15", "ejs": "^3.1.10", "pg": "^8.16.3", "redis": "^5.0.0" From 0f6544300063216852ec6dbe3ecccbc1b5bbd6cf Mon Sep 17 00:00:00 2001 From: Chris M Date: Thu, 29 Oct 2026 11:19:19 -0600 Subject: [PATCH 02/12] morojs: MoroJS 1.8.16 on @morojs/engine 1.1.14 - cached static files, gzip level 3, prepared replies The entry was losing to the other JS entries on four fronts, none of them the harness. Each is fixed where it lived - the engine, the framework, or here. - echo-ws-pipeline (6x behind): the engine answered every WebSocket frame with its own send(). @morojs/engine 1.1.14 corks the connection for the whole read, so a pipelined batch leaves in one write; nothing changes here beyond the version. - static-tls (10x behind): staticFiles() resolved, stat'ed and read the file and its sidecars on every request, five to eight threadpool round trips shared by all 64 worker threads of the process. MoroJS 1.8.16 adds staticFiles({ cache: true }): files and .br/.gz sidecars read once and served from memory with their validators, the disk stat'ed again at most once a second and on directory changes (same-length replacements are noticed, so the staleness probe passes). - json-comp (3x) and gateway-64 (2x, its load sends Accept-Encoding): gzip at the default level 6 per request. The route now asks for gzip level 3 through the middleware's new gzip option, with the encoder reused between bodies (reuseEncoders); brotli quality 3 stays available for a client that accepts nothing else. Measured on the json-comp bodies: a little over a third of the CPU of level 6 for 9% more bytes. - json-tls, 8gbit, baseline POST (10-30%): the static middleware on the TLS listener and every request body forced the engine server off its synchronous dispatch. 1.8.16 runs sync middleware chains and body parsing on the sync path, so the path check stays cheap; /echo and /baseline2 on the engine no longer set a Content-Type by hand, so they go out through the framework's prepared replies. Also: CERTS_DIR, STATIC_ROOT and WORKERS environment overrides for running the entry outside the container, the Dockerfile's threadpool note brought up to date, README and meta description updated. Local A/B on a 4-worker macOS run, published 1.8.15 against this: pipelined WebSocket echo 257K -> 971K frames/s, static over plaintext 11.5K -> 49.5K rps (9.7K -> 33.9K over TLS), /json/50 with gzip 11.8K -> 17.6K rps, baseline POST +12%. --- frameworks/morojs/Dockerfile | 13 +++--- frameworks/morojs/README.md | 32 ++++++++++----- frameworks/morojs/app.mjs | 75 ++++++++++++++++++++++++---------- frameworks/morojs/meta.json | 2 +- frameworks/morojs/package.json | 2 +- 5 files changed, 85 insertions(+), 39 deletions(-) diff --git a/frameworks/morojs/Dockerfile b/frameworks/morojs/Dockerfile index 8898ceefb5..56bb513d7a 100644 --- a/frameworks/morojs/Dockerfile +++ b/frameworks/morojs/Dockerfile @@ -10,11 +10,14 @@ COPY --from=build /app/node_modules ./node_modules COPY app.mjs . COPY views ./views ENV NODE_ENV=production -# The libuv threadpool carries the compression and file reads of every worker -# thread in the process. Moro sizes it to 64 itself, but only at listen(), -# after its startup has already initialised the pool at Node's default of 4; -# an environment variable at process start is what Node reads. 64 matches the -# benchmark cpuset. +# One libuv threadpool serves every worker thread in the process. Little rides +# on it now - compression runs inline on the worker and static files are +# served from the middleware's memory cache, which stats the disk once a +# second at most - but what does (the cache's revalidation, the first read of +# each file, DNS) should not queue behind Node's default of four threads. Moro +# sizes the pool to 64 itself, but only at listen(), after its startup has +# already initialised it; an environment variable at process start is what +# Node reads. 64 matches the benchmark cpuset. ENV UV_THREADPOOL_SIZE=64 EXPOSE 8080 CMD ["node", "app.mjs"] diff --git a/frameworks/morojs/README.md b/frameworks/morojs/README.md index c4ca560036..c15735c4ad 100644 --- a/frameworks/morojs/README.md +++ b/frameworks/morojs/README.md @@ -6,7 +6,7 @@ MoroJS on its native HTTP engine, clustered by the framework itself. - **Language:** JavaScript - **Runtime:** Node.js 26 -- **Framework:** [MoroJS 1.8](https://github.com/Moro-JS/moro) on `@morojs/engine`, its own native HTTP engine +- **Framework:** [MoroJS 1.8](https://github.com/Moro-JS/moro) on `@morojs/engine` 1.1.14, its own native HTTP engine - **Build:** Multi-stage on `node:26-trixie-slim`; the engine ships prebuilt binaries, so no toolchain ## Endpoints @@ -40,10 +40,13 @@ MoroJS on its native HTTP engine, clustered by the framework itself. `--cpuset-cpus`. - `json-comp` is the framework's `compression()` middleware attached to the `/json` route alone, so no other endpoint pays for the encoder. It negotiates off `Accept-Encoding` per request and - leaves the body alone when the header is absent. gzip is preferred over brotli through the - middleware's `encodings` option: at the default level it costs about half the CPU of brotli at - the default quality for a body a tenth larger, and the profile scores bytes squared against - rate; brotli remains available to a client that accepts nothing else. + leaves the body alone when the header is absent. gzip at level 3 is preferred over brotli + through the middleware's `encodings` and `gzip` options: the profile scores bytes squared + against rate, and on these bodies level 3 costs a little over a third of the CPU of the default + level 6 for 9% more bytes, and about half the CPU of brotli at quality 3 for 10% more. The + middleware keeps one gzip encoder per worker and resets it between bodies (`reuseEncoders`, the + default), so no response pays for building one. brotli at quality 3, the setting the other JS + entries use, remains available to a client that accepts nothing else. - `UV_THREADPOOL_SIZE=64` is set in the Dockerfile. The libuv pool serves the compression and file reads of every worker thread in the process; Moro sets 64 itself but only at `listen()`, after its startup has initialised the pool at Node's default of 4, and Node reads the variable @@ -64,13 +67,22 @@ MoroJS on its native HTTP engine, clustered by the framework itself. process for the same one-configuration-per-process reason, clustered as node:cluster processes, which is Moro's transport for its Node servers. - `/echo` sends back `req.rawBody`, the framework's undecoded copy of the request body, so the - bytes are the bytes that arrived, `Content-Length` or chunked alike. + bytes are the bytes that arrived, `Content-Length` or chunked alike; a Buffer handed to + `res.send()` goes out `application/octet-stream` through the framework's prepared reply. - `/ws` is the engine's own RFC 6455 support through `app.websocket()` with `{ raw: true }`, so a text frame reaches the handler as the string it carried and `socket.send()` returns it as one. -- Static files go through `staticFiles({ root: '/data/static', precompressed: true })`, which - stats and reads the file on every request, so a replaced file is served on the next response, - and serves the `.br` or `.gz` sidecar on disk when the client accepts it. It is mounted on the - TLS listener behind a path check so the JSON route on that port does not pay for the lookup. + The engine corks the connection for the duration of each read (`@morojs/engine` 1.1.14), so + the echoes of a pipelined batch of frames leave in one write. +- Static files go through `staticFiles({ root: '/data/static', precompressed: true, cache: true })`: + the framework's handler with its in-memory cache. A file and its `.br` and `.gz` sidecars are + read once and served from memory with their `ETag` and `Last-Modified`, the sidecar negotiated + per request from `Accept-Encoding`. The disk is stat'ed again at most once a second, and as soon + as the directory reports a change, so a replaced file (same length or not) is served promptly. + Without the cache the handler resolves, stats and reads the file and its sidecars on every + request, each a libuv threadpool round trip shared by every worker thread of the process. It is + mounted on the TLS listener behind a path check so the JSON route on that port does not pay for + the lookup; the check calls `next()` synchronously, which keeps that route on the server's + synchronous dispatch. - `fortunes` goes through EJS directly: Moro ships no view layer, so the template is rendered by the engine's own `renderFile`, a separate file with `<%= %>` escaping every cell. - The gateway and production-stack edges are stock nginx and Caddy, configured as the fulmine diff --git a/frameworks/morojs/app.mjs b/frameworks/morojs/app.mjs index 87298e0d4f..19473297d8 100644 --- a/frameworks/morojs/app.mjs +++ b/frameworks/morojs/app.mjs @@ -11,6 +11,7 @@ import { availableParallelism } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isMainThread } from 'node:worker_threads'; +import { constants as zlibConstants } from 'node:zlib'; import { createApp, middleware, MemoryCacheAdapter, RedisCacheAdapter } from '@morojs/moro'; import ejs from 'ejs'; import pg from 'pg'; @@ -18,7 +19,10 @@ import pg from 'pg'; const HERE = dirname(fileURLToPath(import.meta.url)); const FORTUNES_VIEW = join(HERE, 'views', 'fortunes.ejs'); const DATASET_PATH = process.env.DATASET_PATH || '/data/dataset.json'; -const STATIC_ROOT = '/data/static'; +const STATIC_ROOT = process.env.STATIC_ROOT || '/data/static'; +// The harness mounts the pair at /certs; the override is for running the +// entry outside the container. +const CERTS_DIR = process.env.CERTS_DIR || '/certs'; // One process per listener, because Moro locks one configuration per // process. 'plain' serves :8080; 'tls' serves json-tls, static-tls and 8gbit @@ -33,9 +37,9 @@ const STATIC_ROOT = '/data/static'; const ROLE = process.env.MORO_ROLE || 'plain'; const LISTENERS = { plain: { port: 8080 }, - tls: { port: 8081, certs: '/certs' }, + tls: { port: 8081, certs: CERTS_DIR }, tlscheck: { port: 9000, certs: '/certs-tls', watch: true }, - h2: { port: 8443, certs: '/certs', http2: true }, + h2: { port: 8443, certs: CERTS_DIR, http2: true }, h2c: { port: 8082, http2: true }, }; const pairIn = dir => ({ keyFile: join(dir, 'server.key'), certFile: join(dir, 'server.crt') }); @@ -55,7 +59,8 @@ function cpuCount() { } catch {} return availableParallelism(); } -const workers = cpuCount(); +// WORKERS overrides the count, for running the entry outside the container. +const workers = parseInt(process.env.WORKERS, 10) || cpuCount(); // A worker thread on the native engine, a node:cluster process on the // fallback transport. The primary serves nothing, so it loads no dataset and @@ -179,11 +184,14 @@ function defineRoutes(app) { app.post('/baseline11').handler(baseline11); // Behind the gateway proxies, which terminate TLS and h2 and forward this - // over loopback h1, and directly on the HTTP/2 listeners. The type is set - // by hand so a bare number is text/plain on every listener, whatever a - // server's content sniff would make of it. + // over loopback h1, and directly on the HTTP/2 listeners. On the native + // engine a bare number sent as a string is text/plain through the + // framework's prepared reply; on the HTTP/2 server the type is set by + // hand so it is text/plain there too, whatever a content sniff would make + // of it. + const textPlain = listener.http2 === true; app.get('/baseline2').handler((req, res) => { - res.setHeader('Content-Type', 'text/plain'); + if (textPlain) res.setHeader('Content-Type', 'text/plain'); res.send(String(sumQuery(req.query))); }); @@ -203,27 +211,38 @@ function defineRoutes(app) { // json-comp: the framework's compression middleware on this route alone, // so no other endpoint pays for the encoder. It negotiates off // Accept-Encoding per request and sends the body as is when none is sent. - // gzip is preferred over brotli: at the middleware's default level it - // costs about half the CPU of brotli at the default quality for a body one - // tenth larger, and the profile prices bytes squared against rate. brotli - // stays available for a client that accepts nothing else. The TLS listener - // (json-tls) is never asked for an encoding, so its route carries no - // middleware and stays on the router's fast path. + // gzip at level 3 is preferred over brotli: the profile prices bytes + // squared against rate, and on these bodies level 3 costs a little over + // a third of the CPU of the default level 6 for 9% more bytes, and about + // half the CPU of brotli at quality 3 for 10% more - the rate wins. The + // middleware keeps one gzip encoder per worker and resets it between + // bodies, so no response pays for building one. brotli, at the quality + // fulmine and spliffy use, stays available for a client that accepts + // nothing else. The TLS listener (json-tls) is never asked for an + // encoding, so its route carries no middleware and stays on the router's + // fast path. const json = (req, res) => { res.json(jsonItems(req)); }; if (ROLE === 'plain') { app.get('/json/:count') - .before(middleware.compression({ encodings: ['gzip', 'br'] })) + .before( + middleware.compression({ + encodings: ['gzip', 'br'], + gzip: { level: 3 }, + brotli: { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 3 } }, + }) + ) .handler(json); } else { app.get('/json/:count').handler(json); } // 8gbit: the body exactly as it arrived, Content-Length or chunked, sent - // back as the same bytes. req.rawBody is the framework's undecoded copy. + // back as the same bytes. req.rawBody is the framework's undecoded copy, + // and a Buffer sent as is goes out application/octet-stream through the + // framework's prepared reply. app.post('/echo').handler((req, res) => { - res.setHeader('Content-Type', 'application/octet-stream'); res.send(req.rawBody ?? Buffer.alloc(0)); }); @@ -431,11 +450,23 @@ if (ROLE === 'plain') { } if (ROLE === 'tls' || ROLE === 'h2') { - // Static files through the framework's own handler, which stats and - // reads the file on every request and serves the .br or .gz sidecar on - // disk when the client accepts it. Behind a path check so the JSON route - // on this port does not pay for the file lookup. - const serveStatic = middleware.staticFiles({ root: STATIC_ROOT, prefix: '/static', precompressed: true }); + // Static files through the framework's own handler with its in-memory + // cache: a file and its .br and .gz sidecars are read once and served + // from memory with their validators, the sidecar negotiated per request + // off Accept-Encoding. The disk is looked at again once a second at + // most, or as soon as the directory reports a change, so a replaced file + // is served promptly; the uncached handler stats, resolves and reads the + // file and its sidecars on every request, and every one of those is a + // threadpool round trip shared by all the worker threads of the process. + // Behind a path check so the JSON route on this port does not pay for the + // lookup; the check calls next() synchronously, so the route stays on + // the server's synchronous dispatch. + const serveStatic = middleware.staticFiles({ + root: STATIC_ROOT, + prefix: '/static', + precompressed: true, + cache: true, + }); await app.use((req, res, next) => req.path.startsWith('/static/') ? serveStatic(req, res, next) : next() ); diff --git a/frameworks/morojs/meta.json b/frameworks/morojs/meta.json index a5dfdad721..248e2ec6b7 100644 --- a/frameworks/morojs/meta.json +++ b/frameworks/morojs/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "@morojs/engine", - "description": "MoroJS 1.8 on its native HTTP engine (@morojs/engine), default configuration, clustered by the framework itself with one worker thread per core sharing the port through SO_REUSEPORT. Routing, path and query parameters and body parsing through the Moro request, JSON through res.json, compression, static files and the cache-aside through the built-in middleware and cache adapters, WebSockets through the engine's own RFC 6455 support, HTTP/2 through Moro's HTTP/2 server on Node's http2 module.", + "description": "MoroJS 1.8 on its native HTTP engine (@morojs/engine), default configuration, clustered by the framework itself with one worker thread per core sharing the port through SO_REUSEPORT. Routing, path and query parameters and body parsing through the Moro request, JSON through res.json, compression (gzip level 3, one reused encoder per worker), static files from the built-in middleware's in-memory cache and the cache-aside through the built-in cache adapters, WebSockets through the engine's own RFC 6455 support, HTTP/2 through Moro's HTTP/2 server on Node's http2 module.", "repo": "https://github.com/Moro-JS/moro", "enabled": true, "tls_check": true, diff --git a/frameworks/morojs/package.json b/frameworks/morojs/package.json index 7c3198c911..24f0dbad75 100644 --- a/frameworks/morojs/package.json +++ b/frameworks/morojs/package.json @@ -2,7 +2,7 @@ "name": "httparena-morojs", "private": true, "dependencies": { - "@morojs/moro": "^1.8.15", + "@morojs/moro": "^1.8.16", "ejs": "^3.1.10", "pg": "^8.16.3", "redis": "^5.0.0" From 5f5556436a0fbc393a769ea843c0ac13bea4214b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 29 Oct 2026 19:10:17 +0000 Subject: [PATCH 03/12] Benchmark results: morojs [skip ci] --- site/data/frameworks.json | 2 +- site/data/results/morojs.json | 673 +++++++++++------- site/static/logs/baseline-h2/1024/morojs.log | 0 site/static/logs/baseline-h2/256/morojs.log | 0 site/static/logs/baseline-h2c/1024/morojs.log | 0 site/static/logs/baseline-h2c/256/morojs.log | 0 site/static/logs/baseline-h2c/4096/morojs.log | 1 + site/static/logs/json-h2c/1024/morojs.log | 0 site/static/logs/json-h2c/4096/morojs.log | 0 site/static/logs/static-h2/1024/morojs.log | 0 site/static/logs/static-h2/256/morojs.log | 0 site/static/logs/static-tls/1024/morojs.log | 1 + 12 files changed, 425 insertions(+), 252 deletions(-) create mode 100644 site/static/logs/baseline-h2/1024/morojs.log create mode 100644 site/static/logs/baseline-h2/256/morojs.log create mode 100644 site/static/logs/baseline-h2c/1024/morojs.log create mode 100644 site/static/logs/baseline-h2c/256/morojs.log create mode 100644 site/static/logs/baseline-h2c/4096/morojs.log create mode 100644 site/static/logs/json-h2c/1024/morojs.log create mode 100644 site/static/logs/json-h2c/4096/morojs.log create mode 100644 site/static/logs/static-h2/1024/morojs.log create mode 100644 site/static/logs/static-h2/256/morojs.log diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 2397e72709..47fe7b7f89 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -1473,7 +1473,7 @@ }, "morojs": { "dir": "morojs", - "description": "MoroJS 1.8 on its native HTTP engine (@morojs/engine), default configuration, clustered by the framework itself with one worker thread per core sharing the port through SO_REUSEPORT. Routing, path and query parameters and body parsing through the Moro request, JSON through res.json, compression, static files and the cache-aside through the built-in middleware and cache adapters, WebSockets through the engine's own RFC 6455 support.", + "description": "MoroJS 1.8 on its native HTTP engine (@morojs/engine), default configuration, clustered by the framework itself with one worker thread per core sharing the port through SO_REUSEPORT. Routing, path and query parameters and body parsing through the Moro request, JSON through res.json, compression (gzip level 3, one reused encoder per worker), static files from the built-in middleware's in-memory cache and the cache-aside through the built-in cache adapters, WebSockets through the engine's own RFC 6455 support, HTTP/2 through Moro's HTTP/2 server on Node's http2 module.", "repo": "https://github.com/Moro-JS/moro", "type": "emerging", "engine": "@morojs/engine", diff --git a/site/data/results/morojs.json b/site/data/results/morojs.json index 83344a3de1..7a53539550 100644 --- a/site/data/results/morojs.json +++ b/site/data/results/morojs.json @@ -4,44 +4,44 @@ "8gbit-512": { "framework": "morojs", "language": "JS", - "rps": 49336, - "avg_latency": "119.4us", - "p99_latency": "174.0us", - "cpu": "321.7%", - "memory": "4.1GiB", + "rps": 49295, + "avg_latency": "113.4us", + "p99_latency": "153.0us", + "cpu": "322.5%", + "memory": "9.2GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "511.08MB/s", - "input_bw": "481.80MB/s", + "bandwidth": "510.65MB/s", + "input_bw": "481.40MB/s", "reconnects": 0, - "status_2xx": 246751, + "status_2xx": 246542, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 18114219, - "cpu_per_req_us": 73.4109, + "cpu_usec": 16967761, + "cpu_per_req_us": 68.823, "target_rate": 50000, - "rate_ratio": 0.9867, - "p99_9_latency": "5678.0us" + "rate_ratio": 0.9859, + "p99_9_latency": "6370.0us" }, "async-32000": { "framework": "morojs", "language": "JS", - "rps": 1987758, - "avg_latency": "16.01ms", - "p99_latency": "24.90ms", - "cpu": "6270.4%", - "memory": "4.7GiB", + "rps": 1952746, + "avg_latency": "16.28ms", + "p99_latency": "26.60ms", + "cpu": "6205.5%", + "memory": "9.9GiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "223.65MB/s", - "input_bw": "90.99MB/s", + "bandwidth": "219.70MB/s", + "input_bw": "89.39MB/s", "reconnects": 0, - "status_2xx": 19877580, + "status_2xx": 19527462, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -49,19 +49,19 @@ "async-db-1024": { "framework": "morojs", "language": "JS", - "rps": 193185, - "avg_latency": "4.86ms", - "p99_latency": "29.40ms", - "cpu": "5960.4%", - "memory": "4.9GiB", + "rps": 194463, + "avg_latency": "4.67ms", + "p99_latency": "39.50ms", + "cpu": "5175.0%", + "memory": "9.4GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "737.15MB/s", - "input_bw": "12.90MB/s", - "reconnects": 76827, - "status_2xx": 1931850, + "bandwidth": "742.06MB/s", + "input_bw": "12.98MB/s", + "reconnects": 77692, + "status_2xx": 1944632, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -69,19 +69,114 @@ "baseline-4096": { "framework": "morojs", "language": "JS", - "rps": 2204116, - "avg_latency": "1.86ms", - "p99_latency": "3.62ms", - "cpu": "6730.2%", - "memory": "4.0GiB", + "rps": 2423165, + "avg_latency": "1.69ms", + "p99_latency": "3.16ms", + "cpu": "6687.5%", + "memory": "8.4GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "247.97MB/s", - "input_bw": "170.26MB/s", + "bandwidth": "272.62MB/s", + "input_bw": "187.18MB/s", "reconnects": 0, - "status_2xx": 11020580, + "status_2xx": 12115826, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2-1024": { + "framework": "morojs", + "language": "JS", + "rps": 736217, + "avg_latency": "125.19ms", + "p99_latency": "269.40ms", + "cpu": "6351.6%", + "memory": "13.8GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "18.47MB/s", + "reconnects": 0, + "status_2xx": 3717900, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2-256": { + "framework": "morojs", + "language": "JS", + "rps": 792420, + "avg_latency": "31.59ms", + "p99_latency": "127.47ms", + "cpu": "6359.6%", + "memory": "13.2GiB", + "connections": 256, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "19.81MB/s", + "reconnects": 0, + "status_2xx": 3993800, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2c-1024": { + "framework": "morojs", + "language": "JS", + "rps": 872741, + "avg_latency": "115.07ms", + "p99_latency": "1.06s", + "cpu": "6398.7%", + "memory": "13.2GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "21.98MB/s", + "reconnects": 0, + "status_2xx": 4424800, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2c-256": { + "framework": "morojs", + "language": "JS", + "rps": 913697, + "avg_latency": "28.41ms", + "p99_latency": "162.47ms", + "cpu": "6385.4%", + "memory": "12.9GiB", + "connections": 256, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "22.80MB/s", + "reconnects": 0, + "status_2xx": 4595900, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2c-4096": { + "framework": "morojs", + "language": "JS", + "rps": 805234, + "avg_latency": "249.04ms", + "p99_latency": "3.95s", + "cpu": "6292.2%", + "memory": "14.0GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "20.52MB/s", + "reconnects": 0, + "status_2xx": 4122800, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -89,18 +184,18 @@ "echo-ws-16384": { "framework": "morojs", "language": "JS", - "rps": 3134466, - "avg_latency": "5.16ms", - "p99_latency": "9.80ms", - "cpu": "6161.9%", - "memory": "4.1GiB", + "rps": 3206405, + "avg_latency": "5.07ms", + "p99_latency": "9.52ms", + "cpu": "6088.9%", + "memory": "8.3GiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "21.32MB/s", + "bandwidth": "21.80MB/s", "reconnects": 0, - "status_2xx": 15672331, + "status_2xx": 16032026, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -108,18 +203,18 @@ "echo-ws-4096": { "framework": "morojs", "language": "JS", - "rps": 3446745, - "avg_latency": "1.19ms", - "p99_latency": "2.44ms", - "cpu": "6279.3%", - "memory": "3.9GiB", + "rps": 3598655, + "avg_latency": "1.14ms", + "p99_latency": "2.32ms", + "cpu": "6313.1%", + "memory": "8.3GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "23.02MB/s", + "bandwidth": "24.06MB/s", "reconnects": 0, - "status_2xx": 17233727, + "status_2xx": 17993278, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -127,18 +222,18 @@ "echo-ws-512": { "framework": "morojs", "language": "JS", - "rps": 3382028, - "avg_latency": "150us", - "p99_latency": "374us", - "cpu": "6577.8%", - "memory": "3.8GiB", + "rps": 3500108, + "avg_latency": "145us", + "p99_latency": "369us", + "cpu": "6415.4%", + "memory": "8.0GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "22.58MB/s", + "bandwidth": "23.36MB/s", "reconnects": 0, - "status_2xx": 16910141, + "status_2xx": 17500543, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -146,18 +241,18 @@ "echo-ws-limited-4096": { "framework": "morojs", "language": "JS", - "rps": 2007288, - "avg_latency": "1.54ms", - "p99_latency": "5.65ms", - "cpu": "6176.0%", - "memory": "5.1GiB", + "rps": 2255428, + "avg_latency": "1.37ms", + "p99_latency": "4.49ms", + "cpu": "6176.9%", + "memory": "8.6GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "38.08MB/s", - "reconnects": 1003518, - "status_2xx": 10036442, + "bandwidth": "42.82MB/s", + "reconnects": 1126883, + "status_2xx": 11277140, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -165,18 +260,18 @@ "echo-ws-limited-512": { "framework": "morojs", "language": "JS", - "rps": 1684460, - "avg_latency": "207us", - "p99_latency": "777us", - "cpu": "5886.9%", - "memory": "4.3GiB", + "rps": 1841352, + "avg_latency": "184us", + "p99_latency": "683us", + "cpu": "5663.8%", + "memory": "8.5GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "31.96MB/s", - "reconnects": 842241, - "status_2xx": 8422302, + "bandwidth": "34.94MB/s", + "reconnects": 920716, + "status_2xx": 9206764, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -184,18 +279,18 @@ "echo-ws-pipeline-16384": { "framework": "morojs", "language": "JS", - "rps": 4315535, - "avg_latency": "59.38ms", - "p99_latency": "68.70ms", - "cpu": "6280.0%", - "memory": "4.2GiB", + "rps": 20690537, + "avg_latency": "12.57ms", + "p99_latency": "14.40ms", + "cpu": "6706.0%", + "memory": "9.0GiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "29.20MB/s", + "bandwidth": "138.49MB/s", "reconnects": 0, - "status_2xx": 21577677, + "status_2xx": 103452688, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -203,18 +298,18 @@ "echo-ws-pipeline-4096": { "framework": "morojs", "language": "JS", - "rps": 4434002, - "avg_latency": "14.74ms", - "p99_latency": "18.40ms", - "cpu": "6350.8%", - "memory": "4.0GiB", + "rps": 21407936, + "avg_latency": "3.06ms", + "p99_latency": "4.14ms", + "cpu": "6773.0%", + "memory": "8.3GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "29.63MB/s", + "bandwidth": "142.88MB/s", "reconnects": 0, - "status_2xx": 22170014, + "status_2xx": 107039680, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -222,18 +317,18 @@ "echo-ws-pipeline-512": { "framework": "morojs", "language": "JS", - "rps": 4301358, - "avg_latency": "1.90ms", - "p99_latency": "3.54ms", - "cpu": "6489.2%", - "memory": "3.9GiB", + "rps": 20838912, + "avg_latency": "392us", + "p99_latency": "975us", + "cpu": "6712.7%", + "memory": "8.3GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "28.71MB/s", + "bandwidth": "139.08MB/s", "reconnects": 0, - "status_2xx": 21506790, + "status_2xx": 104194560, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -241,18 +336,18 @@ "fortunes-1024": { "framework": "morojs", "language": "JS", - "rps": 61667, - "avg_latency": "15.42ms", - "p99_latency": "29.90ms", - "cpu": "6540.2%", - "memory": "6.1GiB", + "rps": 62933, + "avg_latency": "15.05ms", + "p99_latency": "26.60ms", + "cpu": "6493.6%", + "memory": "10.1GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.43GB/s", + "bandwidth": "1.46GB/s", "reconnects": 0, - "status_2xx": 308338, + "status_2xx": 314667, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -260,115 +355,115 @@ "gateway-64-1024": { "framework": "morojs", "language": "JS", - "rps": 132017, - "avg_latency": "204.29ms", - "p99_latency": "3.73s", - "cpu": "3937.4%", - "memory": "6.2GiB", + "rps": 255138, + "avg_latency": "112.07ms", + "p99_latency": "1.17s", + "cpu": "5679.7%", + "memory": "5.0GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.66GB/s", + "bandwidth": "3.17GB/s", "reconnects": 0, - "status_2xx": 669331, + "status_2xx": 1293551, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 200799, - "tpl_baseline": 133866, - "tpl_json": 234265, - "tpl_async_db": 100399, - "cpu_breakdown": "server: 2254% 4.5GiB | proxy: 1684% 1.7GiB" + "tpl_static": 388065, + "tpl_baseline": 258710, + "tpl_json": 452742, + "tpl_async_db": 194032, + "cpu_breakdown": "server: 2433% 3.4GiB | proxy: 3247% 1.7GiB" }, "gateway-64-512": { "framework": "morojs", "language": "JS", - "rps": 131486, - "avg_latency": "113.98ms", - "p99_latency": "1.14s", - "cpu": "3672.1%", + "rps": 253893, + "avg_latency": "61.91ms", + "p99_latency": "555.57ms", + "cpu": "5028.4%", "memory": "4.6GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.62GB/s", + "bandwidth": "3.11GB/s", "reconnects": 0, - "status_2xx": 662691, + "status_2xx": 1282161, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 198807, - "tpl_baseline": 132538, - "tpl_json": 231941, - "tpl_async_db": 99403, - "cpu_breakdown": "server: 2224% 3.1GiB | proxy: 1449% 1.5GiB" + "tpl_static": 384648, + "tpl_baseline": 256432, + "tpl_json": 448756, + "tpl_async_db": 192324, + "cpu_breakdown": "server: 2371% 3.1GiB | proxy: 2658% 1.4GiB" }, "gateway-h3-256": { "framework": "morojs", "language": "JS", - "rps": 58910, - "avg_latency": "132.87ms", - "p99_latency": "807.76ms", - "cpu": "3229.8%", - "memory": "2.5GiB", + "rps": 59242, + "avg_latency": "134.23ms", + "p99_latency": "791.57ms", + "cpu": "2564.8%", + "memory": "3.6GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "623.43MB/s", + "bandwidth": "626.34MB/s", "reconnects": 0, - "status_2xx": 296319, + "status_2xx": 297396, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 88895, - "tpl_baseline": 59263, - "tpl_json": 103711, - "tpl_async_db": 44447, - "cpu_breakdown": "proxy: 2178% 1.2GiB | server: 1052% 1.3GiB" + "tpl_static": 89218, + "tpl_baseline": 59479, + "tpl_json": 104088, + "tpl_async_db": 44609, + "cpu_breakdown": "proxy: 2032% 1.4GiB | server: 533% 2.2GiB" }, "gateway-h3-64": { "framework": "morojs", "language": "JS", - "rps": 81332, - "avg_latency": "25.05ms", - "p99_latency": "87.65ms", - "cpu": "4101.2%", - "memory": "1.6GiB", + "rps": 82650, + "avg_latency": "24.61ms", + "p99_latency": "86.44ms", + "cpu": "3389.0%", + "memory": "2.6GiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "882.04MB/s", + "bandwidth": "897.78MB/s", "reconnects": 0, - "status_2xx": 407477, + "status_2xx": 414079, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 122243, - "tpl_baseline": 81495, - "tpl_json": 142616, - "tpl_async_db": 61121, - "cpu_breakdown": "proxy: 2687% 312MiB | server: 1414% 1.3GiB" + "tpl_static": 124223, + "tpl_baseline": 82815, + "tpl_json": 144927, + "tpl_async_db": 62111, + "cpu_breakdown": "proxy: 2664% 359MiB | server: 725% 2.2GiB" }, "json-comp-16384": { "framework": "morojs", "language": "JS", - "rps": 129852, - "avg_latency": "123.86ms", - "p99_latency": "234.20ms", - "cpu": "6021.4%", - "memory": "11.7GiB", + "rps": 479027, + "avg_latency": "33.88ms", + "p99_latency": "73.50ms", + "cpu": "6568.6%", + "memory": "11.0GiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "177.54MB/s", - "input_bw": "9.66MB/s", - "reconnects": 16756, - "status_2xx": 649264, + "bandwidth": "692.62MB/s", + "input_bw": "35.63MB/s", + "reconnects": 87628, + "status_2xx": 2395135, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -376,19 +471,57 @@ "json-comp-4096": { "framework": "morojs", "language": "JS", - "rps": 147090, - "avg_latency": "27.89ms", - "p99_latency": "69.30ms", - "cpu": "6208.7%", - "memory": "8.9GiB", + "rps": 483296, + "avg_latency": "8.46ms", + "p99_latency": "23.50ms", + "cpu": "6252.8%", + "memory": "9.7GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "200.69MB/s", - "input_bw": "10.94MB/s", - "reconnects": 27564, - "status_2xx": 735453, + "bandwidth": "698.81MB/s", + "input_bw": "35.95MB/s", + "reconnects": 95117, + "status_2xx": 2416484, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-h2c-1024": { + "framework": "morojs", + "language": "JS", + "rps": 703487, + "avg_latency": "43.87ms", + "p99_latency": "343.73ms", + "cpu": "6462.0%", + "memory": "13.5GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "2.35GB/s", + "reconnects": 0, + "status_2xx": 3566683, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-h2c-4096": { + "framework": "morojs", + "language": "JS", + "rps": 603990, + "avg_latency": "137.22ms", + "p99_latency": "3.94s", + "cpu": "6416.3%", + "memory": "13.0GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "2.03GB/s", + "reconnects": 0, + "status_2xx": 3080353, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -396,18 +529,18 @@ "json-tls-4096": { "framework": "morojs", "language": "JS", - "rps": 844930, - "avg_latency": "4.85ms", - "p99_latency": "74.43ms", - "cpu": "6620.7%", - "memory": "4.2GiB", + "rps": 960598, + "avg_latency": "4.25ms", + "p99_latency": "40.99ms", + "cpu": "6622.1%", + "memory": "9.0GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.86GB", + "bandwidth": "3.25GB", "reconnects": 0, - "status_2xx": 4308783, + "status_2xx": 4898822, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -415,91 +548,91 @@ "latency-10k-1024": { "framework": "morojs", "language": "JS", - "rps": 9981, - "avg_latency": "77.2us", - "p99_latency": "117.0us", + "rps": 9982, + "avg_latency": "79.8us", + "p99_latency": "114.0us", "cpu": "32.4%", - "memory": "3.6GiB", + "memory": "7.8GiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, "bandwidth": "1.17MB/s", "reconnects": 0, - "status_2xx": 199678, + "status_2xx": 199670, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 6796958, - "cpu_per_req_us": 34.0396, + "cpu_usec": 6758859, + "cpu_per_req_us": 33.8501, "target_rate": 10000, - "rate_ratio": 0.9981, - "p99_9_latency": "185.0us" + "rate_ratio": 0.9982, + "p99_9_latency": "233.0us" }, "latency-1m-1024": { "framework": "morojs", "language": "JS", - "rps": 997623, - "avg_latency": "121.5us", - "p99_latency": "214.0us", - "cpu": "3155.9%", - "memory": "3.9GiB", + "rps": 997910, + "avg_latency": "114.7us", + "p99_latency": "213.0us", + "cpu": "3046.4%", + "memory": "8.3GiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "116.72MB/s", + "bandwidth": "116.76MB/s", "reconnects": 0, - "status_2xx": 19956022, + "status_2xx": 19961429, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 573348441, - "cpu_per_req_us": 28.7306, + "cpu_usec": 553652649, + "cpu_per_req_us": 27.7361, "target_rate": 1000000, - "rate_ratio": 0.9976, - "p99_9_latency": "5270.0us" + "rate_ratio": 0.9979, + "p99_9_latency": "2415.0us" }, "latency-500k-8cpu-1024": { "framework": "morojs", "language": "JS", - "rps": 396396, - "avg_latency": "2030071.8us", - "p99_latency": "5015552.0us", - "cpu": "794.2%", - "memory": "494MiB", + "rps": 392420, + "avg_latency": "2023198.3us", + "p99_latency": "5871616.0us", + "cpu": "788.2%", + "memory": "1.1GiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "46.38MB/s", + "bandwidth": "45.91MB/s", "reconnects": 0, - "status_2xx": 7930671, + "status_2xx": 7850972, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 159607857, - "cpu_per_req_us": 20.1254, + "cpu_usec": 159511370, + "cpu_per_req_us": 20.3174, "target_rate": 500000, - "rate_ratio": 0.7928, - "p99_9_latency": "5203968.0us" + "rate_ratio": 0.7848, + "p99_9_latency": "6117376.0us" }, "limited-conn-4096": { "framework": "morojs", "language": "JS", - "rps": 1885683, - "avg_latency": "2.15ms", - "p99_latency": "8.52ms", - "cpu": "6534.4%", - "memory": "4.0GiB", + "rps": 1996238, + "avg_latency": "2.03ms", + "p99_latency": "6.85ms", + "cpu": "6519.4%", + "memory": "8.4GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "212.15MB/s", - "input_bw": "145.66MB/s", - "reconnects": 941345, - "status_2xx": 9428418, + "bandwidth": "224.57MB/s", + "input_bw": "154.20MB/s", + "reconnects": 997717, + "status_2xx": 9981192, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -507,18 +640,18 @@ "pipelined-4096": { "framework": "morojs", "language": "JS", - "rps": 37648418, + "rps": 37587043, "avg_latency": "1.74ms", - "p99_latency": "3.31ms", - "cpu": "6404.5%", - "memory": "3.9GiB", + "p99_latency": "3.24ms", + "cpu": "6665.3%", + "memory": "8.2GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "4.14GB/s", + "bandwidth": "4.13GB/s", "reconnects": 0, - "status_2xx": 188242092, + "status_2xx": 187935216, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -526,66 +659,104 @@ "production-stack-1024": { "framework": "morojs", "language": "JS", - "rps": 106517, - "avg_latency": "131.70ms", - "p99_latency": "4.45s", - "cpu": "2697.0%", - "memory": "3.1GiB", + "rps": 106702, + "avg_latency": "123.01ms", + "p99_latency": "3.53s", + "cpu": "2805.4%", + "memory": "3.9GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.19GB/s", + "bandwidth": "1.20GB/s", "reconnects": 0, - "status_2xx": 540045, + "status_2xx": 540983, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 162013, - "tpl_baseline": 54004, - "tpl_items": 270022, - "tpl_me": 54004, - "cpu_breakdown": "server: 468% 1.2GiB | authsvc: 724% 492MiB | edge: 1289% 1.4GiB | cache: 217% 10MiB" + "tpl_static": 162294, + "tpl_baseline": 54098, + "tpl_items": 270491, + "tpl_me": 54098, + "cpu_breakdown": "server: 590% 2.0GiB | authsvc: 711% 470MiB | edge: 1291% 1.4GiB | cache: 213% 10MiB" }, "production-stack-256": { "framework": "morojs", "language": "JS", - "rps": 120812, - "avg_latency": "34.63ms", - "p99_latency": "148.25ms", - "cpu": "2968.9%", - "memory": "2.4GiB", + "rps": 109749, + "avg_latency": "38.26ms", + "p99_latency": "3.83s", + "cpu": "2610.3%", + "memory": "3.2GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.32GB/s", + "bandwidth": "1.19GB/s", "reconnects": 0, - "status_2xx": 608893, + "status_2xx": 552039, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 182667, - "tpl_baseline": 60889, - "tpl_items": 304446, - "tpl_me": 60889, - "cpu_breakdown": "server: 627% 1.2GiB | authsvc: 757% 153MiB | edge: 1371% 1.1GiB | cache: 214% 19MiB" + "tpl_static": 165611, + "tpl_baseline": 55203, + "tpl_items": 276019, + "tpl_me": 55203, + "cpu_breakdown": "server: 670% 2.0GiB | authsvc: 688% 133MiB | edge: 1040% 1.0GiB | cache: 213% 19MiB" + }, + "static-h2-1024": { + "framework": "morojs", + "language": "JS", + "rps": 405817, + "avg_latency": "76.86ms", + "p99_latency": "372.25ms", + "cpu": "6470.0%", + "memory": "14.9GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "6.24GB/s", + "reconnects": 0, + "status_2xx": 2049377, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "static-h2-256": { + "framework": "morojs", + "language": "JS", + "rps": 455706, + "avg_latency": "17.76ms", + "p99_latency": "187.17ms", + "cpu": "6475.9%", + "memory": "13.6GiB", + "connections": 256, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "6.98GB/s", + "reconnects": 0, + "status_2xx": 2292205, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 }, "static-tls-1024": { "framework": "morojs", "language": "JS", - "rps": 70911, - "avg_latency": "14.41ms", - "p99_latency": "38.15ms", - "cpu": "5950.4%", - "memory": "4.4GiB", + "rps": 391428, + "avg_latency": "2.68ms", + "p99_latency": "27.65ms", + "cpu": "6446.7%", + "memory": "8.5GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.09GB", + "bandwidth": "6.03GB", "reconnects": 0, - "status_2xx": 360843, + "status_2xx": 1996395, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/baseline-h2/1024/morojs.log b/site/static/logs/baseline-h2/1024/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/baseline-h2/256/morojs.log b/site/static/logs/baseline-h2/256/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/baseline-h2c/1024/morojs.log b/site/static/logs/baseline-h2c/1024/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/baseline-h2c/256/morojs.log b/site/static/logs/baseline-h2c/256/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/baseline-h2c/4096/morojs.log b/site/static/logs/baseline-h2c/4096/morojs.log new file mode 100644 index 0000000000..7f37f73edf --- /dev/null +++ b/site/static/logs/baseline-h2c/4096/morojs.log @@ -0,0 +1 @@ +2026-10-29 18:57:34 ERROR [Moro:Session] HTTP/2 session error diff --git a/site/static/logs/json-h2c/1024/morojs.log b/site/static/logs/json-h2c/1024/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/json-h2c/4096/morojs.log b/site/static/logs/json-h2c/4096/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/static-h2/1024/morojs.log b/site/static/logs/static-h2/1024/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/static-h2/256/morojs.log b/site/static/logs/static-h2/256/morojs.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/static/logs/static-tls/1024/morojs.log b/site/static/logs/static-tls/1024/morojs.log index e69de29bb2..912a3d1bbe 100644 --- a/site/static/logs/static-tls/1024/morojs.log +++ b/site/static/logs/static-tls/1024/morojs.log @@ -0,0 +1 @@ +2026-10-29 18:53:10 ERROR [Moro:LeaderElection] Failed to acquire file lock From 2539dbd8e1db9c4769430ddf6603cd11d5a9837a Mon Sep 17 00:00:00 2001 From: Chris M Date: Fri, 30 Oct 2026 08:04:09 -0600 Subject: [PATCH 04/12] morojs: brotli 3 first on json-comp; the delay route answers from a timer callback json-comp is scored on rate times the square of the field's smallest bytes per reply over the entry's own. The first run on 1.8.16 had gzip level 3 at 483K req/s and 1446 bytes per reply, 7% more requests than spliffy's brotli 3 but 10% more bytes, which the square turns into a 9% lower score. brotli at quality 3 goes first in the encodings order, with the window fitted to the body by the framework; gzip level 3 stays for a client that accepts nothing else. /delay/:ms sends its reply from the timer's callback instead of awaiting the timer in an async handler. The reply is the same; what goes away is three promise hops per request (the handler's, the router's, the server's) on a profile that measures what a framework does while two million requests a second are pending. --- frameworks/morojs/README.md | 18 ++++++++-------- frameworks/morojs/app.mjs | 42 ++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/frameworks/morojs/README.md b/frameworks/morojs/README.md index c15735c4ad..836d17cfa3 100644 --- a/frameworks/morojs/README.md +++ b/frameworks/morojs/README.md @@ -16,8 +16,8 @@ MoroJS on its native HTTP engine, clustered by the framework itself. | `/pipeline` | GET | Returns `ok` (plain text) | | `/baseline11` | GET/POST | Sums query parameter values, plus the body for POST | | `/baseline2` | GET | Sums query parameter values (behind the gateway proxies) | -| `/delay/:ms` | GET | Waits `ms` milliseconds on an awaited timer, then returns `ms` | -| `/json/:count` | GET | Serializes a slice of the dataset, brotli or gzip when the client asks for it | +| `/delay/:ms` | GET | Answers `ms` from a timer callback `ms` milliseconds later | +| `/json/:count` | GET | Serializes a slice of the dataset, brotli (quality 3) or gzip (level 3) when the client asks for it | | `/echo` | POST | Returns the request body back verbatim (TLS listener) | | `/ws` | WebSocket | Echoes every text and binary frame unchanged | | `/async-db` | GET | Reads from PostgreSQL through `pg`, prepared statement, pool sized under `DATABASE_MAX_CONN` | @@ -40,13 +40,13 @@ MoroJS on its native HTTP engine, clustered by the framework itself. `--cpuset-cpus`. - `json-comp` is the framework's `compression()` middleware attached to the `/json` route alone, so no other endpoint pays for the encoder. It negotiates off `Accept-Encoding` per request and - leaves the body alone when the header is absent. gzip at level 3 is preferred over brotli - through the middleware's `encodings` and `gzip` options: the profile scores bytes squared - against rate, and on these bodies level 3 costs a little over a third of the CPU of the default - level 6 for 9% more bytes, and about half the CPU of brotli at quality 3 for 10% more. The - middleware keeps one gzip encoder per worker and resets it between bodies (`reuseEncoders`, the - default), so no response pays for building one. brotli at quality 3, the setting the other JS - entries use, remains available to a client that accepts nothing else. + leaves the body alone when the header is absent. brotli at quality 3 is preferred, through the + middleware's `encodings` and `brotli` options, with the encoder's window fitted to the body by + the framework: the profile scores bytes squared against rate, and measured on the board gzip at + level 3 ran 7% more requests than brotli 3 but sent 10% more bytes per reply, a 9% lower score. + gzip at level 3 remains available to a client that accepts nothing else; the middleware keeps one + gzip encoder per worker and resets it between bodies (`reuseEncoders`, the default), while the + brotli encoder is built per body, since it forgets its quality on reset. - `UV_THREADPOOL_SIZE=64` is set in the Dockerfile. The libuv pool serves the compression and file reads of every worker thread in the process; Moro sets 64 itself but only at `listen()`, after its startup has initialised the pool at Node's default of 4, and Node reads the variable diff --git a/frameworks/morojs/app.mjs b/frameworks/morojs/app.mjs index 19473297d8..01ed327825 100644 --- a/frameworks/morojs/app.mjs +++ b/frameworks/morojs/app.mjs @@ -195,32 +195,40 @@ function defineRoutes(app) { res.send(String(sumQuery(req.query))); }); - // An awaited timer suspends the request and frees the thread, so the - // waits in flight are bounded by memory. The delay is read from the path + // The reply is sent from a timer callback, so the request is a pending + // timer and nothing else while it waits: the handler returns at once and + // the response object answers when the timer fires, the same way an + // Express handler would. An async handler awaiting the timer did the same + // work through three promise hops per request (the handler's own, the + // router's, the server's), which at two million requests a second was a + // measurable share of the worker's time. The delay is read from the path // on every request. - app.get('/delay/:ms').handler(async (req, res) => { + app.get('/delay/:ms').handler((req, res) => { const ms = Number.parseInt(req.params.ms, 10); if (!Number.isInteger(ms) || ms < 0) { res.status(404).send('Not found'); return; } - if (ms > 0) await new Promise(resolve => setTimeout(resolve, ms)); - res.send(String(ms)); + if (ms === 0) { + res.send('0'); + return; + } + setTimeout(() => res.send(String(ms)), ms); }); // json-comp: the framework's compression middleware on this route alone, // so no other endpoint pays for the encoder. It negotiates off // Accept-Encoding per request and sends the body as is when none is sent. - // gzip at level 3 is preferred over brotli: the profile prices bytes - // squared against rate, and on these bodies level 3 costs a little over - // a third of the CPU of the default level 6 for 9% more bytes, and about - // half the CPU of brotli at quality 3 for 10% more - the rate wins. The - // middleware keeps one gzip encoder per worker and resets it between - // bodies, so no response pays for building one. brotli, at the quality - // fulmine and spliffy use, stays available for a client that accepts - // nothing else. The TLS listener (json-tls) is never asked for an - // encoding, so its route carries no middleware and stays on the router's - // fast path. + // brotli at quality 3 is preferred, with the encoder's window fitted to + // the body by the framework: the profile prices bytes squared against + // rate, and measured on the board gzip at level 3 ran 7% more requests + // than brotli 3 but sent 10% more bytes per reply, which the square turns + // into a 9% lower score. gzip at level 3 stays available for a client + // that accepts nothing else; the middleware keeps one gzip encoder per + // worker and resets it between bodies (brotli's encoder forgets its + // quality on reset, so it is built per body). The TLS listener (json-tls) + // is never asked for an encoding, so its route carries no middleware and + // stays on the router's fast path. const json = (req, res) => { res.json(jsonItems(req)); }; @@ -228,9 +236,9 @@ function defineRoutes(app) { app.get('/json/:count') .before( middleware.compression({ - encodings: ['gzip', 'br'], - gzip: { level: 3 }, + encodings: ['br', 'gzip'], brotli: { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 3 } }, + gzip: { level: 3 }, }) ) .handler(json); From 9eece4986225f5b3b96dff559f9d5ceb8c8aa9cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 30 Oct 2026 14:20:13 +0000 Subject: [PATCH 05/12] Benchmark results: morojs async [skip ci] --- site/data/results/morojs.json | 16 ++++++++-------- site/static/logs/async/32000/morojs.log | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/site/data/results/morojs.json b/site/data/results/morojs.json index 7a53539550..bf4c56e9c0 100644 --- a/site/data/results/morojs.json +++ b/site/data/results/morojs.json @@ -29,19 +29,19 @@ "async-32000": { "framework": "morojs", "language": "JS", - "rps": 1952746, - "avg_latency": "16.28ms", - "p99_latency": "26.60ms", - "cpu": "6205.5%", - "memory": "9.9GiB", + "rps": 2326696, + "avg_latency": "13.66ms", + "p99_latency": "19.60ms", + "cpu": "5952.9%", + "memory": "10.0GiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "219.70MB/s", - "input_bw": "89.39MB/s", + "bandwidth": "261.79MB/s", + "input_bw": "106.51MB/s", "reconnects": 0, - "status_2xx": 19527462, + "status_2xx": 23266966, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/async/32000/morojs.log b/site/static/logs/async/32000/morojs.log index e69de29bb2..805af7ca98 100644 --- a/site/static/logs/async/32000/morojs.log +++ b/site/static/logs/async/32000/morojs.log @@ -0,0 +1 @@ +2026-10-30 14:19:29 ERROR [Moro:LeaderElection] Failed to acquire file lock From b9aedc9d8565e13166573ea845fcc514715c2250 Mon Sep 17 00:00:00 2001 From: Chris M Date: Fri, 30 Oct 2026 08:34:12 -0600 Subject: [PATCH 06/12] morojs: listen backlog 16384 for the proxied profiles gateway-h3 at 256 connections ran at 59K req/s with a p99 of 0.8 s and production-stack at 256 with a p99 of 3.8 s, both with the server mostly idle and the edge holding memory: requests were waiting, not computing. Both edges keep an idle pool of 2048 upstream connections under 8K-32K requests in flight, so they dial and drop thousands of backend connections per wave. At the engine's default listen backlog of 512 a wave overflows a worker's accept queue, the kernel drops the SYN and the dialer retries it after one second (then two), which is exactly those tails. Reproduced in a Linux container with 4096 concurrent dialers opening a connection per request against four workers: p99 1019 ms at the default backlog, 52 ms at 4096 with the same throughput; uWebSockets.js under the same load sits at 46 ms. The backlog goes to 16384 through server.backlog, the documented option; the harness raises somaxconn to 65535 for this and other entries run 4K-16K. --- frameworks/morojs/README.md | 6 ++++++ frameworks/morojs/app.mjs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/frameworks/morojs/README.md b/frameworks/morojs/README.md index 836d17cfa3..7316063f45 100644 --- a/frameworks/morojs/README.md +++ b/frameworks/morojs/README.md @@ -47,6 +47,12 @@ MoroJS on its native HTTP engine, clustered by the framework itself. gzip at level 3 remains available to a client that accepts nothing else; the middleware keeps one gzip encoder per worker and resets it between bodies (`reuseEncoders`, the default), while the brotli encoder is built per body, since it forgets its quality on reset. +- `server.backlog: 16384`: the edges in front of the gateway and production-stack profiles keep an + idle pool of 2048 upstream connections under tens of thousands of requests in flight, so they + dial and drop thousands of connections per wave. At the engine's default backlog of 512 a wave + overflows a worker's accept queue, the kernel drops the SYN and the proxy retries it a second + later, which was those profiles' tail latency. Measured on a 4096-dialer churn: p99 1019 ms at + 512, 52 ms at 4096, same throughput. The harness raises `somaxconn` to 65535 for this. - `UV_THREADPOOL_SIZE=64` is set in the Dockerfile. The libuv pool serves the compression and file reads of every worker thread in the process; Moro sets 64 itself but only at `listen()`, after its startup has initialised the pool at Node's default of 4, and Node reads the variable diff --git a/frameworks/morojs/app.mjs b/frameworks/morojs/app.mjs index 01ed327825..79baa25659 100644 --- a/frameworks/morojs/app.mjs +++ b/frameworks/morojs/app.mjs @@ -430,6 +430,15 @@ const app = await createApp({ engine: 'moro', requestTracking: { enabled: false }, requestLogging: { enabled: false }, + // The listen backlog. The edges in front of the gateway and + // production-stack profiles keep an idle pool of 2048 upstream + // connections under tens of thousands of requests in flight, so they + // dial and drop thousands of connections per wave; at the engine's + // default of 512 a wave overflows a worker's accept queue, the + // kernel drops the SYN and the proxy retries it a second later. That + // second was the profile's tail (p99 of 0.8 s on gateway-h3, 3.8 s on + // production-stack). The harness raises somaxconn to 65535 for this. + backlog: 16384, ...server, }, performance: { clustering: { enabled: true, workers } }, From b4ba382fb4019e73cfbf5cf62dbcb0307dc24551 Mon Sep 17 00:00:00 2001 From: Chris M Date: Fri, 30 Oct 2026 09:01:09 -0600 Subject: [PATCH 07/12] morojs: async-db and fortunes answer from the driver's callback; pool capped at 240 The delay route gained a fifth more throughput on the board by answering from a timer callback instead of awaiting in an async handler: three promise hops per request (the handler's, the router's, the server's) were the difference. /async-db and /fortunes paid the same three on every request; both now answer from pg's callback, the reply unchanged. fortunes also stops rendering through ejs.renderFile's promise: the template file is compiled once per worker with ejs.compile and rendered per request, with the same <%= %> escaping. The pool is capped at 240 connections across the cluster, as the other entries cap it: DATABASE_MAX_CONN is the server's max_connections, and a cluster sized exactly to it is refused its last connections. pg-native, which gives the uWebSockets-based entries their async-db and fortunes margin, is not an option here: its libpq addon is not context-aware, so it loads in one worker thread and the next one gets "Module did not self-register" (verified on Node 26 with four threads). --- frameworks/morojs/README.md | 12 +++++--- frameworks/morojs/app.mjs | 58 ++++++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/frameworks/morojs/README.md b/frameworks/morojs/README.md index 7316063f45..b51e3a79ce 100644 --- a/frameworks/morojs/README.md +++ b/frameworks/morojs/README.md @@ -20,8 +20,8 @@ MoroJS on its native HTTP engine, clustered by the framework itself. | `/json/:count` | GET | Serializes a slice of the dataset, brotli (quality 3) or gzip (level 3) when the client asks for it | | `/echo` | POST | Returns the request body back verbatim (TLS listener) | | `/ws` | WebSocket | Echoes every text and binary frame unchanged | -| `/async-db` | GET | Reads from PostgreSQL through `pg`, prepared statement, pool sized under `DATABASE_MAX_CONN` | -| `/fortunes` | GET | Reads 200 rows from PostgreSQL, appends the runtime row, sorts and renders `views/fortunes.ejs` | +| `/async-db` | GET | Reads from PostgreSQL through `pg`, prepared statement, pool sized under `DATABASE_MAX_CONN` (240 at most), the driver's callback answering the request | +| `/fortunes` | GET | Reads the fortune rows from PostgreSQL, appends the runtime row, sorts and renders `views/fortunes.ejs`, compiled once per worker | | `/static/:filename` | GET | Serves a file from disk through the framework's `staticFiles` middleware (TLS listener) | | `/public/baseline`, `/public/json/:count` | GET | The baseline and JSON routes as the production-stack edge forwards them, no auth | | `/api/items/:id` | GET/POST | Cache-aside read with `X-Cache: HIT` or `MISS`, or a JSON-body update that invalidates the entry and answers 204 | @@ -89,8 +89,12 @@ MoroJS on its native HTTP engine, clustered by the framework itself. mounted on the TLS listener behind a path check so the JSON route on that port does not pay for the lookup; the check calls `next()` synchronously, which keeps that route on the server's synchronous dispatch. -- `fortunes` goes through EJS directly: Moro ships no view layer, so the template is rendered by - the engine's own `renderFile`, a separate file with `<%= %>` escaping every cell. +- `fortunes` goes through EJS directly: Moro ships no view layer, so the template file is compiled + once per worker with `ejs.compile` and rendered per request, `<%= %>` escaping every cell. +- `/async-db` and `/fortunes` answer from the driver's callback rather than from an async handler: + the reply is the same, and the request stops paying three promise hops (the handler's, the + router's, the server's). `pg-native`, which the uWebSockets-based entries use, loads its libpq + addon once per process and refuses the second worker thread, so it cannot serve a thread cluster. - The gateway and production-stack edges are stock nginx and Caddy, configured as the fulmine entry configures them: `/static/*` off disk at the edge, everything else forwarded over loopback h1 with a keepalive pool, and `/api/*` past the shared JWT verifier first. diff --git a/frameworks/morojs/app.mjs b/frameworks/morojs/app.mjs index 79baa25659..26f0440ffb 100644 --- a/frameworks/morojs/app.mjs +++ b/frameworks/morojs/app.mjs @@ -98,10 +98,12 @@ if (isWorker) { // Postgres for /async-db, /fortunes and the production-stack api, all served // on :8080. The pool is sized from DATABASE_MAX_CONN across the workers, so -// the cluster as a whole stays inside what Postgres allows. +// the cluster as a whole stays inside what Postgres allows; 240 at most +// leaves the server's own reserve (DATABASE_MAX_CONN is its max_connections, +// and a cluster sitting exactly on it is refused its last connections). let pool = null; if (isWorker && ROLE === 'plain' && process.env.DATABASE_URL) { - const maxConn = parseInt(process.env.DATABASE_MAX_CONN, 10) || 256; + const maxConn = Math.min(parseInt(process.env.DATABASE_MAX_CONN, 10) || 256, 240); pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: Math.max(1, Math.floor(maxConn / workers)), @@ -109,6 +111,12 @@ if (isWorker && ROLE === 'plain' && process.env.DATABASE_URL) { pool.on('error', () => {}); } +// The fortunes template, compiled once per worker: EJS escapes every +// interpolated value, so the row carrying a