initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/build/
|
||||
/h3blast
|
||||
*.o
|
||||
@@ -0,0 +1,42 @@
|
||||
# h3blast — links against Bun's already-built vendored deps so we don't
|
||||
# rebuild lsquic/BoringSSL/HdrHistogram. Run `bun run build:release` once
|
||||
# in the repo root first so the .o files exist.
|
||||
|
||||
ROOT := ../..
|
||||
PROFILE ?= release
|
||||
OBJ := $(ROOT)/build/$(PROFILE)/obj/vendor
|
||||
VENDOR := $(ROOT)/vendor
|
||||
|
||||
CC ?= clang
|
||||
CFLAGS := -std=c11 -O3 -g -Wall -Wextra -Wno-unused-parameter \
|
||||
-I$(VENDOR)/lsquic/include \
|
||||
-I$(VENDOR)/hdrhistogram/include \
|
||||
-I$(VENDOR)/boringssl/include
|
||||
LDFLAGS := -lstdc++ -lm -lpthread
|
||||
|
||||
DEPS := lsquic lsqpack lshpack boringssl hdrhistogram zlib
|
||||
DEP_DIRS := $(addprefix $(OBJ)/,$(DEPS))
|
||||
LIBDEPS := build/libdeps.a
|
||||
|
||||
BIN := h3blast
|
||||
|
||||
all: $(BIN)
|
||||
|
||||
$(LIBDEPS):
|
||||
@if [ ! -d "$(OBJ)/lsquic" ]; then \
|
||||
echo "error: no dep objects found under $(OBJ)"; \
|
||||
echo " run 'bun run build:$(PROFILE)' in the repo root first"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p build
|
||||
@echo " AR $@"
|
||||
@find $(DEP_DIRS) -name '*.o' | xargs ar rcs $@
|
||||
|
||||
$(BIN): src/h3blast.c $(LIBDEPS)
|
||||
@echo " CC $@"
|
||||
@$(CC) $(CFLAGS) -o $@ $< $(LIBDEPS) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(BIN) $(LIBDEPS)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -0,0 +1,90 @@
|
||||
# h3blast
|
||||
|
||||
A small, very fast HTTP/3 load generator built directly on **lsquic**. It exists
|
||||
to stress `Bun.serve({ http3: true })` without going through curl or a Node QUIC
|
||||
shim — same lsquic, same BoringSSL, same packet path as the server side.
|
||||
|
||||
```
|
||||
h3blast · HTTP/3 8 threads × 8 connections × 32 streams
|
||||
────────────────────────────────────────────────────────────────
|
||||
|
||||
static 406,114 req/s
|
||||
GET localhost:3001/hi
|
||||
4,061,140 requests in 10.00s 66.12 B/req · p99 1.10ms
|
||||
|
||||
|
||||
js 172,600 req/s
|
||||
GET localhost:3001/
|
||||
1,726,000 requests in 10.00s 46.06 B/req · p99 2.44ms
|
||||
```
|
||||
|
||||
The bytes/req figure is wire bytes received (QUIC payload — headers + body +
|
||||
framing). Pass `-v` for per-target request totals, body-only bytes, total
|
||||
transfer, and the full latency-percentile / status-code charts.
|
||||
|
||||
## Design
|
||||
|
||||
One process, **N worker threads**. Each worker owns one non-blocking UDP
|
||||
socket, one lsquic client engine, and `connections / threads` QUIC connections.
|
||||
Each connection keeps `--streams` HTTP/3 request streams in flight at all
|
||||
times. UDP I/O is batched with `sendmmsg`/`recvmmsg`; the lsquic timer wheel
|
||||
is driven via `epoll` + `timerfd`. The main thread samples per-worker atomic
|
||||
counters at ~10 Hz to render the live TUI, then aggregates per-worker
|
||||
HdrHistograms for the final percentile report.
|
||||
|
||||
No TLS verification by default (it's a load tester).
|
||||
|
||||
## Build
|
||||
|
||||
h3blast links the **already-compiled** lsquic / BoringSSL / HdrHistogram / zlib
|
||||
object files from Bun's build tree, so you need a Bun release build first:
|
||||
|
||||
```sh
|
||||
cd ../.. # repo root
|
||||
bun run build:release # populates build/release/obj/vendor/**
|
||||
cd packages/h3blast
|
||||
make # → ./h3blast
|
||||
```
|
||||
|
||||
`PROFILE=debug make` links against the debug objects instead.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
./h3blast -t 4 -c 8 -m 64 -d 30 https://127.0.0.1:3000/
|
||||
./h3blast -X POST -H 'content-type: application/json' -b '{"a":1}' https://host/api
|
||||
./h3blast --json -d 5 https://host/ | jq '.[0].req_per_sec'
|
||||
|
||||
# compare two servers side-by-side — each positional is [label=]url
|
||||
./h3blast -c 4 -m 32 -d 10 bun=https://127.0.0.1:3443/ node=https://127.0.0.1:3444/
|
||||
```
|
||||
|
||||
When multiple URLs are given they are benchmarked **sequentially** (same
|
||||
`-t`/`-c`/`-m`/`-d` each) so they don't compete for client CPU. The live TUI
|
||||
shows finished targets above the active one; the final report has one labelled
|
||||
block per target with average response-body size. The full latency-percentile
|
||||
and status-code charts are only printed with `-v/--verbose`; the default report
|
||||
shows just `body B/req` and `p50/p99`.
|
||||
|
||||
| flag | meaning |
|
||||
| -------------------------- | --------------------------------------------- |
|
||||
| `-t, --threads N` | worker threads |
|
||||
| `-c, --connections N` | total QUIC connections (split across threads) |
|
||||
| `-m, --streams N` | concurrent request streams per connection |
|
||||
| `-d, --duration SEC` | run length (default 10) |
|
||||
| `-n, --requests N` | stop after N total responses |
|
||||
| `-X, --method M` | HTTP method |
|
||||
| `-H 'k: v'` | extra request header (repeatable) |
|
||||
| `-b STR` / `--body-file P` | request body |
|
||||
| `--warmup SEC` | ignore stats from the first SEC seconds |
|
||||
| `--json` | machine-readable summary (array of targets) |
|
||||
| `-v, --verbose` | full latency/status charts in final report |
|
||||
| `--no-color`, `-q` | disable color / live UI |
|
||||
| `H3BLAST_DEBUG=debug` | turn on lsquic's internal logger |
|
||||
|
||||
## Against a local Bun H3 server
|
||||
|
||||
```sh
|
||||
bun test-server.js 3443 &
|
||||
./h3blast -t 2 -c 4 -m 32 -d 10 https://127.0.0.1:3443/
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
// Minimal Bun HTTP/3 hello-world target for h3blast.
|
||||
// Usage: bun test-server.js [port]
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "h3blast-srv-"));
|
||||
const certPath = join(dir, "cert.pem");
|
||||
const keyPath = join(dir, "key.pem");
|
||||
spawnSync(
|
||||
"openssl",
|
||||
[
|
||||
"req",
|
||||
"-x509",
|
||||
"-nodes",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-days",
|
||||
"365",
|
||||
"-subj",
|
||||
"/CN=localhost",
|
||||
"-keyout",
|
||||
keyPath,
|
||||
"-out",
|
||||
certPath,
|
||||
],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
|
||||
const server = Bun.serve({
|
||||
port: Number(process.argv[2] ?? process.env.PORT ?? 3443),
|
||||
http3: true,
|
||||
http1: false,
|
||||
tls: { cert: readFileSync(certPath, "utf8"), key: readFileSync(keyPath, "utf8") },
|
||||
fetch() {
|
||||
return new Response("Hello, World!");
|
||||
},
|
||||
});
|
||||
|
||||
console.error("listening", String(server.url));
|
||||
setInterval(() => {}, 1 << 30);
|
||||
Reference in New Issue
Block a user