From 925762344cc64f77474240d0101849e218b2fe69 Mon Sep 17 00:00:00 2001 From: Antruly Date: Thu, 15 Oct 2026 13:47:09 +0800 Subject: [PATCH 01/17] Write /async-db with the framework's writer instead of a DOM per row `build_async_db_json` is still there and still answers whenever the writer declines; what changes is which one is on the hot path. The handler now walks the rows through `uvcpp_json_writer` -- the streaming constructor `/json` has used since af2807d -- reading the nine columns by position rather than by name, and copying the `tags` cell's JSONB literal through instead of parsing it into a DOM and dumping it back out. Same items, same DOM, same work per request; what changes is that a tree and a throwaway `dump()` string stop existing. Criterion, measured on this entry before writing this commit: server user instructions per request, callgrind, this entry under `taskset -c 0-8 valgrind --tool=callgrind`, 2000 requests at `-c 32 -m 1` against `/async-db?min=10&max=50&limit=20`, an idle leg subtracted: dom 1,563,529,631 Ir = 781,765 Ir/req writer 687,329,150 Ir = 343,665 Ir/req -56.04% At ~0.335 ns per instruction that is 262 us -> 115 us of user CPU per request. The two largest single items that leave the profile: `parse_tags` + `dump()` for the tags column (13.8% of the profile's client CPU) and the 200 column name lookups per response (12.4%); `dump_escaped` alone was 10.12% of the DOM path's self cost. Both numbers were measured before this commit, on the same rig. Determinism rather than a sample: the line `libuvcpp: /async-db answers from the writer path` is in the server log of the measured leg, which means the writer reproduced the reference bytes on the first non-empty response and answered every request after it -- 2000/2000 2xx, 0 failed. Re-asking the same query returns bytes identical to the first response, and the committed file was re-run end to end after the last edit. Why the bytes do not move, independent of the proof: the writer may answer only after `async_db_json` has compared its output against `build_async_db_json`'s on a live response, and a single difference keeps the entry on the DOM path for the life of the process -- the same shape af2807d put in front of `/json`. The `tags` walker is the one place where the argument is stronger than "the responses we sampled agree": it accepts only a flat array of strings whose bytes are all printable ASCII other than `"` and `\`, which is exactly the set both serializers copy through unchanged (RFC 8259 escapes only `"`, `\` and the bytes below 0x20), and it declines everything else -- a number, a nested value, an escape, a byte >= 0x80 -- to the DOM path, where `parse_tags`' "a value that will not parse becomes []" rule lives. An empty result set proves nothing and does not set the flag, so the proof runs on the first response that actually carries a row. The pin does not move: `uvcpp_db_row::operator[](size_t)` and `uvcpp_json_writer` are both in 6eede33 (v1.7.0), so this is an entry-side change only and needs no library release. Scope: the async-db profile is Reference-only, so this is that profile's `cpu_per_req_us` column and not the composite. The measured half is the user-mode half; the platform's number also carries system time, which this change does not touch. --- frameworks/libuvcpp/main.cc | 189 +++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/frameworks/libuvcpp/main.cc b/frameworks/libuvcpp/main.cc index f9a0ef81c..67c5bd109 100644 --- a/frameworks/libuvcpp/main.cc +++ b/frameworks/libuvcpp/main.cc @@ -939,6 +939,193 @@ std::string build_async_db_json(uvcpp_db_status status, return payload.dump(); } +/// Writes one `tags` cell straight into the writer. +/// +/// `tags` is JSONB and the driver hands JSONB over as text, so the column +/// already arrives as the array literal (`["fast", "new"]`). `parse_tags` above +/// re-parses that into a DOM and the serializer dumps the DOM back out -- a lexer +/// pass and a tree per row, 13.8% of this profile's client CPU under callgrind, +/// to recover bytes the string is already holding. +/// +/// So this walks the literal instead, and normalises the whitespace Postgres +/// puts after the commas, which is exactly what `parse` + `dump()` produce. It +/// copies through only what it can reproduce byte for byte: a flat array of +/// strings whose bytes are all printable ASCII (`0x20`-`0x7E`) other than `"` and +/// `\`, which cannot appear inside one. Every other shape is a decline, and a +/// decline puts the whole response back on the DOM path above, where +/// `parse_tags`' "a value that will not parse becomes `[]`" rule lives: +/// +/// - a number would come back re-rendered from a double (`1.50` -> `1.5`), +/// - `true`/`false`/`null` and a nested array or object would need the tree, +/// - a `\"` or a `\uXXXX` would be re-encoded by `dump()`'s escaper, +/// - a byte outside printable ASCII is where the two serializers may part +/// ways: `dump()` validates and repairs UTF-8, and what a byte `>= 0x80` +/// means depends on the database's encoding, so those bytes are not copied +/// through on a `==` argument, +/// - a literal that is not a well-formed array of strings is not JSON at all. +/// +/// The printable-ASCII rule is what makes the byte identity a property of the +/// accepted set rather than of the responses the proof below happened to sample. +/// RFC 8259 requires escaping only `"`, `\` and the bytes below `0x20`, and the +/// walker admits none of those three: `"` ends the string, `\` is a decline, and +/// the range check leaves the controls out. The library's serializer escapes +/// exactly that set and copies `0x80` upwards through untouched (`uvcpp_json_writer`'s +/// escape table says so in as many words), and `dump()` follows the same rule, so +/// every accepted byte reaches both outputs unchanged. +bool async_db_tags(uvcpp_json_writer& w, const std::string& text) { + const char* p = text.data(); + const size_t n = text.size(); + // A NULL cell and an empty string both read back as "" here, and both parse + // to the empty array -- which is what `parse_tags` answers for either. + if (n == 0) { + return w.array_begin() == json_write_status::OK && + w.array_end() == json_write_status::OK; + } + if (n < 2 || p[0] != '[' || p[n - 1] != ']') return false; + if (w.array_begin() != json_write_status::OK) return false; + const size_t end = n - 1; // the closing `]` + size_t i = 1; // past the opening `[` + bool first = true; + for (;;) { + while (i < end && (p[i] == ' ' || p[i] == '\t' || p[i] == '\n' || + p[i] == '\r')) { + ++i; + } + if (i == end) break; // the `]`, and nothing but whitespace before it + if (!first) { + if (p[i] != ',') return false; + ++i; + while (i < end && (p[i] == ' ' || p[i] == '\t' || p[i] == '\n' || + p[i] == '\r')) { + ++i; + } + if (i == end) return false; // a trailing comma + } + if (p[i] != '"') return false; + size_t j = i + 1; + while (j < end && p[j] != '"') { + if (p[j] == '\\') return false; + const unsigned char c = static_cast(p[j]); + if (c < 0x20 || c > 0x7E) return false; + ++j; + } + if (j == end) return false; // unterminated + if (w.value(p + i + 1, j - i - 1) != json_write_status::OK) return false; + i = j + 1; + first = false; + } + return w.array_end() == json_write_status::OK; +} + +/// The same bytes `build_async_db_json` produces, written straight through +/// `uvcpp_json_writer`: no DOM for the rows and no throwaway `dump()` string. +/// +/// The keys come out in the order nlohmann's `std::map` sorts them into, which +/// is the order `dump()` emits and the order `flat_write_item` hard-codes for +/// `/json` -- `active`, `category`, `id`, `name`, `price`, `quantity`, `rating`, +/// `tags`, with `count` before `score` inside `rating` and `count` before +/// `items` at the top. +/// +/// The columns are read by position, not by name: the SELECT above is a fixed +/// list, and a name lookup is a linear scan of the column names, which at 200 +/// lookups per response was 12.4% of this profile's client CPU. A result whose +/// columns are not that SELECT is caught by the proof in `async_db_json` below, +/// which compares these bytes against the name-based path's before this one is +/// allowed to answer. +/// +/// An empty return means "declined" and `async_db_json` re-runs the DOM path. A +/// built response is never empty -- it always opens with `{"count":`. +std::string build_async_db_json_writer(uvcpp_db_status status, + const uvcpp_db_table& table) { + const std::string declined; + if (!uvcpp_db_ok(status)) return declined; + const std::vector& rows = table.rows(); + std::string out; + out.reserve(rows.size() * 224u + 32u); + uvcpp_json_writer w(out); + // The root object first: `key()` is `MISUSE` outside an object, so a key at + // depth 0 fails the writer for good. `build_json_flat` opens its object the + // same way for the same reason. + if (w.object_begin() != json_write_status::OK) return declined; + if (!flat_key(w, "count")) return declined; + if (w.value(static_cast(rows.size())) != json_write_status::OK) { + return declined; + } + if (!flat_key(w, "items")) return declined; + if (w.array_begin() != json_write_status::OK) return declined; + for (size_t i = 0; i < rows.size(); ++i) { + const uvcpp_db_row& row = rows[i]; + if (w.object_begin() != json_write_status::OK) return declined; + if (!flat_key(w, "active")) return declined; + if (w.value(row[5].to_bool()) != json_write_status::OK) return declined; + if (!flat_key(w, "category")) return declined; + if (w.value(row[2].to_text()) != json_write_status::OK) return declined; + if (!flat_key(w, "id")) return declined; + if (w.value(row[0].to_int64()) != json_write_status::OK) return declined; + if (!flat_key(w, "name")) return declined; + if (w.value(row[1].to_text()) != json_write_status::OK) return declined; + if (!flat_key(w, "price")) return declined; + if (w.value(row[3].to_int64()) != json_write_status::OK) return declined; + if (!flat_key(w, "quantity")) return declined; + if (w.value(row[4].to_int64()) != json_write_status::OK) return declined; + if (!flat_key(w, "rating")) return declined; + if (w.object_begin() != json_write_status::OK) return declined; + if (!flat_key(w, "count")) return declined; + if (w.value(row[8].to_int64()) != json_write_status::OK) return declined; + if (!flat_key(w, "score")) return declined; + if (w.value(row[7].to_int64()) != json_write_status::OK) return declined; + if (w.object_end() != json_write_status::OK) return declined; + if (!flat_key(w, "tags")) return declined; + if (!async_db_tags(w, row[6].to_text())) return declined; + if (w.object_end() != json_write_status::OK) return declined; + } + if (w.array_end() != json_write_status::OK) return declined; + if (w.object_end() != json_write_status::OK) return declined; + if (w.finish() != json_write_status::OK) return declined; + return out; +} + +/// Whether the writer path has been shown to reproduce the reference path's +/// bytes: `-1` is "not proven yet", and while it is unproven the reference path +/// answers. The writer path is never served unproven. +std::atomic g_async_json_ok(-1); + +/// The body `handle_async_db` sends. +/// +/// The proof is the point, and it is the same one `/json` runs before its +/// flattened path is allowed on: the writer path may answer only after it has +/// produced the reference path's bytes on a live response. It runs on the first +/// response that carries at least one row, because every shape that could +/// differ -- a numeric or nested `tags`, an escaped string, columns in another +/// order -- is a row shape, so an empty result set proves nothing and does not +/// count. One extra DOM build per process. After that the flag decides, and the +/// DOM is reached again only when the writer declines a response. +std::string async_db_json(uvcpp_db_status status, const uvcpp_db_table& table) { + const int proven = g_async_json_ok.load(std::memory_order_acquire); + if (proven < 0 && uvcpp_db_ok(status) && !table.rows().empty()) { + std::string fast = build_async_db_json_writer(status, table); + const std::string dom = build_async_db_json(status, table); + if (!fast.empty() && fast == dom) { + g_async_json_ok.store(1, std::memory_order_release); + std::fprintf(stderr, "libuvcpp: /async-db answers from the writer path\n"); + return fast; + } + g_async_json_ok.store(0, std::memory_order_release); + if (!fast.empty()) { + std::fprintf(stderr, + "libuvcpp: the writer path does not reproduce the reference " + "bytes for /async-db -- /async-db stays on the reference " + "path\n"); + } + return dom; + } + if (proven > 0) { + std::string fast = build_async_db_json_writer(status, table); + if (!fast.empty()) return fast; + } + return build_async_db_json(status, table); +} + /// The retry half of init_database(): keep trying to open the pool, and give up /// for good once the budget is spent, so a database that is simply not there /// does not leave a thread working for the life of the run. @@ -1094,7 +1281,7 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, const int rc = g_db.query( app->loop(), kAsyncDbSql, params, [rp, next](uvcpp_db_status status, const uvcpp_db_table& table) { - rp->json_str(build_async_db_json(status, table)); + rp->json_str(async_db_json(status, table)); rp->end(); next(); }); From a19557f49736234492a62a80e23f276af0a83a92 Mon Sep 17 00:00:00 2001 From: zhuweiye Date: Thu, 15 Oct 2026 18:53:04 +0800 Subject: [PATCH 02/17] Move the pin to 8150720 (1.7.2-dev) and correct the version claims `8150720` is the library's `master` after the `v1.7.0` release plus two commits. Both were measured locally against the previous pin, and the criteria are deterministic counters rather than board readings: * `ebeaa20` -- `-1.90%` user-mode instructions per request on the h1 shape (22,459.6 -> 22,032.4 Ir, callgrind). The path normalisation collapsed into one function with a fast path for the already-normal form, so the routing table, the streaming table and the WebSocket table cannot disagree about what path they are looking at. `-1.90%` of the h1 profiles' user-mode time is about `0.35%` of their end-to-end time, which is smaller than the spread between two platform readings of the same profile. * `8150720` -- `204.78 -> 102.77` `malloc`s per response on the async-db shape. The result-set vectors now reserve, which `PQntuples`/`PQnfields` let them do exactly, so the 1-2-4-8 growth of 20 rows x 9 cells is gone. `async-db` is Reference-only: this one lands in its `cpu_per_req_us` (user plus system, and the change is on the user side) and nowhere else. So neither is offered as a predicted board movement -- both are accounted for as "the counter certainly went down". The Dockerfile comment says so where it records the pin history, along with what the distance from `v1.7.0` to this pin actually is: one documentation commit plus these two. The rest of the range from `6eede33` (the previous pin) to here is the `v1.7.0` release work -- build system, packaging, release docs -- which is already justified in the block above and changes no code on this entry's paths. Two things in the entry README were true when the pin was `v1.6.1` and are not true now, so they are corrected rather than left: * "libuvcpp 1.6.x" is `1.7.x`. * the reason given for building the library from source was that the release package has both remote database backends off. At `v1.6.1` that was right -- all six legs of the release workflow passed `-DUVCPP_ENABLE_DB_MYSQL=OFF -DUVCPP_ENABLE_DB_PGSQL=OFF`. At `1.7.0` all six pass `=ON` with `-DUVCPP_DB_RUNTIME_LOAD=ON`, which is why the same `ldd` allow-list still passes: the backends are there, loaded at run time. The reason now is that the pin is a development revision and a release package exists only for a tag. The `/async-db` section also described `tags` as re-parsed into a DOM and dumped back out. The entry no longer does that (`9257623`, the commit below): it walks the array literal and copies the strings through, declining anything it cannot reproduce byte for byte and falling back to the DOM path for that response. The section now says that, and says that the writer path is proved against the reference bytes on the first non-empty response before it is allowed to answer. --- frameworks/libuvcpp/Dockerfile | 50 ++++++++++++++++++++++++++-------- frameworks/libuvcpp/README.md | 40 +++++++++++++++++++++------ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index 0060c5f8c..2febd8e5a 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -37,12 +37,33 @@ ENV DEBIAN_FRONTEND=noninteractive # and the bytes under it re-uploaded, so a SHA is the only thing that fixes # which revision gets built. `3fdd6208` was `v1.6.1` -- what the first version # of this entry built, and what the platform measured as the board's 58 req/s on -# `baseline-h3-64`. The pin has since moved twice: to `2d5fe74` (`1.6.20-dev`), -# and then to `6eede33` (`1.6.51-dev`), which was the tip of the library's -# `master` when it was taken and is the revision the platform's fourth run -# measured. Each move was measured against the previous pin before being taken; -# the criteria for the `6eede33` batch are further down this file, and each -# commit's own criterion is in the library's `CHANGELOG.md`. +# `baseline-h3-64`. The pin has since moved three times: to `2d5fe74` +# (`1.6.20-dev`), then to `6eede33` (`1.6.51-dev`), which was the tip of the +# library's `master` when it was taken and is the revision the platform's fourth +# run measured, and now to `8150720` (`1.7.2-dev`), which is the `v1.7.0` tag +# plus one documentation commit and two that change code. +# Each move was measured against the previous pin before being taken; the +# criteria for the `6eede33` batch are further down this file, and each commit's +# own criterion is in the library's `CHANGELOG.md`. +# +# The `8150720` move is the one batch here whose criterion is *not* an on-board +# measurement. Both commits carry a local deterministic counter as their +# criterion -- `ebeaa20` is `-1.90%` user-mode instructions per request on the +# h1 shape (22,459.6 -> 22,032.4 Ir, callgrind, the path normalisation +# collapsing to one function with a fast path for the already-normal form), and +# `8150720` is `204.78 -> 102.77` `malloc`s per response on the async-db shape +# (the result-set vectors now reserve, which `PQntuples`/`PQnfields` let them do +# exactly). Neither is offered as a predicted board movement. The h1 profiles +# are all `pipeline = 1`, so `-1.90%` of user-mode instructions is about `0.35%` +# of the end-to-end time on those profiles, which is smaller than the spread +# between two platform readings of the same profile; and `async-db` is +# Reference-only, contributing nothing to the board's score and showing up only +# in its own `cpu_per_req_us`, which is user plus system time where the change +# is on the user side. So both are accounted for as "the counter certainly went +# down", not as a score, and what this revision is for is the fifth run, whose +# numbers will say whether either is visible from outside. The library-side +# criteria and the guards (byte-identical response bodies, the postgres suite +# byte-identical across arms, 127/127 ctest) are in its `CHANGELOG.md`. # # `6eede33` is also what the library's `v1.7.0` release carries, in the part # that decides behaviour: `git diff 6eede33 v1.7.0 -- src/ ':!src/db'` is one @@ -53,11 +74,16 @@ ENV DEBIAN_FRONTEND=noninteractive # the range is packaging, build system and CI, and only one piece of that # reaches the ELF build: an `--exclude-libs,ALL` link option that hides symbols # arriving from static archives and leaves the library's own API exported -- it -# changes the dynamic symbol table, not the code. So this entry's numbers on the -# board are the release's numbers. The pin stays on the commit rather than -# moving to the tag because moving it would put the entry's source a revision -# away from the revision the saved results came from, for a library that is -- -# where it counts -- the same one. +# changes the dynamic symbol table, not the code. So the fourth run's numbers +# are `v1.7.0`'s numbers, and the distance from the tag to this pin is small and +# fully accounted for: `git diff --stat v1.7.0 8150720 -- src/ ':!src/db'` is +# the version macros plus four files under `src/webapp/` -- all of them +# `ebeaa20`, the path normalisation -- and `git diff v1.7.0 8150720 -- src/db/` +# is `8150720`, the result-set reservation. The third commit in the range, +# `d9581d9`, is the download table in `README.md`: documentation only. The pin +# stays on the commit rather than moving to the tag because the two behavioural +# commits have no tag -- they are unreleased, and they are the whole reason this +# revision is here. # # The tip is a revision whose CI is green on all four platforms -- Linux, # macOS, MSVC and MinGW-w64 -- which is worth commits that do not otherwise @@ -201,7 +227,7 @@ ENV DEBIAN_FRONTEND=noninteractive # backend and a brotli encoder -- so the platform's flag set above builds neither # and the binary this Dockerfile produces is unaffected by them. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git -ARG LIBUVCPP_REF=6eede33a0662631dcf741989e242b7028392cc12 +ARG LIBUVCPP_REF=81507207a4ad496a66019a40e3993dcf34ba275f ARG OPENSSL_VERSION=3.5.0 diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index 3494fe479..7d6663a45 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -5,16 +5,24 @@ libuvcpp on the libuv event loop, one loop per available core. ## Stack - **Language:** C++11 -- **Library:** libuvcpp 1.6.x ([repo](https://github.com/Antruly/libuvcpp)), +- **Library:** libuvcpp 1.7.x ([repo](https://github.com/Antruly/libuvcpp)), built from source in the image - **Build:** `cmake -DCMAKE_BUILD_TYPE=Release` on `ubuntu:24.04`, with libuv, llhttp, zlib, nghttp2, ngtcp2, nghttp3 and OpenSSL 3.5 all built statically into `libuvcpp.so`; the entry is `g++ -std=c++11 -O2 -DNDEBUG` -The library is built from source rather than installed from the published -`libuvcpp-1.6.x-linux-x64` release package, because that package is built with -both database backends off — the release workflow asserts it with an `ldd` check -that fails if `libpq.so` appears — and `/async-db` needs the Postgres backend. +The library is built from source rather than installed from a published +`libuvcpp--linux-x64` release package. That package was the only +alternative when this entry pinned `v1.6.1`, and at that revision all six of the +release workflow's legs passed `-DUVCPP_ENABLE_DB_MYSQL=OFF +-DUVCPP_ENABLE_DB_PGSQL=OFF` — the workflow asserts it with an `ldd` check that +fails if `libpq.so` appears — while `/async-db` needs the Postgres backend. The +pin has since moved onto development revisions: the `v1.7.0` tag is the nearest +release, the pin is three commits past it, and a release package exists only for +a tag, so building the pinned SHA from source is the only way to build exactly +what the pin names. The `1.7.0` packages do carry both remote backends, behind +the runtime-load seam that is why the same `ldd` list still passes, so the +reason is now "the pin is not a release" rather than "the release cannot do it". Everything else about the build follows that same release recipe. The pinned revision installs `include/zlib.h` and `include/zconf.h` along with the library, which matters because `web/uvcpp_ws_parser.h` includes `` whenever @@ -204,10 +212,24 @@ the profile forbids the framework from doing the JWT verification itself. between dialects, on purpose — `?` is a legal operator in Postgres for jsonb and hstore, so a translation layer could not tell the two apart. This is the entry's only Postgres-specific line. -- **`tags` is re-parsed.** Postgres returns JSONB as text (OID 3802 is not one - of the driver's mapped types, and an unmapped OID degrades to the string libpq - returned), and that string *is* the JSON array — so it is parsed back into an - array rather than spliced in as a quoted string. +- **`tags` is walked, not re-parsed.** Postgres returns JSONB as text (OID 3802 + is not one of the driver's mapped types, and an unmapped OID degrades to the + string libpq returned), and that string *is* the JSON array — so it goes into + the response as an array rather than as a quoted string. The response is built + with the framework's `uvcpp_json_writer`, and for `tags` that means walking the + literal and copying the strings it holds, normalising the whitespace Postgres + puts after the commas — the bytes `nlohmann::json::parse` followed by `dump()` + produced. The walk declines anything it cannot reproduce byte for byte: a + number (which `dump()` would re-render from a double), `true`/`false`/`null`, a + nested value, a `\"` or `\uXXXX` escape, or a byte outside printable ASCII + (where the two serializers may part ways). A decline puts the whole response + back on the DOM path, which is also where `parse_tags`' rule for a value that + will not parse — an empty array — still lives. +- **The writer path is proved before it answers.** The first response that + carries at least one row is built both ways and the writer's bytes must equal + the reference path's before the writer is allowed to answer with them; until + then, and after any decline, the reference path answers. One extra DOM build + per process, and the server logs which path took over. - **Failure is not fatal and not silent-in-the-wrong-way.** With no `DATABASE_URL`, or a pool that will not open, `/async-db` answers the documented `{"items":[],"count":0}` and the other profiles serve normally. From 6fadb862e3111dd6b12f9d82df2ac623c4538f78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Oct 2026 14:21:54 +0000 Subject: [PATCH 03/17] Benchmark results: libuvcpp [skip ci] --- site/data/frameworks.json | 2 +- site/data/results/libuvcpp.json | 694 ++++++++++---------- site/static/logs/async-db/1024/libuvcpp.log | 1 + 3 files changed, 349 insertions(+), 348 deletions(-) diff --git a/site/data/frameworks.json b/site/data/frameworks.json index e6dbe86eb..cefcafdd0 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -2568,4 +2568,4 @@ } ] } -} +} \ No newline at end of file diff --git a/site/data/results/libuvcpp.json b/site/data/results/libuvcpp.json index 3b50ff7dd..4357a11d9 100644 --- a/site/data/results/libuvcpp.json +++ b/site/data/results/libuvcpp.json @@ -4,44 +4,44 @@ "8gbit-512": { "framework": "libuvcpp", "language": "C++", - "rps": 49175, - "avg_latency": "103.6us", - "p99_latency": "152.0us", - "cpu": "319.8%", - "memory": "128MiB", + "rps": 49344, + "avg_latency": "108.1us", + "p99_latency": "148.0us", + "cpu": "299.4%", + "memory": "207MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "511.32MB/s", - "input_bw": "480.22MB/s", + "bandwidth": "513.08MB/s", + "input_bw": "481.88MB/s", "reconnects": 0, - "status_2xx": 245942, + "status_2xx": 246792, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 17414305, - "cpu_per_req_us": 70.8066, + "cpu_usec": 16241895, + "cpu_per_req_us": 65.8121, "target_rate": 50000, - "rate_ratio": 0.9835, - "p99_9_latency": "1656.0us" + "rate_ratio": 0.9869, + "p99_9_latency": "4694.0us" }, "async-32000": { "framework": "libuvcpp", "language": "C++", - "rps": 1513512, - "avg_latency": "20.61ms", - "p99_latency": "23.10ms", - "cpu": "6394.1%", - "memory": "490MiB", + "rps": 1568496, + "avg_latency": "19.84ms", + "p99_latency": "22.20ms", + "cpu": "6409.0%", + "memory": "488MiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "226.57MB/s", - "input_bw": "69.28MB/s", + "bandwidth": "234.80MB/s", + "input_bw": "71.80MB/s", "reconnects": 0, - "status_2xx": 15135122, + "status_2xx": 15684968, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -49,19 +49,19 @@ "async-db-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 83890, - "avg_latency": "11.77ms", - "p99_latency": "41.70ms", - "cpu": "3415.7%", - "memory": "192MiB", + "rps": 98855, + "avg_latency": "9.88ms", + "p99_latency": "37.70ms", + "cpu": "2687.4%", + "memory": "172MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "324.51MB/s", - "input_bw": "5.60MB/s", - "reconnects": 33227, - "status_2xx": 838902, + "bandwidth": "382.29MB/s", + "input_bw": "6.60MB/s", + "reconnects": 39257, + "status_2xx": 988551, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -69,19 +69,19 @@ "baseline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1639189, - "avg_latency": "2.50ms", - "p99_latency": "3.27ms", - "cpu": "6419.0%", + "rps": 1789003, + "avg_latency": "2.30ms", + "p99_latency": "4.86ms", + "cpu": "6320.1%", "memory": "139MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "245.37MB/s", - "input_bw": "126.62MB/s", + "bandwidth": "267.79MB/s", + "input_bw": "138.20MB/s", "reconnects": 0, - "status_2xx": 8195949, + "status_2xx": 8945017, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -89,18 +89,18 @@ "baseline-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 4688932, - "avg_latency": "20.62ms", - "p99_latency": "118.80ms", - "cpu": "6265.0%", - "memory": "178MiB", + "rps": 5247351, + "avg_latency": "18.57ms", + "p99_latency": "103.45ms", + "cpu": "6218.8%", + "memory": "174MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "122.22MB/s", + "bandwidth": "136.78MB/s", "reconnects": 0, - "status_2xx": 23726000, + "status_2xx": 26551600, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -108,18 +108,18 @@ "baseline-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 4498827, - "avg_latency": "6.10ms", - "p99_latency": "37.07ms", - "cpu": "6152.2%", - "memory": "111MiB", + "rps": 5552266, + "avg_latency": "4.82ms", + "p99_latency": "33.27ms", + "cpu": "6202.3%", + "memory": "96MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "116.55MB/s", + "bandwidth": "143.83MB/s", "reconnects": 0, - "status_2xx": 22629100, + "status_2xx": 27927900, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -127,18 +127,18 @@ "baseline-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 4668853, - "avg_latency": "21.21ms", - "p99_latency": "566.60ms", - "cpu": "6423.9%", - "memory": "164MiB", + "rps": 5465988, + "avg_latency": "18.17ms", + "p99_latency": "302.20ms", + "cpu": "6426.9%", + "memory": "182MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "121.70MB/s", + "bandwidth": "142.47MB/s", "reconnects": 0, - "status_2xx": 23624400, + "status_2xx": 27657900, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -146,18 +146,18 @@ "baseline-h2c-256": { "framework": "libuvcpp", "language": "C++", - "rps": 4850874, - "avg_latency": "5.80ms", - "p99_latency": "52.59ms", - "cpu": "6055.3%", - "memory": "82MiB", + "rps": 5752882, + "avg_latency": "4.68ms", + "p99_latency": "58.77ms", + "cpu": "6049.5%", + "memory": "83MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "125.67MB/s", + "bandwidth": "149.03MB/s", "reconnects": 0, - "status_2xx": 24399900, + "status_2xx": 28937000, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -165,18 +165,18 @@ "baseline-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 4702573, - "avg_latency": "81.85ms", - "p99_latency": "682.36ms", - "cpu": "6417.1%", - "memory": "811MiB", + "rps": 5268627, + "avg_latency": "72.83ms", + "p99_latency": "518.07ms", + "cpu": "6152.4%", + "memory": "790MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "123.42MB/s", + "bandwidth": "138.53MB/s", "reconnects": 0, - "status_2xx": 23936100, + "status_2xx": 26870000, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -184,18 +184,18 @@ "baseline-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 1167034, - "avg_latency": "4.46ms", - "p99_latency": "9.15ms", - "cpu": "4113.4%", - "memory": "238MiB", + "rps": 1422023, + "avg_latency": "3.30ms", + "p99_latency": "9.57ms", + "cpu": "4117.6%", + "memory": "195MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "15.56MB/s", + "bandwidth": "18.97MB/s", "reconnects": 0, - "status_2xx": 5846842, + "status_2xx": 7124338, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -203,18 +203,18 @@ "echo-ws-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 2516942, - "avg_latency": "6.08ms", - "p99_latency": "7.46ms", - "cpu": "6118.9%", - "memory": "338MiB", + "rps": 2577600, + "avg_latency": "5.91ms", + "p99_latency": "7.49ms", + "cpu": "6140.4%", + "memory": "336MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "17.20MB/s", + "bandwidth": "17.61MB/s", "reconnects": 0, - "status_2xx": 12584713, + "status_2xx": 12888002, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -222,18 +222,18 @@ "echo-ws-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 2844300, - "avg_latency": "1.44ms", - "p99_latency": "1.93ms", - "cpu": "6375.3%", - "memory": "130MiB", + "rps": 2884971, + "avg_latency": "1.42ms", + "p99_latency": "2.49ms", + "cpu": "6272.5%", + "memory": "134MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "19.04MB/s", + "bandwidth": "19.29MB/s", "reconnects": 0, - "status_2xx": 14221503, + "status_2xx": 14424856, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -241,18 +241,18 @@ "echo-ws-512": { "framework": "libuvcpp", "language": "C++", - "rps": 2751709, - "avg_latency": "185us", - "p99_latency": "458us", - "cpu": "6592.8%", - "memory": "79MiB", + "rps": 2833406, + "avg_latency": "180us", + "p99_latency": "436us", + "cpu": "6616.9%", + "memory": "75MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "18.37MB/s", + "bandwidth": "18.91MB/s", "reconnects": 0, - "status_2xx": 13758545, + "status_2xx": 14167030, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -260,18 +260,18 @@ "echo-ws-limited-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1777709, - "avg_latency": "375us", - "p99_latency": "792us", - "cpu": "6250.1%", - "memory": "97MiB", + "rps": 1767053, + "avg_latency": "382us", + "p99_latency": "820us", + "cpu": "6142.6%", + "memory": "93MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "33.73MB/s", - "reconnects": 889076, - "status_2xx": 8888547, + "bandwidth": "33.52MB/s", + "reconnects": 883828, + "status_2xx": 8835268, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -279,18 +279,18 @@ "echo-ws-limited-512": { "framework": "libuvcpp", "language": "C++", - "rps": 1550796, - "avg_latency": "211us", - "p99_latency": "620us", - "cpu": "5899.1%", - "memory": "90MiB", + "rps": 1600650, + "avg_latency": "205us", + "p99_latency": "618us", + "cpu": "5818.9%", + "memory": "89MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "29.43MB/s", - "reconnects": 775390, - "status_2xx": 7753981, + "bandwidth": "30.37MB/s", + "reconnects": 800296, + "status_2xx": 8003251, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -298,18 +298,18 @@ "echo-ws-pipeline-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 25179465, - "avg_latency": "9.83ms", - "p99_latency": "12.20ms", - "cpu": "6643.7%", - "memory": "357MiB", + "rps": 27866425, + "avg_latency": "8.15ms", + "p99_latency": "10.70ms", + "cpu": "6179.7%", + "memory": "350MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "168.45MB/s", + "bandwidth": "186.38MB/s", "reconnects": 0, - "status_2xx": 125897329, + "status_2xx": 139332128, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -317,18 +317,18 @@ "echo-ws-pipeline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 28938721, - "avg_latency": "2.26ms", - "p99_latency": "2.86ms", - "cpu": "6776.1%", - "memory": "138MiB", + "rps": 31035676, + "avg_latency": "2.10ms", + "p99_latency": "2.66ms", + "cpu": "6378.8%", + "memory": "134MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "193.18MB/s", + "bandwidth": "207.19MB/s", "reconnects": 0, - "status_2xx": 144693608, + "status_2xx": 155178384, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -336,18 +336,18 @@ "echo-ws-pipeline-512": { "framework": "libuvcpp", "language": "C++", - "rps": 30027149, - "avg_latency": "272us", - "p99_latency": "645us", - "cpu": "6641.8%", - "memory": "79MiB", + "rps": 31580057, + "avg_latency": "258us", + "p99_latency": "617us", + "cpu": "6664.2%", + "memory": "78MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "200.49MB/s", + "bandwidth": "210.87MB/s", "reconnects": 0, - "status_2xx": 150135747, + "status_2xx": 157900286, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -355,115 +355,115 @@ "gateway-64-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 68655, - "avg_latency": "420.44ms", - "p99_latency": "4.61s", - "cpu": "3722.8%", - "memory": "4.8GiB", + "rps": 68878, + "avg_latency": "420.25ms", + "p99_latency": "4.31s", + "cpu": "3628.5%", + "memory": "4.9GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "780.52MB/s", + "bandwidth": "784.76MB/s", "reconnects": 0, - "status_2xx": 346709, + "status_2xx": 347836, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 104012, - "tpl_baseline": 69341, - "tpl_json": 121348, - "tpl_async_db": 52006, - "cpu_breakdown": "server: 785% 354MiB | proxy: 2938% 4.5GiB" + "tpl_static": 104350, + "tpl_baseline": 69567, + "tpl_json": 121742, + "tpl_async_db": 52175, + "cpu_breakdown": "server: 743% 406MiB | proxy: 2885% 4.6GiB" }, "gateway-64-512": { "framework": "libuvcpp", "language": "C++", - "rps": 75887, - "avg_latency": "200.90ms", - "p99_latency": "2.15s", - "cpu": "4048.1%", - "memory": "2.5GiB", + "rps": 77207, + "avg_latency": "194.87ms", + "p99_latency": "2.14s", + "cpu": "3824.5%", + "memory": "2.7GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "821.96MB/s", + "bandwidth": "837.34MB/s", "reconnects": 0, - "status_2xx": 383234, + "status_2xx": 389124, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 114970, - "tpl_baseline": 76646, - "tpl_json": 134131, - "tpl_async_db": 57485, - "cpu_breakdown": "server: 962% 210MiB | proxy: 3086% 2.3GiB" + "tpl_static": 116737, + "tpl_baseline": 77824, + "tpl_json": 136193, + "tpl_async_db": 58368, + "cpu_breakdown": "server: 749% 220MiB | proxy: 3076% 2.5GiB" }, "gateway-h3-256": { "framework": "libuvcpp", "language": "C++", - "rps": 62547, - "avg_latency": "126.95ms", - "p99_latency": "783.31ms", - "cpu": "2859.1%", - "memory": "1.5GiB", + "rps": 62335, + "avg_latency": "127.17ms", + "p99_latency": "774.90ms", + "cpu": "2751.7%", + "memory": "1.4GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "631.56MB/s", + "bandwidth": "629.21MB/s", "reconnects": 0, - "status_2xx": 314616, + "status_2xx": 313546, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 94384, - "tpl_baseline": 62923, - "tpl_json": 110115, - "tpl_async_db": 47192, - "cpu_breakdown": "server: 755% 119MiB | proxy: 2104% 1.4GiB" + "tpl_static": 94063, + "tpl_baseline": 62709, + "tpl_json": 109741, + "tpl_async_db": 47031, + "cpu_breakdown": "server: 567% 96MiB | proxy: 2185% 1.3GiB" }, "gateway-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 84167, - "avg_latency": "24.18ms", - "p99_latency": "85.33ms", - "cpu": "3685.2%", - "memory": "390MiB", + "rps": 84432, + "avg_latency": "24.11ms", + "p99_latency": "83.80ms", + "cpu": "3445.3%", + "memory": "371MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "871.37MB/s", + "bandwidth": "874.20MB/s", "reconnects": 0, - "status_2xx": 421679, + "status_2xx": 423005, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 126503, - "tpl_baseline": 84335, - "tpl_json": 147587, - "tpl_async_db": 63251, - "cpu_breakdown": "server: 1018% 97MiB | proxy: 2667% 292MiB" + "tpl_static": 126901, + "tpl_baseline": 84601, + "tpl_json": 148051, + "tpl_async_db": 63450, + "cpu_breakdown": "server: 807% 78MiB | proxy: 2639% 293MiB" }, "json-comp-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 321152, - "avg_latency": "30.31ms", - "p99_latency": "648.00ms", - "cpu": "6091.7%", - "memory": "162MiB", + "rps": 330411, + "avg_latency": "29.91ms", + "p99_latency": "632.40ms", + "cpu": "6504.6%", + "memory": "141MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "459.04MB/s", - "input_bw": "23.89MB/s", - "reconnects": 63491, - "status_2xx": 1605764, + "bandwidth": "472.09MB/s", + "input_bw": "24.58MB/s", + "reconnects": 65344, + "status_2xx": 1652058, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -471,19 +471,19 @@ "json-comp-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 325476, - "avg_latency": "12.32ms", - "p99_latency": "283.50ms", - "cpu": "6476.7%", - "memory": "120MiB", + "rps": 335926, + "avg_latency": "11.93ms", + "p99_latency": "283.90ms", + "cpu": "6263.0%", + "memory": "107MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "465.12MB/s", - "input_bw": "24.21MB/s", - "reconnects": 64685, - "status_2xx": 1627384, + "bandwidth": "480.11MB/s", + "input_bw": "24.99MB/s", + "reconnects": 66863, + "status_2xx": 1679631, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -491,18 +491,18 @@ "json-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 1496312, - "avg_latency": "21.42ms", - "p99_latency": "509.96ms", - "cpu": "6387.4%", - "memory": "298MiB", + "rps": 1653574, + "avg_latency": "19.34ms", + "p99_latency": "404.75ms", + "cpu": "6364.1%", + "memory": "303MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "4.99GB/s", + "bandwidth": "5.52GB/s", "reconnects": 0, - "status_2xx": 7556377, + "status_2xx": 8367089, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -510,18 +510,18 @@ "json-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1486566, - "avg_latency": "85.21ms", - "p99_latency": "538.31ms", - "cpu": "6273.2%", - "memory": "983MiB", + "rps": 1580650, + "avg_latency": "80.18ms", + "p99_latency": "427.19ms", + "cpu": "6241.2%", + "memory": "976MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "4.97GB/s", + "bandwidth": "5.31GB/s", "reconnects": 0, - "status_2xx": 7551760, + "status_2xx": 8061320, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -529,18 +529,18 @@ "json-tls-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 672436, - "avg_latency": "6.03ms", - "p99_latency": "60.84ms", - "cpu": "6373.5%", - "memory": "396MiB", + "rps": 724360, + "avg_latency": "5.58ms", + "p99_latency": "63.30ms", + "cpu": "6332.4%", + "memory": "395MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.31GB", + "bandwidth": "2.48GB", "reconnects": 0, - "status_2xx": 3428958, + "status_2xx": 3693728, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -548,91 +548,91 @@ "latency-10k-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 9983, - "avg_latency": "78.9us", + "rps": 9981, + "avg_latency": "83.3us", "p99_latency": "121.0us", - "cpu": "38.5%", - "memory": "77MiB", + "cpu": "36.3%", + "memory": "78MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, "bandwidth": "1.56MB/s", "reconnects": 0, - "status_2xx": 199683, + "status_2xx": 199636, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 8090832, - "cpu_per_req_us": 40.5184, + "cpu_usec": 7524856, + "cpu_per_req_us": 37.6929, "target_rate": 10000, - "rate_ratio": 0.9983, - "p99_9_latency": "182.0us" + "rate_ratio": 0.9981, + "p99_9_latency": "1007.0us" }, "latency-1m-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 997940, - "avg_latency": "168.6us", - "p99_latency": "462.0us", - "cpu": "4538.1%", + "rps": 998040, + "avg_latency": "144.5us", + "p99_latency": "312.0us", + "cpu": "4324.7%", "memory": "81MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "155.68MB/s", + "bandwidth": "155.69MB/s", "reconnects": 0, - "status_2xx": 19961754, + "status_2xx": 19963925, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 835214855, - "cpu_per_req_us": 41.8408, + "cpu_usec": 793199863, + "cpu_per_req_us": 39.7317, "target_rate": 1000000, - "rate_ratio": 0.9979, - "p99_9_latency": "970.0us" + "rate_ratio": 0.998, + "p99_9_latency": "1319.0us" }, "latency-500k-8cpu-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 245021, - "avg_latency": "5008517.3us", - "p99_latency": "10899456.0us", - "cpu": "775.7%", + "rps": 264277, + "avg_latency": "4654746.7us", + "p99_latency": "9973760.0us", + "cpu": "779.4%", "memory": "32MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "38.22MB/s", + "bandwidth": "41.23MB/s", "reconnects": 0, - "status_2xx": 4902305, + "status_2xx": 5287537, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 159611938, - "cpu_per_req_us": 32.5585, + "cpu_usec": 159636697, + "cpu_per_req_us": 30.1911, "target_rate": 500000, - "rate_ratio": 0.49, - "p99_9_latency": "11636736.0us" + "rate_ratio": 0.5286, + "p99_9_latency": "10342400.0us" }, "limited-conn-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1309891, - "avg_latency": "3.09ms", - "p99_latency": "49.20ms", - "cpu": "6642.4%", - "memory": "101MiB", + "rps": 1414762, + "avg_latency": "2.88ms", + "p99_latency": "45.70ms", + "cpu": "6336.9%", + "memory": "90MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "196.08MB/s", - "input_bw": "101.19MB/s", - "reconnects": 655325, - "status_2xx": 6549456, + "bandwidth": "211.77MB/s", + "input_bw": "109.29MB/s", + "reconnects": 707797, + "status_2xx": 7073811, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -640,18 +640,18 @@ "pipelined-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 6784375, - "avg_latency": "9.68ms", - "p99_latency": "12.30ms", - "cpu": "6360.9%", - "memory": "148MiB", + "rps": 7491589, + "avg_latency": "8.75ms", + "p99_latency": "10.50ms", + "cpu": "6391.6%", + "memory": "149MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "1015.54MB/s", + "bandwidth": "1.10GB/s", "reconnects": 0, - "status_2xx": 33921879, + "status_2xx": 37457948, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -659,66 +659,66 @@ "production-stack-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 46274, - "avg_latency": "346.24ms", - "p99_latency": "2.19s", - "cpu": "3301.1%", + "rps": 48440, + "avg_latency": "332.06ms", + "p99_latency": "1.43s", + "cpu": "3437.3%", "memory": "2.8GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "475.49MB/s", + "bandwidth": "494.29MB/s", "reconnects": 0, - "status_2xx": 234147, + "status_2xx": 245107, "status_3xx": 0, - "status_4xx": 5, + "status_4xx": 1, "status_5xx": 0, - "tpl_static": 70244, - "tpl_baseline": 23414, - "tpl_items": 117073, - "tpl_me": 23414, - "cpu_breakdown": "cache: 0% 5MiB | edge: 2788% 2.4GiB | authsvc: 277% 247MiB | server: 236% 172MiB" + "tpl_static": 73532, + "tpl_baseline": 24510, + "tpl_items": 122553, + "tpl_me": 24510, + "cpu_breakdown": "cache: 0% 4MiB | edge: 2871% 2.4GiB | authsvc: 269% 345MiB | server: 297% 161MiB" }, "production-stack-256": { "framework": "libuvcpp", "language": "C++", - "rps": 53685, - "avg_latency": "78.49ms", - "p99_latency": "436.48ms", - "cpu": "3165.2%", - "memory": "962MiB", + "rps": 54088, + "avg_latency": "78.06ms", + "p99_latency": "531.29ms", + "cpu": "3238.0%", + "memory": "928MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "515.93MB/s", + "bandwidth": "519.39MB/s", "reconnects": 0, - "status_2xx": 269502, + "status_2xx": 272064, "status_3xx": 0, - "status_4xx": 10, + "status_4xx": 2, "status_5xx": 0, - "tpl_static": 80850, - "tpl_baseline": 26950, - "tpl_items": 134751, - "tpl_me": 26950, - "cpu_breakdown": "cache: 0% 14MiB | edge: 2720% 812MiB | authsvc: 239% 60MiB | server: 206% 82MiB" + "tpl_static": 81619, + "tpl_baseline": 27206, + "tpl_items": 136032, + "tpl_me": 27206, + "cpu_breakdown": "cache: 0% 13MiB | edge: 2785% 792MiB | authsvc: 248% 58MiB | server: 204% 75MiB" }, "static-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 242380, - "avg_latency": "131.42ms", - "p99_latency": "473.88ms", - "cpu": "2463.2%", - "memory": "793MiB", + "rps": 252927, + "avg_latency": "122.01ms", + "p99_latency": "385.15ms", + "cpu": "2584.0%", + "memory": "805MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.67GB/s", + "bandwidth": "3.85GB/s", "reconnects": 0, - "status_2xx": 1221596, + "status_2xx": 1279815, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -726,18 +726,18 @@ "static-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 238807, - "avg_latency": "35.15ms", - "p99_latency": "171.99ms", - "cpu": "2420.0%", - "memory": "189MiB", + "rps": 232078, + "avg_latency": "36.58ms", + "p99_latency": "150.32ms", + "cpu": "2212.8%", + "memory": "191MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.65GB/s", + "bandwidth": "3.54GB/s", "reconnects": 0, - "status_2xx": 1201201, + "status_2xx": 1167355, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -745,18 +745,18 @@ "static-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 114206, - "avg_latency": "32.76ms", - "p99_latency": "146.38ms", - "cpu": "3085.9%", - "memory": "472MiB", + "rps": 122803, + "avg_latency": "30.41ms", + "p99_latency": "122.43ms", + "cpu": "3139.0%", + "memory": "788MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.74GB/s", + "bandwidth": "1.87GB/s", "reconnects": 0, - "status_2xx": 572175, + "status_2xx": 615248, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -764,18 +764,18 @@ "static-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 190096, - "avg_latency": "5.36ms", - "p99_latency": "22.78ms", - "cpu": "3118.3%", - "memory": "278MiB", + "rps": 190070, + "avg_latency": "5.38ms", + "p99_latency": "20.49ms", + "cpu": "2930.6%", + "memory": "277MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, "bandwidth": "2.93GB", "reconnects": 0, - "status_2xx": 969434, + "status_2xx": 965990, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -783,18 +783,18 @@ "unary-grpc-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3263606, - "avg_latency": "30.29ms", - "p99_latency": "193.63ms", - "cpu": "6435.7%", - "memory": "412MiB", + "rps": 3760124, + "avg_latency": "26.25ms", + "p99_latency": "268.10ms", + "cpu": "6440.4%", + "memory": "413MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "135.12MB/s", + "bandwidth": "156.01MB/s", "reconnects": 0, - "status_2xx": 16481213, + "status_2xx": 19026230, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -802,18 +802,18 @@ "unary-grpc-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3391572, - "avg_latency": "7.84ms", - "p99_latency": "118.92ms", - "cpu": "6123.1%", - "memory": "145MiB", + "rps": 3818427, + "avg_latency": "7.14ms", + "p99_latency": "113.42ms", + "cpu": "6042.7%", + "memory": "146MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "139.92MB/s", + "bandwidth": "157.22MB/s", "reconnects": 0, - "status_2xx": 17059609, + "status_2xx": 19168506, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -821,18 +821,18 @@ "unary-grpc-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3092506, - "avg_latency": "31.49ms", - "p99_latency": "154.43ms", - "cpu": "6191.1%", + "rps": 3511060, + "avg_latency": "28.50ms", + "p99_latency": "260.84ms", + "cpu": "5982.0%", "memory": "598MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "128.28MB/s", + "bandwidth": "145.67MB/s", "reconnects": 0, - "status_2xx": 15648081, + "status_2xx": 17765966, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -840,18 +840,18 @@ "unary-grpc-tls-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3286145, - "avg_latency": "8.02ms", - "p99_latency": "77.81ms", - "cpu": "6041.9%", - "memory": "165MiB", + "rps": 3720594, + "avg_latency": "7.12ms", + "p99_latency": "97.65ms", + "cpu": "6087.4%", + "memory": "163MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "135.57MB/s", + "bandwidth": "153.50MB/s", "reconnects": 0, - "status_2xx": 16529310, + "status_2xx": 18714592, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/async-db/1024/libuvcpp.log b/site/static/logs/async-db/1024/libuvcpp.log index a31c1a85c..a31abbcc5 100644 --- a/site/static/logs/async-db/1024/libuvcpp.log +++ b/site/static/logs/async-db/1024/libuvcpp.log @@ -1,2 +1,3 @@ libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) libuvcpp: Postgres pool up (max=256) +libuvcpp: /async-db answers from the writer path From c1be8fa80aa1ae42e9d19d4499ff42b0f1f657c3 Mon Sep 17 00:00:00 2001 From: Antruly Date: Thu, 15 Oct 2026 22:12:04 +0800 Subject: [PATCH 04/17] Build the brotli encoder into the entry `json-compressed` is the one profile in this suite scored on bytes -- `effectiveRps = rps * (minBpr / myBpr)^2` -- and it is the one profile whose request asks for both encoders (`Accept-Encoding: gzip, br`, byte for byte in `requests/json-gzip-*.raw`). The library has carried a brotli encoder since 1.7.0 behind `UVCPP_ENABLE_BROTLI`, default OFF, so until now that request was answered with gzip and the entry competed on the byte term with a compressor it had left on the bench. This is the framework's own response-compression middleware, not a tuned library: the same `uvcpp_http_server` filter, the same per-request `Accept-Encoding` negotiation, at the library's own default quality. That is what keeps it inside the entry's declared `mode: standard`, whose rule for this profile reads "Must use the framework standard JSON serialization and the framework or engine's built-in response compression (middleware, filter, or equivalent)" -- and the profile's own title names brotli. Nothing here picks an encoder or a level per profile: a client that asks for gzip alone still gets gzip, and a client that sends no `Accept-Encoding` still gets no `Content-Encoding`. What it is worth, from numbers this entry already has. The three json-comp bodies (4190/6707/8397 B) go from 970/1330/1567 B under gzip to 856/1182/1389 B under brotli q5 -- the encoder's own table in `web/uvcpp_http_compress.h`, whose default quality is the maximum of that table's score model -- and brotli's tie-break over gzip at equal `q` is the rank rule in `web/uvcpp_http_compress.cpp`. Over the profile's round-robin that is ~1142 B of body against ~1288 B, which against the measured bpr of 1429 (140 B of headers) is a byte term of (1429/1283)^2 ~ x1.24. The cost is ~+31.5 us per response, and json-comp runs at 101% of its CPU budget (6476.7% of 6400%), so it pays for that in requests per second: 199.0/230.5 ~ 0.86. Net ~x1.07 on both json-comp profiles. The blast radius is the other half of the argument, because the same flag also answers the h2 and h3 load generators, which ask for `br;q=1, gzip;q=0.8`. Those profiles are not CPU-bound -- gateway-64 sits at 3723% of budget, gateway-h3 at 3685%, production-stack at 3165%, static-h2 at 2463% -- so they absorb the compression without giving up requests, and the 7 `/json` and 3 `/async-db` URIs of gateway's 20-URI mix come back ~11% smaller. `json-tls`, `json-h2c`, `async-db` and the h1 baseline profiles send no `Accept-Encoding` at all. The static legs are unaffected by this flag in either direction: they serve the `.br` twins `data/static` already ships, and that selection is runtime, in `http_pick_precompressed_variants()`. Build side: `libbrotli-dev` in the build stage, `libbrotli1` in the runtime one. The encoder is the third shared library the build cannot absorb -- zlib and libuv are fetched and linked statically, this one is taken from the system -- so `libuvcpp.so` gains `libbrotlienc.so.1` and `libbrotlidec.so.1` in its `DT_NEEDED`, and without them the server does not start at all. On noble both files are `libbrotli1`; Debian's split names (`libbrotlienc1` and friends) are not packages here, which is the kind of mistake that only surfaces as a failed image build. The pin does not move: `UVCPP_ENABLE_BROTLI` and `k_brotli_quality_default` are both in 8150720 (1.7.2-dev), so this is an entry-side change only. --- frameworks/libuvcpp/Dockerfile | 53 +++++++++++++++++++++++++++++----- frameworks/libuvcpp/README.md | 17 +++++++---- frameworks/libuvcpp/meta.json | 2 +- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index 2febd8e5a..5e2dd9859 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -223,9 +223,30 @@ ENV DEBIAN_FRONTEND=noninteractive # LRU and stops copying the whole body on a hit; an h1 pipeline's final # `Connection: close` no longer takes the earlier responses with it; and the h2 # outbound body is held by reference rather than copied per response. Two optional -# modules arrive in the same range and both default to OFF -- a yyjson parse -# backend and a brotli encoder -- so the platform's flag set above builds neither -# and the binary this Dockerfile produces is unaffected by them. +# modules arrive in the same range and both default to OFF. A yyjson parse backend +# is one of them and stays off, so the flag set above builds it out. The brotli +# encoder is the other, and it is the one optional module this entry turns on. +# +# Why brotli is on. It is not a tuning knob -- it is the framework's own response +# compression middleware, reached only when the client asks for `br`, at the +# framework's own default quality (`k_brotli_quality_default`, which the header +# records as the maximum of its own score model). The profile that asks for it is +# `json-compressed`, whose guide puts "the framework or engine's built-in response +# compression" under `standard=`, names brotli in its own title, and whose request +# carries `Accept-Encoding: gzip, br`; equal `q` resolves to `br` by the rank rule in +# `web/uvcpp_http_compress.cpp`. Measured on the three json-comp bodies -- same +# entry binary, only the `.so` swapped -- gzip gives 970/1330/1567 B and `br` gives +# 856/1182/1389 B, which is the encoder's own table in `web/uvcpp_http_compress.h`. +# The cost lands where the profile pays for it: json-comp is the only profile scored +# on bytes (`rps * (minBpr/bpr)^2`) and it runs at 101% of its CPU budget, so ~+31.5 +# us per response buys ~11% fewer bytes -- about x1.07 on both json-comp profiles, +# net of the requests per second that the extra CPU costs. The profiles that merely +# pass `br` through -- gateway-64 at 58% of budget, gateway-h3 at 58%, +# production-stack at 49% -- have the CPU headroom to absorb the same compression and +# collect the smaller bodies for free. `json-tls`, `json-h2c`, `async-db` and the h1 +# baseline profiles send no `Accept-Encoding` at all, and the static legs already +# serve the `.br` twins shipped in `data/static` whether or not this flag is set: that +# selection is runtime, in `http_pick_precompressed_variants()`. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git ARG LIBUVCPP_REF=81507207a4ad496a66019a40e3993dcf34ba275f @@ -253,9 +274,16 @@ ARG OPENSSL_VERSION=3.5.0 # file or directory`. Confirmed against this pin rather than assumed: `g++ -H` # resolves `` to `/include/zlib.h`, and it keeps resolving there # with the package gone, since `-I` is searched before the system directories. +# `libbrotli-dev` is here for `-DUVCPP_ENABLE_BROTLI=ON` below, and only the build +# stage needs it: the library uses `` inside one `.cpp`, so no +# installed public header reaches for it and the consumer below compiles against +# `/opt/uvcpp/include` without it. The CMake side takes the system copy through +# pkg-config (`libbrotlienc` / `libbrotlidec`) and falls back to `find_path` / +# `find_library`, so this is the package that keeps the encoder out of the +# FetchContent branch -- which would otherwise clone brotli at build time. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ca-certificates cmake curl g++ git libpq-dev make ninja-build \ + ca-certificates cmake curl g++ git libbrotli-dev libpq-dev make ninja-build \ && rm -rf /var/lib/apt/lists/* # OpenSSL with the QUIC API: static, PIC, no test suite. @@ -311,6 +339,7 @@ RUN cmake -S . -B build -G Ninja \ -DUVCPP_BUILD_WEB=ON \ -DUVCPP_BUILD_WEBAPP=ON \ -DUVCPP_ENABLE_ZLIB=ON \ + -DUVCPP_ENABLE_BROTLI=ON \ -DUVCPP_ENABLE_OPENSSL=ON \ -DUVCPP_ENABLE_NGHTTP2=ON \ -DUVCPP_ENABLE_QUIC=ON \ @@ -339,14 +368,24 @@ RUN g++ -std=c++11 -O2 -DNDEBUG -o /server main.cc \ FROM ubuntu:24.04 -# libstdc++ for the entry's own objects, libpq5 for the one shared library the -# build above could not absorb. apt pulls in libpq's transitive closure +# libstdc++ for the entry's own objects, libpq5 and libbrotli1 for the two shared +# libraries the build above could not absorb. `libbrotli1` is the second of those: +# unlike zlib and libuv, which the project fetches and links statically, +# `UVCPP_ENABLE_BROTLI` takes the system copy, so `libuvcpp.so` carries +# `libbrotlienc.so.1` and `libbrotlidec.so.1` in its `DT_NEEDED` and the server does +# not start without them. On noble they are both files of that one package -- the +# name is not `libbrotlienc1`, which exists on Debian but not here (`libbrotli-dev` +# itself depends on `libbrotli1`, so the build stage already pulls it in; the +# runtime stage needs it said out loud). +# +# apt pulls in libpq's transitive closure # (libgssapi, libldap, libgnutls, libssl3t64, ...) on its own -- that OpenSSL # 3.0 is libpq's, and it is a different copy from the 3.5 the library carries # statically. Both live in the process; only libuvcpp.so's copy serves the h2 # and h3 listeners. RUN apt-get update \ - && apt-get install -y --no-install-recommends libstdc++6 libpq5 \ + && apt-get install -y --no-install-recommends \ + libbrotli1 libpq5 libstdc++6 \ && rm -rf /var/lib/apt/lists/* COPY --from=build /opt/uvcpp/lib/libuvcpp.so /opt/uvcpp/lib/libuvcpp.so diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index 7d6663a45..15a1a99e8 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -166,11 +166,18 @@ the profile forbids the framework from doing the JWT verification itself. is on at startup. Parsing is now the framework's `uvcpp_json_parse` rather than nlohmann's `parse` directly, so every JSON call the entry makes is one the library publishes. -- **Compression is the framework's built-in response compression** (gzip), which - answers `Accept-Encoding` per request and adds no `Content-Encoding` when the - client sends none. The library has no brotli compressor, so an `Accept-Encoding: - br, gzip` request is answered with gzip for anything the handler compresses - live. +- **Compression is the framework's built-in response compression** (gzip and + brotli), which answers `Accept-Encoding` per request and adds no + `Content-Encoding` when the client sends none. Both encoders are the library's + own middleware at its own defaults; the build turns the brotli one on + (`-DUVCPP_ENABLE_BROTLI=ON` in the Dockerfile) because it is the library's + default quality that the header records as the maximum of its own score model + for this profile. `json-compressed` asks for it — its guide lists the + framework's built-in compression under `standard=`, its title names brotli, and + its request carries `Accept-Encoding: gzip, br`, where equal `q` resolves to + `br`. On the three json-comp bodies the encoder produces 856/1182/1389 B + against gzip's 970/1330/1567 B. `Accept-Encoding` is still honoured per + request, so a client that asks for gzip alone still gets gzip. - **Where the dataset ships a pre-compressed twin, the handler serves it** (`precompressed`, on for every static leg). 15 of the dataset's 20 assets carry a `.br` and a `.gz` beside them, and the static profiles send diff --git a/frameworks/libuvcpp/meta.json b/frameworks/libuvcpp/meta.json index 0bcd3c3d8..2eded61ed 100644 --- a/frameworks/libuvcpp/meta.json +++ b/frameworks/libuvcpp/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "libuv", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON written per request by the framework's own uvcpp_json_writer (values read out of the uvcpp_json DOM, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway stacks put a stock Caddy edge in front of it.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON written per request by the framework's own uvcpp_json_writer (values read out of the uvcpp_json DOM, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway stacks put a stock Caddy edge in front of it.", "repo": "https://github.com/Antruly/libuvcpp", "enabled": true, "tests": [ From 3b902d8a9d113ef8c92893409e2a03063942ec69 Mon Sep 17 00:00:00 2001 From: Antruly Date: Thu, 15 Oct 2026 22:58:28 +0800 Subject: [PATCH 05/17] Serve gateway-64 and production-stack from a stock nginx edge Both stacks put a stock `caddy:2-alpine` at the edge. On both, the edge was the bottleneck and Caddy was why: over the profiles' fixed HTTP/1.1 upstream hop it costs several times what nginx does per request. This replaces it with stock `nginx:1.27-alpine` in ../proxy/ and ../proxy-production/, and the Caddyfiles become gateway-h3's alone, in ../proxy-h3/. What the two edges cost, measured on this machine at each profile's own operating point, same rig, same backend, A B A B (all four on a core budget the rig splits off with taskset; the harness's own numbers are in the platform run that follows): gateway-64, 512 conns x m32, 4 edge cores: Caddy 14,999 / 16,015 rps edge 396-400% of 400%, 306 us/req nginx 32,462 / 37,754 rps edge 345-377%, 86 us/req production-stack, c=256 reads m16 + c=32 writes m8, 4 edge cores: Caddy 8,620 / 8,377 rps edge 359-361%, 425-440 us/req Caddy + a pooled auth upstream 9,457 rps edge 359%, 392 us/req nginx 39,495 / 39,346 rps edge 372-375%, 100 us/req The edge is pinned at its core budget in every arm, so this is an edge-limited number rather than a backend one: on gateway-64 the proxy was at 96% of its 32 CPUs while the server sat at 23%, and with nginx the two come out balanced. The best Caddy arm is still 4.1x behind on throughput and 3.9x behind on edge CPU per request; the platform's own cross-entry figures agree (372-404 us/req for the three Caddy `gateway-64-512` edges against 100-110 for the three nginx ones). Why this is the compliant direction and not just the faster one. The gateway rules list "proxy choice" among the things entries compete on and their example compose uses nginx; `production-stack`'s architecture diagram names nginx and `auth_request`, and its rules put JWT verification at the edge behind exactly that. Neither stack is h3, so unlike the gateway pair this one needs no Caddy twin -- h3 is the single profile that cannot follow, because h3 needs QUIC and nginx has no HTTP/3 without a custom build against a QUIC library. Two things the rig turned up that the old Caddyfiles asserted or configured wrong, both now recorded in ../proxy-production/nginx.conf: - The Caddyfile claimed the server "never" receives the Authorization header. It does. Dumped from a header-dumping backend behind each edge: Caddy's forward_auth passes the bearer token through alongside the X-User-Id it copied. The nginx config drops it deliberately (proxy_set_header Authorization ""), which is the tighter reading of "verification happens at the edge" -- the token cannot be re-verified downstream -- and keeps ~200 B off every /api/* request. Neither behavior is a rule violation; the comment that claimed otherwise was simply false. - That Caddyfile pooled the backend upstream but never configured a transport for its forward_auth upstream. Counted at the authsvc: Caddy opened 0.77 connections per auth subrequest (21,115 for 27,510) against 0.024 for nginx's `keepalive 256` pool (2,997 for 122,818), which is most of why its auth leg cost 96 us/req where nginx's costs 20. Giving the Caddyfile the same pool lifted it from 8.4k to 9.5k rps -- real, and nowhere near enough to change the conclusion. Verified locally before pushing, since the platform queue is long: - `nginx -t` on both configs through the rig's path substitutions; `caddy validate` on ../proxy-h3/Caddyfile; all three compose files parsed and each resolves to a build context that now holds a Dockerfile plus its config. - The full functional set `_validate_production_stack` runs, on both edges: 15/15 each -- h2 negotiated, static Content-Type/size, /public/baseline and /public/json/25, 401 on all four unauthenticated /api/ cases, /api/items/1 id=1, cache-aside MISS then HIT, POST 204, POST invalidation MISS, /api/me id=42 (which is also the proof that X-User-Id survives the hop). - Every path the profile touches dumped from both edges and compared byte for byte: same status and same body throughout. Headers differ only where the gateway swap already documented (content-encoding br->gzip on the precompressed twin, Caddy's accept-ranges/alt-svc, text/javascript vs application/javascript). The one behavioral difference is a path matching none of the three prefixes -- Caddy answered an empty 200, nginx answers 404 -- and nothing reads it: the load URIs cover /static/, /public/ and /api/ only, and neither the edge nor the server declares a healthcheck. Not verified here, and the first thing CI should say: the images themselves. There is no docker on this machine, so both Dockerfiles were written from the base images' documented contents and the configs were only exercised as processes on the host. --- frameworks/libuvcpp/README.md | 90 +++++++--- frameworks/libuvcpp/compose.gateway-h3.yml | 15 +- frameworks/libuvcpp/compose.gateway.yml | 8 +- .../libuvcpp/compose.production-stack.yml | 9 +- frameworks/libuvcpp/main.cc | 6 +- frameworks/libuvcpp/meta.json | 2 +- .../libuvcpp/{proxy => proxy-h3}/Caddyfile | 0 frameworks/libuvcpp/proxy-h3/Dockerfile | 13 ++ .../libuvcpp/proxy-production/Caddyfile | 66 ------- .../libuvcpp/proxy-production/Dockerfile | 23 ++- .../libuvcpp/proxy-production/nginx.conf | 163 ++++++++++++++++++ frameworks/libuvcpp/proxy/Dockerfile | 25 ++- frameworks/libuvcpp/proxy/nginx.conf | 110 ++++++++++++ 13 files changed, 415 insertions(+), 115 deletions(-) rename frameworks/libuvcpp/{proxy => proxy-h3}/Caddyfile (100%) create mode 100644 frameworks/libuvcpp/proxy-h3/Dockerfile delete mode 100644 frameworks/libuvcpp/proxy-production/Caddyfile create mode 100644 frameworks/libuvcpp/proxy-production/nginx.conf create mode 100644 frameworks/libuvcpp/proxy/nginx.conf diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index 15a1a99e8..e2554fafb 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -184,9 +184,9 @@ the profile forbids the framework from doing the JWT verification itself. `Accept-Encoding: br;q=1, gzip;q=0.8`, so those 15 go out as the shipped brotli file with `Content-Encoding: br` and `Vary: Accept-Encoding`. This is the same thing every other entry does — nginx `gzip_static`, Caddy - `precompressed br gzip` (which this entry's own edge Caddyfiles already use), - actix `try_compressed()` — and the entry neither produces nor caches those - files: it is the framework's option, pointed at the dataset as mounted. A + `precompressed br gzip`, actix `try_compressed()` — and the entry neither + produces nor caches those files: it is the framework's option, pointed at the + dataset as mounted. A request for a file with no twin, or one that sends no `Accept-Encoding`, or a `Range` request, gets the live path unchanged. - **The libuv thread pool is sized at the top of `main()`.** Two things ride @@ -286,20 +286,22 @@ Three things about 8443/udp are worth stating, because they are deliberate: ### The compose stacks `gateway-64`, `gateway-h3` and `production-stack` are orchestrated by -`compose..yml`, all of which run `network_mode: host` and put a stock -unmodified `caddy:2-alpine` at the edge. There is no hand-written proxy in this -entry: the gateway profiles' rules exclude one ("No custom proxy -implementations"), and a Caddyfile that says `file_server` and `reverse_proxy` -is the whole of it. +`compose..yml`, all of which run `network_mode: host` and put a stock, +unmodified reverse proxy at the edge: `nginx:1.27-alpine` for `gateway-64` and +`production-stack`, `caddy:2-alpine` for `gateway-h3`. There is no hand-written +proxy in this entry: the gateway profiles' rules exclude one ("No custom proxy +implementations"), and a `nginx.conf` or `Caddyfile` that says `file_server` and +`reverse_proxy` is the whole of it. -| Profile | Compose file | Services | -|---|---|---| -| `gateway-64` | `compose.gateway.yml` | proxy + server | -| `gateway-h3` | `compose.gateway-h3.yml` | proxy + server | -| `production-stack` | `compose.production-stack.yml` | edge + cache + authsvc + server | +| Profile | Compose file | Edge | Services | +|---|---|---|---| +| `gateway-64` | `compose.gateway.yml` | `nginx:1.27-alpine` | proxy + server | +| `gateway-h3` | `compose.gateway-h3.yml` | `caddy:2-alpine` | proxy + server | +| `production-stack` | `compose.production-stack.yml` | `nginx:1.27-alpine` | edge + cache + authsvc + server | -- **The edge serves `/static/*` off the mounted disk itself**, with - `file_server { precompressed br gzip }` — never forwarded to the backend. That +- **The edge serves `/static/*` off the mounted disk itself** — Caddy's + `file_server { precompressed br gzip }`, or for the nginx edges an `alias` + plus `gzip_static on` — never forwarded to the backend. That is what the gateway rules require and what the profile measures: a front door doing TLS, compression and file I/O in front of an application server that answers only the dynamic endpoints. @@ -308,16 +310,52 @@ is the whole of it. the server's per-core loops through a pool of h1 keepalive connections; a single h2 connection to the backend would re-serialize them behind one connection's flow control. -- **The same Caddyfile covers gateway-64 and gateway-h3.** `protocols h1 h2 h3` - binds 8443 on tcp and udp from one server block; only the load generator - differs (`h2load --alpn-list=h3`). +- **The gateway and production edges are nginx; only the h3 stack is Caddy.** + `gateway-64` drives 512 connections × 32 streams = 16,384 concurrent requests, + and over the profile's HTTP/1.1 upstream hop that multiplexing is what the + edge costs. Caddy charges roughly 3× nginx per request there — 372–404 µs/req + for the three entries whose `gateway-64-512` edge is Caddy against 100–110 for + the three whose edge is nginx — which put the proxy at 96% of its 32 CPUs + while the server sat at 23% of its own. The same holds on `production-stack`, + where the edge also runs `auth_request` per `/api/*` call: measured at the + profile's own 256 reads + 32 writes over a four-core stand-in, Caddy's edge + cost 440 µs/req at 8.4k req/s and 392 µs/req at 9.5k once its unconfigured + `forward_auth` upstream was given a connection pool, against 101 µs/req at + 39.2k for nginx in the same shape, with the edge pinned at its core budget in + all three arms. `proxy/nginx.conf` and `proxy-production/nginx.conf` carry the + measurements. The profile lists "proxy choice" among the things entries + compete on, its own example compose uses nginx, and `production-stack`'s + architecture diagram names nginx and `auth_request` — so both are nginx. The + h3 profile cannot follow: h3 needs QUIC, and nginx has no HTTP/3 without a + custom build against a QUIC library, so `gateway-h3` keeps Caddy from + `proxy-h3/` and lives in its own compose file rather than sharing one + Caddyfile with `gateway-64` as it used to. +- **`worker_processes` in both nginx configs is a literal, coupled to the + compose `cpuset`.** nginx's `worker_processes auto` counts the *host's* online + CPUs, ignoring a container cpuset, and `worker_cpu_affinity auto` is worse: it + re-pins each worker to a CPU of its own choosing and leaves the cpuset + entirely. The shipped values are 32 for `proxy/nginx.conf`, matching + `PROXY_CPUSET`'s default of `0-15,64-79`, and 30 for + `proxy-production/nginx.conf`, matching `EDGE_CPUSET`'s `0-14,64-78`; change a + cpuset and its number has to change with it. +- **The nginx edges serve the gzip twin of a static asset, not the brotli + one.** The framework's own static legs pick brotli, but the gateway and + production edges are a stock `nginx:1.27-alpine` and brotli needs + `nginx-mod-http-brotli`, which cannot be loaded into the official image's + nginx (nginx checks the build signature, so the module has to come from the + same build as the binary). Serving brotli from a distribution's nginx plus its + module package was measured on the rig at the gateway profile's concurrency — + 47,275 B of `app.js` against gzip's 54,813, medians of nine runs — and the two + came out within the rig's own run-to-run spread, so they serve the `.gz` + sibling like the profile's other nginx edges do. Both encodings are announced + in `Vary: Accept-Encoding`. - **The server is not given `/certs`.** `main.cc` opens its 8081 and 8443 listeners only when the certificate pair is readable, so with none mounted it comes up on its two cleartext ports and leaves every TLS port to the edge. Mount the certificates into both services and the two race for 8443. - **The backend's ports are 8085/8086, not 8080/8082.** Both ends of the hop are set together — `H1_PORT`/`H2C_PORT` in the compose `server`'s environment and - the upstream in the Caddyfile. The stack runs on the host network, and 8080 + the upstream in the edge's config. The stack runs on the host network, and 8080 and 8082 are exactly the two ports something else can already be holding there, so the backend is given ports of its own rather than racing for the entry's defaults. 8080 is still opened, but as the readiness probe's landing @@ -327,11 +365,15 @@ is the whole of it. one idle loop. - **`production-stack`'s JWT verification is the shared sidecar, unmodified** — `frameworks/_shared/authsvc/`, built from the same directory every entry uses. - The edge fires `forward_auth` at it for every `/api/*` request and copies back - the `X-User-Id` it resolves; the token cost is not cached, because that cost is - part of what the profile measures. The server never parses a token, never reads - `Authorization`, and separately returns 401 for any `/api/*` request that - arrives with no `X-User-Id` at all. + The edge fires an auth subrequest at it for every `/api/*` request (`auth_request` + for nginx, `forward_auth` for Caddy) and copies back the `X-User-Id` it + resolves; the token cost is not cached, because that cost is part of what the + profile measures. The server never parses a token and separately returns 401 + for any `/api/*` request that arrives with no `X-User-Id` at all. The nginx + edge additionally drops the `Authorization` header before forwarding, so the + token cannot be re-verified downstream; Caddy's `forward_auth` passes it + through — checked against a header-dumping backend rather than assumed — which + is not a violation either, just the looser reading. - **Cache-aside is in-process.** The profile's implementation guide lists "in-process only: no Redis for caching, just a local hashmap with TTL" beside the L1+L2 and Redis-only options, and this entry takes it: the profile runs one diff --git a/frameworks/libuvcpp/compose.gateway-h3.yml b/frameworks/libuvcpp/compose.gateway-h3.yml index 0a784696b..579f9cb73 100644 --- a/frameworks/libuvcpp/compose.gateway-h3.yml +++ b/frameworks/libuvcpp/compose.gateway-h3.yml @@ -1,11 +1,14 @@ # gateway-h3 — the same two-service stack as compose.gateway.yml, exercised # over HTTP/3 at the edge. # -# Nothing about the stack changes: stock Caddy binds 8443 on both tcp and udp -# from the same `protocols h1 h2 h3` server block, so the proxy config is -# byte-identical and only the load generator differs (h2load with -# `--alpn-list=h3`). The upstream hop stays plain h1 over loopback, which is -# what the server speaks. +# Same shape, same backend, same upstream hop (plain h1 over loopback), and a +# different edge *image*: this one is stock Caddy from ./proxy-h3, where +# gateway-64 is stock nginx from ./proxy. The split exists because h3 needs +# QUIC and nginx has no HTTP/3 without a custom build against a QUIC library, +# while over the profile's h1 upstream hop Caddy costs ~3x nginx per request at +# gateway-64's multiplexing -- so the two profiles want different edges. Caddy +# binds 8443 on both tcp and udp from its `protocols h1 h2 h3` server block, +# and the h3 load generator reaches the udp half (h2load with `--alpn-list=h3`). # # Kept as a separate file rather than a symlink because the harness resolves # compose files by name (`compose..yml`, with gateway-64 on the legacy @@ -21,7 +24,7 @@ services: proxy: build: - context: ./proxy + context: ./proxy-h3 network_mode: host cpuset: "${PROXY_CPUSET:-0-15,64-79}" ulimits: diff --git a/frameworks/libuvcpp/compose.gateway.yml b/frameworks/libuvcpp/compose.gateway.yml index 47292bcf0..6795d459a 100644 --- a/frameworks/libuvcpp/compose.gateway.yml +++ b/frameworks/libuvcpp/compose.gateway.yml @@ -1,10 +1,16 @@ -# gateway-64 — stock Caddy (TLS + h2 on 8443) in front of the libuvcpp server. +# gateway-64 — stock nginx (TLS + h2 on 8443) in front of the libuvcpp server. # # Two services, which is what the profile fixes: `proxy` terminates TLS, serves # /static/* off disk and forwards the dynamic endpoints over plain h1 to # 127.0.0.1:8085; `server` answers /baseline2, /json/{n} and /async-db and # nothing else. # +# The edge is nginx rather than the Caddy that gateway-h3 still uses, because at +# this profile's 512 connections x 32 streams Caddy costs ~3x nginx per request +# over an h1 upstream and the proxy was therefore the stack's bottleneck (96% of +# its 32 CPUs against the server's 23%). proxy/nginx.conf has the measurement. +# The h3 profile cannot follow: h3 needs QUIC, which nginx lacks. +# # The server is deliberately **not** given /certs. main.cc opens its 8081 and # 8443 listeners only when the certificate pair is readable, so with no certs # mounted it comes up on its two cleartext ports alone and leaves every TLS port diff --git a/frameworks/libuvcpp/compose.production-stack.yml b/frameworks/libuvcpp/compose.production-stack.yml index 1e43d2d99..fbb44c4a5 100644 --- a/frameworks/libuvcpp/compose.production-stack.yml +++ b/frameworks/libuvcpp/compose.production-stack.yml @@ -1,7 +1,7 @@ # production-stack — edge + cache + authsvc + server, four services on a 64 # logical CPU budget. # -# edge (caddy) 30 logical CPUs. TLS + h2 termination, /static served +# edge (nginx) 30 logical CPUs. TLS + h2 termination, /static served # off disk, one auth subrequest per /api/* call. # cache (redis) 2. Redis is single-threaded on the data path; a # second core would sit idle. @@ -14,6 +14,13 @@ # All four on host networking, so what passes between them is loopback: the # edge owns 8443, authsvc 9090, the server 8085, the cache 6379. # +# The edge is nginx rather than the Caddy this stack used before. The profile's +# own architecture diagram names nginx and `auth_request`, and on a 4-core +# stand-in for this shape Caddy's edge cost 392-440 us of CPU per request +# against nginx's 101, with the edge pinned at its core budget in every arm. +# proxy-production/nginx.conf carries the measurement. Nothing here is h3, so +# unlike the gateway pair this stack does not need a Caddy twin. +# # The server is not given /certs, for the same reason as the gateway stacks -- # main.cc opens its TLS listeners only when the certificate pair is readable, so # without it the server stays on its two cleartext ports and the edge has 8443 diff --git a/frameworks/libuvcpp/main.cc b/frameworks/libuvcpp/main.cc index 67c5bd109..49ef9d90d 100644 --- a/frameworks/libuvcpp/main.cc +++ b/frameworks/libuvcpp/main.cc @@ -183,9 +183,9 @@ const char* kStaticRoot = "/data/static"; /// /// It is also what every other entry in this arena does -- nginx `gzip_static`, /// Caddy `precompressed br gzip`, actix `try_compressed()` -- and what this -/// entry's own `proxy/Caddyfile` and `proxy-production/Caddyfile` already ask -/// their edges for. Left off, the direct legs would be the only place in the -/// whole entry serving a live deflate. +/// entry's own `proxy/nginx.conf`, `proxy-production/nginx.conf` and +/// `proxy-h3/Caddyfile` already ask their edges for. Left off, the direct legs +/// would be the only place in the whole entry serving a live deflate. /// /// Off by default in `uvcpp_web_static_options`, so it has to be turned on /// explicitly. `:8443/udp` (static-h3) is a `uvcpp_http_server` + diff --git a/frameworks/libuvcpp/meta.json b/frameworks/libuvcpp/meta.json index 2eded61ed..80f0e626e 100644 --- a/frameworks/libuvcpp/meta.json +++ b/frameworks/libuvcpp/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "libuv", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON written per request by the framework's own uvcpp_json_writer (values read out of the uvcpp_json DOM, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway stacks put a stock Caddy edge in front of it.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON written per request by the framework's own uvcpp_json_writer (values read out of the uvcpp_json DOM, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "enabled": true, "tests": [ diff --git a/frameworks/libuvcpp/proxy/Caddyfile b/frameworks/libuvcpp/proxy-h3/Caddyfile similarity index 100% rename from frameworks/libuvcpp/proxy/Caddyfile rename to frameworks/libuvcpp/proxy-h3/Caddyfile diff --git a/frameworks/libuvcpp/proxy-h3/Dockerfile b/frameworks/libuvcpp/proxy-h3/Dockerfile new file mode 100644 index 000000000..c9abcb782 --- /dev/null +++ b/frameworks/libuvcpp/proxy-h3/Dockerfile @@ -0,0 +1,13 @@ +# Stock Caddy, unmodified. This is the gateway-h3 edge only: the base image +# carries h1/h2/h3 (QUIC) and the Caddyfile binds 8443 on both tcp and udp, of +# which h3 uses the udp half. gateway-64 uses nginx instead, in ../proxy/, for +# the reason recorded in that nginx.conf -- h3 is the profile that cannot +# follow it, because h3 needs QUIC and nginx has no HTTP/3 without a custom +# build against a QUIC library. +FROM caddy:2-alpine + +COPY Caddyfile /etc/caddy/Caddyfile + +EXPOSE 8443/tcp 8443/udp + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/frameworks/libuvcpp/proxy-production/Caddyfile b/frameworks/libuvcpp/proxy-production/Caddyfile deleted file mode 100644 index d9b09e933..000000000 --- a/frameworks/libuvcpp/proxy-production/Caddyfile +++ /dev/null @@ -1,66 +0,0 @@ -# libuvcpp production-stack edge. -# -# Same stock Caddy as the gateway edge, with the three-way path split the -# production-stack profile defines: -# -# /static/* served here from disk, never reaching the server -# /public/* forwarded unauthenticated -# /api/* forwarded only after authsvc has verified the bearer token -# -# forward_auth fires a subrequest to the shared authsvc on every /api/* call and -# nothing caches it: the HMAC-SHA256 that costs is part of what the profile -# measures. What the server receives is the X-User-Id authsvc resolved, never -# the Authorization header -- the profile's rules put JWT verification at the -# edge and forbid the framework from doing it, and the server separately -# refuses any /api/* request that arrives with no X-User-Id at all. -# -# Both upstreams name 127.0.0.1:8085 -- the compose `server` service's H1_PORT, -# not the 8080 the entry uses when it runs alone. The server binds its ports at -# startup and exits if a bind fails, and this stack is on the host network, so -# the backend is given ports of its own; see compose.gateway.yml. -{ - admin off - auto_https off - servers { - protocols h1 h2 - } -} - -https://localhost:8443 { - tls /certs/server.crt /certs/server.key - - handle /static/* { - root * /data - file_server { - precompressed br gzip - } - } - - handle /public/* { - reverse_proxy 127.0.0.1:8085 { - transport http { - versions 1.1 - keepalive 5m - keepalive_idle_conns 2048 - keepalive_idle_conns_per_host 2048 - } - } - } - - # authsvc answers 200 + X-User-Id or 401, and forward_auth returns its - # refusal verbatim, so an unauthenticated /api/* never reaches the server. - handle /api/* { - forward_auth 127.0.0.1:9090 { - uri /_auth - copy_headers X-User-Id - } - reverse_proxy 127.0.0.1:8085 { - transport http { - versions 1.1 - keepalive 5m - keepalive_idle_conns 2048 - keepalive_idle_conns_per_host 2048 - } - } - } -} diff --git a/frameworks/libuvcpp/proxy-production/Dockerfile b/frameworks/libuvcpp/proxy-production/Dockerfile index fd37caf66..3f24aa370 100644 --- a/frameworks/libuvcpp/proxy-production/Dockerfile +++ b/frameworks/libuvcpp/proxy-production/Dockerfile @@ -1,9 +1,22 @@ -# Stock Caddy, same base as the gateway edge. h2 over TCP only: the profile's -# load generator is h2load against 8443/tcp, so no QUIC listener is needed. -FROM caddy:2-alpine +# Stock nginx from the official image, unmodified: ssl, http_v2 and gzip_static +# are all in the default build. The production-stack edge -- TLS + h2, /static/* +# off disk, and one auth_request per /api/* call against the shared authsvc. +# gateway-h3 keeps Caddy, in ../proxy-h3/, because h3 needs QUIC. +FROM nginx:1.27-alpine -COPY Caddyfile /etc/caddy/Caddyfile +COPY nginx.conf /etc/nginx/nginx.conf + +# Unused on this profile -- the auth subrequest carries no body and the write +# path's bodies are small enough to stay in memory -- but nginx creates its +# scratch directories at startup, and left to its own defaults it asks for +# /var/lib/nginx/body, which this image does not create. It then refuses to boot +# with "mkdir() ... failed (2: No such file or directory)". Workers drop to the +# nginx user, so the directories have to be owned by it. +RUN mkdir -p /var/lib/nginx/tmp/body /var/lib/nginx/tmp/proxy \ + /var/lib/nginx/tmp/fastcgi /var/lib/nginx/tmp/uwsgi \ + /var/lib/nginx/tmp/scgi \ + && chown -R nginx:nginx /var/lib/nginx EXPOSE 8443/tcp -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] +CMD ["nginx", "-g", "daemon off;"] diff --git a/frameworks/libuvcpp/proxy-production/nginx.conf b/frameworks/libuvcpp/proxy-production/nginx.conf new file mode 100644 index 000000000..f43fd29de --- /dev/null +++ b/frameworks/libuvcpp/proxy-production/nginx.conf @@ -0,0 +1,163 @@ +# libuvcpp production-stack edge: stock nginx terminating TLS + HTTP/2 on 8443, +# with the profile's three-way path split. +# +# /static/* served here from disk, never reaching the server +# /public/* forwarded unauthenticated +# /api/* forwarded only after authsvc has verified the bearer token +# +# Why nginx rather than Caddy, which this stack used before: the profile's own +# architecture diagram names nginx and `auth_request`, and Caddy's edge is far +# more expensive per request. On a 4-core stand-in for this shape, at the +# profile's own c=256 reads + c=32 writes: Caddy 8.4k req/s with its edge at +# 440 us/req, Caddy again with a connection pool added to the forward_auth +# upstream (which the Caddyfile never configured -- the shipped one pools only +# the backend) 9.5k at 392 us, and this file 39.2k at 101 us. The edge was +# pinned at its core budget in all three arms; the best Caddy arm is still 4.1x +# behind on throughput and 3.9x behind on edge CPU per request. ../proxy/ +# nginx.conf has the same finding at the gateway profile's multiplexing. +# The h3 stacks keep Caddy only because h3 needs QUIC. +# +# Every path this profile exercises was dumped from both edges and compared +# byte for byte: same status and same body throughout, headers differing only in +# the ways ../proxy/nginx.conf documents. The one behavioral difference is a +# request matching none of the three prefixes -- Caddy answered an empty 200, +# this answers nginx's 404 -- and nothing reads it: the load URIs cover only +# /static/, /public/ and /api/, and neither this service nor the server declares +# a healthcheck. +# +# The auth subrequest fires on every /api/* call and nothing caches it: the +# HMAC-SHA256 that costs is part of what the profile measures, and the rules +# forbid proxy_cache on /_auth. authsvc answers 200 with X-User-Id or 401, and +# a 401 from the subrequest becomes nginx's 401 -- the server is never reached. +# +# `Authorization ""` is a decision, not an accident of auth_request. Both edges +# were run against a header-dumping backend on the rig, and Caddy's forward_auth +# *does* pass the bearer token through along with the X-User-Id it copied; the +# comment this file replaced claimed otherwise and was simply wrong. Dropping it +# here is the tighter reading of the profile: JWT verification happens at the +# edge, the server is handed the identity authsvc resolved and nothing else, and +# the token cannot be re-verified downstream. It also keeps ~200 bytes of a +# per-request header off every /api/* hop. The server refuses any /api/* request +# that arrives with no X-User-Id at all, which is what makes the POST branch -- +# the one that writes rows -- safe. +# +# worker_processes is a literal, coupled to the edge service's `cpuset` in +# compose.production-stack.yml (0-14,64-78 = 30 logical CPUs). nginx's +# `worker_processes auto` counts the *host's* online CPUs and ignores a +# container cpuset, and `worker_cpu_affinity auto` re-pins workers outside it +# altogether. ../proxy/nginx.conf has the measurement. + +user nginx; +worker_processes 30; +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 65535; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # The load generator never repeats a request, so an access log is pure cost. + access_log off; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 300s; + keepalive_requests 1000000; + server_tokens off; + + # Serve the shipped .gz twin of a static asset and announce both encodings. + # The gateway edge does the same; ../proxy/nginx.conf explains why the .gz + # twin and not the .br one. + gzip_static on; + gzip_vary on; + + # nginx wants scratch directories that the upstream connection pools spill + # into; the image is built with these created and owned by the nginx user. + client_body_temp_path /var/lib/nginx/tmp/body; + proxy_temp_path /var/lib/nginx/tmp/proxy; + fastcgi_temp_path /var/lib/nginx/tmp/fastcgi; + uwsgi_temp_path /var/lib/nginx/tmp/uwsgi; + scgi_temp_path /var/lib/nginx/tmp/scgi; + + # One pooled h1 connection per in-flight request, for both upstreams. The + # backend numbers are the Caddyfile's. The authsvc pool is not: that + # Caddyfile never configured a transport for its forward_auth upstream, and + # the rig counted the consequence -- 0.77 connections opened per auth + # subrequest against 0.024 with this pool, which is most of why its auth leg + # cost 96 us/req where this one costs 20. + upstream backend { + server 127.0.0.1:8085; + keepalive 2048; + keepalive_requests 1000000; + keepalive_timeout 300s; + } + + upstream authsvc { + server 127.0.0.1:9090; + keepalive 256; + keepalive_requests 1000000; + keepalive_timeout 300s; + } + + server { + listen 8443 ssl http2 backlog=65535 reuseport; + + ssl_certificate /certs/server.crt; + ssl_certificate_key /certs/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + + # The X-User-Id the auth subrequest answered with, handed to whichever + # /api/ location runs it. + auth_request_set $auth_user_id $upstream_http_x_user_id; + + # The auth subrequest itself: GET /_auth at authsvc, with the client's + # Authorization header and no body. `internal` keeps a client from + # reaching it directly. + location = /_auth { + internal; + proxy_pass http://authsvc/_auth; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header Authorization $http_authorization; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + + # The 30% of the profile's URIs that never reach the server: a file open + # plus sendfile, with the .gz twin chosen when the client accepts it. + location /static/ { + alias /data/static/; + } + + # Unauthenticated, uncached: the server answers /public/baseline and + # /public/json/{n} straight from the dataset. + location /public/ { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + } + + # Authenticated. auth_request runs first and its 401 short-circuits the + # location, so an unauthenticated /api/ never reaches the server -- not + # even for the POST that would write a row. + location /api/ { + auth_request /_auth; + + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-User-Id $auth_user_id; + proxy_set_header Authorization ""; + } + } +} diff --git a/frameworks/libuvcpp/proxy/Dockerfile b/frameworks/libuvcpp/proxy/Dockerfile index 11c970331..3214932fc 100644 --- a/frameworks/libuvcpp/proxy/Dockerfile +++ b/frameworks/libuvcpp/proxy/Dockerfile @@ -1,11 +1,20 @@ -# Stock Caddy, unmodified. The base image already carries h1/h2/h3 (QUIC), so -# one image serves both gateway profiles: the Caddyfile binds 8443 on tcp and -# udp, and gateway-64 only ever reaches the tcp half while gateway-h3 only ever -# reaches the udp one. -FROM caddy:2-alpine +# Stock nginx from the official image, unmodified: ssl, http_v2 and gzip_static +# are all in the default build. This edge serves gateway-64 only -- gateway-h3 +# keeps Caddy, in ../proxy-h3/, because h3 needs QUIC. +FROM nginx:1.27-alpine -COPY Caddyfile /etc/caddy/Caddyfile +COPY nginx.conf /etc/nginx/nginx.conf -EXPOSE 8443/tcp 8443/udp +# nginx.conf names its scratch directories explicitly. Left to its own defaults +# it asks for /var/lib/nginx/body, which this image does not create, and the +# master refuses to boot with "mkdir() ... failed (2: No such file or +# directory)". Workers drop to the nginx user, so the directories have to be +# owned by it. +RUN mkdir -p /var/lib/nginx/tmp/body /var/lib/nginx/tmp/proxy \ + /var/lib/nginx/tmp/fastcgi /var/lib/nginx/tmp/uwsgi \ + /var/lib/nginx/tmp/scgi \ + && chown -R nginx:nginx /var/lib/nginx -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] +EXPOSE 8443/tcp + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frameworks/libuvcpp/proxy/nginx.conf b/frameworks/libuvcpp/proxy/nginx.conf new file mode 100644 index 000000000..3ff013033 --- /dev/null +++ b/frameworks/libuvcpp/proxy/nginx.conf @@ -0,0 +1,110 @@ +# libuvcpp gateway edge for gateway-64: stock nginx terminating TLS + HTTP/2 on +# 8443, serving /static/* from disk, and proxying everything else over h1 to the +# framework server on 8085. +# +# Why nginx rather than the Caddy the h3 stack still uses (see ../proxy-h3/): +# gateway-64 drives 512 connections x 32 streams = 16,384 concurrent requests, +# and over the profile's *HTTP/1.1* upstream hop that multiplexing is what the +# edge costs. On the reference machine Caddy edges land at 372-404 us/req there +# (libuvcpp, sark, tako -- three entries, same figure) while nginx edges land at +# 100-110 (vanilla-epoll, fulmine, morojs), and the same ~3x gap reproduces on a +# local two-service rig. That made the edge this stack's bottleneck: the proxy +# sat at 96% of its 32 logical CPUs while the server used 23% of its own. The +# profile lists "proxy choice" as one of the things entries compete on, and its +# own example compose uses nginx. The h3 profile cannot follow, because h3 needs +# QUIC and nginx has no HTTP/3 without a custom build against a QUIC library. +# +# worker_processes is a literal on purpose, and it is coupled to the proxy +# service's `cpuset` in compose.gateway.yml (0-15,64-79 = 32 logical CPUs). +# nginx's `worker_processes auto` counts the *host's* online CPUs and ignores a +# container cpuset, and `worker_cpu_affinity auto` is worse than that: it +# re-pins each worker to a CPU of its own choosing and escapes the cpuset +# entirely. Both were measured on this box under `taskset -c 8-11`, where +# `auto` + `worker_cpu_affinity auto` produced 18 workers pinned across CPUs +# 0-17. Change the cpuset and this number has to change with it. +# +# The static assets ship a `.gz` and a `.br` twin each; this edge serves the +# gzip one (`gzip_static`), which is what the official nginx image can do and +# what the profile's other nginx edges do. Serving the brotli twin instead needs +# nginx-mod-http-brotli, and a module built for one nginx build cannot be loaded +# into another, so it would mean taking both nginx and the module from a +# distribution rather than the official image. It was measured on the rig at +# this profile's concurrency and the two are a wash (brotli sends 47,275 B of +# app.js against gzip's 54,813, and the medians of nine runs came out within the +# rig's own run-to-run spread), so this keeps the stock image. + +user nginx; +worker_processes 32; +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 65535; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # The load generator never repeats a request, so an access log is pure cost. + access_log off; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 300s; + keepalive_requests 1000000; + server_tokens off; + + # Serve the shipped .gz twin in place of the identity file when the client + # accepts gzip, and say so in Vary so the two encodings stay distinguishable + # to anything caching in front of this. + gzip_static on; + gzip_vary on; + + # nginx wants scratch directories that the upstream connection pool spills + # into; the image is built with these created and owned by the nginx user. + client_body_temp_path /var/lib/nginx/tmp/body; + proxy_temp_path /var/lib/nginx/tmp/proxy; + fastcgi_temp_path /var/lib/nginx/tmp/fastcgi; + uwsgi_temp_path /var/lib/nginx/tmp/uwsgi; + scgi_temp_path /var/lib/nginx/tmp/scgi; + + # One pooled h1 connection per in-flight request is what this profile asks + # for; `keepalive 2048` is the idle pool, and nginx opens beyond it under + # load rather than queueing. Raising it changed nothing measurable on the + # rig, so it stays at the value the Caddy config used. + upstream backend { + server 127.0.0.1:8085; + keepalive 2048; + keepalive_requests 1000000; + keepalive_timeout 300s; + } + + server { + listen 8443 ssl http2 backlog=65535 reuseport; + + ssl_certificate /certs/server.crt; + ssl_certificate_key /certs/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + + # The 30% of the profile's URIs that never reach the server: a file open + # plus sendfile, with the .gz twin chosen when the client sends + # `Accept-Encoding: br;q=1, gzip;q=0.8` -- which is what the load + # generator sends. A file with no twin, a request with no + # Accept-Encoding, and a Range request all fall through to the identity + # path unchanged. + location /static/ { + alias /data/static/; + } + + location / { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + } + } +} From 581ca9e1866511afad5aa5b0315604a4bc2959ee Mon Sep 17 00:00:00 2001 From: Antruly Date: Thu, 15 Oct 2026 23:14:54 +0800 Subject: [PATCH 06/17] Bind the nginx edges to the two loopback addresses, not the wildcard The 2026-10-15 validate-gateway run failed four production-stack checks -- the three auth-wall 401s plus /api/items/1 -- and the response carries `server: uvcpp`, so the server answered them itself. The harness runs a compose stack's checks while the entry's own standalone container is still up: `--network host` and /certs mounted, because this entry subscribes to an h2 test, so uvcpp binds 0.0.0.0:8443 as well -- one SO_REUSEPORT socket per loop, 64 of them on the runner. `listen 8443` put the edge in the same reuseport group, so the kernel hashed localhost:8443 across the edge and the server, and every request that landed on the server got its 8443 listener's 404 for /api/* (those routes live on h1 only, by design, main.cc:1924). Reproduced on the rig against a 64-socket wildcard holder: 3/24 requests reach the edge with the wildcard listen, 24/24 with the two loopbacks, and the full production-stack check set passes with the holder up. Caddy never had this because `https://localhost:8443` is a hostname it resolves into these same two binds, and a specific bind wins the listener lookup outright -- the wildcard socket is then never chosen at all. gateway-64's edge has the same wildcard and its checks cannot see the split: /static/*, /json, /async-db and /baseline2 are exactly the paths the standalone entry answers too. Both edges get the fix. --- .../libuvcpp/proxy-production/nginx.conf | 18 +++++++++++++++++- frameworks/libuvcpp/proxy/nginx.conf | 14 +++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/frameworks/libuvcpp/proxy-production/nginx.conf b/frameworks/libuvcpp/proxy-production/nginx.conf index f43fd29de..b6495df10 100644 --- a/frameworks/libuvcpp/proxy-production/nginx.conf +++ b/frameworks/libuvcpp/proxy-production/nginx.conf @@ -106,8 +106,24 @@ http { keepalive_timeout 300s; } + # The two loopback addresses rather than the wildcard `listen 8443`, and + # that is load-bearing. The harness validates a compose stack while the + # entry's own standalone container is still up (`--network host`, and + # /certs mounted because this profile subscribes to an h2 test), so that + # process holds 0.0.0.0:8443 as well -- one listening socket per loop with + # SO_REUSEPORT, 64 of them on the reference runner. A wildcard `listen` + # lands in the *same* reuseport group as those sockets and the kernel + # hashes localhost:8443 across all of them, so most of the checks reach + # the server directly, where /api/* is deliberately unregistered + # (main.cc:1924) and answers 404. Measured on the rig against a 64-socket + # wildcard holder: wildcard 3/24 requests to the edge, loopback 24/24. + # Caddy, which this file replaced, never had the problem because + # `https://localhost:8443` makes it bind these same two addresses, and a + # specific bind wins the listener lookup outright -- the wildcard socket is + # then never chosen at all. Every URL the harness uses is `localhost`. server { - listen 8443 ssl http2 backlog=65535 reuseport; + listen 127.0.0.1:8443 ssl http2 backlog=65535 reuseport; + listen [::1]:8443 ssl http2 backlog=65535 reuseport; ssl_certificate /certs/server.crt; ssl_certificate_key /certs/server.key; diff --git a/frameworks/libuvcpp/proxy/nginx.conf b/frameworks/libuvcpp/proxy/nginx.conf index 3ff013033..9e2e0cfb0 100644 --- a/frameworks/libuvcpp/proxy/nginx.conf +++ b/frameworks/libuvcpp/proxy/nginx.conf @@ -83,8 +83,20 @@ http { keepalive_timeout 300s; } + # Loopback addresses rather than the wildcard `listen 8443`, for the same + # reason ../proxy-production/nginx.conf spells out at length: the harness + # validates a compose stack while the entry's own standalone container is + # still up on `--network host` with /certs mounted, so a wildcard listen + # shares its reuseport group with that process's per-loop sockets and + # `localhost:8443` gets hashed across both servers. This stack's checks do + # not notice -- /static/*, /json, /async-db and /baseline2 are exactly the + # paths the standalone entry answers too, which is why the split went + # unseen here until the production-stack checks, whose /api/* is not on + # that entry's 8443 listener, caught it. Every URL the harness uses is + # `localhost`, so binding the loopbacks costs nothing. server { - listen 8443 ssl http2 backlog=65535 reuseport; + listen 127.0.0.1:8443 ssl http2 backlog=65535 reuseport; + listen [::1]:8443 ssl http2 backlog=65535 reuseport; ssl_certificate /certs/server.crt; ssl_certificate_key /certs/server.key; From 9964fafb2768ef1762012616010b412dad795be5 Mon Sep 17 00:00:00 2001 From: Antruly Date: Thu, 15 Oct 2026 23:28:58 +0800 Subject: [PATCH 07/17] Answer the JSON profiles with the framework's standard serializer MDA2AV on #1568: "You need to use a proper json parser (for example nlohmann/json) for tests that do json serialization, unless you classify as an engine." That is the rule this entry was on the wrong side of. Where it comes from: implementation-rules/frameworks/standard.md:55 says to use the ecosystem's production-grade library for a task like JSON serialization and that "Experimental or hand-rolled alternatives solely for benchmark performance are not permitted"; json-compressed/implementation.md's type-rule for `standard` asks for "the framework standard JSON serialization"; and tuned.md lists "Alternative JSON serializers" among the things *Tuned* allows. `uvcpp_json_writer` -- a public, documented API of this library, but an alternative serializer -- is a Tuned technique, and meta.json declares `mode: standard`. So it is not this entry's to answer these profiles with. /json, /public/json/:count and /async-db now build an `nlohmann::json` DOM per request from the live data and hand it to `resp.json()`, which is the framework's own `uvcpp_json_dump()` (`src/webapp/uvcpp_web_response.cpp:470`). Nothing is pre-rendered or cached. Reading goes the same way: parsing is `uvcpp_json_parse` and field access is `uvcpp_json_get_string`/`_get_int`, not nlohmann's `parse`/`value()`/`[]` -- the library publishes those accessors precisely so a handler cannot throw out of a libuv callback. The hand-written payload literals are gone with them: `{}`, `{"error":"unauthorized"}` and the empty `/async-db` result set are built by the serializer like every other body. What this gives up, in the entry's own measurements: at the 1.7.2-dev pin, /json/50?m=6 cost 63.2 us of server CPU per request through the writer against 176.0 us through the DOM path on the same 6 pinned cores, and the writer path also skipped the per-item DOM copy. The rules decide this, not the measurement, so all three profiles take the DOM path. What it does not give up: a single response byte that any check looks at. Both binaries were run side by side on the same machine -- 49 URLs covering every serialization route, both dataset routes, the five /async-db shapes and /api/* -- and every body compared after canonicalisation: 0 mismatches, /json/50?m=6 at 8397 bytes on both. The only raw difference is top-level key order, which is now the sorted order nlohmann's default `std::map` object produces, and is the order this entry's reference path and /api/items have always used. scripts/validate.sh parses every JSON body with Python's `json` and diffs fields against data/dataset.json (json-comp at 1515-1575, json-h2c at 1930-1960, json-tls at 1655+, async-db at 2180-2219); no check compares a body byte for byte. The 401, 503, malformed-body and non-object-body paths were exercised by hand afterwards and are unchanged. --- frameworks/libuvcpp/Dockerfile | 9 +- frameworks/libuvcpp/README.md | 96 +++-- frameworks/libuvcpp/main.cc | 673 +++++---------------------------- frameworks/libuvcpp/meta.json | 2 +- 4 files changed, 138 insertions(+), 642 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index 5e2dd9859..25707ecc1 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -165,7 +165,14 @@ ENV DEBIAN_FRONTEND=noninteractive # 0.71 segments/frame and 1,014k frames/s. # # The pin then sat on `perf(json): 写出器的键与字符串值改成一次追加` (c8950ae), the -# change this entry's `/json` handler was rewritten onto. `uvcpp_json_writer`'s +# change this entry's `/json` handler was rewritten onto. **The entry no longer +# uses it.** `uvcpp_json_writer` is an alternative JSON serializer, and the JSON +# profiles measure serialization, so Standard mode wants the framework's +# standard serializer: the response is built as an `nlohmann::json` DOM and sent +# through `resp.json()`. That is a deliberate, measured loss on `json-comp`, +# `json-tls` and `/async-db` (see the README's `/json` note); the paragraphs +# below are kept as the record of why the pin moved when it did, not as a +# description of what this entry does now. `uvcpp_json_writer`'s # `key()` used to append the comma, the opening quote, the escaped body, the # closing quote and the colon separately -- four or five out-of-line # `std::string::append` calls per key, three per string value, 4073 of them on a diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index e2554fafb..458aabd7d 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -132,40 +132,36 @@ the profile forbids the framework from doing the JWT verification itself. documented static handler. Nothing in the entry reads or caches static bytes itself — the handler revalidates each request against the file's size and `mtime` (seconds and nanoseconds), so a replaced file is served immediately. -- **JSON is serialized per request through the framework's own writer.** The body - is built by walking the live dataset with `uvcpp_json_writer` — libuvcpp's - documented streaming constructor, the one `doc/json-guide.md` describes — and - the `total` field is computed inside the loop, from the values the writer is - reading. Nothing is pre-serialized, cached, or spliced out of a previously - rendered buffer, and the response is byte-for-byte what the previous - implementation produced. What moved is what writing it costs: a key now goes - out in one `append` instead of four or five, which on this box takes - `/json/50?m=6` from 80.4 to 63.2 us of server CPU per request — a repeat run - put the same pair at 73.8 and 59.2 us — measured best-of-3 interleaved rounds - with the server pinned to 6 single-threaded cores and saturated at 100% of - them. With `Accept-Encoding: gzip, br` the pair is 155.0 to - 141.1 us: compressing the 8.4 KB body costs more than serializing it, so the - gain is smaller there, not absent. End to end (h2load, best of 3): 73.7k -> - 94.2k req/s over h2c and 81.0k -> 96.3k over h1. Against the copy-then-dump - shape this handler used before — each item duplicated into a second DOM and - the whole payload dumped, still in `main.cc` as `build_json_general`, the - fallback for any value the writer cannot reproduce byte-for-byte — the whole - path costs 3.0x less CPU per request (176.0 -> 59.2 us). - The dataset is now read out of the DOM once at startup into a flat array - (`flat_item` plus a single character pool, ~5 KB for all 50 items), so the - handler walks that instead of 50 `std::map` trees; the walk it replaces cost - about 140 ns per item. That takes the serialization step for `/json/50?m=6` - from 31.0 to 23.4 us (9 interleaved rounds in one process, median of the - per-round ratios 0.751 / 0.769 / 0.793 over three repeats) and 10.8-11.7% of - server CPU per request end to end (11 paired rounds: 0.883 and 0.893 in two - runs). Nothing is pre-rendered — every request still walks the live items and - writes their bytes. The flattening is not trusted on faith: - `build_flat_dataset` writes each item both ways and enables the fast path only - if every one matches the reference byte for byte at three multipliers, so a - dataset of any other shape stays on the reference path and says which path it - is on at startup. Parsing is now the framework's `uvcpp_json_parse` rather - than nlohmann's `parse` directly, so every JSON call the entry makes is one - the library publishes. +- **JSON is serialized per request by the framework's standard serializer.** + The handler builds an `nlohmann::json` DOM out of the live dataset — the + framework's own `uvcpp_json` is nlohmann's `json`, and the response goes out + through `resp.json()`, which is the framework's `uvcpp_json_dump()` — and + `total` is computed inside the loop, per request. Nothing is pre-serialized, + cached, or spliced out of a previously rendered buffer. + + This is deliberate, and it is the one place this entry gives up speed for the + rules. The profile's JSON tests exist to measure serialization, Standard mode + asks for "the framework standard JSON serialization" + (`test-profiles/h1/json-compressed/implementation.md`) and for the + ecosystem's production-grade library over a hand-rolled alternative + (`implementation-rules/frameworks/standard.md`: "Experimental or hand-rolled + alternatives solely for benchmark performance are not permitted"), and + `implementation-rules/frameworks/tuned.md` lists "Alternative JSON + serializers" among the things *Tuned* allows. `uvcpp_json_writer` — the + library's streaming writer, a public and documented API that + `doc/json-guide.md` describes — is such an alternative serializer, so this + entry does not answer these profiles with it. The same applies to `/async-db`, + which used to walk the result rows straight into the writer. Reading is the + same rule the other way: `uvcpp_json_parse` and the `uvcpp_json_get_*` + accessors rather than nlohmann's `parse` and `value()` directly, so every JSON + call the entry makes is one the library publishes — and none of them can throw + out of a libuv callback, which is what those accessors exist for. +- **The cost of that choice, measured.** At the 1.7.2-dev pin this repository + was built on, `/json/50?m=6` through the writer cost 63.2 us of server CPU per + request against the DOM path's 176.0 us on the same 6 pinned cores, and the + writer also skipped the per-item DOM copy the DOM path makes. Both figures are + the ones this entry reported in earlier revisions; they are recorded here + because the entry no longer collects them. - **Compression is the framework's built-in response compression** (gzip and brotli), which answers `Accept-Encoding` per request and adds no `Content-Encoding` when the client sends none. Both encoders are the library's @@ -219,27 +215,23 @@ the profile forbids the framework from doing the JWT verification itself. between dialects, on purpose — `?` is a legal operator in Postgres for jsonb and hstore, so a translation layer could not tell the two apart. This is the entry's only Postgres-specific line. -- **`tags` is walked, not re-parsed.** Postgres returns JSONB as text (OID 3802 +- **`tags` is parsed, not spliced.** Postgres returns JSONB as text (OID 3802 is not one of the driver's mapped types, and an unmapped OID degrades to the - string libpq returned), and that string *is* the JSON array — so it goes into - the response as an array rather than as a quoted string. The response is built - with the framework's `uvcpp_json_writer`, and for `tags` that means walking the - literal and copying the strings it holds, normalising the whitespace Postgres - puts after the commas — the bytes `nlohmann::json::parse` followed by `dump()` - produced. The walk declines anything it cannot reproduce byte for byte: a - number (which `dump()` would re-render from a double), `true`/`false`/`null`, a - nested value, a `\"` or `\uXXXX` escape, or a byte outside printable ASCII - (where the two serializers may part ways). A decline puts the whole response - back on the DOM path, which is also where `parse_tags`' rule for a value that - will not parse — an empty array — still lives. -- **The writer path is proved before it answers.** The first response that - carries at least one row is built both ways and the writer's bytes must equal - the reference path's before the writer is allowed to answer with them; until - then, and after any decline, the reference path answers. One extra DOM build - per process, and the server logs which path took over. + string libpq returned), and that string *is* the JSON array — so it has to + reach the response as an array rather than as a quoted string. `parse_tags` + runs `uvcpp_json_parse` over it and puts the array in the DOM, which is then + serialized with everything else by `resp.json()`. It used to be walked and + copied through instead, which was faster and is what the writer path did; see + the `/json` note above for why that is not this entry's call to make. +- **One serializer, one path.** Every response body this profile serves — + including the ones carrying `tags` — is built as an `nlohmann::json` DOM and + dumped by the framework. There is no second, hand-written path to prove + against the first. - **Failure is not fatal and not silent-in-the-wrong-way.** With no `DATABASE_URL`, or a pool that will not open, `/async-db` answers the - documented `{"items":[],"count":0}` and the other profiles serve normally. + documented empty result set — `{"count":0,"items":[]}`, built by the same + serializer and in the same key order as a populated one — and the other + profiles serve normally. - **At the profile's own generator shape the pool saturates without failing.** `scripts/lib/tools/gcannon.sh` drives this endpoint with `-r 25`, so each socket serves 25 requests and is then retired and replaced; 1024 is how many diff --git a/frameworks/libuvcpp/main.cc b/frameworks/libuvcpp/main.cc index 49ef9d90d..feb8e7c8c 100644 --- a/frameworks/libuvcpp/main.cc +++ b/frameworks/libuvcpp/main.cc @@ -47,7 +47,6 @@ #include #include #include -#include #include #include #include @@ -254,11 +253,6 @@ const char kAsyncDbSql[] = "rating_score, rating_count FROM items " "WHERE price BETWEEN $1 AND $2 LIMIT $3"; -/// The documented fallback, used when the pool is not up. Byte-for-byte the -/// response the profile asks for when Postgres is unreachable or matches -/// nothing, so there is one shape to produce rather than two. -const char kAsyncDbEmpty[] = "{\"items\":[],\"count\":0}"; - // --------------------------------------------------------------------------- // production-stack -- cache-aside, in process // --------------------------------------------------------------------------- @@ -479,15 +473,39 @@ long long baseline_sum(const uvcpp_web_request& req) { return sum; } -/// The reference path, kept for the shapes the fast path declines: one DOM per -/// request, exactly what this handler did for every request before. Expects an -/// already-clamped `n`. -std::string build_json_general(int n, long long m) { +/// `{"count": N, "items": [...]}` for the first `count` dataset items, each +/// carrying `total = price * quantity * m`. `count` leads because the object is +/// nlohmann's default -- a `std::map` -- so keys come out sorted. +/// +/// Serialized per request, out of the live dataset, by the framework's standard +/// JSON serialization: an `nlohmann::json` DOM built from the live items and +/// handed to `resp.json()`, which is the framework's own `uvcpp_json_dump()` -- +/// `nlohmann::json::dump()` (`src/webapp/uvcpp_web_response.cpp:470`). Nothing +/// is rendered ahead of time and no response is cached; every request walks the +/// live items and writes their bytes. +/// +/// The serializer is not the entry's to pick. These profiles exist to measure +/// serialization, and Standard mode asks for "the framework standard JSON +/// serialization" (test-profiles/h1/json-compressed/implementation.md) and for +/// the ecosystem's production-grade library over a hand-rolled alternative +/// (implementation-rules/frameworks/standard.md:55). `uvcpp_json_writer`, the +/// streaming writer `` offers, is an alternative +/// serializer -- implementation-rules/frameworks/tuned.md lists "Alternative +/// JSON serializers" under Tuned -- so this entry does not route these +/// responses through it. +nlohmann::json build_json(int count, long long m) { + const int available = static_cast(g_dataset.size()); + int n = count; + if (n < 0) n = 0; + if (n > available) n = available; + nlohmann::json items = nlohmann::json::array(); for (int i = 0; i < n; ++i) { - nlohmann::json item = g_dataset[i]; - const long long price = item.value("price", 0LL); - const long long quantity = item.value("quantity", 0LL); + nlohmann::json item = g_dataset[static_cast(i)]; + long long price = 0; + long long quantity = 0; + uvcpp_json_get_int(item, "price", price); + uvcpp_json_get_int(item, "quantity", quantity); item["total"] = price * quantity * m; items.push_back(std::move(item)); } @@ -495,346 +513,7 @@ std::string build_json_general(int n, long long m) { nlohmann::json payload; payload["items"] = std::move(items); payload["count"] = n; - return payload.dump(); -} - -/// Writes one JSON value with the framework's own writer. Only the types whose -/// bytes that writer reproduces exactly are accepted: integers, strings, -/// booleans, arrays, objects. A float is declined -- `dump()` renders doubles -/// with a shortest-round-trip algorithm while the writer uses a different -/// format -- and so are binary and discarded values; the caller falls back to -/// the reference path for those. -bool put_json_value(const nlohmann::json& v, uvcpp_json_writer& w) { - switch (v.type()) { - case nlohmann::json::value_t::string: - return w.value(v.get_ref()) == json_write_status::OK; - case nlohmann::json::value_t::boolean: - return w.value(v.get()) == json_write_status::OK; - case nlohmann::json::value_t::number_integer: - case nlohmann::json::value_t::number_unsigned: - return w.value(static_cast(v.get())) == - json_write_status::OK; - case nlohmann::json::value_t::array: { - if (w.array_begin() != json_write_status::OK) return false; - for (auto e = v.begin(); e != v.end(); ++e) - if (!put_json_value(*e, w)) return false; - return w.array_end() == json_write_status::OK; - } - case nlohmann::json::value_t::object: { - if (w.object_begin() != json_write_status::OK) return false; - for (auto f = v.begin(); f != v.end(); ++f) { - if (w.key(f.key()) != json_write_status::OK) return false; - if (!put_json_value(f.value(), w)) return false; - } - return w.object_end() == json_write_status::OK; - } - default: - return false; - } -} - -// --------------------------------------------------------------------------- -// The flattened dataset -// --------------------------------------------------------------------------- - -/// A slice of `g_flat_pool`. -struct flat_str { - std::uint32_t off; - std::uint32_t len; -}; - -/// One dataset item, read out of the DOM once at startup. Every field is a -/// scalar or an offset into `g_flat_pool`, so all 50 items are one contiguous -/// block of a few KB: serializing a response walks that array instead of 50 -/// `std::map` trees (eight individually-allocated nodes each, plus two more -/// lookups for `price`/`quantity`). -struct flat_item { - long long id; - long long price; - long long quantity; - long long rating_count; - long long rating_score; - bool active; - flat_str name; - flat_str category; - std::uint32_t tag_first; // index into g_flat_tags - std::uint32_t tag_n; -}; - -std::vector g_flat_pool; // every string, one block; frozen after startup -std::vector g_flat_tags; // every item's tags, flattened -std::vector g_flat; // the items themselves - -/// False until build_flat_dataset() has built the flat form **and** proved it -/// reproduces the reference path byte for byte. `build_json` declines to the -/// slower paths while this is false, so a dataset of any other shape is still -/// answered correctly -- just at the reference path's cost. -bool g_flat_ok = false; - -/// Appends `s` to the pool and returns its slice. -flat_str flat_push(const std::string& s) { - flat_str r; - r.off = static_cast(g_flat_pool.size()); - r.len = static_cast(s.size()); - g_flat_pool.insert(g_flat_pool.end(), s.begin(), s.end()); - return r; -} - -/// An integer, or false. A float counts as "not an integer" on purpose: the -/// writer renders doubles with a different algorithm than `dump()` does, so -/// those items have to keep going through the reference path. -bool flat_int(const nlohmann::json& v, long long* out) { - if (v.is_number_unsigned()) { - const unsigned long long u = v.get(); - // Past `LLONG_MAX` the writer and `dump()` disagree on the digits, and - // truncating would be a lie either way -- decline. - if (u > static_cast(9223372036854775807LL)) return false; - *out = static_cast(u); - return true; - } - if (!v.is_number_integer()) return false; - *out = v.get(); - return true; -} - -/// A string, or false. Borrows from the DOM, which is not written before -/// startup ends; the callers copy into the pool immediately. -bool flat_str_of(const nlohmann::json& v, const std::string** out) { - if (!v.is_string()) return false; - *out = &v.get_ref(); - return true; -} - -/// The keys below are compile-time literals and their order is the one -/// nlohmann's default `std::map` object sorts them into -- which is what -/// `dump()` and the reference path emit. -template -bool flat_key(uvcpp_json_writer& w, const char (&k)[N]) { - return w.key(k, N - 1) == json_write_status::OK; -} - -/// Writes one flattened item. `total` is separate because it depends on the -/// request's `m`. -bool flat_write_item(const flat_item& it, long long total, - uvcpp_json_writer& w) { - const char* pool = g_flat_pool.data(); - if (!flat_key(w, "active")) return false; - if (w.value(it.active) != json_write_status::OK) return false; - if (!flat_key(w, "category")) return false; - if (w.value(pool + it.category.off, it.category.len) != json_write_status::OK) - return false; - if (!flat_key(w, "id")) return false; - if (w.value(it.id) != json_write_status::OK) return false; - if (!flat_key(w, "name")) return false; - if (w.value(pool + it.name.off, it.name.len) != json_write_status::OK) - return false; - if (!flat_key(w, "price")) return false; - if (w.value(it.price) != json_write_status::OK) return false; - if (!flat_key(w, "quantity")) return false; - if (w.value(it.quantity) != json_write_status::OK) return false; - if (!flat_key(w, "rating")) return false; - if (w.object_begin() != json_write_status::OK) return false; - if (!flat_key(w, "count")) return false; - if (w.value(it.rating_count) != json_write_status::OK) return false; - if (!flat_key(w, "score")) return false; - if (w.value(it.rating_score) != json_write_status::OK) return false; - if (w.object_end() != json_write_status::OK) return false; - if (!flat_key(w, "tags")) return false; - if (w.array_begin() != json_write_status::OK) return false; - for (std::uint32_t t = 0; t < it.tag_n; ++t) { - const flat_str& s = g_flat_tags[it.tag_first + t]; - if (w.value(pool + s.off, s.len) != json_write_status::OK) return false; - } - if (w.array_end() != json_write_status::OK) return false; - if (!flat_key(w, "total")) return false; - return w.value(total) == json_write_status::OK; -} - -/// Reads the DOM into the flat form and **proves** the flat form reproduces the -/// reference path before the fast path is allowed on. The proof is the point: -/// `flat_write_item` hard-codes this dataset's key order and field types, so a -/// dataset that differs in any way -- an extra key, a float, a missing -/// `rating` -- has to fall back rather than answer with different bytes. -void build_flat_dataset() { - g_flat_ok = false; - g_flat.clear(); - g_flat_tags.clear(); - g_flat_pool.clear(); - if (!g_dataset.is_array()) return; - - for (std::size_t i = 0; i < g_dataset.size(); ++i) { - const nlohmann::json& item = g_dataset[i]; - if (!item.is_object()) return; - - flat_item f; - { - auto it = item.find("id"); - if (it == item.end() || !flat_int(*it, &f.id)) return; - } - { - auto it = item.find("price"); - if (it == item.end() || !flat_int(*it, &f.price)) return; - } - { - auto it = item.find("quantity"); - if (it == item.end() || !flat_int(*it, &f.quantity)) return; - } - { - auto it = item.find("active"); - if (it == item.end() || !it->is_boolean()) return; - f.active = it->get(); - } - { - const std::string* s = nullptr; - auto it = item.find("name"); - if (it == item.end() || !flat_str_of(*it, &s)) return; - f.name = flat_push(*s); - } - { - const std::string* s = nullptr; - auto it = item.find("category"); - if (it == item.end() || !flat_str_of(*it, &s)) return; - f.category = flat_push(*s); - } - { - auto it = item.find("rating"); - if (it == item.end() || !it->is_object()) return; - auto c = it->find("count"); - auto s = it->find("score"); - if (c == it->end() || s == it->end()) return; - if (!flat_int(*c, &f.rating_count)) return; - if (!flat_int(*s, &f.rating_score)) return; - } - { - auto it = item.find("tags"); - if (it == item.end() || !it->is_array()) return; - const std::uint32_t first = static_cast(g_flat_tags.size()); - for (auto e = it->begin(); e != it->end(); ++e) { - const std::string* s = nullptr; - if (!flat_str_of(*e, &s)) return; - g_flat_tags.push_back(flat_push(*s)); - } - f.tag_first = first; - f.tag_n = static_cast(g_flat_tags.size()) - first; - } - g_flat.push_back(f); - } - - // Self-check: every item, written the flat way, has to come out byte for byte - // the same as writing it the reference way. Three multipliers, so a `total` - // that only lines up for one of them is caught too. - static const long long kCheckM[] = {1LL, 7LL, 1000003LL}; - for (std::size_t i = 0; i < g_flat.size(); ++i) { - for (std::size_t k = 0; k < sizeof(kCheckM) / sizeof(kCheckM[0]); ++k) { - const long long m = kCheckM[k]; - std::string got; - uvcpp_json_writer w(got); - if (w.object_begin() != json_write_status::OK) return; - const flat_item& fi = g_flat[i]; - if (!flat_write_item(fi, fi.price * fi.quantity * m, w)) return; - if (w.object_end() != json_write_status::OK) return; - if (w.finish() != json_write_status::OK) return; - - nlohmann::json ref = g_dataset[i]; - ref["total"] = g_flat[i].price * g_flat[i].quantity * m; - if (ref.dump() != got) { - std::fprintf(stderr, - "libuvcpp: dataset item %zu does not reproduce the " - "reference bytes -- /json stays on the reference path\n", - i); - g_flat_ok = false; - return; - } - } - } - g_flat_ok = true; -} - -/// The fast path itself: the same bytes the writer path produces, out of the -/// flat array. Declines to `build_json_general`, not to the writer path, if -/// anything goes wrong -- the writer path's own declines are all decided before -/// this point, and re-entering it would only re-walk the trees. -std::string build_json_flat(int n, long long m) { - std::string out; - out.reserve(static_cast(n) * 224u + 64u); - uvcpp_json_writer w(out); - if (w.object_begin() != json_write_status::OK) return build_json_general(n, m); - if (!flat_key(w, "items")) return build_json_general(n, m); - if (w.array_begin() != json_write_status::OK) return build_json_general(n, m); - for (int i = 0; i < n; ++i) { - const flat_item& it = g_flat[static_cast(i)]; - if (w.object_begin() != json_write_status::OK) return build_json_general(n, m); - if (!flat_write_item(it, it.price * it.quantity * m, w)) - return build_json_general(n, m); - if (w.object_end() != json_write_status::OK) return build_json_general(n, m); - } - if (w.array_end() != json_write_status::OK) return build_json_general(n, m); - if (!flat_key(w, "count")) return build_json_general(n, m); - if (w.value(static_cast(n)) != json_write_status::OK) - return build_json_general(n, m); - if (w.object_end() != json_write_status::OK) return build_json_general(n, m); - if (w.finish() != json_write_status::OK) return build_json_general(n, m); - return out; -} - -/// `{"items": [...], "count": N}` for the first `count` dataset items, each -/// carrying `total = price * quantity * m`. -/// -/// The body is serialized here, per request, out of the live dataset, through -/// **the framework's own documented serializer** (`uvcpp_json_writer`, the -/// streaming writer `doc/json-guide.md` documents): nothing is rendered ahead -/// of time, no response is cached, and no field is copied verbatim out of a -/// pre-serialized buffer -- every request walks the live items and writes their -/// bytes. What the writer buys over dumping each item with nlohmann is that a -/// key's four or five separate `std::string::append` calls become one, which on -/// a 50-item response is 4073 appends down to 1536 (see the library's -/// `CHANGELOG.md`, "uvcpp_json_writer 把「一个键」从 4~5 次追加压成 1 次"). -/// A DOM is still walked to read the values; what is skipped relative to the -/// reference path is only the throwaway `dump()` string per item and the second -/// DOM holding them all. -/// -/// The output is byte-for-byte what the previous implementation produced (the -/// two were compared over 70 `(count, m)` combinations, including the clamped -/// and empty ones), so this is a change of serializer, not of response. -std::string build_json(int count, long long m) { - const int available = static_cast(g_dataset.size()); - int n = count; - if (n < 0) n = 0; - if (n > available) n = available; - if (g_flat_ok) return build_json_flat(n, m); - - std::string out; - // Pure capacity hint: the largest item in this dataset serializes to 172 - // bytes and `,"total":N}` adds at most ~20, so 224 covers every item. An item - // longer than that is not an error, just one reallocation. - out.reserve(static_cast(n) * 224u + 64u); - uvcpp_json_writer w(out); - if (w.object_begin() != json_write_status::OK) return build_json_general(n, m); - if (w.key("items") != json_write_status::OK) return build_json_general(n, m); - if (w.array_begin() != json_write_status::OK) return build_json_general(n, m); - for (int i = 0; i < n; ++i) { - const nlohmann::json& item = g_dataset[static_cast(i)]; - if (!item.is_object()) return build_json_general(n, m); - if (w.object_begin() != json_write_status::OK) return build_json_general(n, m); - // nlohmann's default object is a `std::map`, so this walks the keys in the - // same sorted order `dump()` would have emitted them in. - for (auto f = item.begin(); f != item.end(); ++f) { - if (w.key(f.key()) != json_write_status::OK) return build_json_general(n, m); - if (!put_json_value(f.value(), w)) return build_json_general(n, m); - } - if (w.member("total", item.value("price", 0LL) * item.value("quantity", 0LL) * m) - != json_write_status::OK) { - return build_json_general(n, m); - } - if (w.object_end() != json_write_status::OK) return build_json_general(n, m); - } - if (w.array_end() != json_write_status::OK) return build_json_general(n, m); - if (w.member("count", static_cast(n)) != json_write_status::OK) { - return build_json_general(n, m); - } - if (w.object_end() != json_write_status::OK) return build_json_general(n, m); - if (w.finish() != json_write_status::OK) return build_json_general(n, m); - return out; + return payload; } // --------------------------------------------------------------------------- @@ -864,9 +543,8 @@ void handle_json(uvcpp_web_request& req, uvcpp_web_response& resp, const std::string* count = req.param("count"); const std::string* multiplier = req.query("m"); const long long m = multiplier != nullptr ? to_ll(*multiplier) : 1; - resp.json_str(build_json(count != nullptr ? static_cast(to_ll(*count)) - : 0, - m)); + resp.json(build_json(count != nullptr ? static_cast(to_ll(*count)) : 0, + m)); resp.end(); } @@ -892,28 +570,29 @@ int query_int(const uvcpp_web_request& req, const char* name, int fallback) { /// request: the profile has exactly one documented fallback shape and this is /// not it. nlohmann::json parse_tags(const std::string& text) { - if (!text.empty()) { - try { - nlohmann::json parsed = nlohmann::json::parse(text); - if (parsed.is_array()) return parsed; - } catch (const std::exception&) { - // falls through to the empty array - } - } + nlohmann::json parsed; + if (uvcpp_json_parse(text, parsed) == json_status::OK && parsed.is_array()) + return parsed; return nlohmann::json::array(); } -/// `{"items": [...], "count": N}` for the rows Postgres returned. +/// `{"count": N, "items": [...]}` for the rows Postgres returned, as a DOM the +/// framework's `resp.json()` serializes per request. `count` leads because the +/// serializer is nlohmann's and an object there is a `std::map`, so keys come +/// out sorted; that is also the order this entry's reference path and +/// `/api/items` have always used. /// /// Column names are the profile's, not an invention: `id`, `name`, `category`, /// `price`, `quantity`, `active`, `tags`, then `rating_score`/`rating_count` /// folded into one nested `rating` object. Every number stays an integer -- /// the profile reads `price` and `rating.score` as INTEGER and says so. /// `count` is the number of rows actually returned, never the requested limit. -std::string build_async_db_json(uvcpp_db_status status, - const uvcpp_db_table& table) { - if (!uvcpp_db_ok(status)) return kAsyncDbEmpty; - +/// +/// `tags` is JSONB and the driver hands JSONB over as text, so the column +/// arrives as the array literal `["fast", "new"]`. `parse_tags` above is what +/// turns that literal into the array the response carries -- an nlohmann +/// `parse()` per row, which is the documented reading of that column. +nlohmann::json build_async_db_json(const uvcpp_db_table& table) { const std::vector& rows = table.rows(); nlohmann::json items = nlohmann::json::array(); for (size_t i = 0; i < rows.size(); ++i) { @@ -936,194 +615,18 @@ std::string build_async_db_json(uvcpp_db_status status, nlohmann::json payload; payload["count"] = static_cast(rows.size()); payload["items"] = std::move(items); - return payload.dump(); + return payload; } -/// Writes one `tags` cell straight into the writer. -/// -/// `tags` is JSONB and the driver hands JSONB over as text, so the column -/// already arrives as the array literal (`["fast", "new"]`). `parse_tags` above -/// re-parses that into a DOM and the serializer dumps the DOM back out -- a lexer -/// pass and a tree per row, 13.8% of this profile's client CPU under callgrind, -/// to recover bytes the string is already holding. -/// -/// So this walks the literal instead, and normalises the whitespace Postgres -/// puts after the commas, which is exactly what `parse` + `dump()` produce. It -/// copies through only what it can reproduce byte for byte: a flat array of -/// strings whose bytes are all printable ASCII (`0x20`-`0x7E`) other than `"` and -/// `\`, which cannot appear inside one. Every other shape is a decline, and a -/// decline puts the whole response back on the DOM path above, where -/// `parse_tags`' "a value that will not parse becomes `[]`" rule lives: -/// -/// - a number would come back re-rendered from a double (`1.50` -> `1.5`), -/// - `true`/`false`/`null` and a nested array or object would need the tree, -/// - a `\"` or a `\uXXXX` would be re-encoded by `dump()`'s escaper, -/// - a byte outside printable ASCII is where the two serializers may part -/// ways: `dump()` validates and repairs UTF-8, and what a byte `>= 0x80` -/// means depends on the database's encoding, so those bytes are not copied -/// through on a `==` argument, -/// - a literal that is not a well-formed array of strings is not JSON at all. -/// -/// The printable-ASCII rule is what makes the byte identity a property of the -/// accepted set rather than of the responses the proof below happened to sample. -/// RFC 8259 requires escaping only `"`, `\` and the bytes below `0x20`, and the -/// walker admits none of those three: `"` ends the string, `\` is a decline, and -/// the range check leaves the controls out. The library's serializer escapes -/// exactly that set and copies `0x80` upwards through untouched (`uvcpp_json_writer`'s -/// escape table says so in as many words), and `dump()` follows the same rule, so -/// every accepted byte reaches both outputs unchanged. -bool async_db_tags(uvcpp_json_writer& w, const std::string& text) { - const char* p = text.data(); - const size_t n = text.size(); - // A NULL cell and an empty string both read back as "" here, and both parse - // to the empty array -- which is what `parse_tags` answers for either. - if (n == 0) { - return w.array_begin() == json_write_status::OK && - w.array_end() == json_write_status::OK; - } - if (n < 2 || p[0] != '[' || p[n - 1] != ']') return false; - if (w.array_begin() != json_write_status::OK) return false; - const size_t end = n - 1; // the closing `]` - size_t i = 1; // past the opening `[` - bool first = true; - for (;;) { - while (i < end && (p[i] == ' ' || p[i] == '\t' || p[i] == '\n' || - p[i] == '\r')) { - ++i; - } - if (i == end) break; // the `]`, and nothing but whitespace before it - if (!first) { - if (p[i] != ',') return false; - ++i; - while (i < end && (p[i] == ' ' || p[i] == '\t' || p[i] == '\n' || - p[i] == '\r')) { - ++i; - } - if (i == end) return false; // a trailing comma - } - if (p[i] != '"') return false; - size_t j = i + 1; - while (j < end && p[j] != '"') { - if (p[j] == '\\') return false; - const unsigned char c = static_cast(p[j]); - if (c < 0x20 || c > 0x7E) return false; - ++j; - } - if (j == end) return false; // unterminated - if (w.value(p + i + 1, j - i - 1) != json_write_status::OK) return false; - i = j + 1; - first = false; - } - return w.array_end() == json_write_status::OK; -} - -/// The same bytes `build_async_db_json` produces, written straight through -/// `uvcpp_json_writer`: no DOM for the rows and no throwaway `dump()` string. -/// -/// The keys come out in the order nlohmann's `std::map` sorts them into, which -/// is the order `dump()` emits and the order `flat_write_item` hard-codes for -/// `/json` -- `active`, `category`, `id`, `name`, `price`, `quantity`, `rating`, -/// `tags`, with `count` before `score` inside `rating` and `count` before -/// `items` at the top. -/// -/// The columns are read by position, not by name: the SELECT above is a fixed -/// list, and a name lookup is a linear scan of the column names, which at 200 -/// lookups per response was 12.4% of this profile's client CPU. A result whose -/// columns are not that SELECT is caught by the proof in `async_db_json` below, -/// which compares these bytes against the name-based path's before this one is -/// allowed to answer. -/// -/// An empty return means "declined" and `async_db_json` re-runs the DOM path. A -/// built response is never empty -- it always opens with `{"count":`. -std::string build_async_db_json_writer(uvcpp_db_status status, - const uvcpp_db_table& table) { - const std::string declined; - if (!uvcpp_db_ok(status)) return declined; - const std::vector& rows = table.rows(); - std::string out; - out.reserve(rows.size() * 224u + 32u); - uvcpp_json_writer w(out); - // The root object first: `key()` is `MISUSE` outside an object, so a key at - // depth 0 fails the writer for good. `build_json_flat` opens its object the - // same way for the same reason. - if (w.object_begin() != json_write_status::OK) return declined; - if (!flat_key(w, "count")) return declined; - if (w.value(static_cast(rows.size())) != json_write_status::OK) { - return declined; - } - if (!flat_key(w, "items")) return declined; - if (w.array_begin() != json_write_status::OK) return declined; - for (size_t i = 0; i < rows.size(); ++i) { - const uvcpp_db_row& row = rows[i]; - if (w.object_begin() != json_write_status::OK) return declined; - if (!flat_key(w, "active")) return declined; - if (w.value(row[5].to_bool()) != json_write_status::OK) return declined; - if (!flat_key(w, "category")) return declined; - if (w.value(row[2].to_text()) != json_write_status::OK) return declined; - if (!flat_key(w, "id")) return declined; - if (w.value(row[0].to_int64()) != json_write_status::OK) return declined; - if (!flat_key(w, "name")) return declined; - if (w.value(row[1].to_text()) != json_write_status::OK) return declined; - if (!flat_key(w, "price")) return declined; - if (w.value(row[3].to_int64()) != json_write_status::OK) return declined; - if (!flat_key(w, "quantity")) return declined; - if (w.value(row[4].to_int64()) != json_write_status::OK) return declined; - if (!flat_key(w, "rating")) return declined; - if (w.object_begin() != json_write_status::OK) return declined; - if (!flat_key(w, "count")) return declined; - if (w.value(row[8].to_int64()) != json_write_status::OK) return declined; - if (!flat_key(w, "score")) return declined; - if (w.value(row[7].to_int64()) != json_write_status::OK) return declined; - if (w.object_end() != json_write_status::OK) return declined; - if (!flat_key(w, "tags")) return declined; - if (!async_db_tags(w, row[6].to_text())) return declined; - if (w.object_end() != json_write_status::OK) return declined; - } - if (w.array_end() != json_write_status::OK) return declined; - if (w.object_end() != json_write_status::OK) return declined; - if (w.finish() != json_write_status::OK) return declined; - return out; -} - -/// Whether the writer path has been shown to reproduce the reference path's -/// bytes: `-1` is "not proven yet", and while it is unproven the reference path -/// answers. The writer path is never served unproven. -std::atomic g_async_json_ok(-1); - -/// The body `handle_async_db` sends. -/// -/// The proof is the point, and it is the same one `/json` runs before its -/// flattened path is allowed on: the writer path may answer only after it has -/// produced the reference path's bytes on a live response. It runs on the first -/// response that carries at least one row, because every shape that could -/// differ -- a numeric or nested `tags`, an escaped string, columns in another -/// order -- is a row shape, so an empty result set proves nothing and does not -/// count. One extra DOM build per process. After that the flag decides, and the -/// DOM is reached again only when the writer declines a response. -std::string async_db_json(uvcpp_db_status status, const uvcpp_db_table& table) { - const int proven = g_async_json_ok.load(std::memory_order_acquire); - if (proven < 0 && uvcpp_db_ok(status) && !table.rows().empty()) { - std::string fast = build_async_db_json_writer(status, table); - const std::string dom = build_async_db_json(status, table); - if (!fast.empty() && fast == dom) { - g_async_json_ok.store(1, std::memory_order_release); - std::fprintf(stderr, "libuvcpp: /async-db answers from the writer path\n"); - return fast; - } - g_async_json_ok.store(0, std::memory_order_release); - if (!fast.empty()) { - std::fprintf(stderr, - "libuvcpp: the writer path does not reproduce the reference " - "bytes for /async-db -- /async-db stays on the reference " - "path\n"); - } - return dom; - } - if (proven > 0) { - std::string fast = build_async_db_json_writer(status, table); - if (!fast.empty()) return fast; - } - return build_async_db_json(status, table); +/// The documented fallback shape, for when the pool is not up or the statement +/// did not run. Built here rather than spelled out as a literal, so the empty +/// answer and the populated one are the same object graph and cannot drift +/// apart; `resp.json()` serializes it exactly like any other. +nlohmann::json empty_table_json() { + nlohmann::json payload; + payload["count"] = 0; + payload["items"] = nlohmann::json::array(); + return payload; } /// The retry half of init_database(): keep trying to open the pool, and give up @@ -1211,8 +714,8 @@ void init_database() { const char* url = std::getenv("DATABASE_URL"); if (url == nullptr || *url == '\0') { std::fprintf(stderr, - "libuvcpp: DATABASE_URL is not set -- /async-db answers %s\n", - kAsyncDbEmpty); + "libuvcpp: DATABASE_URL is not set -- /async-db answers the " + "empty result set\n"); return; } const char* env_max = std::getenv("DATABASE_MAX_CONN"); @@ -1264,7 +767,7 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, if (limit > 50) limit = 50; if (!g_db_ready) { - resp.json_str(kAsyncDbEmpty); + resp.json(empty_table_json()); resp.end(); return; } @@ -1281,7 +784,11 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, const int rc = g_db.query( app->loop(), kAsyncDbSql, params, [rp, next](uvcpp_db_status status, const uvcpp_db_table& table) { - rp->json_str(async_db_json(status, table)); + if (uvcpp_db_ok(status)) { + rp->json(build_async_db_json(table)); + } else { + rp->json(empty_table_json()); + } rp->end(); next(); }); @@ -1289,7 +796,7 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, // Not dispatched: per the pool's contract the callback will not run, so // this is the only reply this request gets. `next` was never copied, so // the chain is not left suspended. - resp.json_str(kAsyncDbEmpty); + resp.json(empty_table_json()); resp.end(); } } @@ -1308,7 +815,9 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, /// A request that reached the framework without the edge's verified user id. /// 401 rather than a redirect: the profile's auth wall is checked as a status. void send_unauthorized(uvcpp_web_response& resp) { - resp.status(401).json_str("{\"error\":\"unauthorized\"}"); + nlohmann::json error; + error["error"] = "unauthorized"; + resp.status(401).json(error); resp.end(); } @@ -1381,7 +890,7 @@ void handle_api_item_get(uvcpp_web_app* app, uvcpp_web_request& req, return; } if (!g_db_ready) { - send_cached(resp, "{}", false); + send_cached(resp, nlohmann::json::object().dump(), false); return; } @@ -1393,13 +902,14 @@ void handle_api_item_get(uvcpp_web_app* app, uvcpp_web_request& req, [rp, next, key](uvcpp_db_status status, const uvcpp_db_table& table) { const std::string json = item_body(status, table); if (!json.empty()) cache_store(key, json, kItemTtlMs); - send_cached(*rp, json.empty() ? std::string("{}") : json, false); + send_cached(*rp, json.empty() ? nlohmann::json::object().dump() : json, + false); next(); }); if (rc < 0) { // Not dispatched, so no callback is coming. `next` was never copied and // the chain is not left suspended. - send_cached(resp, "{}", false); + send_cached(resp, nlohmann::json::object().dump(), false); } } @@ -1427,13 +937,14 @@ void handle_api_item_post(uvcpp_web_app* app, uvcpp_web_request& req, std::string name; long long price = 0; long long quantity = 0; - try { - const nlohmann::json body = nlohmann::json::parse(req.body_str()); - name = body.value("name", std::string()); - price = body.value("price", 0LL); - quantity = body.value("quantity", 0LL); - } catch (const std::exception&) { - // defaults above + nlohmann::json body; + if (uvcpp_json_parse(req.body_str(), body) == json_status::OK) { + // The library's getters, not `body["k"]` or `body.value()`: both of those + // throw out of a libuv callback on the wrong shape, and this handler runs + // inside one. These leave the defaults above in place instead. + name = uvcpp_json_get_string(body, "name", std::string()); + uvcpp_json_get_int(body, "price", price); + uvcpp_json_get_int(body, "quantity", quantity); } if (!g_db_ready) { @@ -1482,7 +993,7 @@ void handle_api_me(uvcpp_web_app* app, uvcpp_web_request& req, return; } if (!g_db_ready) { - resp.status(503).json_str("{}"); + resp.status(503).json(nlohmann::json::object()); resp.end(); return; } @@ -1495,7 +1006,7 @@ void handle_api_me(uvcpp_web_app* app, uvcpp_web_request& req, [rp, next, key](uvcpp_db_status status, const uvcpp_db_table& table) { const std::string json = user_body(status, table); if (!json.empty()) cache_store(key, json, kUserTtlMs); - rp->json_str(json.empty() ? std::string("{}") : json); + rp->json_str(json.empty() ? nlohmann::json::object().dump() : json); rp->end(); next(); }); @@ -1503,7 +1014,7 @@ void handle_api_me(uvcpp_web_app* app, uvcpp_web_request& req, // Unlike `/api/items`, there is no cache header to get right here and no // documented empty shape -- an unavailable database is a 503, not a body // that claims a user. - resp.status(503).json_str("{}"); + resp.status(503).json(nlohmann::json::object()); resp.end(); } } @@ -1665,20 +1176,6 @@ int main() { "with an empty item list\n"); } - // Reads the dataset into the flat array and self-checks it against the - // reference path; a dataset it cannot reproduce leaves `/json` on the - // reference path (see build_flat_dataset). Which of the two it ended up on - // is printed, because "the two arms answered the same bytes" is worth - // nothing on its own -- it is also what a fast path that never ran looks - // like. - build_flat_dataset(); - std::fprintf(stderr, - "libuvcpp: /json fast path: %s (%zu items, %zu tag strings, " - "%zu B pool + %zu B items)\n", - g_flat_ok ? "flattened dataset" : "reference DOM path", - g_flat.size(), g_flat_tags.size(), g_flat_pool.size(), - g_flat.size() * sizeof(flat_item)); - // The one connection pool for the process, opened before any listener so a // bad DATABASE_URL is reported at startup rather than on the first request. // A pool per worker loop would be wrong here: the pool owns the connections, diff --git a/frameworks/libuvcpp/meta.json b/frameworks/libuvcpp/meta.json index 80f0e626e..acfd643af 100644 --- a/frameworks/libuvcpp/meta.json +++ b/frameworks/libuvcpp/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "libuv", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON written per request by the framework's own uvcpp_json_writer (values read out of the uvcpp_json DOM, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "enabled": true, "tests": [ From 9b0c2e2f16379bfb9783c55ac9772d0332a75a54 Mon Sep 17 00:00:00 2001 From: Antruly Date: Fri, 16 Oct 2026 00:30:33 +0800 Subject: [PATCH 08/17] Move the pin to 02ae418 (1.7.2-dev), four code commits past 8150720 The batch, and why each member is taken, is written out in the Dockerfile. In one line each: `e716010` stops building a log line nobody installed a callback to receive (106.16/106.15 -> 102.18/102.22 allocations per response, A B B A); `3cbfc9a` lets one QUIC datagram carry STREAM frames from several streams (sendmsg per request 1.0316/1.0360 -> 0.0428/0.0561); `370d6ef` sizes each QUIC send chunk from what that write needs instead of reserving 64 KiB flat (72 374.8 -> 7 507.5 bytes allocated per request, -89.6%, peak RSS 91 -> 50 MiB); and `02ae418` lets `resp.json()` take the dumped body instead of copying it (-1.0000 memcpy per request, -8 397 bytes copied per request -- the body's own length -- and -0.83% instructions per request on /json/50?m=6). Every one of those is a local deterministic counter, not an on-board measurement, and none is offered as a predicted board movement. What this revision is actually for is the entry side, which is not in the pin: the fifth run measured `uvcpp_json_writer` (its `async-db-1024` row at 271.85 us of CPU per response against the fourth run's 407.2), and this entry answers those profiles through the framework's standard serializer again (9964faf). So the fifth run's `json-*` and `async-db` rows are not this entry's baseline; the fourth run's are. Validated locally before asking for a run, on this machine, against the pin's own library built here: the entry's five listeners up with the whole fragment suite 9/9 and the WebSocket suite 7/7; `/json/50?m=6` 8 397 bytes plain and 1 393 bytes under `Accept-Encoding: gzip, br` (gzip alone 1 567); the `.br` twin of a static file served with `Content-Encoding: br`; and the two h3 shapes at 783 971 req/s (baseline) and 17 219 req/s (static) with 0 `5xx` between them. --- frameworks/libuvcpp/Dockerfile | 75 +++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index 25707ecc1..6296cbc4d 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -37,31 +37,78 @@ ENV DEBIAN_FRONTEND=noninteractive # and the bytes under it re-uploaded, so a SHA is the only thing that fixes # which revision gets built. `3fdd6208` was `v1.6.1` -- what the first version # of this entry built, and what the platform measured as the board's 58 req/s on -# `baseline-h3-64`. The pin has since moved three times: to `2d5fe74` +# `baseline-h3-64`. The pin has since moved four times: to `2d5fe74` # (`1.6.20-dev`), then to `6eede33` (`1.6.51-dev`), which was the tip of the # library's `master` when it was taken and is the revision the platform's fourth -# run measured, and now to `8150720` (`1.7.2-dev`), which is the `v1.7.0` tag -# plus one documentation commit and two that change code. +# run measured, then to `8150720` (`1.7.2-dev`), which is the `v1.7.0` tag plus +# one documentation commit and two that change code, and now to `02ae418` +# (`1.7.2-dev`), which is four code commits past it. # Each move was measured against the previous pin before being taken; the # criteria for the `6eede33` batch are further down this file, and each commit's # own criterion is in the library's `CHANGELOG.md`. # -# The `8150720` move is the one batch here whose criterion is *not* an on-board -# measurement. Both commits carry a local deterministic counter as their -# criterion -- `ebeaa20` is `-1.90%` user-mode instructions per request on the -# h1 shape (22,459.6 -> 22,032.4 Ir, callgrind, the path normalisation -# collapsing to one function with a fast path for the already-normal form), and -# `8150720` is `204.78 -> 102.77` `malloc`s per response on the async-db shape -# (the result-set vectors now reserve, which `PQntuples`/`PQnfields` let them do -# exactly). Neither is offered as a predicted board movement. The h1 profiles +# The `02ae418` move is for the next run, and like the `8150720` move it is a +# batch whose members carry local deterministic counters rather than on-board +# measurements. The four code commits between the two pins -- the other two are +# documentation -- and what each one's criterion is: +# +# * `e716010` stops the database path from building a log line when no log +# callback is installed: `106.16 / 106.15 -> 102.18 / 102.22` allocations +# per response (A B B A), with `utime + stime` unmoved -- 3.96 allocations +# is about 0.12 us, under the rig's resolution, so the allocation count is +# the criterion. `async-db` is Reference-only and contributes nothing to +# the score; this one is a counter and nothing more. +# * `3cbfc9a` and `370d6ef` are the QUIC send path. The first lets one +# datagram carry STREAM frames from several streams (`sendmsg` per request +# `1.0316 / 1.0360 -> 0.0428 / 0.0561`), the second sizes each send chunk +# from what that write needs instead of reserving 64 KiB flat (`72 374.8 -> +# 7 507.5` bytes allocated per request, `-89.6%`, peak RSS 91 -> 50 MiB on +# the local h3 rig, where the flat reserve is the same shape as the 238 MiB +# the board reports for `baseline-h3-64`). Both are measured on the h3 +# profiles this entry subscribes to, on this machine's rig rather than the +# board's, and are not offered as predictions. +# * `02ae418` lets `resp.json()` hand the dumped body to the response instead +# of copying it: `-1.0000` `memcpy` per request and `-8 397` bytes copied +# per request -- the body's own length, so it is that copy and not some +# other one -- with `-0.83%` instructions per request on `/json/50?m=6`. +# The instruction figure is 0.83% of one handler, smaller than the spread +# between two board readings; the counters are the criterion. +# +# What this run also carries, and what is *not* in the pin, is the entry side: +# the JSON profiles answer through the framework's standard serializer again +# (see the `/json` note below), and the brotli encoder is built on. The first of +# those is the larger effect, and the fifth run measured its size for us by +# accident: that run's tree had the entry on `uvcpp_json_writer` (9257623), the +# revision the maintainer then objected to, and it reported `async-db-1024` at +# `271.85` us of CPU per response against the fourth run's `407.2` -- a fall that +# the doc attributes to that one entry commit and to nothing in the library. +# This entry has the DOM path back, so its `async-db` row starts from the higher +# number, and its `json-*` rows are likewise the ones to compare against the +# fourth run's rather than the fifth's. Brotli works the other way, on a profile +# whose score is quadratic in bytes per response (see the note above +# `ARG LIBUVCPP_REF`). +# +# The `8150720` move, like the `02ae418` one above and for the same reason, is a +# batch whose criterion is *not* an on-board measurement. Its two commits carry +# a local deterministic counter as their criterion -- `ebeaa20` is `-1.90%` +# user-mode instructions per request on the h1 shape (22,459.6 -> 22,032.4 Ir, +# callgrind, the path normalisation collapsing to one function with a fast path +# for the already-normal form), and `8150720` is `204.78 -> 102.77` `malloc`s per +# response on the async-db shape (the result-set vectors now reserve, which +# `PQntuples`/`PQnfields` let them do exactly). Neither is offered as a predicted +# board movement. The h1 profiles # are all `pipeline = 1`, so `-1.90%` of user-mode instructions is about `0.35%` # of the end-to-end time on those profiles, which is smaller than the spread # between two platform readings of the same profile; and `async-db` is # Reference-only, contributing nothing to the board's score and showing up only # in its own `cpu_per_req_us`, which is user plus system time where the change # is on the user side. So both are accounted for as "the counter certainly went -# down", not as a score, and what this revision is for is the fifth run, whose -# numbers will say whether either is visible from outside. The library-side +# down", not as a score. The fifth run has since reported on them, and neither is +# separable in it: that round's rps carried an upward bias of up to 3.2% even on +# the profiles pinned by external factors, where the paragraph above puts +# `ebeaa20`'s `-1.90%` of user-mode instructions at about 0.35% of end-to-end +# time; and `async-db` is one row per round, where that same round's entry-side +# change moved the number by 135 us. The library-side # criteria and the guards (byte-identical response bodies, the postgres suite # byte-identical across arms, 127/127 ctest) are in its `CHANGELOG.md`. # @@ -255,7 +302,7 @@ ENV DEBIAN_FRONTEND=noninteractive # serve the `.br` twins shipped in `data/static` whether or not this flag is set: that # selection is runtime, in `http_pick_precompressed_variants()`. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git -ARG LIBUVCPP_REF=81507207a4ad496a66019a40e3993dcf34ba275f +ARG LIBUVCPP_REF=02ae41806a25112ee7050ccfd4132138b1eb2630 ARG OPENSSL_VERSION=3.5.0 From 16e6e499bc029bfeeed095995a3de2d0eff9d448 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Oct 2026 17:03:25 +0000 Subject: [PATCH 09/17] Benchmark results: libuvcpp [skip ci] --- site/data/frameworks.json | 2 +- site/data/results/libuvcpp.json | 692 +++++++++--------- site/static/logs/8gbit/512/libuvcpp.log | 3 +- site/static/logs/async-db/1024/libuvcpp.log | 2 - site/static/logs/async/32000/libuvcpp.log | 3 +- .../static/logs/baseline-h2/1024/libuvcpp.log | 3 +- site/static/logs/baseline-h2/256/libuvcpp.log | 3 +- .../logs/baseline-h2c/1024/libuvcpp.log | 3 +- .../static/logs/baseline-h2c/256/libuvcpp.log | 3 +- .../logs/baseline-h2c/4096/libuvcpp.log | 3 +- site/static/logs/baseline-h3/64/libuvcpp.log | 3 +- site/static/logs/baseline/4096/libuvcpp.log | 3 +- .../logs/echo-ws-limited/4096/libuvcpp.log | 3 +- .../logs/echo-ws-limited/512/libuvcpp.log | 3 +- .../logs/echo-ws-pipeline/16384/libuvcpp.log | 3 +- .../logs/echo-ws-pipeline/4096/libuvcpp.log | 3 +- .../logs/echo-ws-pipeline/512/libuvcpp.log | 3 +- site/static/logs/echo-ws/16384/libuvcpp.log | 3 +- site/static/logs/echo-ws/4096/libuvcpp.log | 3 +- site/static/logs/echo-ws/512/libuvcpp.log | 3 +- site/static/logs/json-comp/16384/libuvcpp.log | 3 +- site/static/logs/json-comp/4096/libuvcpp.log | 3 +- site/static/logs/json-h2c/1024/libuvcpp.log | 3 +- site/static/logs/json-h2c/4096/libuvcpp.log | 3 +- site/static/logs/json-tls/4096/libuvcpp.log | 3 +- .../static/logs/latency-10k/1024/libuvcpp.log | 3 +- site/static/logs/latency-1m/1024/libuvcpp.log | 3 +- .../logs/latency-500k-8cpu/1024/libuvcpp.log | 3 +- .../logs/limited-conn/4096/libuvcpp.log | 3 +- site/static/logs/pipelined/4096/libuvcpp.log | 3 +- site/static/logs/static-h2/1024/libuvcpp.log | 3 +- site/static/logs/static-h2/256/libuvcpp.log | 3 +- site/static/logs/static-h3/64/libuvcpp.log | 3 +- site/static/logs/static-tls/1024/libuvcpp.log | 3 +- .../logs/unary-grpc-tls/1024/libuvcpp.log | 3 +- .../logs/unary-grpc-tls/256/libuvcpp.log | 3 +- site/static/logs/unary-grpc/1024/libuvcpp.log | 3 +- site/static/logs/unary-grpc/256/libuvcpp.log | 3 +- 38 files changed, 382 insertions(+), 419 deletions(-) diff --git a/site/data/frameworks.json b/site/data/frameworks.json index cefcafdd0..9f34e9246 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -1153,7 +1153,7 @@ }, "libuvcpp": { "dir": "libuvcpp", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON written per request by the framework's own uvcpp_json_writer (values read out of the uvcpp_json DOM, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway stacks put a stock Caddy edge in front of it.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "type": "emerging", "engine": "libuv", diff --git a/site/data/results/libuvcpp.json b/site/data/results/libuvcpp.json index 4357a11d9..c71e6bb19 100644 --- a/site/data/results/libuvcpp.json +++ b/site/data/results/libuvcpp.json @@ -4,44 +4,44 @@ "8gbit-512": { "framework": "libuvcpp", "language": "C++", - "rps": 49344, - "avg_latency": "108.1us", - "p99_latency": "148.0us", - "cpu": "299.4%", - "memory": "207MiB", + "rps": 49371, + "avg_latency": "111.1us", + "p99_latency": "144.0us", + "cpu": "327.1%", + "memory": "166MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "513.08MB/s", - "input_bw": "481.88MB/s", + "bandwidth": "513.36MB/s", + "input_bw": "482.14MB/s", "reconnects": 0, - "status_2xx": 246792, + "status_2xx": 246927, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 16241895, - "cpu_per_req_us": 65.8121, + "cpu_usec": 16205469, + "cpu_per_req_us": 65.6286, "target_rate": 50000, - "rate_ratio": 0.9869, - "p99_9_latency": "4694.0us" + "rate_ratio": 0.9874, + "p99_9_latency": "5938.0us" }, "async-32000": { "framework": "libuvcpp", "language": "C++", - "rps": 1568496, - "avg_latency": "19.84ms", - "p99_latency": "22.20ms", - "cpu": "6409.0%", - "memory": "488MiB", + "rps": 1533457, + "avg_latency": "20.03ms", + "p99_latency": "22.50ms", + "cpu": "6212.9%", + "memory": "490MiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "234.80MB/s", - "input_bw": "71.80MB/s", + "bandwidth": "229.56MB/s", + "input_bw": "70.20MB/s", "reconnects": 0, - "status_2xx": 15684968, + "status_2xx": 15334577, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -49,19 +49,19 @@ "async-db-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 98855, - "avg_latency": "9.88ms", - "p99_latency": "37.70ms", - "cpu": "2687.4%", - "memory": "172MiB", + "rps": 87341, + "avg_latency": "11.33ms", + "p99_latency": "39.90ms", + "cpu": "3381.8%", + "memory": "174MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "382.29MB/s", - "input_bw": "6.60MB/s", - "reconnects": 39257, - "status_2xx": 988551, + "bandwidth": "337.88MB/s", + "input_bw": "5.83MB/s", + "reconnects": 34627, + "status_2xx": 873417, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -69,19 +69,19 @@ "baseline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1789003, - "avg_latency": "2.30ms", - "p99_latency": "4.86ms", - "cpu": "6320.1%", - "memory": "139MiB", + "rps": 1708528, + "avg_latency": "2.40ms", + "p99_latency": "4.97ms", + "cpu": "6713.7%", + "memory": "141MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "267.79MB/s", - "input_bw": "138.20MB/s", + "bandwidth": "255.75MB/s", + "input_bw": "131.98MB/s", "reconnects": 0, - "status_2xx": 8945017, + "status_2xx": 8542643, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -89,18 +89,18 @@ "baseline-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 5247351, - "avg_latency": "18.57ms", - "p99_latency": "103.45ms", - "cpu": "6218.8%", + "rps": 5435683, + "avg_latency": "17.94ms", + "p99_latency": "113.99ms", + "cpu": "6189.7%", "memory": "174MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "136.78MB/s", + "bandwidth": "141.40MB/s", "reconnects": 0, - "status_2xx": 26551600, + "status_2xx": 27450200, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -108,18 +108,18 @@ "baseline-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 5552266, - "avg_latency": "4.82ms", - "p99_latency": "33.27ms", - "cpu": "6202.3%", - "memory": "96MiB", + "rps": 5425606, + "avg_latency": "4.95ms", + "p99_latency": "23.19ms", + "cpu": "6345.3%", + "memory": "112MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "143.83MB/s", + "bandwidth": "140.55MB/s", "reconnects": 0, - "status_2xx": 27927900, + "status_2xx": 27290800, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -127,18 +127,18 @@ "baseline-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 5465988, - "avg_latency": "18.17ms", - "p99_latency": "302.20ms", - "cpu": "6426.9%", - "memory": "182MiB", + "rps": 5552958, + "avg_latency": "17.88ms", + "p99_latency": "373.11ms", + "cpu": "6334.9%", + "memory": "144MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "142.47MB/s", + "bandwidth": "145.02MB/s", "reconnects": 0, - "status_2xx": 27657900, + "status_2xx": 28153500, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -146,18 +146,18 @@ "baseline-h2c-256": { "framework": "libuvcpp", "language": "C++", - "rps": 5752882, - "avg_latency": "4.68ms", - "p99_latency": "58.77ms", - "cpu": "6049.5%", + "rps": 5680497, + "avg_latency": "4.79ms", + "p99_latency": "37.21ms", + "cpu": "6189.5%", "memory": "83MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "149.03MB/s", + "bandwidth": "147.16MB/s", "reconnects": 0, - "status_2xx": 28937000, + "status_2xx": 28572900, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -165,18 +165,18 @@ "baseline-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 5268627, - "avg_latency": "72.83ms", - "p99_latency": "518.07ms", - "cpu": "6152.4%", - "memory": "790MiB", + "rps": 5316594, + "avg_latency": "72.97ms", + "p99_latency": "523.77ms", + "cpu": "6380.1%", + "memory": "795MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "138.53MB/s", + "bandwidth": "139.23MB/s", "reconnects": 0, - "status_2xx": 26870000, + "status_2xx": 27008300, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -184,18 +184,18 @@ "baseline-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 1422023, - "avg_latency": "3.30ms", - "p99_latency": "9.57ms", - "cpu": "4117.6%", - "memory": "195MiB", + "rps": 3030258, + "avg_latency": "1.28ms", + "p99_latency": "4.00ms", + "cpu": "3451.5%", + "memory": "134MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "18.97MB/s", + "bandwidth": "40.49MB/s", "reconnects": 0, - "status_2xx": 7124338, + "status_2xx": 15181595, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -203,18 +203,18 @@ "echo-ws-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 2577600, - "avg_latency": "5.91ms", - "p99_latency": "7.49ms", - "cpu": "6140.4%", - "memory": "336MiB", + "rps": 2611015, + "avg_latency": "5.57ms", + "p99_latency": "11.60ms", + "cpu": "6099.8%", + "memory": "329MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "17.61MB/s", + "bandwidth": "17.83MB/s", "reconnects": 0, - "status_2xx": 12888002, + "status_2xx": 13055079, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -222,18 +222,18 @@ "echo-ws-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 2884971, - "avg_latency": "1.42ms", - "p99_latency": "2.49ms", - "cpu": "6272.5%", - "memory": "134MiB", + "rps": 2929961, + "avg_latency": "1.39ms", + "p99_latency": "1.91ms", + "cpu": "6350.7%", + "memory": "130MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "19.29MB/s", + "bandwidth": "19.61MB/s", "reconnects": 0, - "status_2xx": 14424856, + "status_2xx": 14649807, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -241,18 +241,18 @@ "echo-ws-512": { "framework": "libuvcpp", "language": "C++", - "rps": 2833406, - "avg_latency": "180us", - "p99_latency": "436us", - "cpu": "6616.9%", - "memory": "75MiB", + "rps": 2851128, + "avg_latency": "179us", + "p99_latency": "424us", + "cpu": "6547.2%", + "memory": "78MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "18.91MB/s", + "bandwidth": "19.03MB/s", "reconnects": 0, - "status_2xx": 14167030, + "status_2xx": 14255642, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -260,18 +260,18 @@ "echo-ws-limited-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1767053, - "avg_latency": "382us", - "p99_latency": "820us", - "cpu": "6142.6%", - "memory": "93MiB", + "rps": 1795680, + "avg_latency": "381us", + "p99_latency": "822us", + "cpu": "6180.1%", + "memory": "95MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "33.52MB/s", - "reconnects": 883828, - "status_2xx": 8835268, + "bandwidth": "34.07MB/s", + "reconnects": 897786, + "status_2xx": 8978400, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -279,18 +279,18 @@ "echo-ws-limited-512": { "framework": "libuvcpp", "language": "C++", - "rps": 1600650, - "avg_latency": "205us", - "p99_latency": "618us", - "cpu": "5818.9%", - "memory": "89MiB", + "rps": 1593577, + "avg_latency": "206us", + "p99_latency": "617us", + "cpu": "5883.1%", + "memory": "93MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "30.37MB/s", - "reconnects": 800296, - "status_2xx": 8003251, + "bandwidth": "30.24MB/s", + "reconnects": 796788, + "status_2xx": 7967888, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -298,18 +298,18 @@ "echo-ws-pipeline-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 27866425, - "avg_latency": "8.15ms", - "p99_latency": "10.70ms", - "cpu": "6179.7%", - "memory": "350MiB", + "rps": 28207107, + "avg_latency": "8.53ms", + "p99_latency": "10.50ms", + "cpu": "6280.7%", + "memory": "358MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "186.38MB/s", + "bandwidth": "188.66MB/s", "reconnects": 0, - "status_2xx": 139332128, + "status_2xx": 141035537, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -317,18 +317,18 @@ "echo-ws-pipeline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 31035676, - "avg_latency": "2.10ms", - "p99_latency": "2.66ms", - "cpu": "6378.8%", - "memory": "134MiB", + "rps": 31601507, + "avg_latency": "2.07ms", + "p99_latency": "2.77ms", + "cpu": "6412.9%", + "memory": "136MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "207.19MB/s", + "bandwidth": "210.96MB/s", "reconnects": 0, - "status_2xx": 155178384, + "status_2xx": 158007536, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -336,18 +336,18 @@ "echo-ws-pipeline-512": { "framework": "libuvcpp", "language": "C++", - "rps": 31580057, - "avg_latency": "258us", - "p99_latency": "617us", - "cpu": "6664.2%", - "memory": "78MiB", + "rps": 30734953, + "avg_latency": "265us", + "p99_latency": "618us", + "cpu": "6655.7%", + "memory": "79MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "210.87MB/s", + "bandwidth": "205.13MB/s", "reconnects": 0, - "status_2xx": 157900286, + "status_2xx": 153674768, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -355,115 +355,115 @@ "gateway-64-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 68878, - "avg_latency": "420.25ms", - "p99_latency": "4.31s", - "cpu": "3628.5%", - "memory": "4.9GiB", + "rps": 120098, + "avg_latency": "184.89ms", + "p99_latency": "4.87s", + "cpu": "5119.2%", + "memory": "2.0GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "784.76MB/s", + "bandwidth": "1.48GB/s", "reconnects": 0, - "status_2xx": 347836, + "status_2xx": 608899, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 104350, - "tpl_baseline": 69567, - "tpl_json": 121742, - "tpl_async_db": 52175, - "cpu_breakdown": "server: 743% 406MiB | proxy: 2885% 4.6GiB" + "tpl_static": 182669, + "tpl_baseline": 121779, + "tpl_json": 213114, + "tpl_async_db": 91334, + "cpu_breakdown": "server: 2702% 475MiB | proxy: 2417% 1.5GiB" }, "gateway-64-512": { "framework": "libuvcpp", "language": "C++", - "rps": 77207, - "avg_latency": "194.87ms", - "p99_latency": "2.14s", - "cpu": "3824.5%", - "memory": "2.7GiB", + "rps": 122364, + "avg_latency": "63.61ms", + "p99_latency": "4.95s", + "cpu": "4870.7%", + "memory": "1.4GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "837.34MB/s", + "bandwidth": "1.46GB/s", "reconnects": 0, - "status_2xx": 389124, + "status_2xx": 617941, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 116737, - "tpl_baseline": 77824, - "tpl_json": 136193, - "tpl_async_db": 58368, - "cpu_breakdown": "server: 749% 220MiB | proxy: 3076% 2.5GiB" + "tpl_static": 185382, + "tpl_baseline": 123588, + "tpl_json": 216279, + "tpl_async_db": 92691, + "cpu_breakdown": "server: 3357% 225MiB | proxy: 1514% 1.2GiB" }, "gateway-h3-256": { "framework": "libuvcpp", "language": "C++", - "rps": 62335, - "avg_latency": "127.17ms", - "p99_latency": "774.90ms", - "cpu": "2751.7%", + "rps": 62641, + "avg_latency": "126.31ms", + "p99_latency": "779.62ms", + "cpu": "3509.0%", "memory": "1.4GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "629.21MB/s", + "bandwidth": "630.18MB/s", "reconnects": 0, - "status_2xx": 313546, + "status_2xx": 315089, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 94063, - "tpl_baseline": 62709, - "tpl_json": 109741, - "tpl_async_db": 47031, - "cpu_breakdown": "server: 567% 96MiB | proxy: 2185% 1.3GiB" + "tpl_static": 94526, + "tpl_baseline": 63017, + "tpl_json": 110281, + "tpl_async_db": 47263, + "cpu_breakdown": "server: 1388% 105MiB | proxy: 2120% 1.3GiB" }, "gateway-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 84432, - "avg_latency": "24.11ms", - "p99_latency": "83.80ms", - "cpu": "3445.3%", - "memory": "371MiB", + "rps": 80155, + "avg_latency": "25.38ms", + "p99_latency": "83.09ms", + "cpu": "4577.5%", + "memory": "446MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "874.20MB/s", + "bandwidth": "826.79MB/s", "reconnects": 0, - "status_2xx": 423005, + "status_2xx": 401577, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 126901, - "tpl_baseline": 84601, - "tpl_json": 148051, - "tpl_async_db": 63450, - "cpu_breakdown": "server: 807% 78MiB | proxy: 2639% 293MiB" + "tpl_static": 120473, + "tpl_baseline": 80315, + "tpl_json": 140551, + "tpl_async_db": 60236, + "cpu_breakdown": "server: 1977% 103MiB | proxy: 2600% 344MiB" }, "json-comp-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 330411, - "avg_latency": "29.91ms", - "p99_latency": "632.40ms", - "cpu": "6504.6%", - "memory": "141MiB", + "rps": 59118, + "avg_latency": "107.88ms", + "p99_latency": "3.37s", + "cpu": "6127.5%", + "memory": "140MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "472.09MB/s", - "input_bw": "24.58MB/s", - "reconnects": 65344, - "status_2xx": 1652058, + "bandwidth": "76.15MB/s", + "input_bw": "4.40MB/s", + "reconnects": 11117, + "status_2xx": 295594, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -471,19 +471,19 @@ "json-comp-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 335926, - "avg_latency": "11.93ms", - "p99_latency": "283.90ms", - "cpu": "6263.0%", - "memory": "107MiB", + "rps": 50389, + "avg_latency": "73.49ms", + "p99_latency": "1.40s", + "cpu": "6239.0%", + "memory": "131MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "480.11MB/s", - "input_bw": "24.99MB/s", - "reconnects": 66863, - "status_2xx": 1679631, + "bandwidth": "64.92MB/s", + "input_bw": "3.75MB/s", + "reconnects": 9429, + "status_2xx": 251946, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -491,18 +491,18 @@ "json-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 1653574, - "avg_latency": "19.34ms", - "p99_latency": "404.75ms", - "cpu": "6364.1%", - "memory": "303MiB", + "rps": 656607, + "avg_latency": "49.04ms", + "p99_latency": "1.03s", + "cpu": "6343.2%", + "memory": "306MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "5.52GB/s", + "bandwidth": "2.19GB/s", "reconnects": 0, - "status_2xx": 8367089, + "status_2xx": 3315867, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -510,18 +510,18 @@ "json-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1580650, - "avg_latency": "80.18ms", - "p99_latency": "427.19ms", - "cpu": "6241.2%", - "memory": "976MiB", + "rps": 634021, + "avg_latency": "189.03ms", + "p99_latency": "4.98s", + "cpu": "6240.8%", + "memory": "1.0GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "5.31GB/s", + "bandwidth": "2.11GB/s", "reconnects": 0, - "status_2xx": 8061320, + "status_2xx": 3220828, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -529,18 +529,18 @@ "json-tls-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 724360, - "avg_latency": "5.58ms", - "p99_latency": "63.30ms", - "cpu": "6332.4%", - "memory": "395MiB", + "rps": 403673, + "avg_latency": "9.93ms", + "p99_latency": "58.04ms", + "cpu": "6210.0%", + "memory": "411MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.48GB", + "bandwidth": "1.38GB", "reconnects": 0, - "status_2xx": 3693728, + "status_2xx": 2058790, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -548,58 +548,58 @@ "latency-10k-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 9981, - "avg_latency": "83.3us", - "p99_latency": "121.0us", - "cpu": "36.3%", - "memory": "78MiB", + "rps": 9982, + "avg_latency": "81.3us", + "p99_latency": "117.0us", + "cpu": "36.2%", + "memory": "76MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, "bandwidth": "1.56MB/s", "reconnects": 0, - "status_2xx": 199636, + "status_2xx": 199671, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 7524856, - "cpu_per_req_us": 37.6929, + "cpu_usec": 7558233, + "cpu_per_req_us": 37.8534, "target_rate": 10000, - "rate_ratio": 0.9981, - "p99_9_latency": "1007.0us" + "rate_ratio": 0.9982, + "p99_9_latency": "603.0us" }, "latency-1m-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 998040, - "avg_latency": "144.5us", - "p99_latency": "312.0us", - "cpu": "4324.7%", + "rps": 997684, + "avg_latency": "150.4us", + "p99_latency": "314.0us", + "cpu": "4218.0%", "memory": "81MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "155.69MB/s", + "bandwidth": "155.64MB/s", "reconnects": 0, - "status_2xx": 19963925, + "status_2xx": 19956937, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 793199863, - "cpu_per_req_us": 39.7317, + "cpu_usec": 775551605, + "cpu_per_req_us": 38.8613, "target_rate": 1000000, - "rate_ratio": 0.998, - "p99_9_latency": "1319.0us" + "rate_ratio": 0.9977, + "p99_9_latency": "2651.0us" }, "latency-500k-8cpu-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 264277, - "avg_latency": "4654746.7us", - "p99_latency": "9973760.0us", - "cpu": "779.4%", + "rps": 264320, + "avg_latency": "4661938.9us", + "p99_latency": "9891840.0us", + "cpu": "779.2%", "memory": "32MiB", "connections": 1024, "threads": 64, @@ -607,32 +607,32 @@ "pipeline": 1, "bandwidth": "41.23MB/s", "reconnects": 0, - "status_2xx": 5287537, + "status_2xx": 5288405, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 159636697, - "cpu_per_req_us": 30.1911, + "cpu_usec": 159603498, + "cpu_per_req_us": 30.1799, "target_rate": 500000, "rate_ratio": 0.5286, - "p99_9_latency": "10342400.0us" + "p99_9_latency": "10579968.0us" }, "limited-conn-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1414762, - "avg_latency": "2.88ms", - "p99_latency": "45.70ms", - "cpu": "6336.9%", - "memory": "90MiB", + "rps": 1385176, + "avg_latency": "2.91ms", + "p99_latency": "46.30ms", + "cpu": "6288.4%", + "memory": "88MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "211.77MB/s", - "input_bw": "109.29MB/s", - "reconnects": 707797, - "status_2xx": 7073811, + "bandwidth": "207.34MB/s", + "input_bw": "107.00MB/s", + "reconnects": 692877, + "status_2xx": 6925881, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -640,18 +640,18 @@ "pipelined-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 7491589, - "avg_latency": "8.75ms", - "p99_latency": "10.50ms", - "cpu": "6391.6%", - "memory": "149MiB", + "rps": 7326797, + "avg_latency": "8.97ms", + "p99_latency": "13.40ms", + "cpu": "6643.2%", + "memory": "153MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "1.10GB/s", + "bandwidth": "1.07GB/s", "reconnects": 0, - "status_2xx": 37457948, + "status_2xx": 36633989, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -659,66 +659,66 @@ "production-stack-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 48440, - "avg_latency": "332.06ms", - "p99_latency": "1.43s", - "cpu": "3437.3%", - "memory": "2.8GiB", + "rps": 204370, + "avg_latency": "71.66ms", + "p99_latency": "2.60s", + "cpu": "4218.6%", + "memory": "2.0GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "494.29MB/s", + "bandwidth": "2.26GB/s", "reconnects": 0, - "status_2xx": 245107, + "status_2xx": 1034115, "status_3xx": 0, - "status_4xx": 1, + "status_4xx": 0, "status_5xx": 0, - "tpl_static": 73532, - "tpl_baseline": 24510, - "tpl_items": 122553, - "tpl_me": 24510, - "cpu_breakdown": "cache: 0% 4MiB | edge: 2871% 2.4GiB | authsvc: 269% 345MiB | server: 297% 161MiB" + "tpl_static": 310234, + "tpl_baseline": 103411, + "tpl_items": 517057, + "tpl_me": 103411, + "cpu_breakdown": "cache: 0% 5MiB | edge: 2749% 1.3GiB | authsvc: 726% 565MiB | server: 743% 116MiB" }, "production-stack-256": { "framework": "libuvcpp", "language": "C++", - "rps": 54088, - "avg_latency": "78.06ms", - "p99_latency": "531.29ms", - "cpu": "3238.0%", - "memory": "928MiB", + "rps": 258622, + "avg_latency": "16.77ms", + "p99_latency": "87.65ms", + "cpu": "4823.0%", + "memory": "1.2GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "519.39MB/s", + "bandwidth": "2.80GB/s", "reconnects": 0, - "status_2xx": 272064, + "status_2xx": 1300873, "status_3xx": 0, - "status_4xx": 2, + "status_4xx": 0, "status_5xx": 0, - "tpl_static": 81619, - "tpl_baseline": 27206, - "tpl_items": 136032, - "tpl_me": 27206, - "cpu_breakdown": "cache: 0% 13MiB | edge: 2785% 792MiB | authsvc: 248% 58MiB | server: 204% 75MiB" + "tpl_static": 390261, + "tpl_baseline": 130087, + "tpl_items": 650436, + "tpl_me": 130087, + "cpu_breakdown": "cache: 0% 14MiB | edge: 3131% 988MiB | authsvc: 687% 118MiB | server: 1005% 88MiB" }, "static-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 252927, - "avg_latency": "122.01ms", - "p99_latency": "385.15ms", - "cpu": "2584.0%", - "memory": "805MiB", + "rps": 239512, + "avg_latency": "128.29ms", + "p99_latency": "440.36ms", + "cpu": "2285.9%", + "memory": "511MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.85GB/s", + "bandwidth": "3.64GB/s", "reconnects": 0, - "status_2xx": 1279815, + "status_2xx": 1211933, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -726,18 +726,18 @@ "static-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 232078, - "avg_latency": "36.58ms", - "p99_latency": "150.32ms", - "cpu": "2212.8%", - "memory": "191MiB", + "rps": 242138, + "avg_latency": "35.82ms", + "p99_latency": "156.09ms", + "cpu": "2181.2%", + "memory": "187MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.54GB/s", + "bandwidth": "3.69GB/s", "reconnects": 0, - "status_2xx": 1167355, + "status_2xx": 1215536, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -745,18 +745,18 @@ "static-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 122803, - "avg_latency": "30.41ms", - "p99_latency": "122.43ms", - "cpu": "3139.0%", - "memory": "788MiB", + "rps": 113382, + "avg_latency": "32.94ms", + "p99_latency": "115.37ms", + "cpu": "2980.8%", + "memory": "272MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.87GB/s", + "bandwidth": "1.72GB/s", "reconnects": 0, - "status_2xx": 615248, + "status_2xx": 568047, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -764,18 +764,18 @@ "static-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 190070, - "avg_latency": "5.38ms", - "p99_latency": "20.49ms", - "cpu": "2930.6%", - "memory": "277MiB", + "rps": 198927, + "avg_latency": "5.13ms", + "p99_latency": "18.62ms", + "cpu": "3077.4%", + "memory": "409MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.93GB", + "bandwidth": "3.07GB", "reconnects": 0, - "status_2xx": 965990, + "status_2xx": 1014429, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -783,18 +783,18 @@ "unary-grpc-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3760124, - "avg_latency": "26.25ms", - "p99_latency": "268.10ms", - "cpu": "6440.4%", - "memory": "413MiB", + "rps": 3725148, + "avg_latency": "26.52ms", + "p99_latency": "408.94ms", + "cpu": "6439.3%", + "memory": "414MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "156.01MB/s", + "bandwidth": "154.54MB/s", "reconnects": 0, - "status_2xx": 19026230, + "status_2xx": 18849250, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -802,18 +802,18 @@ "unary-grpc-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3818427, - "avg_latency": "7.14ms", - "p99_latency": "113.42ms", - "cpu": "6042.7%", - "memory": "146MiB", + "rps": 3827901, + "avg_latency": "6.90ms", + "p99_latency": "80.09ms", + "cpu": "5973.8%", + "memory": "145MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "157.22MB/s", + "bandwidth": "158.24MB/s", "reconnects": 0, - "status_2xx": 19168506, + "status_2xx": 19292624, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -821,18 +821,18 @@ "unary-grpc-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3511060, - "avg_latency": "28.50ms", - "p99_latency": "260.84ms", - "cpu": "5982.0%", - "memory": "598MiB", + "rps": 3533276, + "avg_latency": "27.60ms", + "p99_latency": "157.81ms", + "cpu": "6160.5%", + "memory": "594MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "145.67MB/s", + "bandwidth": "146.87MB/s", "reconnects": 0, - "status_2xx": 17765966, + "status_2xx": 17913710, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -840,18 +840,18 @@ "unary-grpc-tls-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3720594, - "avg_latency": "7.12ms", - "p99_latency": "97.65ms", - "cpu": "6087.4%", + "rps": 3695725, + "avg_latency": "7.17ms", + "p99_latency": "93.63ms", + "cpu": "6138.1%", "memory": "163MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "153.50MB/s", + "bandwidth": "152.47MB/s", "reconnects": 0, - "status_2xx": 18714592, + "status_2xx": 18589499, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/8gbit/512/libuvcpp.log b/site/static/logs/8gbit/512/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/8gbit/512/libuvcpp.log +++ b/site/static/logs/8gbit/512/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/async-db/1024/libuvcpp.log b/site/static/logs/async-db/1024/libuvcpp.log index a31abbcc5..a679d3d0b 100644 --- a/site/static/logs/async-db/1024/libuvcpp.log +++ b/site/static/logs/async-db/1024/libuvcpp.log @@ -1,3 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) libuvcpp: Postgres pool up (max=256) -libuvcpp: /async-db answers from the writer path diff --git a/site/static/logs/async/32000/libuvcpp.log b/site/static/logs/async/32000/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/async/32000/libuvcpp.log +++ b/site/static/logs/async/32000/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline-h2/1024/libuvcpp.log b/site/static/logs/baseline-h2/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline-h2/1024/libuvcpp.log +++ b/site/static/logs/baseline-h2/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline-h2/256/libuvcpp.log b/site/static/logs/baseline-h2/256/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline-h2/256/libuvcpp.log +++ b/site/static/logs/baseline-h2/256/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline-h2c/1024/libuvcpp.log b/site/static/logs/baseline-h2c/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline-h2c/1024/libuvcpp.log +++ b/site/static/logs/baseline-h2c/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline-h2c/256/libuvcpp.log b/site/static/logs/baseline-h2c/256/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline-h2c/256/libuvcpp.log +++ b/site/static/logs/baseline-h2c/256/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline-h2c/4096/libuvcpp.log b/site/static/logs/baseline-h2c/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline-h2c/4096/libuvcpp.log +++ b/site/static/logs/baseline-h2c/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline-h3/64/libuvcpp.log b/site/static/logs/baseline-h3/64/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline-h3/64/libuvcpp.log +++ b/site/static/logs/baseline-h3/64/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/baseline/4096/libuvcpp.log b/site/static/logs/baseline/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/baseline/4096/libuvcpp.log +++ b/site/static/logs/baseline/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws-limited/4096/libuvcpp.log b/site/static/logs/echo-ws-limited/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws-limited/4096/libuvcpp.log +++ b/site/static/logs/echo-ws-limited/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws-limited/512/libuvcpp.log b/site/static/logs/echo-ws-limited/512/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws-limited/512/libuvcpp.log +++ b/site/static/logs/echo-ws-limited/512/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws-pipeline/16384/libuvcpp.log b/site/static/logs/echo-ws-pipeline/16384/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws-pipeline/16384/libuvcpp.log +++ b/site/static/logs/echo-ws-pipeline/16384/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws-pipeline/4096/libuvcpp.log b/site/static/logs/echo-ws-pipeline/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws-pipeline/4096/libuvcpp.log +++ b/site/static/logs/echo-ws-pipeline/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws-pipeline/512/libuvcpp.log b/site/static/logs/echo-ws-pipeline/512/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws-pipeline/512/libuvcpp.log +++ b/site/static/logs/echo-ws-pipeline/512/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws/16384/libuvcpp.log b/site/static/logs/echo-ws/16384/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws/16384/libuvcpp.log +++ b/site/static/logs/echo-ws/16384/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws/4096/libuvcpp.log b/site/static/logs/echo-ws/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws/4096/libuvcpp.log +++ b/site/static/logs/echo-ws/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/echo-ws/512/libuvcpp.log b/site/static/logs/echo-ws/512/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/echo-ws/512/libuvcpp.log +++ b/site/static/logs/echo-ws/512/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/json-comp/16384/libuvcpp.log b/site/static/logs/json-comp/16384/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/json-comp/16384/libuvcpp.log +++ b/site/static/logs/json-comp/16384/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/json-comp/4096/libuvcpp.log b/site/static/logs/json-comp/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/json-comp/4096/libuvcpp.log +++ b/site/static/logs/json-comp/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/json-h2c/1024/libuvcpp.log b/site/static/logs/json-h2c/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/json-h2c/1024/libuvcpp.log +++ b/site/static/logs/json-h2c/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/json-h2c/4096/libuvcpp.log b/site/static/logs/json-h2c/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/json-h2c/4096/libuvcpp.log +++ b/site/static/logs/json-h2c/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/json-tls/4096/libuvcpp.log b/site/static/logs/json-tls/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/json-tls/4096/libuvcpp.log +++ b/site/static/logs/json-tls/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/latency-10k/1024/libuvcpp.log b/site/static/logs/latency-10k/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/latency-10k/1024/libuvcpp.log +++ b/site/static/logs/latency-10k/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/latency-1m/1024/libuvcpp.log b/site/static/logs/latency-1m/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/latency-1m/1024/libuvcpp.log +++ b/site/static/logs/latency-1m/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/latency-500k-8cpu/1024/libuvcpp.log b/site/static/logs/latency-500k-8cpu/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/latency-500k-8cpu/1024/libuvcpp.log +++ b/site/static/logs/latency-500k-8cpu/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/limited-conn/4096/libuvcpp.log b/site/static/logs/limited-conn/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/limited-conn/4096/libuvcpp.log +++ b/site/static/logs/limited-conn/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/pipelined/4096/libuvcpp.log b/site/static/logs/pipelined/4096/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/pipelined/4096/libuvcpp.log +++ b/site/static/logs/pipelined/4096/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/static-h2/1024/libuvcpp.log b/site/static/logs/static-h2/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/static-h2/1024/libuvcpp.log +++ b/site/static/logs/static-h2/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/static-h2/256/libuvcpp.log b/site/static/logs/static-h2/256/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/static-h2/256/libuvcpp.log +++ b/site/static/logs/static-h2/256/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/static-h3/64/libuvcpp.log b/site/static/logs/static-h3/64/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/static-h3/64/libuvcpp.log +++ b/site/static/logs/static-h3/64/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/static-tls/1024/libuvcpp.log b/site/static/logs/static-tls/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/static-tls/1024/libuvcpp.log +++ b/site/static/logs/static-tls/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/unary-grpc-tls/1024/libuvcpp.log b/site/static/logs/unary-grpc-tls/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/unary-grpc-tls/1024/libuvcpp.log +++ b/site/static/logs/unary-grpc-tls/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/unary-grpc-tls/256/libuvcpp.log b/site/static/logs/unary-grpc-tls/256/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/unary-grpc-tls/256/libuvcpp.log +++ b/site/static/logs/unary-grpc-tls/256/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/unary-grpc/1024/libuvcpp.log b/site/static/logs/unary-grpc/1024/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/unary-grpc/1024/libuvcpp.log +++ b/site/static/logs/unary-grpc/1024/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set diff --git a/site/static/logs/unary-grpc/256/libuvcpp.log b/site/static/logs/unary-grpc/256/libuvcpp.log index c561222e5..365775ad6 100644 --- a/site/static/logs/unary-grpc/256/libuvcpp.log +++ b/site/static/logs/unary-grpc/256/libuvcpp.log @@ -1,2 +1 @@ -libuvcpp: /json fast path: flattened dataset (50 items, 115 tag strings, 1508 B pool + 3600 B items) -libuvcpp: DATABASE_URL is not set -- /async-db answers {"items":[],"count":0} +libuvcpp: DATABASE_URL is not set -- /async-db answers the empty result set From 16cba4ad3eaa3432b452e6951b20384e73990b7b Mon Sep 17 00:00:00 2001 From: Antruly Date: Fri, 16 Oct 2026 05:10:58 +0800 Subject: [PATCH 10/17] Build the brotli encoder back out of the entry The sixth run turned the framework's brotli encoder on to see what the profile's own `Accept-Encoding: gzip, br` would negotiate; it negotiates `br`, and the run measured what that costs. `json-compressed` is the one profile in this repository scored on response bytes (`effectiveRps = rps * (minBpr / bpr)^2`). Against the fifth run, whose tree answered with gzip, `json-comp-4096` went 335 926 -> 50 389 rps (-85.0%) with `CPU/req` 186.4 -> 1 238.2 us, and `json-comp-16384` 330 411 -> 59 118 (-82.1%) with 196.9 -> 1 036.5 us. Both runs saturate the same 62 cores (62.4 / 61.3), so that is cost per request. The batch's other three `json-*` profiles pay 58-66 us each for the standard serializer they share with these two, which leaves about 990 us of the increase on the encoder. On the same request measured here with the server pinned to 8 cores, only `Accept-Encoding` swapped, gzip is 257 us and `br` 508 us -- and the `br` arm takes 72.43 minor faults per request, the encoder's ~700 KB of state being returned to the kernel and re-faulted between requests. The library's `master` removes that churn (`0aaa415`, one commit past this pin), which takes `br` to 385 us; 385 against 257 is still 1.5x, and `br` only buys `(1429.2 / 1288.4)^2 = 1.23` on the byte term that the profile scores. So it is a net loss on the only term it can move. Nothing else is given up. `Accept-Encoding` is still honoured per request; every client here that offers `br` also accepts gzip (`gzip, br` on json-comp, `br;q=1, gzip;q=0.8` on the h2 and h3 generators); the static legs' `.br` twins are chosen at runtime by `http_pick_precompressed_variants()` and are unaffected by this flag; and the profile's own guide asks for what it now gets -- its `standard=` rule wants "the framework or engine's built-in response compression (middleware, filter, or equivalent)", its `tuned=` rule only adds "as long as the output is valid gzip or brotli", and the worked example under "Expected response" answers a request that carried `Accept-Encoding: gzip, br` with `Content-Encoding: gzip`. Brotli appears in the page's title; no rule there prefers it. `frameworks/libuvcpp/{Dockerfile,README.md,meta.json}` carry the flag, the two package drops and the rationale; `site/data/frameworks.json` is `scripts/rebuild_site_data.py` re-run over the new `meta.json`. The pin is unchanged at `02ae418`, so this is one variable against the sixth run. --- frameworks/libuvcpp/Dockerfile | 136 ++++++++++++++++++++------------- frameworks/libuvcpp/README.md | 39 +++++++--- frameworks/libuvcpp/meta.json | 2 +- site/data/frameworks.json | 2 +- 4 files changed, 114 insertions(+), 65 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index 6296cbc4d..756dfd30a 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -76,17 +76,21 @@ ENV DEBIAN_FRONTEND=noninteractive # # What this run also carries, and what is *not* in the pin, is the entry side: # the JSON profiles answer through the framework's standard serializer again -# (see the `/json` note below), and the brotli encoder is built on. The first of -# those is the larger effect, and the fifth run measured its size for us by -# accident: that run's tree had the entry on `uvcpp_json_writer` (9257623), the -# revision the maintainer then objected to, and it reported `async-db-1024` at -# `271.85` us of CPU per response against the fourth run's `407.2` -- a fall that -# the doc attributes to that one entry commit and to nothing in the library. +# (see the `/json` note below). The fifth run measured the size of that change +# for us by accident: its tree had the entry on `uvcpp_json_writer` (9257623), +# the revision the maintainer then objected to, and it reported `async-db-1024` +# at `271.85` us of CPU per response against the fourth run's `407.2` -- a fall +# that the doc attributes to that one entry commit and to nothing in the library. # This entry has the DOM path back, so its `async-db` row starts from the higher # number, and its `json-*` rows are likewise the ones to compare against the -# fourth run's rather than the fifth's. Brotli works the other way, on a profile -# whose score is quadratic in bytes per response (see the note above -# `ARG LIBUVCPP_REF`). +# fourth run's rather than the fifth's. +# +# The sixth run also turned the framework's brotli encoder on, and this tree turns +# it back off. That is the one change here that is not a like-for-like restoration +# and it is argued in full above `ARG LIBUVCPP_REF`; the short version is that the +# profile's score is quadratic in bytes per response, brotli buys 1.23x on that +# term, and it costs 1.5x of gzip's CPU even after the encoder state is no longer +# re-faulted every request. # # The `8150720` move, like the `02ae418` one above and for the same reason, is a # batch whose criterion is *not* an on-board measurement. Its two commits carry @@ -277,30 +281,63 @@ ENV DEBIAN_FRONTEND=noninteractive # LRU and stops copying the whole body on a hit; an h1 pipeline's final # `Connection: close` no longer takes the earlier responses with it; and the h2 # outbound body is held by reference rather than copied per response. Two optional -# modules arrive in the same range and both default to OFF. A yyjson parse backend -# is one of them and stays off, so the flag set above builds it out. The brotli -# encoder is the other, and it is the one optional module this entry turns on. +# modules arrive in the same range and both default to OFF -- a yyjson parse backend +# and a brotli encoder -- and neither is in the flag set above. +# +# Why brotli is off, given that `json-compressed` is the one profile in this suite +# whose request asks for it (`Accept-Encoding: gzip, br`, byte for byte in +# `requests/json-gzip-*.raw`; equal `q` resolves to `br` by the rank rule in +# `web/uvcpp_http_compress.cpp`). It was on for exactly one run, the sixth, and that +# run measured it. Against the fifth run, whose tree built this flag out and answered +# with gzip, `json-comp-4096` went 335 926 -> 50 389 rps (-85.0%) with `CPU/req` +# 186.4 -> 1 238.2 us, and `json-comp-16384` 330 411 -> 59 118 with 196.9 -> 1 036.5 +# us. Both runs saturate the same 62 cores (62.4 / 61.3), so that is cost per request +# and not a core given up. Three other `json-*` profiles carry the same batch's +# standard serializer and pay 58-66 us each for it (`json-h2c-1024` 38.5 -> 96.6, +# `json-h2c-4096` 39.5 -> 98.4, `json-tls-4096` 87.4 -> 153.8), which leaves about +# 990 us of the json-comp increase on the encoder. +# +# The ~+31.5 us that was predicted for that encoder was measured single-threaded, and +# the cost is a function of concurrency too. Same entry, same request, server pinned +# to 8 cores, only `Accept-Encoding` swapped: gzip 257 us, `br` 508 us with 72.43 +# minor faults per request -- the encoder's ~700 KB of state being handed back to the +# kernel between requests and re-faulted in. Removing that churn is a real fix and +# the library's `master` now carries it (`0aaa415`'s thread-local arena -- it is one +# commit past this pin, and with brotli off it is a no-op for this entry; raising +# `MALLOC_MMAP_THRESHOLD_` and `MALLOC_TRIM_THRESHOLD_` does the same thing from +# outside, which is what made the cause unambiguous): faults 72.43 -> 0.00, `br` +# 508 -> 385 us. 385 against gzip's 257 is still 1.5x, and that is the ratio at its +# kindest -- 1.94x before the fix. The gap this machine cannot reproduce, the board's +# 990 us against the 251 us it measures here, is in the direction that makes `br` +# worse, not better. +# +# 1.5x is more than the profile pays for. Its score is `rps * (minBpr/bpr)^2`, so `br` +# buys `(1429.2/1288.4)^2 = 1.23` on the byte term and has to cost less than 1.23x of +# gzip to be worth having. It costs 1.5x with the known fix already applied. +# +# The other half of the earlier case -- that the profiles merely passing `br` through +# (gateway-64, gateway-h3, production-stack) are not CPU-bound and so collect the same +# ~11% smaller bodies for free -- protects a quantity that is not scored anywhere. +# `effectiveRps` and `minBpr` occur in exactly one file of this repository's profile +# documentation, `h1/json-compressed/implementation.md`; no other profile is scored on +# response bytes. `json-tls`, `json-h2c`, `async-db` and the h1 baselines send no +# `Accept-Encoding` at all. The static legs serve the `.br` twins `data/static` already +# ships, and that selection is runtime in `http_pick_precompressed_variants()`, +# unaffected by this flag in either direction. And no client here asks for `br` +# without also accepting gzip: `json-comp` sends `gzip, br`, the h2 and h3 load +# generators send `br;q=1, gzip;q=0.8`, and `scripts/validate.sh` reads "Must return +# Content-Encoding: gzip or br when Accept-Encoding is sent". So gzip is a valid +# negotiated answer on every profile, and no request comes back uncompressed. # -# Why brotli is on. It is not a tuning knob -- it is the framework's own response -# compression middleware, reached only when the client asks for `br`, at the -# framework's own default quality (`k_brotli_quality_default`, which the header -# records as the maximum of its own score model). The profile that asks for it is -# `json-compressed`, whose guide puts "the framework or engine's built-in response -# compression" under `standard=`, names brotli in its own title, and whose request -# carries `Accept-Encoding: gzip, br`; equal `q` resolves to `br` by the rank rule in -# `web/uvcpp_http_compress.cpp`. Measured on the three json-comp bodies -- same -# entry binary, only the `.so` swapped -- gzip gives 970/1330/1567 B and `br` gives -# 856/1182/1389 B, which is the encoder's own table in `web/uvcpp_http_compress.h`. -# The cost lands where the profile pays for it: json-comp is the only profile scored -# on bytes (`rps * (minBpr/bpr)^2`) and it runs at 101% of its CPU budget, so ~+31.5 -# us per response buys ~11% fewer bytes -- about x1.07 on both json-comp profiles, -# net of the requests per second that the extra CPU costs. The profiles that merely -# pass `br` through -- gateway-64 at 58% of budget, gateway-h3 at 58%, -# production-stack at 49% -- have the CPU headroom to absorb the same compression and -# collect the smaller bodies for free. `json-tls`, `json-h2c`, `async-db` and the h1 -# baseline profiles send no `Accept-Encoding` at all, and the static legs already -# serve the `.br` twins shipped in `data/static` whether or not this flag is set: that -# selection is runtime, in `http_pick_precompressed_variants()`. +# The profile's own guide settles it rather than merely permitting it. Its +# `standard=` rule asks for "the framework standard JSON serialization and the +# framework or engine's built-in response compression (middleware, filter, or +# equivalent)"; `tuned=` only adds "as long as the output is valid gzip or brotli"; +# and the worked example under its "Expected response" heading answers a request +# that carried `Accept-Encoding: gzip, br` with `Content-Encoding: gzip`. The page's +# title and its SEO title both name brotli, but no rule in it prefers one encoding, +# and this entry is `mode: standard` -- so the framework's own middleware, answering +# gzip, is what the guide asks for. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git ARG LIBUVCPP_REF=02ae41806a25112ee7050ccfd4132138b1eb2630 @@ -328,16 +365,14 @@ ARG OPENSSL_VERSION=3.5.0 # file or directory`. Confirmed against this pin rather than assumed: `g++ -H` # resolves `` to `/include/zlib.h`, and it keeps resolving there # with the package gone, since `-I` is searched before the system directories. -# `libbrotli-dev` is here for `-DUVCPP_ENABLE_BROTLI=ON` below, and only the build -# stage needs it: the library uses `` inside one `.cpp`, so no -# installed public header reaches for it and the consumer below compiles against -# `/opt/uvcpp/include` without it. The CMake side takes the system copy through -# pkg-config (`libbrotlienc` / `libbrotlidec`) and falls back to `find_path` / -# `find_library`, so this is the package that keeps the encoder out of the -# FetchContent branch -- which would otherwise clone brotli at build time. +# `libbrotli-dev` was listed here while `-DUVCPP_ENABLE_BROTLI` was ON (it is the +# package that keeps the encoder off the FetchContent branch, which would otherwise +# clone brotli at build time) and is dropped with the flag: the build out in the flag +# set below means CMake never looks for it, and the library's `` is +# inside one `.cpp`, so nothing in the installed tree reaches for the package either. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ca-certificates cmake curl g++ git libbrotli-dev libpq-dev make ninja-build \ + ca-certificates cmake curl g++ git libpq-dev make ninja-build \ && rm -rf /var/lib/apt/lists/* # OpenSSL with the QUIC API: static, PIC, no test suite. @@ -393,7 +428,7 @@ RUN cmake -S . -B build -G Ninja \ -DUVCPP_BUILD_WEB=ON \ -DUVCPP_BUILD_WEBAPP=ON \ -DUVCPP_ENABLE_ZLIB=ON \ - -DUVCPP_ENABLE_BROTLI=ON \ + -DUVCPP_ENABLE_BROTLI=OFF \ -DUVCPP_ENABLE_OPENSSL=ON \ -DUVCPP_ENABLE_NGHTTP2=ON \ -DUVCPP_ENABLE_QUIC=ON \ @@ -422,15 +457,14 @@ RUN g++ -std=c++11 -O2 -DNDEBUG -o /server main.cc \ FROM ubuntu:24.04 -# libstdc++ for the entry's own objects, libpq5 and libbrotli1 for the two shared -# libraries the build above could not absorb. `libbrotli1` is the second of those: -# unlike zlib and libuv, which the project fetches and links statically, -# `UVCPP_ENABLE_BROTLI` takes the system copy, so `libuvcpp.so` carries -# `libbrotlienc.so.1` and `libbrotlidec.so.1` in its `DT_NEEDED` and the server does -# not start without them. On noble they are both files of that one package -- the -# name is not `libbrotlienc1`, which exists on Debian but not here (`libbrotli-dev` -# itself depends on `libbrotli1`, so the build stage already pulls it in; the -# runtime stage needs it said out loud). +# libstdc++ for the entry's own objects, and libpq5 for the one shared library the +# build above could not absorb. `libbrotli1` was the other one while +# `-DUVCPP_ENABLE_BROTLI` was ON: unlike zlib and libuv, which the project fetches +# and links statically, that flag takes the system copy, so `libuvcpp.so` carried +# `libbrotlienc.so.1` and `libbrotlidec.so.1` in its `DT_NEEDED` and the server would +# not start without them (on noble both are files of that one package -- the name is +# not `libbrotlienc1`, which exists on Debian but not here). With the flag off the +# library carries neither, and this stage does not need them. # # apt pulls in libpq's transitive closure # (libgssapi, libldap, libgnutls, libssl3t64, ...) on its own -- that OpenSSL @@ -439,7 +473,7 @@ FROM ubuntu:24.04 # and h3 listeners. RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libbrotli1 libpq5 libstdc++6 \ + libpq5 libstdc++6 \ && rm -rf /var/lib/apt/lists/* COPY --from=build /opt/uvcpp/lib/libuvcpp.so /opt/uvcpp/lib/libuvcpp.so diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index 458aabd7d..5a67c0e1c 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -162,18 +162,33 @@ the profile forbids the framework from doing the JWT verification itself. writer also skipped the per-item DOM copy the DOM path makes. Both figures are the ones this entry reported in earlier revisions; they are recorded here because the entry no longer collects them. -- **Compression is the framework's built-in response compression** (gzip and - brotli), which answers `Accept-Encoding` per request and adds no - `Content-Encoding` when the client sends none. Both encoders are the library's - own middleware at its own defaults; the build turns the brotli one on - (`-DUVCPP_ENABLE_BROTLI=ON` in the Dockerfile) because it is the library's - default quality that the header records as the maximum of its own score model - for this profile. `json-compressed` asks for it — its guide lists the - framework's built-in compression under `standard=`, its title names brotli, and - its request carries `Accept-Encoding: gzip, br`, where equal `q` resolves to - `br`. On the three json-comp bodies the encoder produces 856/1182/1389 B - against gzip's 970/1330/1567 B. `Accept-Encoding` is still honoured per - request, so a client that asks for gzip alone still gets gzip. +- **Compression is the framework's built-in response compression** (gzip), which + answers `Accept-Encoding` per request and adds no `Content-Encoding` when the + client sends none, at the library's own default level. The library also + carries a brotli encoder; the build turns it off (`-DUVCPP_ENABLE_BROTLI=OFF` + in the Dockerfile). It was on for one run, and that run is why it is off. + `json-compressed` is the one profile in the suite scored on response bytes + (`effectiveRps = rps * (minBpr / bpr)^2`), so it is the only place a smaller + body buys anything, and there the smaller body has to be paid for out of + requests per second. The two runs' own reported `bpr` put brotli at 1288.4 B + per response against gzip's 1429.2, so brotli's whole win on that one term is + `(1429.2 / 1288.4)^2 = 1.23`. Its cost was above that: 5.06x gzip's + per-request CPU on the board (1238.2 us against the 244.5 us that leg reads + with gzip, both including the serializer change the other json legs pay too), + and 1.94x on the same request measured here with the server pinned to 8 cores + (508.0 us against 257.0 us). So it is a net loss on the only term it can move. + Turning it off costs nothing else: `Accept-Encoding` is still honoured per + request, so a client that asks for gzip alone gets gzip; every client these + profiles use that offers `br` also accepts gzip (`gzip, br` on json-comp, + `br;q=1, gzip;q=0.8` on the h2 and h3 generators); and the static legs' + pre-compressed twins are announced the same way whichever encoder the build + links -- see the next bullet. The profile's own guide asks for what it gets: its + `standard=` rule wants "the framework or engine's built-in response compression + (middleware, filter, or equivalent)", its `tuned=` rule only adds "as long as the + output is valid gzip or brotli", and the worked example under "Expected response" + answers a request that carried `Accept-Encoding: gzip, br` with + `Content-Encoding: gzip`. Brotli appears in the page's title, but no rule there + prefers it, and this entry is `mode: standard`. - **Where the dataset ships a pre-compressed twin, the handler serves it** (`precompressed`, on for every static leg). 15 of the dataset's 20 assets carry a `.br` and a `.gz` beside them, and the static profiles send diff --git a/frameworks/libuvcpp/meta.json b/frameworks/libuvcpp/meta.json index acfd643af..304207283 100644 --- a/frameworks/libuvcpp/meta.json +++ b/frameworks/libuvcpp/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "libuv", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "enabled": true, "tests": [ diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 9f34e9246..2ef663a3b 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -1153,7 +1153,7 @@ }, "libuvcpp": { "dir": "libuvcpp", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip and brotli through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "type": "emerging", "engine": "libuv", From ea38462e17b8381e0713b5fca8d345da7762fa0d Mon Sep 17 00:00:00 2001 From: zhuweiye Date: Fri, 16 Oct 2026 06:51:31 +0800 Subject: [PATCH 11/17] Give the h3 static leg the cache-validation thread the h1/h2 legs have `uvcpp_static_server` defaults to `uvcpp_static_revalidate::POOL`, so every `static-h3` request queued to the libuv thread pool, woke a worker, ran the `stat` that decides whether the cached file is still good, and came back through `after_work`. The h1 and h2 static legs have been on `LOOP_STAT` since `af3b4fc` -- they get it through `kStaticOpts.revalidate` -- but this leg is a plain `uvcpp_http_server` with no `uvcpp_web_static_options` to receive it, so it never got the choice at all. It is one line here. Measured on the entry built twice, one line apart, same library, same ext4 corpus, the profile's own URIs and the profile's own `Accept-Encoding: br;q=1, gzip;q=0.8` (`--alpn-list=h3 -c 64 -m 64 -t 8 -D 5s`, three reps a side, interleaved, server on 8 cores and the generator on 8 disjoint ones): rps 29 434 -> 38 280 (rep range 29 129-30 454 against 33 855-42 360 -- even the worst `loopstat` rep beats the best `pool` one) us/req 216.8 -> 193.3 The mechanism is the library's `doc/benchmark.md` section 4.8, which ran the same A/B on the same `uvcpp_static_server`: `futex`/req 6.28 -> 0.66, with `statx`/req 2.24 -> 2.24 as the control -- the mtime/size check still happens on every request, only the thread it happens on moved. Tail latency improved there as well (p50 63 661 -> 56 896 us, p99 170 526 -> 140 458 us). That tail latency is the risk this option carries, and the reason the library default stays `POOL`: on a FUSE or network mount one `stat` is ~46 us and blocking a loop thread on it -- which on this leg also carries QUIC's own timers and retransmits -- is the wrong trade. The platform bind-mounts the dataset into the container (`scripts/lib/framework.sh`), so this leg is on the same local disk the h1/h2 legs are already doing this on. The pin is unchanged by this commit, which is one variable on its own; the pin move is the commit after it, so the next run carries both. --- frameworks/libuvcpp/main.cc | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frameworks/libuvcpp/main.cc b/frameworks/libuvcpp/main.cc index feb8e7c8c..444177862 100644 --- a/frameworks/libuvcpp/main.cc +++ b/frameworks/libuvcpp/main.cc @@ -1457,6 +1457,36 @@ int main() { // response on the loop thread -- 933.15 rps against 82,451 with it, on the // rig described above kStaticOpts. h3_static.set_precompressed(true); + // The same cache-validation thread choice the h1/h2 legs get through + // kStaticOpts.revalidate, which this leg has no uvcpp_web_static_options to + // receive -- and did not get at all until now: uvcpp_static_server defaults + // to POOL, so every static-h3 request (a `.br` twin makes it two stats) + // queued to the libuv thread pool, woke a worker, stat()ed there and came + // back through after_work. LOOP_STAT does the stat where the request already + // is. The h1/h2 static legs have been on it since `af3b4fc`. + // + // Measured on this leg with the entry built twice, one line apart, same + // library, same ext4 corpus, the profile's own URIs and Accept-Encoding + // (--alpn-list=h3 -c 64 -m 64 -t 8 -D 5s, three reps a side, interleaved; + // server on 8 cores, generator on 8 disjoint ones): + // + // rps 29 434 -> 38 280 (rep range 29 129-30 454 vs 33 855-42 360; + // even the worst `loopstat` rep beats the + // best `pool` one) + // us/req 216.8 -> 193.3 + // + // The mechanism is in the library's doc/benchmark.md section 4.8, which ran + // the same A/B on the same `uvcpp_static_server`: `futex`/req 6.28 -> 0.66 + // with `statx`/req 2.24 -> 2.24 as the control -- the mtime/size check that + // decides whether the cache is still good still happens on every request, + // only the thread it happens on moved. Tail latency improved there too + // (p50 63,661 -> 56,896 us, p99 170,526 -> 140,458 us), which is the risk + // this option carries and the reason the library default stays POOL: on a + // FUSE or network mount one stat() is ~46 us and blocking a loop thread on + // it would be the wrong trade. The platform mounts the dataset into the + // container, so this leg is on the same local overlayfs the h1/h2 legs are + // already doing this on. + h3_static.set_revalidate(uvcpp_static_revalidate::LOOP_STAT); uvcpp_http_server h3; if (have_certs) { if (!quic_tls.load_certificate_file(kCertFile) || From a501776b092acce1cbe298e20ff4b1d15518d63e Mon Sep 17 00:00:00 2001 From: Antruly Date: Fri, 16 Oct 2026 07:06:40 +0800 Subject: [PATCH 12/17] Move the pin to 1f6a8fb (1.7.2-dev), three commits past 02ae418 The pin is the tip of the library's `master` as of now. Two of the three commits change code and one is documentation (`b7cae85`, the sixth run's readings), and neither of the two touches a public header -- `git diff --stat 02ae418..1f6a8fb -- include/ 'src/**/*.h'` is empty -- so the entry needs no code change for it. `0aaa415` gives the brotli encoder the ~700 KiB of state it needs per request out of a thread-local arena instead of the heap. brotli has no `BrotliEncoderReset()`, so that state was created and destroyed on every request, two of its blocks are 256 KiB, and those are above glibc's 128 KiB threshold, so `free` handed them back to the kernel for the next request to fault in again: `72.43 -> 0.00` minor faults per request and `508 -> 385` us of CPU per request on the local `br` arm, with a gzip arm as the negative control (`+0.8%`, no faults either way) and compressed bytes bit-for-bit identical. This build cannot be moved by any of it: the arena, the encoder call and the frame in between all sit under `#if UVCPP_BROTLI_ENABLE` in `src/web/uvcpp_http_compress.cpp`, and this entry passes `-DUVCPP_ENABLE_BROTLI=OFF`. It comes along because a pin is a commit and not a patch list. `1f6a8fb` moves the two tables behind the static handler's `LOOP_STAT` cache lookup to a front copy per loop, so the thread that reads them is the thread that wrote them. Before it every loop read the global tables under one shared lock, and at `1 / held` that lock admitted 240 000-270 000 requests a second -- the order the board's `static-h2` rows sit at (~235 000) while the same round's `json-tls-4096` spends 63.3 cores at a lower per-request cost, so the static profiles were short of parallelism and not of work. The counters, A B B A on one entry binary with only the `.so` swapped: `mutex_lock` per request `2.03 / 2.04 -> 0.03 / 0.02`, time held `3 679 / 4 171 -> 969 / 1 259` ns, time waiting `7 761 / 9 368 -> 183 / 239` ns; `collect_hints()` disappears from the per-call-site table and `lookup_cache()` falls to 0.0041 per request. The local end-to-end moved `+19.6%` (109 997 -> 131 608 req/s, two interleaved rounds on 9 cores with the generator on the same machine), which is offered as direction only. This is the h1/h2 static handler; the h3 leg goes through `uvcpp_static_server` and does not take this path. README: the pin is now twelve commits past `v1.7.0`, not three. That sentence was written when the pin was `8150720` and was not updated by the `02ae418` move (`git rev-list --count v1.7.0..1f6a8fb` is 12). Both criteria are local deterministic counters measured against the previous revision; neither is offered as a predicted board movement. --- frameworks/libuvcpp/Dockerfile | 84 ++++++++++++++++++++++++++++------ frameworks/libuvcpp/README.md | 2 +- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index 756dfd30a..de75a1e2c 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -37,20 +37,23 @@ ENV DEBIAN_FRONTEND=noninteractive # and the bytes under it re-uploaded, so a SHA is the only thing that fixes # which revision gets built. `3fdd6208` was `v1.6.1` -- what the first version # of this entry built, and what the platform measured as the board's 58 req/s on -# `baseline-h3-64`. The pin has since moved four times: to `2d5fe74` +# `baseline-h3-64`. The pin has since moved five times: to `2d5fe74` # (`1.6.20-dev`), then to `6eede33` (`1.6.51-dev`), which was the tip of the # library's `master` when it was taken and is the revision the platform's fourth # run measured, then to `8150720` (`1.7.2-dev`), which is the `v1.7.0` tag plus -# one documentation commit and two that change code, and now to `02ae418` -# (`1.7.2-dev`), which is four code commits past it. +# one documentation commit and two that change code, then to `02ae418` +# (`1.7.2-dev`), which is four code commits past it and is what the sixth run +# carried, and now to `1f6a8fb` (`1.7.2-dev`), which is two code commits and a +# documentation commit past `02ae418` and was the tip of the library's `master` +# when it was taken. # Each move was measured against the previous pin before being taken; the # criteria for the `6eede33` batch are further down this file, and each commit's # own criterion is in the library's `CHANGELOG.md`. # -# The `02ae418` move is for the next run, and like the `8150720` move it is a -# batch whose members carry local deterministic counters rather than on-board -# measurements. The four code commits between the two pins -- the other two are -# documentation -- and what each one's criterion is: +# The `02ae418` move was the pin the sixth run carried, and like the `8150720` +# move it is a batch whose members carry local deterministic counters rather than +# on-board measurements. The four code commits between the two pins -- the other +# two are documentation -- and what each one's criterion is: # # * `e716010` stops the database path from building a log line when no log # callback is installed: `106.16 / 106.15 -> 102.18 / 102.22` allocations @@ -74,6 +77,45 @@ ENV DEBIAN_FRONTEND=noninteractive # The instruction figure is 0.83% of one handler, smaller than the spread # between two board readings; the counters are the criterion. # +# The `1f6a8fb` move is for the next run and is the shortest batch so far: two +# code commits and a documentation one past `02ae418`, neither of the two +# touching a public header, so this entry needs no code change for it. One of the +# two cannot reach this build at all, and the other is the one that matters. +# +# * `0aaa415` gives the brotli encoder the ~700 KiB of state it needs per +# request out of a thread-local arena instead of the heap. brotli has no +# `BrotliEncoderReset()` -- which is why the deflate arm can carry one kernel +# across requests and this one cannot -- so the state was created and +# destroyed on every request, two of its blocks are 256 KiB, and those are +# above glibc's 128 KiB threshold, so `free` handed them back to the kernel +# for the next request to fault in again. On the local `br` arm: `72.43 -> +# 0.00` minor faults per request and `508 -> 385` us of CPU per request, with +# a gzip arm as the negative control (`+0.8%`, no faults either way) and the +# compressed bytes bit-for-bit the same. The commit's own run of the same +# effect reads `72.165 -> 0.001` and `515.5 -> 398.5` us; the numbers already +# in this file above `ARG LIBUVCPP_REF` are this entry's rig on the same +# request. +# **This build compiles none of it**: the arena, the encoder +# call and the frame in between all sit under `#if UVCPP_BROTLI_ENABLE` in +# `src/web/uvcpp_http_compress.cpp`, and the flag below is +# `-DUVCPP_ENABLE_BROTLI=OFF`. It comes along because a pin is a commit and +# not a patch list. +# * `1f6a8fb` moves the two tables behind the static handler's `LOOP_STAT` +# cache lookup to a front copy per loop, so that the thread reading them is +# the thread that wrote them. Before it, every loop read the global tables +# under one shared lock and the lock was the ceiling: `mutex_lock` per request +# `2.03 / 2.04 -> 0.03 / 0.02`, time held `3 679 / 4 171 -> 969 / 1 259` ns, +# time waiting `7 761 / 9 368 -> 183 / 239` ns. At `1 / held` the old lock +# admits 240 000-270 000 requests a second, which is the order the board's +# `static-h2` rows sit at (about 235 000) while the same round's +# `json-tls-4096` spends 63.3 cores at a lower per-request cost -- so the +# static profiles were short of parallelism, not of work. The local +# end-to-end moved `+19.6%` (109 997 -> 131 608 req/s, two interleaved +# rounds, server on 9 cores with the generator pinned to the same machine), +# which is offered as direction only; the counter and the `1 / held` +# arithmetic are the criterion. This is the h1/h2 static handler; the h3 leg +# goes through `uvcpp_static_server` and does not take this path. +# # What this run also carries, and what is *not* in the pin, is the entry side: # the JSON profiles answer through the framework's standard serializer again # (see the `/json` note below). The fifth run measured the size of that change @@ -81,9 +123,24 @@ ENV DEBIAN_FRONTEND=noninteractive # the revision the maintainer then objected to, and it reported `async-db-1024` # at `271.85` us of CPU per response against the fourth run's `407.2` -- a fall # that the doc attributes to that one entry commit and to nothing in the library. -# This entry has the DOM path back, so its `async-db` row starts from the higher -# number, and its `json-*` rows are likewise the ones to compare against the -# fourth run's rather than the fifth's. +# This entry has the DOM path back, so its `async-db` and `json-*` rows read +# against the sixth run's -- the most recent one that also had it back -- and not +# against the fifth's, whose tree answered through `uvcpp_json_writer`. +# +# The other entry-side change since the sixth run is on the h3 static leg, and it +# is the same shape of omission the JSON one was. `uvcpp_static_server` is a +# different handler from the `uvcpp_web_app` static code the h1/h2 legs use -- +# `uvcpp_web_app` has no QUIC, so `static-h3-64` has to mount this one -- and +# options set on one are not inherited by the other. Its `revalidate` had been +# left on the `POOL` default, so every `static-h3-64` request queued to the libuv +# thread pool to `stat()` there and came back through `after_work`; the h1/h2 +# legs have been on `LOOP_STAT` since `af3b4fc`. One line sets it here. On this +# machine's h3 rig, one line apart with everything else equal: `29 434 -> 38 280` +# req/s and `216.8 -> 193.3` us per request, the two arms' rep ranges not +# overlapping (29 129-30 454 against 33 855-42 360). The library's section 4.8 +# ran the same A/B on the same handler with `statx` per request as its control, +# and the control did not move (`2.24` on both arms) -- the mtime and size check +# still happens on every request, only the thread it happens on changed. # # The sixth run also turned the framework's brotli encoder on, and this tree turns # it back off. That is the one change here that is not a like-for-like restoration @@ -302,8 +359,9 @@ ENV DEBIAN_FRONTEND=noninteractive # to 8 cores, only `Accept-Encoding` swapped: gzip 257 us, `br` 508 us with 72.43 # minor faults per request -- the encoder's ~700 KB of state being handed back to the # kernel between requests and re-faulted in. Removing that churn is a real fix and -# the library's `master` now carries it (`0aaa415`'s thread-local arena -- it is one -# commit past this pin, and with brotli off it is a no-op for this entry; raising +# the library's `master` now carries it (`0aaa415`'s thread-local arena -- with +# brotli off it is a no-op for this entry, which is why the pin now carrying it +# changes nothing in these rows; raising # `MALLOC_MMAP_THRESHOLD_` and `MALLOC_TRIM_THRESHOLD_` does the same thing from # outside, which is what made the cause unambiguous): faults 72.43 -> 0.00, `br` # 508 -> 385 us. 385 against gzip's 257 is still 1.5x, and that is the ratio at its @@ -339,7 +397,7 @@ ENV DEBIAN_FRONTEND=noninteractive # and this entry is `mode: standard` -- so the framework's own middleware, answering # gzip, is what the guide asks for. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git -ARG LIBUVCPP_REF=02ae41806a25112ee7050ccfd4132138b1eb2630 +ARG LIBUVCPP_REF=1f6a8fbf54907ce942eb0963cf9a391bacb1bacd ARG OPENSSL_VERSION=3.5.0 diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index 5a67c0e1c..a3053b3a7 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -18,7 +18,7 @@ release workflow's legs passed `-DUVCPP_ENABLE_DB_MYSQL=OFF -DUVCPP_ENABLE_DB_PGSQL=OFF` — the workflow asserts it with an `ldd` check that fails if `libpq.so` appears — while `/async-db` needs the Postgres backend. The pin has since moved onto development revisions: the `v1.7.0` tag is the nearest -release, the pin is three commits past it, and a release package exists only for +release, the pin is twelve commits past it, and a release package exists only for a tag, so building the pinned SHA from source is the only way to build exactly what the pin names. The `1.7.0` packages do carry both remote backends, behind the runtime-load seam that is why the same `ldd` list still passes, so the From c1f38b7b8e128a1213725f6883101e7104f9bad5 Mon Sep 17 00:00:00 2001 From: Antruly Date: Fri, 16 Oct 2026 07:07:55 +0800 Subject: [PATCH 13/17] Correct the commit the h1/h2 static legs went on `LOOP_STAT` at The h3 commit and this branch's `main.cc` comment both cited `af3b4fc` for when the h1/h2 static legs started validating the static cache on the loop thread instead of through the pool. `af3b4fc` is where the two lines (`o.precompressed = true`, `o.revalidate = uvcpp_web_revalidate::LOOP_STAT`) entered this branch's history -- PR #1553 brought the gateway work in -- but they were not introduced there: `git log -S 'o.revalidate' --all -- frameworks/libuvcpp/main.cc` names `7095e92`, "Validate the static cache on the loop thread instead of through the pool" (2026-10-14), and `7095e92` is an ancestor of `ae437de`, the tree the fourth run measured and which carries both lines at `main.cc:196` and `:211`. So the legs have been on it since `7095e92`; `main.cc` now says so, and the Dockerfile says both. No behaviour change: `main.cc` differs by one SHA inside a comment. --- frameworks/libuvcpp/Dockerfile | 7 ++++--- frameworks/libuvcpp/main.cc | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index de75a1e2c..def04c09a 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -134,9 +134,10 @@ ENV DEBIAN_FRONTEND=noninteractive # options set on one are not inherited by the other. Its `revalidate` had been # left on the `POOL` default, so every `static-h3-64` request queued to the libuv # thread pool to `stat()` there and came back through `after_work`; the h1/h2 -# legs have been on `LOOP_STAT` since `af3b4fc`. One line sets it here. On this -# machine's h3 rig, one line apart with everything else equal: `29 434 -> 38 280` -# req/s and `216.8 -> 193.3` us per request, the two arms' rep ranges not +# legs have been on `LOOP_STAT` since `7095e92` (`af3b4fc` is where this branch +# got it). One line sets it here. On this machine's h3 rig, one line apart with +# everything else equal: `29 434 -> 38 280` req/s and `216.8 -> 193.3` us per +# request, the two arms' rep ranges not # overlapping (29 129-30 454 against 33 855-42 360). The library's section 4.8 # ran the same A/B on the same handler with `statx` per request as its control, # and the control did not move (`2.24` on both arms) -- the mtime and size check diff --git a/frameworks/libuvcpp/main.cc b/frameworks/libuvcpp/main.cc index 444177862..58678d4d0 100644 --- a/frameworks/libuvcpp/main.cc +++ b/frameworks/libuvcpp/main.cc @@ -1463,7 +1463,7 @@ int main() { // to POOL, so every static-h3 request (a `.br` twin makes it two stats) // queued to the libuv thread pool, woke a worker, stat()ed there and came // back through after_work. LOOP_STAT does the stat where the request already - // is. The h1/h2 static legs have been on it since `af3b4fc`. + // is. The h1/h2 static legs have been on it since `7095e92`. // // Measured on this leg with the entry built twice, one line apart, same // library, same ext4 corpus, the profile's own URIs and Accept-Encoding From 69ff88422ea155462368a16996f8141349cbd319 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 16 Oct 2026 08:58:58 +0000 Subject: [PATCH 14/17] Benchmark results: libuvcpp [skip ci] --- site/data/results/libuvcpp.json | 686 ++++++++++++++++---------------- 1 file changed, 343 insertions(+), 343 deletions(-) diff --git a/site/data/results/libuvcpp.json b/site/data/results/libuvcpp.json index c71e6bb19..0d7bb5d03 100644 --- a/site/data/results/libuvcpp.json +++ b/site/data/results/libuvcpp.json @@ -4,44 +4,44 @@ "8gbit-512": { "framework": "libuvcpp", "language": "C++", - "rps": 49371, - "avg_latency": "111.1us", - "p99_latency": "144.0us", - "cpu": "327.1%", - "memory": "166MiB", + "rps": 49306, + "avg_latency": "111.5us", + "p99_latency": "149.0us", + "cpu": "300.5%", + "memory": "208MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "513.36MB/s", - "input_bw": "482.14MB/s", + "bandwidth": "512.68MB/s", + "input_bw": "481.50MB/s", "reconnects": 0, - "status_2xx": 246927, + "status_2xx": 246605, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 16205469, - "cpu_per_req_us": 65.6286, + "cpu_usec": 16157098, + "cpu_per_req_us": 65.5181, "target_rate": 50000, - "rate_ratio": 0.9874, - "p99_9_latency": "5938.0us" + "rate_ratio": 0.9861, + "p99_9_latency": "5482.0us" }, "async-32000": { "framework": "libuvcpp", "language": "C++", - "rps": 1533457, - "avg_latency": "20.03ms", - "p99_latency": "22.50ms", - "cpu": "6212.9%", - "memory": "490MiB", + "rps": 1541917, + "avg_latency": "19.78ms", + "p99_latency": "22.40ms", + "cpu": "6138.9%", + "memory": "488MiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "229.56MB/s", - "input_bw": "70.20MB/s", + "bandwidth": "230.83MB/s", + "input_bw": "70.58MB/s", "reconnects": 0, - "status_2xx": 15334577, + "status_2xx": 15419171, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -49,19 +49,19 @@ "async-db-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 87341, - "avg_latency": "11.33ms", - "p99_latency": "39.90ms", - "cpu": "3381.8%", - "memory": "174MiB", + "rps": 86449, + "avg_latency": "11.41ms", + "p99_latency": "41.00ms", + "cpu": "3388.5%", + "memory": "176MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "337.88MB/s", - "input_bw": "5.83MB/s", - "reconnects": 34627, - "status_2xx": 873417, + "bandwidth": "334.35MB/s", + "input_bw": "5.77MB/s", + "reconnects": 34274, + "status_2xx": 864495, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -69,19 +69,19 @@ "baseline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1708528, - "avg_latency": "2.40ms", - "p99_latency": "4.97ms", - "cpu": "6713.7%", - "memory": "141MiB", + "rps": 1732068, + "avg_latency": "2.37ms", + "p99_latency": "5.11ms", + "cpu": "6322.2%", + "memory": "140MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "255.75MB/s", - "input_bw": "131.98MB/s", + "bandwidth": "259.27MB/s", + "input_bw": "133.80MB/s", "reconnects": 0, - "status_2xx": 8542643, + "status_2xx": 8660343, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -89,18 +89,18 @@ "baseline-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 5435683, - "avg_latency": "17.94ms", - "p99_latency": "113.99ms", - "cpu": "6189.7%", - "memory": "174MiB", + "rps": 5327411, + "avg_latency": "18.50ms", + "p99_latency": "104.92ms", + "cpu": "6158.9%", + "memory": "172MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "141.40MB/s", + "bandwidth": "138.86MB/s", "reconnects": 0, - "status_2xx": 27450200, + "status_2xx": 26956700, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -108,18 +108,18 @@ "baseline-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 5425606, - "avg_latency": "4.95ms", - "p99_latency": "23.19ms", - "cpu": "6345.3%", - "memory": "112MiB", + "rps": 5304166, + "avg_latency": "5.10ms", + "p99_latency": "51.38ms", + "cpu": "6114.1%", + "memory": "96MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "140.55MB/s", + "bandwidth": "137.68MB/s", "reconnects": 0, - "status_2xx": 27290800, + "status_2xx": 26733000, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -127,18 +127,18 @@ "baseline-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 5552958, - "avg_latency": "17.88ms", - "p99_latency": "373.11ms", - "cpu": "6334.9%", - "memory": "144MiB", + "rps": 5280019, + "avg_latency": "19.13ms", + "p99_latency": "281.41ms", + "cpu": "6328.3%", + "memory": "160MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "145.02MB/s", + "bandwidth": "138.17MB/s", "reconnects": 0, - "status_2xx": 28153500, + "status_2xx": 26822500, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -146,18 +146,18 @@ "baseline-h2c-256": { "framework": "libuvcpp", "language": "C++", - "rps": 5680497, - "avg_latency": "4.79ms", - "p99_latency": "37.21ms", - "cpu": "6189.5%", + "rps": 5550996, + "avg_latency": "4.89ms", + "p99_latency": "39.82ms", + "cpu": "6072.9%", "memory": "83MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "147.16MB/s", + "bandwidth": "143.52MB/s", "reconnects": 0, - "status_2xx": 28572900, + "status_2xx": 27866000, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -165,18 +165,18 @@ "baseline-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 5316594, - "avg_latency": "72.97ms", - "p99_latency": "523.77ms", - "cpu": "6380.1%", - "memory": "795MiB", + "rps": 5031650, + "avg_latency": "76.42ms", + "p99_latency": "446.98ms", + "cpu": "6417.7%", + "memory": "798MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "139.23MB/s", + "bandwidth": "132.05MB/s", "reconnects": 0, - "status_2xx": 27008300, + "status_2xx": 25611100, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -184,18 +184,18 @@ "baseline-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 3030258, - "avg_latency": "1.28ms", - "p99_latency": "4.00ms", - "cpu": "3451.5%", - "memory": "134MiB", + "rps": 3077350, + "avg_latency": "1.20ms", + "p99_latency": "3.06ms", + "cpu": "3656.2%", + "memory": "133MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "40.49MB/s", + "bandwidth": "41.12MB/s", "reconnects": 0, - "status_2xx": 15181595, + "status_2xx": 15417528, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -203,18 +203,18 @@ "echo-ws-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 2611015, - "avg_latency": "5.57ms", - "p99_latency": "11.60ms", - "cpu": "6099.8%", - "memory": "329MiB", + "rps": 2565508, + "avg_latency": "6.03ms", + "p99_latency": "7.37ms", + "cpu": "6604.4%", + "memory": "335MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "17.83MB/s", + "bandwidth": "17.53MB/s", "reconnects": 0, - "status_2xx": 13055079, + "status_2xx": 12827542, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -222,18 +222,18 @@ "echo-ws-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 2929961, - "avg_latency": "1.39ms", - "p99_latency": "1.91ms", - "cpu": "6350.7%", - "memory": "130MiB", + "rps": 2838290, + "avg_latency": "1.44ms", + "p99_latency": "1.88ms", + "cpu": "6414.9%", + "memory": "132MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "19.61MB/s", + "bandwidth": "18.96MB/s", "reconnects": 0, - "status_2xx": 14649807, + "status_2xx": 14191452, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -241,18 +241,18 @@ "echo-ws-512": { "framework": "libuvcpp", "language": "C++", - "rps": 2851128, + "rps": 2849239, "avg_latency": "179us", - "p99_latency": "424us", - "cpu": "6547.2%", - "memory": "78MiB", + "p99_latency": "434us", + "cpu": "6733.9%", + "memory": "77MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "19.03MB/s", + "bandwidth": "19.02MB/s", "reconnects": 0, - "status_2xx": 14255642, + "status_2xx": 14246198, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -260,18 +260,18 @@ "echo-ws-limited-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1795680, - "avg_latency": "381us", - "p99_latency": "822us", - "cpu": "6180.1%", - "memory": "95MiB", + "rps": 1778136, + "avg_latency": "377us", + "p99_latency": "797us", + "cpu": "6289.9%", + "memory": "94MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "34.07MB/s", - "reconnects": 897786, - "status_2xx": 8978400, + "bandwidth": "33.74MB/s", + "reconnects": 889041, + "status_2xx": 8890682, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -279,18 +279,18 @@ "echo-ws-limited-512": { "framework": "libuvcpp", "language": "C++", - "rps": 1593577, - "avg_latency": "206us", - "p99_latency": "617us", - "cpu": "5883.1%", - "memory": "93MiB", + "rps": 1574604, + "avg_latency": "209us", + "p99_latency": "618us", + "cpu": "6025.8%", + "memory": "92MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "30.24MB/s", - "reconnects": 796788, - "status_2xx": 7967888, + "bandwidth": "29.88MB/s", + "reconnects": 787294, + "status_2xx": 7873020, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -298,18 +298,18 @@ "echo-ws-pipeline-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 28207107, - "avg_latency": "8.53ms", + "rps": 28115031, + "avg_latency": "8.08ms", "p99_latency": "10.50ms", - "cpu": "6280.7%", - "memory": "358MiB", + "cpu": "6196.7%", + "memory": "350MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "188.66MB/s", + "bandwidth": "188.04MB/s", "reconnects": 0, - "status_2xx": 141035537, + "status_2xx": 140575156, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -317,18 +317,18 @@ "echo-ws-pipeline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 31601507, - "avg_latency": "2.07ms", + "rps": 29440675, + "avg_latency": "2.22ms", "p99_latency": "2.77ms", - "cpu": "6412.9%", - "memory": "136MiB", + "cpu": "6798.7%", + "memory": "137MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "210.96MB/s", + "bandwidth": "196.52MB/s", "reconnects": 0, - "status_2xx": 158007536, + "status_2xx": 147203376, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -336,18 +336,18 @@ "echo-ws-pipeline-512": { "framework": "libuvcpp", "language": "C++", - "rps": 30734953, - "avg_latency": "265us", - "p99_latency": "618us", - "cpu": "6655.7%", - "memory": "79MiB", + "rps": 30670502, + "avg_latency": "266us", + "p99_latency": "642us", + "cpu": "6642.9%", + "memory": "78MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "205.13MB/s", + "bandwidth": "204.70MB/s", "reconnects": 0, - "status_2xx": 153674768, + "status_2xx": 153352512, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -355,115 +355,115 @@ "gateway-64-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 120098, - "avg_latency": "184.89ms", - "p99_latency": "4.87s", - "cpu": "5119.2%", - "memory": "2.0GiB", + "rps": 196673, + "avg_latency": "57.01ms", + "p99_latency": "4.90s", + "cpu": "5245.2%", + "memory": "1.8GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.48GB/s", + "bandwidth": "2.36GB/s", "reconnects": 0, - "status_2xx": 608899, + "status_2xx": 993201, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 182669, - "tpl_baseline": 121779, - "tpl_json": 213114, - "tpl_async_db": 91334, - "cpu_breakdown": "server: 2702% 475MiB | proxy: 2417% 1.5GiB" + "tpl_static": 297960, + "tpl_baseline": 198640, + "tpl_json": 347620, + "tpl_async_db": 148980, + "cpu_breakdown": "server: 2870% 264MiB | proxy: 2376% 1.5GiB" }, "gateway-64-512": { "framework": "libuvcpp", "language": "C++", - "rps": 122364, - "avg_latency": "63.61ms", - "p99_latency": "4.95s", - "cpu": "4870.7%", - "memory": "1.4GiB", + "rps": 199358, + "avg_latency": "57.82ms", + "p99_latency": "4.92s", + "cpu": "5431.8%", + "memory": "1.5GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.46GB/s", + "bandwidth": "2.35GB/s", "reconnects": 0, - "status_2xx": 617941, + "status_2xx": 1006761, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 185382, - "tpl_baseline": 123588, - "tpl_json": 216279, - "tpl_async_db": 92691, - "cpu_breakdown": "server: 3357% 225MiB | proxy: 1514% 1.2GiB" + "tpl_static": 302028, + "tpl_baseline": 201352, + "tpl_json": 352366, + "tpl_async_db": 151014, + "cpu_breakdown": "server: 3044% 276MiB | proxy: 2388% 1.2GiB" }, "gateway-h3-256": { "framework": "libuvcpp", "language": "C++", - "rps": 62641, - "avg_latency": "126.31ms", - "p99_latency": "779.62ms", - "cpu": "3509.0%", + "rps": 61721, + "avg_latency": "128.83ms", + "p99_latency": "795.62ms", + "cpu": "2954.5%", "memory": "1.4GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "630.18MB/s", + "bandwidth": "623.14MB/s", "reconnects": 0, - "status_2xx": 315089, + "status_2xx": 310458, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 94526, - "tpl_baseline": 63017, - "tpl_json": 110281, - "tpl_async_db": 47263, - "cpu_breakdown": "server: 1388% 105MiB | proxy: 2120% 1.3GiB" + "tpl_static": 93137, + "tpl_baseline": 62091, + "tpl_json": 108660, + "tpl_async_db": 46568, + "cpu_breakdown": "server: 859% 101MiB | proxy: 2096% 1.3GiB" }, "gateway-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 80155, - "avg_latency": "25.38ms", - "p99_latency": "83.09ms", - "cpu": "4577.5%", - "memory": "446MiB", + "rps": 83510, + "avg_latency": "24.37ms", + "p99_latency": "84.47ms", + "cpu": "3796.8%", + "memory": "431MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "826.79MB/s", + "bandwidth": "864.52MB/s", "reconnects": 0, - "status_2xx": 401577, + "status_2xx": 418389, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 120473, - "tpl_baseline": 80315, - "tpl_json": 140551, - "tpl_async_db": 60236, - "cpu_breakdown": "server: 1977% 103MiB | proxy: 2600% 344MiB" + "tpl_static": 125516, + "tpl_baseline": 83677, + "tpl_json": 146436, + "tpl_async_db": 62758, + "cpu_breakdown": "server: 1176% 85MiB | proxy: 2621% 346MiB" }, "json-comp-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 59118, - "avg_latency": "107.88ms", - "p99_latency": "3.37s", - "cpu": "6127.5%", - "memory": "140MiB", + "rps": 197549, + "avg_latency": "46.58ms", + "p99_latency": "1.05s", + "cpu": "6099.2%", + "memory": "120MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "76.15MB/s", - "input_bw": "4.40MB/s", - "reconnects": 11117, - "status_2xx": 295594, + "bandwidth": "282.38MB/s", + "input_bw": "14.69MB/s", + "reconnects": 38791, + "status_2xx": 987747, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -471,19 +471,19 @@ "json-comp-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 50389, - "avg_latency": "73.49ms", - "p99_latency": "1.40s", - "cpu": "6239.0%", - "memory": "131MiB", + "rps": 199935, + "avg_latency": "19.89ms", + "p99_latency": "420.90ms", + "cpu": "6273.8%", + "memory": "114MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "64.92MB/s", - "input_bw": "3.75MB/s", - "reconnects": 9429, - "status_2xx": 251946, + "bandwidth": "285.89MB/s", + "input_bw": "14.87MB/s", + "reconnects": 39487, + "status_2xx": 999678, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -491,18 +491,18 @@ "json-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 656607, - "avg_latency": "49.04ms", - "p99_latency": "1.03s", - "cpu": "6343.2%", - "memory": "306MiB", + "rps": 655129, + "avg_latency": "48.94ms", + "p99_latency": "1.28s", + "cpu": "6422.9%", + "memory": "303MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.19GB/s", + "bandwidth": "2.18GB/s", "reconnects": 0, - "status_2xx": 3315867, + "status_2xx": 3308404, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -510,18 +510,18 @@ "json-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 634021, - "avg_latency": "189.03ms", - "p99_latency": "4.98s", - "cpu": "6240.8%", - "memory": "1.0GiB", + "rps": 639897, + "avg_latency": "187.10ms", + "p99_latency": "5.00s", + "cpu": "6432.8%", + "memory": "1016MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.11GB/s", + "bandwidth": "2.13GB/s", "reconnects": 0, - "status_2xx": 3220828, + "status_2xx": 3250681, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -529,18 +529,18 @@ "json-tls-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 403673, - "avg_latency": "9.93ms", - "p99_latency": "58.04ms", - "cpu": "6210.0%", - "memory": "411MiB", + "rps": 399878, + "avg_latency": "10.07ms", + "p99_latency": "64.54ms", + "cpu": "6527.5%", + "memory": "662MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.38GB", + "bandwidth": "1.37GB", "reconnects": 0, - "status_2xx": 2058790, + "status_2xx": 2039136, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -548,91 +548,91 @@ "latency-10k-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 9982, - "avg_latency": "81.3us", - "p99_latency": "117.0us", - "cpu": "36.2%", - "memory": "76MiB", + "rps": 9980, + "avg_latency": "79.9us", + "p99_latency": "119.0us", + "cpu": "35.7%", + "memory": "77MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, "bandwidth": "1.56MB/s", "reconnects": 0, - "status_2xx": 199671, + "status_2xx": 199628, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 7558233, - "cpu_per_req_us": 37.8534, + "cpu_usec": 7454906, + "cpu_per_req_us": 37.344, "target_rate": 10000, - "rate_ratio": 0.9982, - "p99_9_latency": "603.0us" + "rate_ratio": 0.998, + "p99_9_latency": "178.0us" }, "latency-1m-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 997684, - "avg_latency": "150.4us", - "p99_latency": "314.0us", - "cpu": "4218.0%", + "rps": 997811, + "avg_latency": "156.4us", + "p99_latency": "329.0us", + "cpu": "4125.5%", "memory": "81MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "155.64MB/s", + "bandwidth": "155.66MB/s", "reconnects": 0, - "status_2xx": 19956937, + "status_2xx": 19959660, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 775551605, - "cpu_per_req_us": 38.8613, + "cpu_usec": 799053707, + "cpu_per_req_us": 40.0334, "target_rate": 1000000, - "rate_ratio": 0.9977, - "p99_9_latency": "2651.0us" + "rate_ratio": 0.9978, + "p99_9_latency": "6018.0us" }, "latency-500k-8cpu-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 264320, - "avg_latency": "4661938.9us", - "p99_latency": "9891840.0us", - "cpu": "779.2%", - "memory": "32MiB", + "rps": 260201, + "avg_latency": "4731018.6us", + "p99_latency": "10104832.0us", + "cpu": "779.3%", + "memory": "33MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "41.23MB/s", + "bandwidth": "40.59MB/s", "reconnects": 0, - "status_2xx": 5288405, + "status_2xx": 5205921, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 159603498, - "cpu_per_req_us": 30.1799, + "cpu_usec": 159570865, + "cpu_per_req_us": 30.6518, "target_rate": 500000, - "rate_ratio": 0.5286, - "p99_9_latency": "10579968.0us" + "rate_ratio": 0.5204, + "p99_9_latency": "10522624.0us" }, "limited-conn-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1385176, - "avg_latency": "2.91ms", - "p99_latency": "46.30ms", - "cpu": "6288.4%", - "memory": "88MiB", + "rps": 1414686, + "avg_latency": "2.87ms", + "p99_latency": "45.60ms", + "cpu": "6295.6%", + "memory": "92MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "207.34MB/s", - "input_bw": "107.00MB/s", - "reconnects": 692877, - "status_2xx": 6925881, + "bandwidth": "211.76MB/s", + "input_bw": "109.28MB/s", + "reconnects": 708045, + "status_2xx": 7073431, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -640,18 +640,18 @@ "pipelined-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 7326797, - "avg_latency": "8.97ms", - "p99_latency": "13.40ms", - "cpu": "6643.2%", - "memory": "153MiB", + "rps": 7502178, + "avg_latency": "8.74ms", + "p99_latency": "11.50ms", + "cpu": "6335.2%", + "memory": "149MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "1.07GB/s", + "bandwidth": "1.10GB/s", "reconnects": 0, - "status_2xx": 36633989, + "status_2xx": 37510893, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -659,66 +659,66 @@ "production-stack-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 204370, - "avg_latency": "71.66ms", - "p99_latency": "2.60s", - "cpu": "4218.6%", + "rps": 208224, + "avg_latency": "74.58ms", + "p99_latency": "3.38s", + "cpu": "4292.0%", "memory": "2.0GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.26GB/s", + "bandwidth": "2.31GB/s", "reconnects": 0, - "status_2xx": 1034115, + "status_2xx": 1055699, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 310234, - "tpl_baseline": 103411, - "tpl_items": 517057, - "tpl_me": 103411, - "cpu_breakdown": "cache: 0% 5MiB | edge: 2749% 1.3GiB | authsvc: 726% 565MiB | server: 743% 116MiB" + "tpl_static": 316709, + "tpl_baseline": 105569, + "tpl_items": 527849, + "tpl_me": 105569, + "cpu_breakdown": "cache: 0% 4MiB | edge: 2757% 1.3GiB | authsvc: 780% 593MiB | server: 754% 112MiB" }, "production-stack-256": { "framework": "libuvcpp", "language": "C++", - "rps": 258622, - "avg_latency": "16.77ms", - "p99_latency": "87.65ms", - "cpu": "4823.0%", + "rps": 256484, + "avg_latency": "16.86ms", + "p99_latency": "84.90ms", + "cpu": "4827.6%", "memory": "1.2GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.80GB/s", + "bandwidth": "2.78GB/s", "reconnects": 0, - "status_2xx": 1300873, + "status_2xx": 1290116, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 390261, - "tpl_baseline": 130087, - "tpl_items": 650436, - "tpl_me": 130087, - "cpu_breakdown": "cache: 0% 14MiB | edge: 3131% 988MiB | authsvc: 687% 118MiB | server: 1005% 88MiB" + "tpl_static": 387034, + "tpl_baseline": 129011, + "tpl_items": 645058, + "tpl_me": 129011, + "cpu_breakdown": "cache: 0% 13MiB | edge: 3123% 991MiB | authsvc: 714% 106MiB | server: 991% 77MiB" }, "static-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 239512, - "avg_latency": "128.29ms", - "p99_latency": "440.36ms", - "cpu": "2285.9%", - "memory": "511MiB", + "rps": 547363, + "avg_latency": "57.75ms", + "p99_latency": "227.04ms", + "cpu": "6422.8%", + "memory": "659MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.64GB/s", + "bandwidth": "8.36GB/s", "reconnects": 0, - "status_2xx": 1211933, + "status_2xx": 2764184, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -726,18 +726,18 @@ "static-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 242138, - "avg_latency": "35.82ms", - "p99_latency": "156.09ms", - "cpu": "2181.2%", - "memory": "187MiB", + "rps": 802360, + "avg_latency": "11.00ms", + "p99_latency": "55.09ms", + "cpu": "6439.5%", + "memory": "245MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.69GB/s", + "bandwidth": "12.27GB/s", "reconnects": 0, - "status_2xx": 1215536, + "status_2xx": 4035872, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -745,18 +745,18 @@ "static-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 113382, - "avg_latency": "32.94ms", - "p99_latency": "115.37ms", - "cpu": "2980.8%", - "memory": "272MiB", + "rps": 239028, + "avg_latency": "17.74ms", + "p99_latency": "86.69ms", + "cpu": "4193.0%", + "memory": "302MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.72GB/s", + "bandwidth": "3.64GB/s", "reconnects": 0, - "status_2xx": 568047, + "status_2xx": 1197531, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -764,18 +764,18 @@ "static-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 198927, - "avg_latency": "5.13ms", - "p99_latency": "18.62ms", - "cpu": "3077.4%", - "memory": "409MiB", + "rps": 568833, + "avg_latency": "1.81ms", + "p99_latency": "26.94ms", + "cpu": "6466.9%", + "memory": "274MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.07GB", + "bandwidth": "8.77GB", "reconnects": 0, - "status_2xx": 1014429, + "status_2xx": 2901012, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -783,18 +783,18 @@ "unary-grpc-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3725148, - "avg_latency": "26.52ms", - "p99_latency": "408.94ms", - "cpu": "6439.3%", - "memory": "414MiB", + "rps": 3667663, + "avg_latency": "26.80ms", + "p99_latency": "397.16ms", + "cpu": "6346.3%", + "memory": "423MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "154.54MB/s", + "bandwidth": "152.16MB/s", "reconnects": 0, - "status_2xx": 18849250, + "status_2xx": 18558379, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -802,18 +802,18 @@ "unary-grpc-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3827901, - "avg_latency": "6.90ms", - "p99_latency": "80.09ms", - "cpu": "5973.8%", - "memory": "145MiB", + "rps": 3798244, + "avg_latency": "7.07ms", + "p99_latency": "79.40ms", + "cpu": "6222.9%", + "memory": "148MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "158.24MB/s", + "bandwidth": "156.70MB/s", "reconnects": 0, - "status_2xx": 19292624, + "status_2xx": 19105168, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -821,18 +821,18 @@ "unary-grpc-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3533276, - "avg_latency": "27.60ms", - "p99_latency": "157.81ms", - "cpu": "6160.5%", - "memory": "594MiB", + "rps": 3444335, + "avg_latency": "28.27ms", + "p99_latency": "178.12ms", + "cpu": "6441.0%", + "memory": "520MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "146.87MB/s", + "bandwidth": "142.60MB/s", "reconnects": 0, - "status_2xx": 17913710, + "status_2xx": 17393894, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -840,18 +840,18 @@ "unary-grpc-tls-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3695725, - "avg_latency": "7.17ms", - "p99_latency": "93.63ms", - "cpu": "6138.1%", + "rps": 3681783, + "avg_latency": "7.00ms", + "p99_latency": "54.54ms", + "cpu": "6185.2%", "memory": "163MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "152.47MB/s", + "bandwidth": "151.90MB/s", "reconnects": 0, - "status_2xx": 18589499, + "status_2xx": 18519369, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 From 437dec5e4a5c7a9f8cd16c73b01517c864f293e0 Mon Sep 17 00:00:00 2001 From: Antruly Date: Fri, 16 Oct 2026 21:55:12 +0800 Subject: [PATCH 15/17] Move the pin to 0f34937 (1.7.2-dev), two code commits past 1f6a8fb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seventh run carried `1f6a8fb` and its own regression is what this batch answers: `echo-ws-pipeline-4096` came back `-6.8%` in rate and `+13.8%` in CPU per request, the largest single-profile regression in that round. * `239fef0` moves the buffer allocator's free list out of the blocks and into per-thread tables. The family serves 1-7 byte blocks, and the old scheme put the "next block" pointer in the block's first 8 bytes, so a block smaller than that could never be cached -- four blocks per WebSocket echo message went to `malloc` every time and none was ever reused. Message-allocations per message `11.255 -> 6.255` at `PIPE=1`, `3.692 -> 0.567` at `PIPE=16`, and `0.775 -> 0.730` us per message end to end at `PIPE=16`. * `2872c20` gives the TCP read buffer a per-thread table of exact-size slots, so a `pipeline = 1` profile no longer costs a 64 KiB `malloc`/`free` pair per request. callgrind on this entry: `/json/50?m=6` `-16.57%` instructions, `/baseline11` `-1.36%`; end to end `-14.1%` on `/json/50?m=6`, `-8.9%` on the h2c leg, `baseline11` neutral. `239fef0` changes the layout of `uvcpp_malloc_block_cache` in the public `uvcpp_alloc.h`, but the entry does not include that header, so no entry-side change comes with it. Full criteria, the rigs and the A/B readings are in the library's `doc/benchmark.md` §4.19 and §4.20 and in each commit's message. --- frameworks/libuvcpp/Dockerfile | 60 +++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index def04c09a..d3ce805fa 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -37,14 +37,16 @@ ENV DEBIAN_FRONTEND=noninteractive # and the bytes under it re-uploaded, so a SHA is the only thing that fixes # which revision gets built. `3fdd6208` was `v1.6.1` -- what the first version # of this entry built, and what the platform measured as the board's 58 req/s on -# `baseline-h3-64`. The pin has since moved five times: to `2d5fe74` +# `baseline-h3-64`. The pin has since moved six times: to `2d5fe74` # (`1.6.20-dev`), then to `6eede33` (`1.6.51-dev`), which was the tip of the # library's `master` when it was taken and is the revision the platform's fourth # run measured, then to `8150720` (`1.7.2-dev`), which is the `v1.7.0` tag plus # one documentation commit and two that change code, then to `02ae418` # (`1.7.2-dev`), which is four code commits past it and is what the sixth run -# carried, and now to `1f6a8fb` (`1.7.2-dev`), which is two code commits and a -# documentation commit past `02ae418` and was the tip of the library's `master` +# carried, then to `1f6a8fb` (`1.7.2-dev`), which is two code commits and a +# documentation commit past `02ae418` and is what the seventh run carried, and +# now to `0f34937` (`1.7.2-dev`), which is two code commits and four +# documentation commits past `1f6a8fb` and is the tip of the library's `master` # when it was taken. # Each move was measured against the previous pin before being taken; the # criteria for the `6eede33` batch are further down this file, and each commit's @@ -116,6 +118,56 @@ ENV DEBIAN_FRONTEND=noninteractive # arithmetic are the criterion. This is the h1/h2 static handler; the h3 leg # goes through `uvcpp_static_server` and does not take this path. # +# The `0f34937` move is for the next run: two code commits and four documentation +# ones past `1f6a8fb`. One of the two does touch a public header -- `239fef0` +# changes the layout of `uvcpp_malloc_block_cache` in `uvcpp_alloc.h`, keeping +# its interface -- but this entry does not include that header (it is reached +# only from `uvcpp_buf.cpp` and `uvcpp_write.cpp`), so no entry change is needed +# and this build gets both sides of it from the same revision. Both commits are +# aimed at regressions the seventh run reported, and both carry a local +# deterministic counter as their criterion. +# +# * `239fef0` moves the free list of the buffer allocator's own blocks out of +# the blocks and into per-thread tables. The family serves 1-7 byte blocks -- +# a WebSocket frame header is two bytes and `echo-ws`'s payload is `"hello"` +# -- and the old scheme wrote the "next block" pointer into the block's first +# 8 bytes, so a block below that size could never go on the list: `put()` +# freed it in place and `try_take()` could never hit. Four blocks per +# WebSocket echo message went to `malloc` every time (3 x 5 bytes + 1 x 7 +# bytes, measured at the call site) and none of them was ever reused. On the +# local rig, message-allocations per message `11.255 -> 6.255` at `PIPE=1` +# and `3.692 -> 0.567` at `PIPE=16` (A B B A, the same entry binary with only +# the `.so` swapped, zero spread within a group), and `0.775 -> 0.730` us per +# message end to end with `PIPE=16` (`-5.8%`, 255 891 -> 267 986 msg/s). +# `echo-ws-pipeline-4096` is the profile it is aimed at: the seventh run put +# it at `29 440 675` req/s and `2.31` us of CPU per message, `-6.8%` in rate +# and `+13.8%` in CPU per request against the sixth run -- the largest +# single-profile regression in that round -- and its 5-byte payload means +# none of that cost can be amortised over bytes, so per-message overhead is +# the whole story. +# * `2872c20` gives the TCP read buffer a per-thread table of exact-size slots. +# libuv's `suggested_size` for a TCP stream is 64 KiB flat, so on the +# `pipeline = 1` profiles, where one read event is one request, every request +# costs a 64 KiB `malloc`/`free` pair. The pair is cheap by itself; what is +# not is dragging the large-bin `unlink_chunk` and the fastbin-consolidation +# scan into the hot path on profiles that already allocate heavily per +# response. callgrind on this entry, same URL, same binary: `/json/50?m=6` +# `4 550 109 494 -> 3 796 143 977` instructions (`-16.57%`, with `_int_malloc` +# 12.01% -> 4.86% and `malloc_consolidate` 4.72% -> 0.02%), `/baseline11` +# `-1.36%`. End to end on the local rig, same entry binary with only the +# `.so` swapped: `-14.1%` on `/json/50?m=6`, `-8.9%` on the h2c leg, and +# `baseline11` **neutral** (`11.40 -> 11.60` us, inside the spread). +# +# (The two ends of this one are worth stating together, because the json +# profiles are where the mandated `nlohmann` DOM costs the most and this is +# the only lever left on them: the change cannot touch the serialisation the +# profile measures, only the allocator traffic underneath it.) +# +# Neither is offered as a predicted board movement. On the h1 profiles the +# `-16.57%` of instructions is one handler's user-mode share of an end-to-end +# cost that those profiles spend mostly elsewhere, and the `-14.1%` and `-5.8%` +# figures are this machine's rig, not the board's. +# # What this run also carries, and what is *not* in the pin, is the entry side: # the JSON profiles answer through the framework's standard serializer again # (see the `/json` note below). The fifth run measured the size of that change @@ -398,7 +450,7 @@ ENV DEBIAN_FRONTEND=noninteractive # and this entry is `mode: standard` -- so the framework's own middleware, answering # gzip, is what the guide asks for. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git -ARG LIBUVCPP_REF=1f6a8fbf54907ce942eb0963cf9a391bacb1bacd +ARG LIBUVCPP_REF=0f34937fc68e34fc0309dcbc6c539ef5393b36d1 ARG OPENSSL_VERSION=3.5.0 From 6f4f7504dc0cab92d7acf615fc1a4a6fba00cc62 Mon Sep 17 00:00:00 2001 From: Antruly Date: Sat, 17 Oct 2026 02:26:55 +0800 Subject: [PATCH 16/17] Answer /json and /async-db through the framework's bundled yyjson backend The pin moves to `134f34d`, the tip of the library's `master`, and it moves for this one thing: `a063645` gives the library's yyjson module a construction side and a write-back through yyjson's own serializer, and without it the handlers below cannot exist. The same range carries two other things. `638512b` takes three copies off the h1 dynamic hot path in the library -- a library-side change, so not a like-for-like factor on this entry's h1 rows against the seventh run's -- and the library's fourth database backend (`1607dff`, ODBC, PR #36) lands with its CI and packaging, which is off in the flag set here. The two JSON handlers no longer build an `nlohmann::json` DOM per request. They build a `uvcpp_json_builder` tree and hand it to `uvcpp_json_write`, which is yyjson's own serializer (`yyjson_mut_val_write_buf`), and those bytes go to `resp.json_str()` by move exactly as before. The dataset is parsed once at startup by `uvcpp_json_parse` instead of being held as an `nlohmann::json`; that parse happens before the first request is accepted, so it is in neither measurement below. **The response bytes did not change.** Same library, two `main.cc`, 17 URLs -- `/json/0` through `/json/50` with and without `m`, `count` past the end of the dataset, one negative `count`, one `m` of a million, and five `/async-db` ranges against the seeded Postgres: every body `cmp`-equal, every status line identical, every `Content-Type` identical. That criterion is what makes this safe to ship, and it is not free. A builder keeps insertion order where the DOM path got sorted order for nothing from `std::map`, so the key order is spelled out in the code; and the nested `rating` object is rebuilt key by key, because the data file lists it `score, count` while the response has always answered `count, score`. Getting either wrong would have been a silently different body, not a crash. Measured on the local rig, one server binary per arm, only `main.cc` swapped, arms alternating over four rounds, the client at the shape these profiles actually use -- `-c 1024 -m 1`, because `scripts/lib/profiles.sh` has `json-comp`, `json-tls` and `async-db` at pipeline 1, and a depth past the library's documented per-connection in-flight cap answers `503` (a sweep of `-m` 1/2/4/8/16/32 gave 0/0/0/0/12 336/11 096 of them): `/json/50?m=6` 228.60 -> 125.31 us of CPU per request 60 350 -> 89 605 req/s (four-round ranges 228.60-303.57 against 125.31-156.23 us -- they do not overlap) `/async-db` `min=1&max=500&limit=20` 258.62 -> 158.95 us 30 794 -> 41 120 req/s (258.62-319.94 against 158.95-194.29) Both arms answered zero non-2xx in every round. Both arms also run the same library, so `638512b` and the ODBC batch are held constant and are in neither column. The compliance argument is that this is the framework's own bundled backend, not an entry-side substitution. `standard.md` requires a production-grade library where the ecosystem has one and forbids hand-rolled alternatives built for the benchmark, with one exception -- "If the framework itself bundles or officially recommends a specific library, that library is acceptable" -- and yyjson is a library this framework bundles: all six release legs pass `-DUVCPP_ENABLE_YYJSON=ON` in their package builds, so this is the released library's own configuration. The entry takes it by turning that flag on rather than by adding a dependency. What the entry does *not* do is reach into yyjson directly: it goes through `uvcpp_json_parse` / `uvcpp_json_builder` / `uvcpp_json_write`, so main.cc does not include `` and would not notice if the backend under those three names changed. `UVCPP_ENABLE_DB_ODBC=OFF` is stated in the flag set rather than left to the default, and the default is the reason: it is ON at this pin and needs `sql.h` plus a driver manager. CMake would force it off itself and say so in a configure-time warning, but a flag set that names it is one whose result does not depend on what the image happens to have installed. README.md and meta.json follow the change, and the Dockerfile's comments say why the pin moved, what the criterion was, and what the two arms measured. Verified before the push: the four CI legs on `134f34d` (Linux, macOS, MSVC, MinGW-w64) are green; the yyjson FetchContent path (tag 0.13.0) and a `--depth 1` fetch of the pin SHA both reproduce; and the whole entry was prevalidated locally against the same tree -- h2c/json-h2c/grpc/h1 15 assertions pass, `baseline-h2` 3 395 100 requests, `baseline-h3` 1 777 674, `static-h2` 46 413, `static-h3` 53 271, `gateway-64` 54 200, `gateway-h3` 46 909, all with zero 4xx and zero 5xx, the 12 gateway assertions and the 15 production-stack assertions pass, and the h3 legs' own `json` body assertion reads back count and item count correctly from the new writer's output. --- frameworks/libuvcpp/Dockerfile | 114 +++++++++++-- frameworks/libuvcpp/README.md | 96 +++++++---- frameworks/libuvcpp/main.cc | 281 +++++++++++++++++++++++---------- frameworks/libuvcpp/meta.json | 2 +- 4 files changed, 356 insertions(+), 137 deletions(-) diff --git a/frameworks/libuvcpp/Dockerfile b/frameworks/libuvcpp/Dockerfile index d3ce805fa..a38473ca5 100644 --- a/frameworks/libuvcpp/Dockerfile +++ b/frameworks/libuvcpp/Dockerfile @@ -159,9 +159,10 @@ ENV DEBIAN_FRONTEND=noninteractive # `baseline11` **neutral** (`11.40 -> 11.60` us, inside the spread). # # (The two ends of this one are worth stating together, because the json -# profiles are where the mandated `nlohmann` DOM costs the most and this is -# the only lever left on them: the change cannot touch the serialisation the -# profile measures, only the allocator traffic underneath it.) +# profiles are where the `nlohmann` DOM the entry answered with at the time +# costs the most and this is the only lever left on them: the change cannot +# touch the serialisation the profile measures, only the allocator traffic +# underneath it.) # # Neither is offered as a predicted board movement. On the h1 profiles the # `-16.57%` of instructions is one handler's user-mode share of an end-to-end @@ -179,6 +180,13 @@ ENV DEBIAN_FRONTEND=noninteractive # against the sixth run's -- the most recent one that also had it back -- and not # against the fifth's, whose tree answered through `uvcpp_json_writer`. # +# The pin this file now names carries the entry side one step further, and its +# `json-*` and `async-db` rows are *not* like-for-like with that run's either: +# both handlers build their bodies with the library's yyjson backend instead of +# the `nlohmann` DOM, so the serializer is a second variable on those rows +# alongside whatever the pin itself moved. That change is argued in full at the +# end of the pin block below, criterion first. +# # The other entry-side change since the sixth run is on the h3 static leg, and it # is the same shape of omission the JSON one was. `uvcpp_static_server` is a # different handler from the `uvcpp_web_app` static code the h1/h2 legs use -- @@ -327,13 +335,17 @@ ENV DEBIAN_FRONTEND=noninteractive # # The pin then sat on `perf(json): 写出器的键与字符串值改成一次追加` (c8950ae), the # change this entry's `/json` handler was rewritten onto. **The entry no longer -# uses it.** `uvcpp_json_writer` is an alternative JSON serializer, and the JSON -# profiles measure serialization, so Standard mode wants the framework's -# standard serializer: the response is built as an `nlohmann::json` DOM and sent -# through `resp.json()`. That is a deliberate, measured loss on `json-comp`, -# `json-tls` and `/async-db` (see the README's `/json` note); the paragraphs -# below are kept as the record of why the pin moved when it did, not as a -# description of what this entry does now. `uvcpp_json_writer`'s +# uses it.** `uvcpp_json_writer` is an alternative JSON serializer -- the rule +# that lists "Alternative JSON serializers" under Tuned is `tuned.md`, and this +# entry is `mode: standard` -- so these profiles are answered by the framework's +# own JSON backend instead: an `nlohmann::json` DOM through `resp.json()` at the +# pin this paragraph was written at, and a `uvcpp_json_builder` tree written by +# `uvcpp_json_write` since the pin moved to `134f34d`. Both are the framework's +# backend, and the cost of not answering through the streaming writer is on +# `json-comp`, `json-tls` and `/async-db` -- the README's `/json` note is where +# that is recorded. The paragraphs below are kept as the record of why the pin +# moved when it did, not as a description of what this entry does now. +# `uvcpp_json_writer`'s # `key()` used to append the comma, the opening quote, the escaped body, the # closing quote and the colon separately -- four or five out-of-line # `std::string::append` calls per key, three per string value, 4073 of them on a @@ -391,8 +403,57 @@ ENV DEBIAN_FRONTEND=noninteractive # LRU and stops copying the whole body on a hit; an h1 pipeline's final # `Connection: close` no longer takes the earlier responses with it; and the h2 # outbound body is held by reference rather than copied per response. Two optional -# modules arrive in the same range and both default to OFF -- a yyjson parse backend -# and a brotli encoder -- and neither is in the flag set above. +# modules arrive in the same range and both default to OFF -- a yyjson backend and +# a brotli encoder. Of the two, the yyjson one is now on in the flag set above, +# and the next paragraph is why; brotli stays off two paragraphs further down. +# +# `UVCPP_ENABLE_YYJSON`. The pin moves to `134f34d`, the tip of the library's +# `master` when it was taken. Three things are in that range. `a063645` gives the +# JSON backend a construction side and yyjson's own serializer, and that is what +# `/json` and `/async-db` answer through now -- it is the reason for this move. +# `638512b` takes three copies off the h1 dynamic hot path (`-11.03%` user-mode +# instructions per request on the library's own callgrind rig, which the commit +# sizes at `-3` to `-4%` end to end and does not claim as a board movement +# either); it is a library-side change and it is not a like-for-like factor on +# this entry's h1 rows against the previous run's. The library's fourth database +# backend (ODBC, PR #36) lands in the same range with its CI and packaging; it is +# off in the flag set below. Four CI legs on `134f34d` -- Linux, macOS, MSVC and +# MinGW-w64 -- are green. +# +# What changed, in one sentence: the handlers no longer build an `nlohmann::json` DOM +# per request. They build a `uvcpp_json_builder` tree and hand it to +# `uvcpp_json_write`, which is yyjson's own serializer (`yyjson_mut_val_write_buf`) +# -- and yyjson is a production-grade library the framework itself bundles, which +# is the standard rule's own exception +# ("If the framework itself bundles or officially recommends a specific library, that +# library is acceptable", `implementation-rules/frameworks/standard.md`). It is also +# the backend decision the shipped packages already make: all six release legs pass +# `-DUVCPP_ENABLE_YYJSON=ON` in their package builds, so this is the released +# library's own configuration rather than an entry-only one. +# +# The criterion is byte-identity, and it is met. Same library, two `main.cc`, 17 URLs +# -- `/json/0` through `/json/50` with and without `m`, `count` past the end of the +# dataset, one negative `count`, one `m` of a million, and five `/async-db` ranges +# against the seeded Postgres: every body `cmp`-equal, every status line identical, +# every `Content-Type` identical. The key order the DOM path got for free from +# `std::map` is spelled out in the builder -- insertion order is what a builder keeps +# -- and the nested `rating` object is rebuilt key by key, because the data file lists +# it `score, count` while the response has always answered `count, score`. Getting +# that wrong would have been a silently different body, not a crash. +# +# Measured on the local rig, the same library with only `main.cc` swapped, arms +# alternating over four rounds, the client at the shape these profiles actually use +# (`pipeline = 1`, 1024 connections -- `scripts/lib/profiles.sh` has `json-comp`, +# `json-tls` and `async-db` at pipeline 1, and depth past the library's documented +# per-connection cap answers `503`). `/json/50?m=6`: 228.60 -> 125.31 us of server CPU +# per request and 60,350 -> 89,605 req/s, the arms' four-round ranges not overlapping +# (228.60-303.57 against 125.31-156.23 us). `/async-db` at `min=1&max=500&limit=20`: +# 258.62 -> 158.95 us and 30,794 -> 41,120 req/s (258.62-319.94 against 158.95-194.29). +# Both arms answered zero non-2xx. Both arms also run the same library, so `638512b` +# and the ODBC batch are held constant and are in neither column. The dataset is +# additionally parsed once at startup by `uvcpp_json_parse` instead of being held as +# an `nlohmann::json`; that parse happens before the first request is accepted, so it +# is in neither figure. # # Why brotli is off, given that `json-compressed` is the one profile in this suite # whose request asks for it (`Accept-Encoding: gzip, br`, byte for byte in @@ -450,7 +511,7 @@ ENV DEBIAN_FRONTEND=noninteractive # and this entry is `mode: standard` -- so the framework's own middleware, answering # gzip, is what the guide asks for. ARG LIBUVCPP_REPO=https://github.com/Antruly/libuvcpp.git -ARG LIBUVCPP_REF=0f34937fc68e34fc0309dcbc6c539ef5393b36d1 +ARG LIBUVCPP_REF=134f34db3b7eba08290504480fa89062fde5a3b5 ARG OPENSSL_VERSION=3.5.0 @@ -516,12 +577,31 @@ RUN git init . \ && git checkout FETCH_HEAD # `BUILD_SHARED_LIBS=OFF` makes every fetched dependency (libuv, llhttp, zlib, -# nghttp2, ngtcp2, nghttp3) a static library, and `OPENSSL_USE_STATIC_LIBS=ON` -# does the same for OpenSSL, so libpq is the only shared object the result -# needs beyond libc and libstdc++. `CMAKE_POSITION_INDEPENDENT_CODE=ON` is +# yyjson, nghttp2, ngtcp2, nghttp3) a static library, and +# `OPENSSL_USE_STATIC_LIBS=ON` does the same for OpenSSL, so libpq is the only +# shared object the result needs beyond libc and libstdc++. +# `CMAKE_POSITION_INDEPENDENT_CODE=ON` is # required, not defensive: BUILD_SHARED_LIBS=OFF also flips CMake's PIC default # to OFF, and a non-PIC libuv/llhttp cannot be linked into libuvcpp.so. # +# `UVCPP_ENABLE_YYJSON=ON` is the JSON backend the `/json` and `/async-db` +# handlers answer through, and it is the flag the six release legs already pass +# in their package builds (`release.yml`), so what this entry builds is the +# released configuration and not an entry-only one. It fetches yyjson 0.13.0 +# over FetchContent exactly like the other five dependencies and compiles its +# single `.c` into this same `libuvcpp.so` -- nothing extra is installed and +# nothing else in the image changes. The paragraph at the pin note above is the +# argument; the short version is that the profile's standard rule admits a +# library the framework itself bundles, and this one it bundles. +# +# `UVCPP_ENABLE_DB_ODBC=OFF` is stated rather than left to the default, and the +# default is why: the pin below carries the library's fourth database backend +# (`1607dff`), whose option defaults ON and needs `sql.h` plus a driver manager. +# This image installs neither, so CMake would force the backend off itself and +# say so in a configure-time warning -- the same end state, reached by a warning +# instead of a flag. The entry needs Postgres and nothing else, which is what the +# two `OFF`s next to it already say. +# # websocket, static files, h2 and h3 all come from these same flags -- there is # no second build for the TLS legs. RUN cmake -S . -B build -G Ninja \ @@ -539,6 +619,7 @@ RUN cmake -S . -B build -G Ninja \ -DUVCPP_BUILD_WEB=ON \ -DUVCPP_BUILD_WEBAPP=ON \ -DUVCPP_ENABLE_ZLIB=ON \ + -DUVCPP_ENABLE_YYJSON=ON \ -DUVCPP_ENABLE_BROTLI=OFF \ -DUVCPP_ENABLE_OPENSSL=ON \ -DUVCPP_ENABLE_NGHTTP2=ON \ @@ -548,6 +629,7 @@ RUN cmake -S . -B build -G Ninja \ -DUVCPP_ENABLE_DB_PGSQL=ON \ -DUVCPP_ENABLE_DB_MYSQL=OFF \ -DUVCPP_ENABLE_DB_SQLITE=OFF \ + -DUVCPP_ENABLE_DB_ODBC=OFF \ -DUVCPP_ENABLE_CAPI=OFF \ -DUVCPP_ENABLE_WSDL=OFF \ -DOPENSSL_USE_STATIC_LIBS=ON \ diff --git a/frameworks/libuvcpp/README.md b/frameworks/libuvcpp/README.md index a3053b3a7..83bb8025c 100644 --- a/frameworks/libuvcpp/README.md +++ b/frameworks/libuvcpp/README.md @@ -8,8 +8,8 @@ libuvcpp on the libuv event loop, one loop per available core. - **Library:** libuvcpp 1.7.x ([repo](https://github.com/Antruly/libuvcpp)), built from source in the image - **Build:** `cmake -DCMAKE_BUILD_TYPE=Release` on `ubuntu:24.04`, with libuv, - llhttp, zlib, nghttp2, ngtcp2, nghttp3 and OpenSSL 3.5 all built statically - into `libuvcpp.so`; the entry is `g++ -std=c++11 -O2 -DNDEBUG` + llhttp, zlib, yyjson 0.13.0, nghttp2, ngtcp2, nghttp3 and OpenSSL 3.5 all + built statically into `libuvcpp.so`; the entry is `g++ -std=c++11 -O2 -DNDEBUG` The library is built from source rather than installed from a published `libuvcpp--linux-x64` release package. That package was the only @@ -18,7 +18,7 @@ release workflow's legs passed `-DUVCPP_ENABLE_DB_MYSQL=OFF -DUVCPP_ENABLE_DB_PGSQL=OFF` — the workflow asserts it with an `ldd` check that fails if `libpq.so` appears — while `/async-db` needs the Postgres backend. The pin has since moved onto development revisions: the `v1.7.0` tag is the nearest -release, the pin is twelve commits past it, and a release package exists only for +release, the pin is 37 commits past it, and a release package exists only for a tag, so building the pinned SHA from source is the only way to build exactly what the pin names. The `1.7.0` packages do carry both remote backends, behind the runtime-load seam that is why the same `ldd` list still passes, so the @@ -132,12 +132,15 @@ the profile forbids the framework from doing the JWT verification itself. documented static handler. Nothing in the entry reads or caches static bytes itself — the handler revalidates each request against the file's size and `mtime` (seconds and nanoseconds), so a replaced file is served immediately. -- **JSON is serialized per request by the framework's standard serializer.** - The handler builds an `nlohmann::json` DOM out of the live dataset — the - framework's own `uvcpp_json` is nlohmann's `json`, and the response goes out - through `resp.json()`, which is the framework's `uvcpp_json_dump()` — and - `total` is computed inside the loop, per request. Nothing is pre-serialized, - cached, or spliced out of a previously rendered buffer. +- **JSON is serialized per request by the framework's JSON backend.** The + handler walks the live dataset, builds a `uvcpp_json_builder` tree — a public + and documented API in `doc/json-guide.md` — and writes it out through + `uvcpp_json_write`, which is yyjson's own serializer. The + backend is the library's yyjson one; the Dockerfile turns it on with + `-DUVCPP_ENABLE_YYJSON=ON`, which is the flag all six release legs pass in + their package builds, so this is the shipped library's configuration and not + an entry-only one. `total` is computed inside the loop, per request. Nothing + is pre-serialized, cached, or spliced out of a previously rendered buffer. This is deliberate, and it is the one place this entry gives up speed for the rules. The profile's JSON tests exist to measure serialization, Standard mode @@ -145,23 +148,46 @@ the profile forbids the framework from doing the JWT verification itself. (`test-profiles/h1/json-compressed/implementation.md`) and for the ecosystem's production-grade library over a hand-rolled alternative (`implementation-rules/frameworks/standard.md`: "Experimental or hand-rolled - alternatives solely for benchmark performance are not permitted"), and - `implementation-rules/frameworks/tuned.md` lists "Alternative JSON - serializers" among the things *Tuned* allows. `uvcpp_json_writer` — the - library's streaming writer, a public and documented API that - `doc/json-guide.md` describes — is such an alternative serializer, so this - entry does not answer these profiles with it. The same applies to `/async-db`, - which used to walk the result rows straight into the writer. Reading is the - same rule the other way: `uvcpp_json_parse` and the `uvcpp_json_get_*` - accessors rather than nlohmann's `parse` and `value()` directly, so every JSON - call the entry makes is one the library publishes — and none of them can throw - out of a libuv callback, which is what those accessors exist for. -- **The cost of that choice, measured.** At the 1.7.2-dev pin this repository - was built on, `/json/50?m=6` through the writer cost 63.2 us of server CPU per - request against the DOM path's 176.0 us on the same 6 pinned cores, and the - writer also skipped the per-item DOM copy the DOM path makes. Both figures are - the ones this entry reported in earlier revisions; they are recorded here - because the entry no longer collects them. + alternatives solely for benchmark performance are not permitted"), and the + same file's exception is what settles which library: "If the framework itself + bundles or officially recommends a specific library, that library is + acceptable" — yyjson is bundled by libuvcpp itself and compiled into the same + `libuvcpp.so` the entry links. `implementation-rules/frameworks/tuned.md` + lists "Alternative JSON serializers" among the things *Tuned* allows, and + `uvcpp_json_writer` — the library's streaming writer, a public and documented + API that `doc/json-guide.md` describes — is such an alternative serializer, so + this entry does not answer these profiles with it. The same applies to + `/async-db`, which used to walk the result rows straight into the writer. + Reading is the same rule the other way: `uvcpp_json_parse` and the + `uvcpp_json_get_*` accessors rather than nlohmann's `parse` and `value()` + directly, so every JSON call the entry makes is one the library publishes — + and none of them can throw out of a libuv callback, which is what those + accessors exist for. +- **The response bytes did not change.** The handlers used to build an + `nlohmann::json` DOM and send it through `resp.json()`; both now build a + builder tree and write it with yyjson. Over 17 URLs — `/json/0` to `/json/50` + with and without `m`, a `count` past the end of the dataset, a negative + `count`, an `m` of a million, and five `/async-db` ranges against the seeded + Postgres — every body is byte-identical between the two, with the same status + lines and the same `Content-Type`. The order a `std::map` sorted for the DOM + path is now spelled out in the builder, because insertion order is what a + builder keeps, and the nested `rating` object is rebuilt key by key for the + same reason (the data file lists it `score, count`; the response has always + answered `count, score`). +- **The cost of choosing the backend over the writer, measured.** At the + 1.7.2-dev pin this repository was built on, `/json/50?m=6` through the writer + cost 63.2 us of server CPU per request against the DOM path's 176.0 us on the + same 6 pinned cores, and the writer also skipped the per-item DOM copy the DOM + path makes. Those two figures are the ones this entry reported in earlier + revisions; they are recorded here as the size of the gap the rules ask it to + accept, not as a pair it still collects. What it does collect is the backend + swap itself, same library and only `main.cc` different, arms alternating over + four rounds at the shape these profiles use (`pipeline = 1`, 1024 + connections): `/json/50?m=6` went from 228.60 to 125.31 us of server CPU per + request and from 60,350 to 89,605 req/s (the four-round ranges do not overlap: + 228.60-303.57 against 125.31-156.23 us), and `/async-db` at + `min=1&max=500&limit=20` from 258.62 to 158.95 us and from 30,794 to 41,120 + req/s. Both arms answered zero non-2xx. - **Compression is the framework's built-in response compression** (gzip), which answers `Accept-Encoding` per request and adds no `Content-Encoding` when the client sends none, at the library's own default level. The library also @@ -233,15 +259,17 @@ the profile forbids the framework from doing the JWT verification itself. - **`tags` is parsed, not spliced.** Postgres returns JSONB as text (OID 3802 is not one of the driver's mapped types, and an unmapped OID degrades to the string libpq returned), and that string *is* the JSON array — so it has to - reach the response as an array rather than as a quoted string. `parse_tags` - runs `uvcpp_json_parse` over it and puts the array in the DOM, which is then - serialized with everything else by `resp.json()`. It used to be walked and - copied through instead, which was faster and is what the writer path did; see - the `/json` note above for why that is not this entry's call to make. + reach the response as an array rather than as a quoted string. `copy_tags` + runs `uvcpp_json_parse` over it and copies the array into the builder — an + array keeps its order in both serializers, so it can be copied whole where an + object could not — and it is written out with everything else. It used to be + walked and copied through instead, which was faster and is what the writer + path did; see the `/json` note above for why that is not this entry's call to + make. - **One serializer, one path.** Every response body this profile serves — - including the ones carrying `tags` — is built as an `nlohmann::json` DOM and - dumped by the framework. There is no second, hand-written path to prove - against the first. + including the ones carrying `tags` — is built with the framework's JSON + backend and written by the framework's serializer. There is no second, + hand-written path to prove against the first. - **Failure is not fatal and not silent-in-the-wrong-way.** With no `DATABASE_URL`, or a pool that will not open, `/async-db` answers the documented empty result set — `{"count":0,"items":[]}`, built by the same diff --git a/frameworks/libuvcpp/main.cc b/frameworks/libuvcpp/main.cc index 58678d4d0..95ae0972a 100644 --- a/frameworks/libuvcpp/main.cc +++ b/frameworks/libuvcpp/main.cc @@ -58,6 +58,7 @@ #include #include #include +#include #include @@ -212,8 +213,9 @@ uvcpp_web_static_options kStaticOpts = [] { }(); /// The 50-item dataset, read once at startup. Read-only afterwards, so every -/// worker loop can share it without a lock. -uvcpp_json g_dataset; +/// worker loop can share it without a lock: `uvcpp_json_value` reads touch +/// nothing in the document, and nothing here ever mutates it. +uvcpp_json_doc g_dataset; /// The Postgres pool behind `/async-db`. One pool for the process, not one per /// worker loop: the pool is the thing that owns the connections, and @@ -388,19 +390,25 @@ void load_dataset() { const std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); // Through the framework's own documented parser rather than nlohmann's - // directly: same backend, but the entry then touches no JSON API the library - // does not publish. `uvcpp_json_parse` reports a `json_status` instead of - // throwing, and it applies the depth and size limits `nlohmann::json::parse` - // leaves to the caller. The 12 KB dataset is three orders of magnitude under - // its 8 MiB default. - uvcpp_json parsed; - const json_status st = uvcpp_json_parse(text, parsed); - if (st != json_status::OK) { + // directly: the entry then touches no JSON API the library does not publish. + // `uvcpp_json_parse` reports a `json_doc_status` instead of throwing, and it + // applies the depth and size limits (64 levels, 8 MiB) that + // `nlohmann::json::parse` leaves to the caller. The 12 KB dataset is three + // orders of magnitude under both. + // + // The parser here is the framework's yyjson backend, not its nlohmann one + // (`webapp/uvcpp_web_json.h`), because `/json` and `/async-db` build their + // responses with the same backend's construction side -- see `build_json`. + // A second parse of the same bytes into another DOM would only be a second + // copy of the dataset, and the two would have to be kept in step. + uvcpp_json_doc parsed; + const json_doc_status st = uvcpp_json_parse(text, parsed); + if (st != json_doc_status::OK) { std::fprintf(stderr, "libuvcpp: cannot parse `%s`: %s\n", path.c_str(), - json_status_name(st)); + json_doc_status_name(st)); return; } - if (!parsed.is_array()) { + if (!parsed.root().is_array()) { std::fprintf(stderr, "libuvcpp: `%s` is not a JSON array\n", path.c_str()); return; } @@ -473,47 +481,91 @@ long long baseline_sum(const uvcpp_web_request& req) { return sum; } +/// One key of a parsed dataset item, copied whole into the builder: value, +/// type and nesting included. A key the dataset does not have stays absent -- +/// `add_copy` on an invalid view would hang a `null` there, and the nlohmann +/// path this replaced never invented a key either. +void copy_item_key(uvcpp_json_builder& b, uvcpp_json_node obj, const char* key, + const uvcpp_json_value& item) { + const uvcpp_json_value v = item.get(key); + if (v.valid()) b.add_copy(obj, key, v); +} + /// `{"count": N, "items": [...]}` for the first `count` dataset items, each -/// carrying `total = price * quantity * m`. `count` leads because the object is -/// nlohmann's default -- a `std::map` -- so keys come out sorted. +/// carrying `total = price * quantity * m`. `count` leads because that is the +/// order the response has always used: nlohmann's object is a `std::map` and +/// its keys come out sorted, so `count` before `items`. /// -/// Serialized per request, out of the live dataset, by the framework's standard -/// JSON serialization: an `nlohmann::json` DOM built from the live items and -/// handed to `resp.json()`, which is the framework's own `uvcpp_json_dump()` -- -/// `nlohmann::json::dump()` (`src/webapp/uvcpp_web_response.cpp:470`). Nothing -/// is rendered ahead of time and no response is cached; every request walks the +/// Built per request, out of the live dataset, by the framework's yyjson +/// backend: a `uvcpp_json_builder` tree written out by `uvcpp_json_write`, +/// which is yyjson's own serializer (`src/yyjson/uvcpp_json_doc.h`). The bytes +/// come from a production-grade JSON library the framework bundles -- what the +/// JSON profiles ask for when they ask for "the framework standard JSON +/// serialization" and for the ecosystem's library over a hand-rolled +/// alternative (`implementation-rules/frameworks/standard.md`). Nothing is +/// rendered ahead of time and no response is cached; every request walks the /// live items and writes their bytes. /// -/// The serializer is not the entry's to pick. These profiles exist to measure -/// serialization, and Standard mode asks for "the framework standard JSON -/// serialization" (test-profiles/h1/json-compressed/implementation.md) and for -/// the ecosystem's production-grade library over a hand-rolled alternative -/// (implementation-rules/frameworks/standard.md:55). `uvcpp_json_writer`, the -/// streaming writer `` offers, is an alternative -/// serializer -- implementation-rules/frameworks/tuned.md lists "Alternative -/// JSON serializers" under Tuned -- so this entry does not route these -/// responses through it. -nlohmann::json build_json(int count, long long m) { - const int available = static_cast(g_dataset.size()); +/// `uvcpp_json_writer`, the streaming writer `` +/// offers, is an alternative serializer -- `implementation-rules/frameworks/ +/// tuned.md` lists "Alternative JSON serializers" under Tuned -- and this entry +/// still does not route these responses through it. What the 1.7.2 pin added is +/// a construction side to the framework's own backend, so the standard path and +/// the fast path stopped being different paths. +/// +/// **The key order below is load-bearing.** A `uvcpp_json_builder` object keeps +/// insertion order while nlohmann's sorts, so the order has to be spelled out, +/// and it is spelled out as that sorted order: `active`, `category`, `id`, +/// `name`, `price`, `quantity`, `rating`, `tags`, `total`. Copying a whole item +/// with `add_copy` would carry the data file's order instead. For the same +/// reason the nested `rating` object is rebuilt key by key -- the dataset lists +/// it `score, count` -- while `tags`, an array, can be copied whole, because +/// arrays keep their order in both serializers. +bool build_json(std::string& out, int count, long long m) { + const uvcpp_json_value items = g_dataset.root(); + const int available = static_cast(items.size()); int n = count; if (n < 0) n = 0; if (n > available) n = available; - nlohmann::json items = nlohmann::json::array(); + uvcpp_json_builder b; + uvcpp_json_node root = b.make_object(); + b.set_root(root); + b.add_int(root, "count", n); + uvcpp_json_node arr = b.add_array(root, "items"); for (int i = 0; i < n; ++i) { - nlohmann::json item = g_dataset[static_cast(i)]; - long long price = 0; - long long quantity = 0; - uvcpp_json_get_int(item, "price", price); - uvcpp_json_get_int(item, "quantity", quantity); - item["total"] = price * quantity * m; - items.push_back(std::move(item)); + const uvcpp_json_value item = items.at(static_cast(i)); + // Read with the same reading the nlohmann path used: a missing or + // non-numeric value counts as zero. Only `total` is arithmetic; every other + // value goes through `copy_item_key` untouched. + const long long price = item.get("price").as_int(); + const long long quantity = item.get("quantity").as_int(); + + uvcpp_json_node o = b.push_object(arr); + copy_item_key(b, o, "active", item); + copy_item_key(b, o, "category", item); + copy_item_key(b, o, "id", item); + copy_item_key(b, o, "name", item); + copy_item_key(b, o, "price", item); + copy_item_key(b, o, "quantity", item); + const uvcpp_json_value rating = item.get("rating"); + if (rating.valid()) { + uvcpp_json_node r = b.add_object(o, "rating"); + copy_item_key(b, r, "count", rating); + copy_item_key(b, r, "score", rating); + } + copy_item_key(b, o, "tags", item); + b.add_int(o, "total", price * quantity * m); } - nlohmann::json payload; - payload["items"] = std::move(items); - payload["count"] = n; - return payload; + const json_write_status st = uvcpp_json_write(root, out); + if (st != json_write_status::OK) { + // Either `NO_MEMORY` (a resize of `out` failed) or `MISUSE` (the tree is + // not there at all, i.e. the pools failed); `last_error()` says which. + std::fprintf(stderr, "libuvcpp: `/json` could not be written: %s\n", + b.last_error()); + } + return st == json_write_status::OK; } // --------------------------------------------------------------------------- @@ -543,8 +595,19 @@ void handle_json(uvcpp_web_request& req, uvcpp_web_response& resp, const std::string* count = req.param("count"); const std::string* multiplier = req.query("m"); const long long m = multiplier != nullptr ? to_ll(*multiplier) : 1; - resp.json(build_json(count != nullptr ? static_cast(to_ll(*count)) : 0, - m)); + std::string body; + if (build_json(body, + count != nullptr ? static_cast(to_ll(*count)) : 0, m)) { + // `json_str(std::string&&)` takes the buffer over rather than copying it, + // exactly as `resp.json()` does with the string `dump()` returns. + resp.json_str(std::move(body)); + } else { + // Not answerable, so say so rather than answer a body that is missing + // items. The documented empty result set is `/async-db`'s fallback, not + // this profile's; a short answer that claims a `count` it does not have + // would be the one thing worse than a 500. + resp.status(500).json_str("{\"error\":\"json build failed\"}"); + } resp.end(); } @@ -576,11 +639,38 @@ nlohmann::json parse_tags(const std::string& text) { return nlohmann::json::array(); } -/// `{"count": N, "items": [...]}` for the rows Postgres returned, as a DOM the -/// framework's `resp.json()` serializes per request. `count` leads because the -/// serializer is nlohmann's and an object there is a `std::map`, so keys come -/// out sorted; that is also the order this entry's reference path and -/// `/api/items` have always used. +/// The same column, into a builder instead of a DOM. Copying the parsed array +/// whole is safe here -- an array keeps its order in both serializers, and the +/// elements keep their types (strings, on this column) rather than being +/// re-typed on the way through. +/// +/// The `[]` fallback is the documented one from `parse_tags` above: the key is +/// always there, and a literal that will not parse becomes an empty array +/// rather than failing the request. +void copy_tags(uvcpp_json_builder& b, uvcpp_json_node obj, + const std::string& text) { + uvcpp_json_doc parsed; + if (uvcpp_json_parse(text, parsed) == json_doc_status::OK) { + // `root()` after the parse, never before: a value view borrows the + // document, and a successful `parse()` replaces the document underneath it. + const uvcpp_json_value v = parsed.root(); + if (v.is_array()) { + b.add_copy(obj, "tags", v); + return; + } + } + b.add_array(obj, "tags"); +} + +/// `{"count": N, "items": [...]}` for the rows Postgres returned, built the same +/// way `/json` builds its body: a `uvcpp_json_builder` tree written out by +/// `uvcpp_json_write`, the framework's yyjson backend. +/// +/// `count` leads and the item keys follow the order below, which is the order +/// this response has always used -- nlohmann's `std::map` sort, and the same +/// order `/api/items` uses. So `rating` is rebuilt key by key: the response +/// says `count` before `score`, while the old code inserted `score` first and +/// let nlohmann sort it. /// /// Column names are the profile's, not an invention: `id`, `name`, `category`, /// `price`, `quantity`, `active`, `tags`, then `rating_score`/`rating_count` @@ -588,45 +678,55 @@ nlohmann::json parse_tags(const std::string& text) { /// the profile reads `price` and `rating.score` as INTEGER and says so. /// `count` is the number of rows actually returned, never the requested limit. /// -/// `tags` is JSONB and the driver hands JSONB over as text, so the column -/// arrives as the array literal `["fast", "new"]`. `parse_tags` above is what -/// turns that literal into the array the response carries -- an nlohmann -/// `parse()` per row, which is the documented reading of that column. -nlohmann::json build_async_db_json(const uvcpp_db_table& table) { +/// Unlike `/json` there is no `total`: the profile's `/async-db` shape does not +/// have one, and this handler has never added it. +bool build_async_db_json(std::string& out, const uvcpp_db_table& table) { const std::vector& rows = table.rows(); - nlohmann::json items = nlohmann::json::array(); + uvcpp_json_builder b; + uvcpp_json_node root = b.make_object(); + b.set_root(root); + b.add_int(root, "count", static_cast(rows.size())); + uvcpp_json_node arr = b.add_array(root, "items"); for (size_t i = 0; i < rows.size(); ++i) { const uvcpp_db_row& row = rows[i]; - nlohmann::json item; - item["id"] = row["id"].to_int64(); - item["name"] = row["name"].to_text(); - item["category"] = row["category"].to_text(); - item["price"] = row["price"].to_int64(); - item["quantity"] = row["quantity"].to_int64(); - item["active"] = row["active"].to_bool(); - item["tags"] = parse_tags(row["tags"].to_text()); - nlohmann::json rating; - rating["score"] = row["rating_score"].to_int64(); - rating["count"] = row["rating_count"].to_int64(); - item["rating"] = std::move(rating); - items.push_back(std::move(item)); + uvcpp_json_node o = b.push_object(arr); + b.add_bool(o, "active", row["active"].to_bool()); + b.add_string(o, "category", row["category"].to_text()); + b.add_int(o, "id", row["id"].to_int64()); + b.add_string(o, "name", row["name"].to_text()); + b.add_int(o, "price", row["price"].to_int64()); + b.add_int(o, "quantity", row["quantity"].to_int64()); + uvcpp_json_node r = b.add_object(o, "rating"); + b.add_int(r, "count", row["rating_count"].to_int64()); + b.add_int(r, "score", row["rating_score"].to_int64()); + copy_tags(b, o, row["tags"].to_text()); } - nlohmann::json payload; - payload["count"] = static_cast(rows.size()); - payload["items"] = std::move(items); - return payload; + const json_write_status st = uvcpp_json_write(root, out); + if (st != json_write_status::OK) { + std::fprintf(stderr, "libuvcpp: `/async-db` could not be written: %s\n", + b.last_error()); + } + return st == json_write_status::OK; } /// The documented fallback shape, for when the pool is not up or the statement /// did not run. Built here rather than spelled out as a literal, so the empty /// answer and the populated one are the same object graph and cannot drift -/// apart; `resp.json()` serializes it exactly like any other. -nlohmann::json empty_table_json() { - nlohmann::json payload; - payload["count"] = 0; - payload["items"] = nlohmann::json::array(); - return payload; +/// apart; the same serializer writes it. +/// +/// The write status is deliberately not reported: these 26 bytes can only fail +/// on a `std::bad_alloc` from `out`'s first resize, and a fallback body for +/// that would have to allocate as well. `out` is then empty, which every +/// profile reads as a failed request -- the honest answer from a process that +/// has run out of memory. +void write_empty_table(std::string& out) { + uvcpp_json_builder b; + uvcpp_json_node root = b.make_object(); + b.set_root(root); + b.add_int(root, "count", 0); + b.add_array(root, "items"); + (void)uvcpp_json_write(root, out); } /// The retry half of init_database(): keep trying to open the pool, and give up @@ -767,7 +867,9 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, if (limit > 50) limit = 50; if (!g_db_ready) { - resp.json(empty_table_json()); + std::string body; + write_empty_table(body); + resp.json_str(std::move(body)); resp.end(); return; } @@ -784,11 +886,13 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, const int rc = g_db.query( app->loop(), kAsyncDbSql, params, [rp, next](uvcpp_db_status status, const uvcpp_db_table& table) { - if (uvcpp_db_ok(status)) { - rp->json(build_async_db_json(table)); - } else { - rp->json(empty_table_json()); - } + std::string body; + if (uvcpp_db_ok(status)) build_async_db_json(body, table); + // A statement that did not run and a body that could not be written + // (the writer leaves `out` empty on failure) are the same answer here: + // the documented empty result set, not a body that is short by rows. + if (body.empty()) write_empty_table(body); + rp->json_str(std::move(body)); rp->end(); next(); }); @@ -796,7 +900,9 @@ void handle_async_db(uvcpp_web_app* app, uvcpp_web_request& req, // Not dispatched: per the pool's contract the callback will not run, so // this is the only reply this request gets. `next` was never copied, so // the chain is not left suspended. - resp.json(empty_table_json()); + std::string body; + write_empty_table(body); + resp.json_str(std::move(body)); resp.end(); } } @@ -1171,7 +1277,10 @@ int main() { uvcpp_logger::instance().set_level(log_level::OFF); load_dataset(); - if (g_dataset.empty()) { + // `valid()` is "the parse succeeded" and `size()` on the root is the item + // count, so this covers both a dataset that would not load and one that + // loaded as `[]`. + if (!g_dataset.valid() || g_dataset.root().size() == 0) { std::fprintf(stderr, "libuvcpp: dataset is empty -- /json will answer " "with an empty item list\n"); } diff --git a/frameworks/libuvcpp/meta.json b/frameworks/libuvcpp/meta.json index 304207283..a7d9409a4 100644 --- a/frameworks/libuvcpp/meta.json +++ b/frameworks/libuvcpp/meta.json @@ -10,7 +10,7 @@ "response": true }, "engine": "libuv", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's bundled yyjson backend (uvcpp_json_builder, written by yyjson's own serializer, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "enabled": true, "tests": [ From fcfd2d0f61c31322682dca39573b70f74cc5b961 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Oct 2026 10:40:18 +0000 Subject: [PATCH 17/17] Benchmark results: libuvcpp [skip ci] --- site/data/frameworks.json | 5 +- site/data/results/libuvcpp.json | 684 ++++++++++++++++---------------- 2 files changed, 346 insertions(+), 343 deletions(-) diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 37b96a391..9197500a5 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -736,6 +736,7 @@ "type": "emerging", "engine": "ioxide", "mode": "standard", + "website": "https://genhttp.org/documentation/server/engines/ioxide/", "completeness": { "routing": true, "middleware": true, @@ -750,6 +751,7 @@ "type": "emerging", "engine": "kestrel", "mode": "standard", + "website": "https://genhttp.org/documentation/server/engines/kestrel/", "completeness": { "routing": true, "middleware": true, @@ -764,6 +766,7 @@ "type": "emerging", "engine": "genhttp", "mode": "standard", + "website": "https://genhttp.org/", "completeness": { "routing": true, "middleware": true, @@ -1167,7 +1170,7 @@ }, "libuvcpp": { "dir": "libuvcpp", - "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's uvcpp_json DOM (nlohmann/json, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", + "description": "libuvcpp on the libuv event loop, one loop per available core. Routing, path and query params, static files and WebSocket through uvcpp_web_app, JSON serialized per request through the framework's bundled yyjson backend (uvcpp_json_builder, written by yyjson's own serializer, no pre-rendered bytes), gzip through the built-in response compression, async Postgres through uvcpp_db_pool, and gRPC unary calls through the uvcpp_grpc module. One process serves HTTP/1.1 and prior-knowledge h2c on 8080, h2c on 8082, HTTP/2 over TLS on 8443, and HTTP/3 on 8443/udp, all from the same build; the gateway-64 and production-stack profiles put a stock nginx edge in front of it and gateway-h3 a stock Caddy one.", "repo": "https://github.com/Antruly/libuvcpp", "type": "emerging", "engine": "libuv", diff --git a/site/data/results/libuvcpp.json b/site/data/results/libuvcpp.json index 0d7bb5d03..cdc60b37f 100644 --- a/site/data/results/libuvcpp.json +++ b/site/data/results/libuvcpp.json @@ -4,44 +4,44 @@ "8gbit-512": { "framework": "libuvcpp", "language": "C++", - "rps": 49306, - "avg_latency": "111.5us", - "p99_latency": "149.0us", - "cpu": "300.5%", - "memory": "208MiB", + "rps": 49340, + "avg_latency": "112.4us", + "p99_latency": "147.0us", + "cpu": "309.1%", + "memory": "209MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "512.68MB/s", - "input_bw": "481.50MB/s", + "bandwidth": "513.04MB/s", + "input_bw": "481.84MB/s", "reconnects": 0, - "status_2xx": 246605, + "status_2xx": 246773, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 16157098, - "cpu_per_req_us": 65.5181, + "cpu_usec": 16579179, + "cpu_per_req_us": 67.1839, "target_rate": 50000, - "rate_ratio": 0.9861, - "p99_9_latency": "5482.0us" + "rate_ratio": 0.9868, + "p99_9_latency": "6130.0us" }, "async-32000": { "framework": "libuvcpp", "language": "C++", - "rps": 1541917, - "avg_latency": "19.78ms", - "p99_latency": "22.40ms", - "cpu": "6138.9%", - "memory": "488MiB", + "rps": 1529805, + "avg_latency": "19.93ms", + "p99_latency": "24.60ms", + "cpu": "6125.0%", + "memory": "489MiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "230.83MB/s", - "input_bw": "70.58MB/s", + "bandwidth": "229.02MB/s", + "input_bw": "70.03MB/s", "reconnects": 0, - "status_2xx": 15419171, + "status_2xx": 15298051, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -49,19 +49,19 @@ "async-db-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 86449, - "avg_latency": "11.41ms", - "p99_latency": "41.00ms", - "cpu": "3388.5%", - "memory": "176MiB", + "rps": 95435, + "avg_latency": "10.21ms", + "p99_latency": "39.70ms", + "cpu": "2796.2%", + "memory": "185MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "334.35MB/s", - "input_bw": "5.77MB/s", - "reconnects": 34274, - "status_2xx": 864495, + "bandwidth": "369.07MB/s", + "input_bw": "6.37MB/s", + "reconnects": 37933, + "status_2xx": 954355, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -69,19 +69,19 @@ "baseline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1732068, - "avg_latency": "2.37ms", - "p99_latency": "5.11ms", - "cpu": "6322.2%", - "memory": "140MiB", + "rps": 1802593, + "avg_latency": "2.27ms", + "p99_latency": "5.07ms", + "cpu": "6284.8%", + "memory": "139MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "259.27MB/s", - "input_bw": "133.80MB/s", + "bandwidth": "269.83MB/s", + "input_bw": "139.25MB/s", "reconnects": 0, - "status_2xx": 8660343, + "status_2xx": 9012969, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -89,18 +89,18 @@ "baseline-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 5327411, - "avg_latency": "18.50ms", - "p99_latency": "104.92ms", - "cpu": "6158.9%", + "rps": 5430750, + "avg_latency": "17.83ms", + "p99_latency": "103.49ms", + "cpu": "6284.9%", "memory": "172MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "138.86MB/s", + "bandwidth": "141.55MB/s", "reconnects": 0, - "status_2xx": 26956700, + "status_2xx": 27479600, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -108,18 +108,18 @@ "baseline-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 5304166, - "avg_latency": "5.10ms", - "p99_latency": "51.38ms", - "cpu": "6114.1%", - "memory": "96MiB", + "rps": 5501093, + "avg_latency": "5.02ms", + "p99_latency": "28.45ms", + "cpu": "6319.2%", + "memory": "110MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "137.68MB/s", + "bandwidth": "142.51MB/s", "reconnects": 0, - "status_2xx": 26733000, + "status_2xx": 27670500, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -127,18 +127,18 @@ "baseline-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 5280019, - "avg_latency": "19.13ms", - "p99_latency": "281.41ms", - "cpu": "6328.3%", - "memory": "160MiB", + "rps": 5709089, + "avg_latency": "17.54ms", + "p99_latency": "457.39ms", + "cpu": "6424.8%", + "memory": "164MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "138.17MB/s", + "bandwidth": "148.51MB/s", "reconnects": 0, - "status_2xx": 26822500, + "status_2xx": 28830900, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -146,18 +146,18 @@ "baseline-h2c-256": { "framework": "libuvcpp", "language": "C++", - "rps": 5550996, - "avg_latency": "4.89ms", - "p99_latency": "39.82ms", - "cpu": "6072.9%", - "memory": "83MiB", + "rps": 5889601, + "avg_latency": "4.51ms", + "p99_latency": "41.60ms", + "cpu": "6197.9%", + "memory": "81MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "143.52MB/s", + "bandwidth": "152.27MB/s", "reconnects": 0, - "status_2xx": 27866000, + "status_2xx": 29565800, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -165,18 +165,18 @@ "baseline-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 5031650, - "avg_latency": "76.42ms", - "p99_latency": "446.98ms", - "cpu": "6417.7%", - "memory": "798MiB", + "rps": 5489431, + "avg_latency": "70.66ms", + "p99_latency": "779.10ms", + "cpu": "6391.8%", + "memory": "766MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "132.05MB/s", + "bandwidth": "144.33MB/s", "reconnects": 0, - "status_2xx": 25611100, + "status_2xx": 27996100, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -184,18 +184,18 @@ "baseline-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 3077350, - "avg_latency": "1.20ms", - "p99_latency": "3.06ms", - "cpu": "3656.2%", - "memory": "133MiB", + "rps": 3239525, + "avg_latency": "1.18ms", + "p99_latency": "4.23ms", + "cpu": "3752.8%", + "memory": "103MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "41.12MB/s", + "bandwidth": "43.29MB/s", "reconnects": 0, - "status_2xx": 15417528, + "status_2xx": 16230024, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -203,18 +203,18 @@ "echo-ws-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 2565508, - "avg_latency": "6.03ms", - "p99_latency": "7.37ms", - "cpu": "6604.4%", - "memory": "335MiB", + "rps": 2673301, + "avg_latency": "5.44ms", + "p99_latency": "7.04ms", + "cpu": "6159.0%", + "memory": "329MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "17.53MB/s", + "bandwidth": "18.24MB/s", "reconnects": 0, - "status_2xx": 12827542, + "status_2xx": 13366508, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -222,18 +222,18 @@ "echo-ws-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 2838290, - "avg_latency": "1.44ms", - "p99_latency": "1.88ms", - "cpu": "6414.9%", - "memory": "132MiB", + "rps": 3007035, + "avg_latency": "1.36ms", + "p99_latency": "2.30ms", + "cpu": "6314.7%", + "memory": "131MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "18.96MB/s", + "bandwidth": "20.12MB/s", "reconnects": 0, - "status_2xx": 14191452, + "status_2xx": 15035178, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -241,18 +241,18 @@ "echo-ws-512": { "framework": "libuvcpp", "language": "C++", - "rps": 2849239, - "avg_latency": "179us", - "p99_latency": "434us", - "cpu": "6733.9%", - "memory": "77MiB", + "rps": 2892280, + "avg_latency": "176us", + "p99_latency": "418us", + "cpu": "6512.2%", + "memory": "79MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "19.02MB/s", + "bandwidth": "19.30MB/s", "reconnects": 0, - "status_2xx": 14246198, + "status_2xx": 14461400, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -260,18 +260,18 @@ "echo-ws-limited-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1778136, - "avg_latency": "377us", - "p99_latency": "797us", - "cpu": "6289.9%", - "memory": "94MiB", + "rps": 1832895, + "avg_latency": "378us", + "p99_latency": "818us", + "cpu": "6204.7%", + "memory": "93MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "33.74MB/s", - "reconnects": 889041, - "status_2xx": 8890682, + "bandwidth": "34.78MB/s", + "reconnects": 916379, + "status_2xx": 9164476, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -279,18 +279,18 @@ "echo-ws-limited-512": { "framework": "libuvcpp", "language": "C++", - "rps": 1574604, - "avg_latency": "209us", - "p99_latency": "618us", - "cpu": "6025.8%", - "memory": "92MiB", + "rps": 1633110, + "avg_latency": "202us", + "p99_latency": "605us", + "cpu": "5850.3%", + "memory": "85MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "29.88MB/s", - "reconnects": 787294, - "status_2xx": 7873020, + "bandwidth": "30.99MB/s", + "reconnects": 816568, + "status_2xx": 8165553, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -298,18 +298,18 @@ "echo-ws-pipeline-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 28115031, - "avg_latency": "8.08ms", - "p99_latency": "10.50ms", - "cpu": "6196.7%", - "memory": "350MiB", + "rps": 29129603, + "avg_latency": "7.85ms", + "p99_latency": "10.00ms", + "cpu": "6193.9%", + "memory": "348MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "188.04MB/s", + "bandwidth": "194.82MB/s", "reconnects": 0, - "status_2xx": 140575156, + "status_2xx": 145648016, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -317,18 +317,18 @@ "echo-ws-pipeline-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 29440675, - "avg_latency": "2.22ms", - "p99_latency": "2.77ms", - "cpu": "6798.7%", - "memory": "137MiB", + "rps": 32736147, + "avg_latency": "1.99ms", + "p99_latency": "2.71ms", + "cpu": "6337.8%", + "memory": "135MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "196.52MB/s", + "bandwidth": "218.54MB/s", "reconnects": 0, - "status_2xx": 147203376, + "status_2xx": 163680736, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -336,18 +336,18 @@ "echo-ws-pipeline-512": { "framework": "libuvcpp", "language": "C++", - "rps": 30670502, - "avg_latency": "266us", - "p99_latency": "642us", - "cpu": "6642.9%", + "rps": 33213203, + "avg_latency": "246us", + "p99_latency": "592us", + "cpu": "6810.3%", "memory": "78MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "204.70MB/s", + "bandwidth": "221.58MB/s", "reconnects": 0, - "status_2xx": 153352512, + "status_2xx": 166066016, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -355,115 +355,115 @@ "gateway-64-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 196673, - "avg_latency": "57.01ms", - "p99_latency": "4.90s", - "cpu": "5245.2%", - "memory": "1.8GiB", + "rps": 233849, + "avg_latency": "107.33ms", + "p99_latency": "4.88s", + "cpu": "5998.6%", + "memory": "2.0GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.36GB/s", + "bandwidth": "2.80GB/s", "reconnects": 0, - "status_2xx": 993201, + "status_2xx": 1183276, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 297960, - "tpl_baseline": 198640, - "tpl_json": 347620, - "tpl_async_db": 148980, - "cpu_breakdown": "server: 2870% 264MiB | proxy: 2376% 1.5GiB" + "tpl_static": 354982, + "tpl_baseline": 236655, + "tpl_json": 414146, + "tpl_async_db": 177491, + "cpu_breakdown": "server: 2774% 521MiB | proxy: 3225% 1.5GiB" }, "gateway-64-512": { "framework": "libuvcpp", "language": "C++", - "rps": 199358, - "avg_latency": "57.82ms", - "p99_latency": "4.92s", - "cpu": "5431.8%", + "rps": 240386, + "avg_latency": "52.45ms", + "p99_latency": "4.83s", + "cpu": "5635.2%", "memory": "1.5GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.35GB/s", + "bandwidth": "2.82GB/s", "reconnects": 0, - "status_2xx": 1006761, + "status_2xx": 1211549, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 302028, - "tpl_baseline": 201352, - "tpl_json": 352366, - "tpl_async_db": 151014, - "cpu_breakdown": "server: 3044% 276MiB | proxy: 2388% 1.2GiB" + "tpl_static": 363464, + "tpl_baseline": 242309, + "tpl_json": 424042, + "tpl_async_db": 181732, + "cpu_breakdown": "server: 2744% 309MiB | proxy: 2892% 1.2GiB" }, "gateway-h3-256": { "framework": "libuvcpp", "language": "C++", - "rps": 61721, - "avg_latency": "128.83ms", - "p99_latency": "795.62ms", - "cpu": "2954.5%", + "rps": 62533, + "avg_latency": "127.00ms", + "p99_latency": "772.31ms", + "cpu": "2792.4%", "memory": "1.4GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "623.14MB/s", + "bandwidth": "631.92MB/s", "reconnects": 0, - "status_2xx": 310458, + "status_2xx": 314545, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 93137, - "tpl_baseline": 62091, - "tpl_json": 108660, - "tpl_async_db": 46568, - "cpu_breakdown": "server: 859% 101MiB | proxy: 2096% 1.3GiB" + "tpl_static": 94363, + "tpl_baseline": 62909, + "tpl_json": 110090, + "tpl_async_db": 47181, + "cpu_breakdown": "server: 668% 107MiB | proxy: 2125% 1.3GiB" }, "gateway-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 83510, - "avg_latency": "24.37ms", - "p99_latency": "84.47ms", - "cpu": "3796.8%", - "memory": "431MiB", + "rps": 84759, + "avg_latency": "24.01ms", + "p99_latency": "84.19ms", + "cpu": "3578.1%", + "memory": "415MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "864.52MB/s", + "bandwidth": "877.31MB/s", "reconnects": 0, - "status_2xx": 418389, + "status_2xx": 424644, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 125516, - "tpl_baseline": 83677, - "tpl_json": 146436, - "tpl_async_db": 62758, - "cpu_breakdown": "server: 1176% 85MiB | proxy: 2621% 346MiB" + "tpl_static": 127393, + "tpl_baseline": 84928, + "tpl_json": 148625, + "tpl_async_db": 63696, + "cpu_breakdown": "server: 889% 81MiB | proxy: 2690% 335MiB" }, "json-comp-16384": { "framework": "libuvcpp", "language": "C++", - "rps": 197549, - "avg_latency": "46.58ms", - "p99_latency": "1.05s", - "cpu": "6099.2%", - "memory": "120MiB", + "rps": 291220, + "avg_latency": "34.62ms", + "p99_latency": "724.90ms", + "cpu": "6503.1%", + "memory": "152MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "282.38MB/s", - "input_bw": "14.69MB/s", - "reconnects": 38791, - "status_2xx": 987747, + "bandwidth": "416.29MB/s", + "input_bw": "21.66MB/s", + "reconnects": 57518, + "status_2xx": 1456104, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -471,19 +471,19 @@ "json-comp-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 199935, - "avg_latency": "19.89ms", - "p99_latency": "420.90ms", - "cpu": "6273.8%", - "memory": "114MiB", + "rps": 285759, + "avg_latency": "14.01ms", + "p99_latency": "294.60ms", + "cpu": "6234.1%", + "memory": "111MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "285.89MB/s", - "input_bw": "14.87MB/s", - "reconnects": 39487, - "status_2xx": 999678, + "bandwidth": "408.56MB/s", + "input_bw": "21.26MB/s", + "reconnects": 56722, + "status_2xx": 1428796, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -491,18 +491,18 @@ "json-h2c-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 655129, - "avg_latency": "48.94ms", - "p99_latency": "1.28s", - "cpu": "6422.9%", - "memory": "303MiB", + "rps": 1187769, + "avg_latency": "26.88ms", + "p99_latency": "506.39ms", + "cpu": "6357.1%", + "memory": "332MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.18GB/s", + "bandwidth": "3.97GB/s", "reconnects": 0, - "status_2xx": 3308404, + "status_2xx": 6010116, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -510,18 +510,18 @@ "json-h2c-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 639897, - "avg_latency": "187.10ms", - "p99_latency": "5.00s", - "cpu": "6432.8%", - "memory": "1016MiB", + "rps": 1130987, + "avg_latency": "112.44ms", + "p99_latency": "888.05ms", + "cpu": "6187.0%", + "memory": "1.3GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "2.13GB/s", + "bandwidth": "3.80GB/s", "reconnects": 0, - "status_2xx": 3250681, + "status_2xx": 5779348, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -529,18 +529,18 @@ "json-tls-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 399878, - "avg_latency": "10.07ms", - "p99_latency": "64.54ms", - "cpu": "6527.5%", - "memory": "662MiB", + "rps": 619671, + "avg_latency": "6.52ms", + "p99_latency": "58.00ms", + "cpu": "6361.6%", + "memory": "406MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.37GB", + "bandwidth": "2.13GB", "reconnects": 0, - "status_2xx": 2039136, + "status_2xx": 3160219, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -548,91 +548,91 @@ "latency-10k-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 9980, - "avg_latency": "79.9us", - "p99_latency": "119.0us", - "cpu": "35.7%", - "memory": "77MiB", + "rps": 9981, + "avg_latency": "83.3us", + "p99_latency": "120.0us", + "cpu": "36.7%", + "memory": "79MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, "bandwidth": "1.56MB/s", "reconnects": 0, - "status_2xx": 199628, + "status_2xx": 199650, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 7454906, - "cpu_per_req_us": 37.344, + "cpu_usec": 7641473, + "cpu_per_req_us": 38.2743, "target_rate": 10000, - "rate_ratio": 0.998, - "p99_9_latency": "178.0us" + "rate_ratio": 0.9981, + "p99_9_latency": "433.0us" }, "latency-1m-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 997811, - "avg_latency": "156.4us", - "p99_latency": "329.0us", - "cpu": "4125.5%", - "memory": "81MiB", + "rps": 997805, + "avg_latency": "145.6us", + "p99_latency": "312.0us", + "cpu": "4179.8%", + "memory": "82MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, "bandwidth": "155.66MB/s", "reconnects": 0, - "status_2xx": 19959660, + "status_2xx": 19959352, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 799053707, - "cpu_per_req_us": 40.0334, + "cpu_usec": 764408261, + "cpu_per_req_us": 38.2983, "target_rate": 1000000, "rate_ratio": 0.9978, - "p99_9_latency": "6018.0us" + "p99_9_latency": "977.0us" }, "latency-500k-8cpu-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 260201, - "avg_latency": "4731018.6us", - "p99_latency": "10104832.0us", - "cpu": "779.3%", - "memory": "33MiB", + "rps": 271901, + "avg_latency": "4536264.9us", + "p99_latency": "9293824.0us", + "cpu": "778.7%", + "memory": "32MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "40.59MB/s", + "bandwidth": "42.42MB/s", "reconnects": 0, - "status_2xx": 5205921, + "status_2xx": 5439773, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 159570865, - "cpu_per_req_us": 30.6518, + "cpu_usec": 159574321, + "cpu_per_req_us": 29.3347, "target_rate": 500000, - "rate_ratio": 0.5204, - "p99_9_latency": "10522624.0us" + "rate_ratio": 0.5438, + "p99_9_latency": "9654272.0us" }, "limited-conn-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 1414686, - "avg_latency": "2.87ms", - "p99_latency": "45.60ms", - "cpu": "6295.6%", - "memory": "92MiB", + "rps": 1480567, + "avg_latency": "2.74ms", + "p99_latency": "43.20ms", + "cpu": "6296.5%", + "memory": "86MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "211.76MB/s", - "input_bw": "109.28MB/s", - "reconnects": 708045, - "status_2xx": 7073431, + "bandwidth": "221.63MB/s", + "input_bw": "114.37MB/s", + "reconnects": 740631, + "status_2xx": 7402836, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -640,18 +640,18 @@ "pipelined-4096": { "framework": "libuvcpp", "language": "C++", - "rps": 7502178, - "avg_latency": "8.74ms", - "p99_latency": "11.50ms", - "cpu": "6335.2%", - "memory": "149MiB", + "rps": 7848400, + "avg_latency": "8.38ms", + "p99_latency": "10.60ms", + "cpu": "6474.9%", + "memory": "150MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "1.10GB/s", + "bandwidth": "1.15GB/s", "reconnects": 0, - "status_2xx": 37510893, + "status_2xx": 39242004, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -659,10 +659,10 @@ "production-stack-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 208224, - "avg_latency": "74.58ms", - "p99_latency": "3.38s", - "cpu": "4292.0%", + "rps": 208500, + "avg_latency": "73.82ms", + "p99_latency": "2.60s", + "cpu": "4284.8%", "memory": "2.0GiB", "connections": 1024, "threads": 64, @@ -670,23 +670,23 @@ "pipeline": 1, "bandwidth": "2.31GB/s", "reconnects": 0, - "status_2xx": 1055699, + "status_2xx": 1059181, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 316709, - "tpl_baseline": 105569, - "tpl_items": 527849, - "tpl_me": 105569, - "cpu_breakdown": "cache: 0% 4MiB | edge: 2757% 1.3GiB | authsvc: 780% 593MiB | server: 754% 112MiB" + "tpl_static": 317754, + "tpl_baseline": 105918, + "tpl_items": 529590, + "tpl_me": 105918, + "cpu_breakdown": "cache: 0% 4MiB | edge: 2803% 1.3GiB | authsvc: 728% 589MiB | server: 754% 122MiB" }, "production-stack-256": { "framework": "libuvcpp", "language": "C++", - "rps": 256484, - "avg_latency": "16.86ms", - "p99_latency": "84.90ms", - "cpu": "4827.6%", + "rps": 256279, + "avg_latency": "17.06ms", + "p99_latency": "131.13ms", + "cpu": "4765.6%", "memory": "1.2GiB", "connections": 256, "threads": 64, @@ -694,31 +694,31 @@ "pipeline": 1, "bandwidth": "2.78GB/s", "reconnects": 0, - "status_2xx": 1290116, + "status_2xx": 1289088, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_static": 387034, - "tpl_baseline": 129011, - "tpl_items": 645058, - "tpl_me": 129011, - "cpu_breakdown": "cache: 0% 13MiB | edge: 3123% 991MiB | authsvc: 714% 106MiB | server: 991% 77MiB" + "tpl_static": 386726, + "tpl_baseline": 128908, + "tpl_items": 644544, + "tpl_me": 128908, + "cpu_breakdown": "cache: 0% 15MiB | edge: 3095% 994MiB | authsvc: 714% 107MiB | server: 956% 100MiB" }, "static-h2-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 547363, - "avg_latency": "57.75ms", - "p99_latency": "227.04ms", - "cpu": "6422.8%", - "memory": "659MiB", + "rps": 550039, + "avg_latency": "57.34ms", + "p99_latency": "243.84ms", + "cpu": "6240.6%", + "memory": "507MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "8.36GB/s", + "bandwidth": "8.42GB/s", "reconnects": 0, - "status_2xx": 2764184, + "status_2xx": 2783199, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -726,18 +726,18 @@ "static-h2-256": { "framework": "libuvcpp", "language": "C++", - "rps": 802360, - "avg_latency": "11.00ms", - "p99_latency": "55.09ms", - "cpu": "6439.5%", - "memory": "245MiB", + "rps": 806684, + "avg_latency": "10.69ms", + "p99_latency": "61.94ms", + "cpu": "6459.0%", + "memory": "240MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "12.27GB/s", + "bandwidth": "12.33GB/s", "reconnects": 0, - "status_2xx": 4035872, + "status_2xx": 4057623, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -745,18 +745,18 @@ "static-h3-64": { "framework": "libuvcpp", "language": "C++", - "rps": 239028, - "avg_latency": "17.74ms", - "p99_latency": "86.69ms", - "cpu": "4193.0%", - "memory": "302MiB", + "rps": 238175, + "avg_latency": "17.31ms", + "p99_latency": "82.81ms", + "cpu": "4207.0%", + "memory": "475MiB", "connections": 64, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.64GB/s", + "bandwidth": "3.62GB/s", "reconnects": 0, - "status_2xx": 1197531, + "status_2xx": 1193260, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -764,18 +764,18 @@ "static-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 568833, - "avg_latency": "1.81ms", - "p99_latency": "26.94ms", - "cpu": "6466.9%", - "memory": "274MiB", + "rps": 571215, + "avg_latency": "1.79ms", + "p99_latency": "14.38ms", + "cpu": "6554.2%", + "memory": "273MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "8.77GB", + "bandwidth": "8.81GB", "reconnects": 0, - "status_2xx": 2901012, + "status_2xx": 2913314, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -783,18 +783,18 @@ "unary-grpc-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3667663, - "avg_latency": "26.80ms", - "p99_latency": "397.16ms", - "cpu": "6346.3%", - "memory": "423MiB", + "rps": 3600218, + "avg_latency": "27.30ms", + "p99_latency": "236.44ms", + "cpu": "6352.8%", + "memory": "428MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "152.16MB/s", + "bandwidth": "149.35MB/s", "reconnects": 0, - "status_2xx": 18558379, + "status_2xx": 18217105, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -802,18 +802,18 @@ "unary-grpc-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3798244, - "avg_latency": "7.07ms", - "p99_latency": "79.40ms", - "cpu": "6222.9%", - "memory": "148MiB", + "rps": 3837633, + "avg_latency": "6.78ms", + "p99_latency": "44.98ms", + "cpu": "6228.5%", + "memory": "173MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "156.70MB/s", + "bandwidth": "158.32MB/s", "reconnects": 0, - "status_2xx": 19105168, + "status_2xx": 19303294, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -821,18 +821,18 @@ "unary-grpc-tls-1024": { "framework": "libuvcpp", "language": "C++", - "rps": 3444335, - "avg_latency": "28.27ms", - "p99_latency": "178.12ms", - "cpu": "6441.0%", - "memory": "520MiB", + "rps": 3517793, + "avg_latency": "27.85ms", + "p99_latency": "181.47ms", + "cpu": "6142.2%", + "memory": "591MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "142.60MB/s", + "bandwidth": "145.95MB/s", "reconnects": 0, - "status_2xx": 17393894, + "status_2xx": 17800036, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -840,18 +840,18 @@ "unary-grpc-tls-256": { "framework": "libuvcpp", "language": "C++", - "rps": 3681783, - "avg_latency": "7.00ms", - "p99_latency": "54.54ms", - "cpu": "6185.2%", - "memory": "163MiB", + "rps": 3664147, + "avg_latency": "7.22ms", + "p99_latency": "60.27ms", + "cpu": "6168.1%", + "memory": "165MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "151.90MB/s", + "bandwidth": "151.17MB/s", "reconnects": 0, - "status_2xx": 18519369, + "status_2xx": 18430664, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0