Repository navigation
Morojs 1.8.16 - #1678
Morojs 1.8.16#1678
Conversation
…r 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.
… 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%.
|
/benchmark -f morojs --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
Benchmark ResultsFramework:
Composite score, main against this run, in the board's default view:
The results on main were measured in another session, so a few points up or down can be the machine and not the change. Full log |
…imer 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.
|
/benchmark -f morojs -t json-comp --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
|
/benchmark -f morojs -t async --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
Benchmark ResultsFramework:
Composite score, main against this run, in the board's default view:
The results on main were measured in another session, so a few points up or down can be the machine and not the change. Full log |
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.
|
/benchmark -f morojs -t gateway-h3 --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
|
/benchmark -f morojs -t production-stack --save |
|
/benchmark -f morojs -t gateway-64 --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
1 similar comment
|
👋 Benchmark request received. A collaborator will review and approve the run. |
… 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).
Benchmark ResultsFramework:
Composite score, main against this run, in the board's default view:
The results on main were measured in another session, so a few points up or down can be the machine and not the change. Full log |
…t upgrades The engine answers a valid WebSocket handshake itself and opens the socket (engine 1.1.15, MoroJS 1.8.17): no request and response objects, no native round trips for the headers, query and peer address, and no trip through JavaScript per upgrade, which is what echo-ws-limited measures with its ten frames per connection. The handshake stays readable on the socket on demand. Inside a read's batch each frame is now built straight into the corked write, and the TLS listeners keep their record buffers between replies instead of freeing and reallocating them per reply (json-tls, 8gbit, static-tls). Nothing in the entry's own code changes; the pins and the README do.
Benchmark ResultsFramework:
Composite score, main against this run, in the board's default view:
The results on main were measured in another session, so a few points up or down can be the machine and not the change. Full log |
|
/benchmark -f morojs --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
Benchmark ResultsFramework:
Composite score, main against this run, in the board's default view:
The results on main were measured in another session, so a few points up or down can be the machine and not the change. Full log |
… the engine's measured serve order json-comp: the first run on brotli 3 sent the bytes the score wants (1319 per reply) but cost 352K req/s against 483K on gzip 3, because every reply built and tore down a brotli encoder. MoroJS 1.8.19 keeps one brotli encoder per setting and resets it between bodies - on the Node 26 the image ships a reset keeps its quality - so a reply costs the compression alone: 30 us a body against 45 or more on these bodies. The same cost decided gateway-64, whose load negotiates brotli through nginx. The route keeps brotli 3 first, gzip 3 behind it. gateway-h3 at 256 connections and production-stack at 256 did not move with the listen backlog: at par at 64 connections, a quarter behind at 256, the server mostly idle. That is the shape the engine's design notes measured for its epoll serve order - grouped by peer CPU, which loses about a quarter at 256 connections to arrival order when every request enters JavaScript under a peer that oversubscribes its cores - and the engine ships a measured 'auto' mode for it. The three proxied stacks run the server with MORO_ENGINE_SERVE_ORDER=auto; the standalone container keeps the default, where the grouped order wins. @morojs/engine 1.1.16: wsSend() borrows its payload (the backpressure shed is queued to a later turn), ASCII text frames arrive as one-byte strings, and the TLS record buffers are released between records again, since keeping them moved nothing on the TLS profiles.
|
/benchmark -f morojs --save |
|
👋 Benchmark request received. A collaborator will review and approve the run. |
Benchmark ResultsFramework:
Composite score, main against this run, in the board's default view:
The results on main were measured in another session, so a few points up or down can be the machine and not the change. Full log |
Description
PR Commands — comment on this PR to trigger (requires collaborator approval):
/benchmark -f <framework>/benchmark -f <framework> -t <test>/benchmark -f <framework> --save/benchmark -f <framework> -t <test> --save/benchmark -f <framework> --compare <other>/benchmark-multiple -f <fw1>,<fw2>,...-tand--savetoo; saved results land in a single commit/benchmark-multiple --save-fneeded: benchmark and save every framework the PR touches/benchmark-test -t <test><test>and save the resultsFor
/benchmark, always specify-f <framework>; the flags combine in any order. Results come back as a comment with a per-profile table of RPS, p99, CPU and memory — one table per framework on multi runs. A new benchmark comment while a run is in flight queues behind it (one deep) instead of cancelling it. For multi-framework PRs (dependency bumps, same-language refactors) prefer/benchmark-multiple, which runs everything in a single job and commits all saved results together, so no run overwrites another.--compareworks on single-framework runs only.What the deltas are measured against. By default, this framework's own results published on
main- answering "did this change help?". When you are tuning a variant or a successor entry,--comparere-bases them on another entry instead:The reply states which baseline it used, and profiles the other framework does not run show
n/arather than a delta.Run benchmarks locally
You can validate and benchmark your framework locally with the lite script — no CPU pinning, fixed connection counts, all load generators run in Docker.
Requirements: Docker Engine on Linux. Load generators (gcannon, h2load, h2load-h3, wrk) are built as self-contained Docker images on first run.