Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ The reply states which baseline it used, and profiles the other framework does n
| Concurrency | `async` | `GET /delay/{ms}` with the wait named in the route, 32K held connections. Isolates what a framework does while a request is pending, with no database or I/O in the way. Scored for frameworks and engines, not for infrastructure, since the spec settled at a flat 10 ms over 32K connections |
| Efficiency | `latency-1m`, `latency-10k`, `latency-500k-8cpu` * | One million req/s offered at a fixed rate with zrk; the metric is CPU spent per request, read exactly from the container's cgroup rather than sampled. Everyone who finishes serves the same million, so what separates them is the bill. Scored on its own 0-100 formula rather than on rps, since the rate is pinned. `latency-10k` is the identical setup - same cores, connections and endpoint - at a near-idle 10K req/s, so what it reports is the standing cost of being a running server rather than of serving load, and it is scored the same way. Both score CPU at 0.50, p99 at 0.25 and mean latency at 0.25, the mean being the queueing signal: ten times the best mean is worth nothing. `latency-500k-8cpu` pins 500K req/s with the server on four cores plus their SMT siblings, so the load sits near saturation and the queue a server builds shows in its mean; reference-only while the first entries are measured |
| Workload | `json-comp`, `json-tls`, `8gbit`, `static-tls` | JSON serialization, gzip/brotli compression, HTTP/1.1 over TLS, a 10 KB body echoed over TLS at a pinned 50K req/s so ingest and egress load together - scored on the same 0-100 formula as the latency profiles, since its rate is pinned too, 20-file static asset serving over TLS |
| Database | `async-db` * | Async Postgres sequential scan. Reference-only: the database and its driver dominate it far more than the framework does, so it is measured and shown but no longer decides the ranking |
| Templates | `fortunes` * | DB query + HTML template render (TechEmpower-style Fortunes). Reference-only: measures template-engine throughput, and is not part of the composite score |
| Database | `async-db`, `fortunes` * | Postgres-backed HTTP/1.1, on their own board: an async range query, and a DB query + HTML template render (TechEmpower-style Fortunes, reference-only). The server gets 32 logical CPUs, the load generator 32 and Postgres the rest |
| H/2 | `baseline-h2`, `static-h2`, `baseline-h2c`, `json-h2c` | Baseline + static over TLS with h2 stream multiplexing; baseline + JSON over cleartext h2 (prior-knowledge, port 8082) |
| H/3 | `baseline-h3`, `static-h3` | Baseline and static over QUIC with TLS 1.3 |
| gRPC | `unary-grpc`, `unary-grpc-tls` | Unary gRPC over plaintext HTTP/2 and TLS |
Expand Down Expand Up @@ -99,12 +98,13 @@ Swap `actix` for your entry and `h1` for the family you want:

| Family | `<family>` | Composite it reports |
|---|---|---|
| HTTP/1.1 | `h1` | Connection, Workload, Database and Multi-endpoint profiles |
| HTTP/1.1 | `h1` | Connection, Workload and Multi-endpoint profiles |
| HTTP/2 | `h2` | the HTTP/2 profiles |
| HTTP/3 | `h3` | the HTTP/3 profiles |
| Gateway | `gw` | reverse-proxy and production-stack profiles |
| gRPC | `grpc` | unary and streaming, plaintext and TLS |
| WebSocket | `ws` | the echo profiles |
| Database | `db` | the Async DB profile |

`<framework>` is your entry's name on the board, lowercased, with anything
outside `A-Z a-z 0-9 . _ -` turned into `-`, the same name as your file in
Expand Down
15 changes: 15 additions & 0 deletions scripts/benchmark.sh
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ run_one() {

banner "$FRAMEWORK / $profile / ${CONNS}c (tool=$tool)"

# Local so the adapters and save_result() see the database split below, and
# the next profile does not.
local GCANNON_CPUS="$GCANNON_CPUS" THREADS="$THREADS"

# Reset Postgres before each DB profile so it sees the same clean,
# freshly-seeded server a standalone `benchmark.sh <fw> <profile>` run gets.
# Postgres is started once and shared across the whole run, so otherwise the
Expand All @@ -240,6 +244,17 @@ run_one() {
postgres_start
fi
PG_DIRTY=true
if db_split_fits; then
GCANNON_CPUS="$DB_LOADGEN_CPUS"
THREADS="$DB_THREADS"
postgres_pin "$DB_POSTGRES_CPUS"
info "cpus: server $PROF_CPU | generator $GCANNON_CPUS ($THREADS threads) | postgres $DB_POSTGRES_CPUS"
fi
;;
gateway-64|gateway-h3|production-stack)
# A database profile leaves it pinned, on what are this profile's
# generator CPUs.
postgres_pin
;;
esac

Expand Down
2 changes: 1 addition & 1 deletion scripts/check_badge_parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const run = new Function('D', `
`)(D);

// ── rank the same way write_badges() does ───────────────────────────────────
const FAMILIES = ['h1', 'h2', 'h3', 'gw', 'grpc', 'ws'];
const FAMILIES = ['h1', 'h2', 'h3', 'gw', 'grpc', 'ws', 'db'];
const LEAGUES = [['flagship', 'emerging'], ['engine'], ['experimental'], ['infrastructure']];
const MIN_FIELD = 2;

Expand Down
33 changes: 19 additions & 14 deletions scripts/gen_leaderboard_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@
("static-tls","Static TLS", "20-file static serving over TLS (reference for frameworks).", [1024], [1024], False,False,True),
]),
("Database", [
# Reference-only. The database and its driver dominate these two far
# more than the framework does, which is the case #1310 makes: they
# measure the connector, not the HTTP path. Still measured and shown,
# just no longer deciding the ranking.
("async-db", "Async DB", "Async Postgres sequential scan (reference).", [1024], [1024], False,True,False),
# Their own family. The database and its driver dominate these two far
# more than the framework does, which is the case #1310 makes: that rules
# them out of a ranking of HTTP paths, and it is what this one ranks. They
# run over HTTP/1.1 with the box split three ways, see DB_* in profiles.sh.
# Only async-db counts for now: 63 of the 101 entries running it have no
# Fortunes, and scoring it would cap all of them at half the board.
("async-db", "Async DB", "Async Postgres sequential scan.", [1024], [1024], True,True,False),
("fortunes", "Fortunes", "DB query + HTML template render (reference).", [1024], [1024], False,False,False),
]),
("HTTP/2", [
Expand Down Expand Up @@ -167,8 +169,8 @@
"latency-1m": "test-profiles/h1/latency-1m/implementation",
"latency-10k": "test-profiles/h1/latency-10k/implementation",
"latency-500k-8cpu": "test-profiles/h1/latency-500k-8cpu/implementation",
"async-db": "test-profiles/h1/async-database/implementation",
"fortunes": "test-profiles/h1/fortunes/implementation",
"async-db": "test-profiles/database/async-database/implementation",
"fortunes": "test-profiles/database/fortunes/implementation",
"baseline-h2": "test-profiles/h2/baseline-h2/implementation",
"static-h2": "test-profiles/h2/static-h2/implementation",
"baseline-h2c": "test-profiles/h2/baseline-h2c/implementation",
Expand Down Expand Up @@ -1250,11 +1252,11 @@ def _slug(name):
return (re.sub(r"[^A-Za-z0-9._-]+", "-", name).strip("-") or "unnamed").lower()

# category -> family key. Mirrors familyOf() in index.html; anything unlisted is
# HTTP/1.1 (Connection, Workload, Database, Multi-endpoint).
# HTTP/1.1 (Connection, Concurrency, Efficiency, Workload).
FAMILY_OF_CAT = {"Gateway": "gw", "HTTP/2": "h2", "HTTP/3": "h3",
"gRPC": "grpc", "WebSocket": "ws"}
"gRPC": "grpc", "WebSocket": "ws", "Database": "db"}
FAMILY_LABEL = {"h1": "H/1.1", "h2": "H/2", "h3": "H/3",
"gw": "Gateway", "grpc": "gRPC", "ws": "WebSocket"}
"gw": "Gateway", "grpc": "gRPC", "ws": "WebSocket", "db": "Database"}

# Leagues are the board's type filter. flagship+emerging is its default view and
# ranks as one field; engine and infrastructure entries are each scored on their
Expand Down Expand Up @@ -1846,16 +1848,18 @@ def compute_achievements(agg, profiles, meta, badge_index):
OG_MUTED = (154, 160, 166)

SCOPE_NAME = {"h1": "HTTP/1.1", "h2": "HTTP/2", "h3": "HTTP/3",
"gw": "Gateway", "grpc": "gRPC", "ws": "WebSocket"}
"gw": "Gateway", "grpc": "gRPC", "ws": "WebSocket", "db": "Database"}
# The board's <h1> per family. renderHead() writes the same strings, so the
# pre-rendered heading and the rendered one match.
SCOPE_H1 = {"h1": "HTTP/1.1 web server benchmarks", "h2": "HTTP/2 web server benchmarks",
"h3": "HTTP/3 web server benchmarks",
"gw": "Gateway and production stack benchmarks",
"grpc": "gRPC server benchmarks", "ws": "WebSocket server benchmarks"}
"grpc": "gRPC server benchmarks", "ws": "WebSocket server benchmarks",
"db": "Database-backed web server benchmarks"}
SCOPE_BLURB = {"h1": "all HTTP/1.1 profiles", "h2": "all HTTP/2 profiles",
"h3": "all HTTP/3 profiles", "gw": "the gateway & production-stack profiles",
"grpc": "all gRPC profiles", "ws": "all WebSocket profiles"}
"grpc": "all gRPC profiles", "ws": "all WebSocket profiles",
"db": "the database profiles"}
# The board's own default: view=composite, scope=h1, flagship+emerging, tuned
# shown, useMem and rescale off. Prerendering anything else would show a crawler
# a page no visitor lands on.
Expand Down Expand Up @@ -2760,7 +2764,8 @@ def _lang_faq(lang, scopes, n_entries, round_name):
continue
fw, _k, _t, score, rank, of, _l, _lr, _lo = rows[0]
qa.append((
f"What is the fastest {lang} {SCOPE_NAME[scope]} server?",
f"What is the fastest {lang} "
f"{'database-backed' if scope == 'db' else SCOPE_NAME[scope]} server?",
f"{fw}, with a composite of {score:.0f} on {SCOPE_NAME[scope]} and "
f"{rank} of {of} against all languages in its league."))
qa.append((
Expand Down
8 changes: 8 additions & 0 deletions scripts/lib/postgres.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ postgres_start() {
fail "postgres sidecar did not become ready within 60s"
}

# Move the running sidecar onto these CPUs. With no argument it goes back to where
# postgres_start() puts it: PG_CPUSET if set, otherwise anywhere.
postgres_pin() {
local cpus="${1:-${PG_CPUSET:-0-$(( $(nproc) - 1 ))}}"
docker update --cpuset-cpus="$cpus" "$PG_CONTAINER" >/dev/null \
|| warn "could not pin postgres to cpuset=$cpus"
}

postgres_stop() {
# -v for the same reason as postgres_start: nuke any attached
# anonymous volumes along with the container.
Expand Down
24 changes: 22 additions & 2 deletions scripts/lib/profiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
#
# Adding a profile: add a line to PROFILES and append to PROFILE_ORDER.

# The database profiles split the box three ways instead of two, because Postgres
# does real work on them and needs CPUs of its own: 32 logical CPUs for the server,
# 32 for the load generator and the rest for Postgres. Each is a run of cores plus
# their SMT siblings (N and N+64 on the bench host), the way every other profile
# pairs them. The other profiles give the server and the generator 64 each, which
# leaves Postgres nowhere to run but on top of them.
DB_SERVER_CPUS="${DB_SERVER_CPUS:-0-15,64-79}"
DB_LOADGEN_CPUS="${DB_LOADGEN_CPUS:-16-31,80-95}"
DB_POSTGRES_CPUS="${DB_POSTGRES_CPUS:-32-63,96-127}"
DB_THREADS="${DB_THREADS:-32}"

declare -A PROFILES=(
[baseline]="1|0|0-31,64-95|4096|"
[pipelined]="16|0|0-31,64-95|4096|pipeline"
Expand Down Expand Up @@ -92,8 +103,8 @@ declare -A PROFILES=(
# reporting a lower number that reads like a like-for-like result.
[8gbit]="1|0|0-31,64-95|512|8gbit"
[static-tls]="1|200|0-31,64-95|1024|static-tls"
[async-db]="1|0|0-31,64-95|1024|async-db"
[fortunes]="1|0|0-31,64-95|1024|fortunes"
[async-db]="1|0|$DB_SERVER_CPUS|1024|async-db"
[fortunes]="1|0|$DB_SERVER_CPUS|1024|fortunes"
[baseline-h2]="1|0|0-31,64-95|256,1024|h2"
[static-h2]="1|0|0-31,64-95|256,1024|static-h2"
[baseline-h2c]="1|0|0-31,64-95|256,1024,4096|h2c"
Expand Down Expand Up @@ -144,6 +155,15 @@ parse_profile() {
IFS='|' read -r PROF_PIPELINE PROF_REQ PROF_CPU PROF_CONNS PROF_ENDPOINT <<< "$spec"
}

# Does every CPU the database split names exist on this box? On one that is too
# small, run_one() leaves the generator and Postgres where they are, the same way
# framework_start() falls back when a profile's cpuset does not fit.
db_split_fits() {
local highest
highest=$(grep -oE '[0-9]+' <<< "$DB_SERVER_CPUS,$DB_LOADGEN_CPUS,$DB_POSTGRES_CPUS" | sort -n | tail -1)
[ "$highest" -lt "$(nproc 2>/dev/null || echo 1)" ]
}

# Map an endpoint to the tool name that handles it.
# Returns one of: gcannon, wrk, zrk, h2load, h2load-h3
endpoint_tool() {
Expand Down
4 changes: 2 additions & 2 deletions scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2180,7 +2180,7 @@ fi
# ───── Async Database (GET /async-db) ─────

if has_test "async-db"; then
ASYNCDB_DOCS="$DOCS_BASE/h1/async-database/validation"
ASYNCDB_DOCS="$DOCS_BASE/database/async-database/validation"
echo "[test] async-db endpoint"
asyncdb_fail=false
# ranges and limits drawn per run
Expand Down Expand Up @@ -2242,7 +2242,7 @@ fi
# content, and is sized like a real page (not stripped to win the bench).

if has_test "fortunes"; then
FORTUNES_DOCS="$DOCS_BASE/h1/fortunes/validation"
FORTUNES_DOCS="$DOCS_BASE/database/fortunes/validation"
echo "[test] fortunes endpoint"

body=$(curl -s --max-time 30 "http://localhost:$PORT/fortunes" || true)
Expand Down
3 changes: 2 additions & 1 deletion site/content/docs/add-framework/badges.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ under `scopes.<family>.default.markdown`.

| Family | `<family>` | What the composite covers |
|---|---|---|
| HTTP/1.1 | `h1` | Connection, Workload, Database and Multi-endpoint profiles |
| HTTP/1.1 | `h1` | Connection, Workload and Multi-endpoint profiles |
| HTTP/2 | `h2` | the HTTP/2 profiles |
| HTTP/3 | `h3` | the HTTP/3 profiles |
| Gateway | `gw` | reverse-proxy and production-stack profiles |
| gRPC | `grpc` | unary and streaming, plaintext and TLS |
| WebSocket | `ws` | the echo profiles |
| Database | `db` | the Async DB profile |

`<framework>` is your entry's board name, lowercased, with anything outside
`A-Z a-z 0-9 . _ -` replaced by `-`. It is the same name as your file under
Expand Down
4 changes: 4 additions & 0 deletions site/content/docs/running-locally/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Every framework `Dockerfile` reads the same defaults from its env, so you rarely
| `LOADGEN_DOCKER` | `false` | When `true`, every load generator runs from its Docker image instead of the host binary. Builds missing images automatically from `docker/*.Dockerfile`. Forced `true` by `benchmark-lite.sh`. |
| `GCANNON_MODE` | `native` | `native` or `docker`. Implied by `LOADGEN_DOCKER`. |
| `GCANNON_CPUS` | dynamic | CPU list the load generators run on. Native mode wraps calls in `taskset -c $GCANNON_CPUS`; docker mode passes it via `--cpuset-cpus`. Defaults to the upper half of available cores on smaller machines to avoid resource contention with the server. |
| `DB_SERVER_CPUS` | `0-15,64-79` | Server cpuset on the database profiles. |
| `DB_LOADGEN_CPUS` | `16-31,80-95` | Load-generator cpuset on the database profiles. Replaces `GCANNON_CPUS` there. |
| `DB_POSTGRES_CPUS` | `32-63,96-127` | Postgres cpuset on the database profiles. On a box too small for these three, the generator and Postgres stay where they are. |
| `DB_THREADS` | `32` | Load-generator threads on the database profiles. Replaces `THREADS` there. |

## Tool binaries and images

Expand Down
6 changes: 5 additions & 1 deletion site/content/docs/running-locally/scripts/benchmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ LOADGEN_DOCKER=true ./scripts/benchmark.sh aspnet-minimal
| `LOADGEN_DOCKER` | `false` | When `true`, every load generator runs from its Docker image instead of the host binary. Builds missing images automatically. |
| `GCANNON_MODE` | `native` | `native` or `docker`. `LOADGEN_DOCKER=true` sets this to `docker` for you. |
| `GCANNON_CPUS` | `32-63,96-127` | Cores the load generators are pinned to (via `taskset` in native mode or `--cpuset-cpus` in docker mode). Framework containers use the *other* half of the CPU topology. |
| `DB_SERVER_CPUS` | `0-15,64-79` | Server cpuset on the database profiles. |
| `DB_LOADGEN_CPUS` | `16-31,80-95` | Load-generator cpuset on the database profiles. Replaces `GCANNON_CPUS` there. |
| `DB_POSTGRES_CPUS` | `32-63,96-127` | Postgres cpuset on the database profiles. On a box too small for these three, the generator and Postgres stay where they are. |
| `DB_THREADS` | `32` | Load-generator threads on the database profiles. Replaces `THREADS` there. |

### Tool binaries + images

Expand Down Expand Up @@ -131,7 +135,7 @@ pipeline | req_per_conn | cpu_limit | connections | endpoint
| `api-4` | 1 | 5 | `0-3` | 256 | gcannon | 8-template mix (baseline / json / async-db) |
| `api-16` | 1 | 5 | `0-7,64-71` | 1024 | gcannon | 8-template mix |
| `static` | 1 | 200 | `0-31,64-95` | 1024, 4096, 6800 | wrk | 20 files via `static-rotate.lua` |
| `async-db` | 1 | ∞ | `0-31,64-95` | 1024 | gcannon | 5 limit variants (5/10/20/35/50), `-r 25` |
| `async-db` | 1 | ∞ | `0-15,64-79` | 1024 | gcannon | 5 limit variants (5/10/20/35/50), `-r 25` |
| `baseline-h2` | 1 | ∞ | `0-31,64-95` | 256, 1024 | h2load | `/baseline2` on `H2PORT` TLS |
| `static-h2` | 1 | ∞ | `0-31,64-95` | 256, 1024 | h2load | `/static/*` on `H2PORT` TLS |
| `baseline-h3` | 1 | ∞ | `0-31,64-95` | 64 | h2load-h3 | `/baseline2` on `H2PORT` QUIC |
Expand Down
Loading
Loading