๐Ÿ‘ a fluffy Gleam web server
23

Configure Feed

Select the types of activity you want to include in your feed.

first attempts at setting up proper benchmarks

vshakitskiy (Jul 24, 2026, 11:51 AM +0300) 22efcd2c b79103a9

+1159 -96
+5 -1
.gitignore
··· 8 8 # examples 9 9 /bench 10 10 /dev/priv/file_1gb.bin 11 - /dev/priv/file_100kb.bin 11 + /dev/priv/file_100kb.bin 12 + /benchmark/priv/file_1gb.bin 13 + /benchmark/priv/file_100kb.bin 14 + /benchmark/results 15 + /benchmark/.wrk2
+4
benchmark/bandit/.gitignore
··· 1 + /_build 2 + /deps 3 + *.ez 4 + erl_crash.dump
+15
benchmark/bandit/lib/bandit_bench/application.ex
··· 1 + defmodule BanditBench.Application do 2 + @moduledoc false 3 + 4 + use Application 5 + 6 + @impl true 7 + def start(_type, _args) do 8 + children = [ 9 + {Bandit, plug: BanditBench.Router, scheme: :http, port: 3004} 10 + ] 11 + 12 + opts = [strategy: :one_for_one, name: BanditBench.Supervisor] 13 + Supervisor.start_link(children, opts) 14 + end 15 + end
+51
benchmark/bandit/lib/bandit_bench/router.ex
··· 1 + defmodule BanditBench.Router do 2 + use Plug.Router 3 + 4 + plug(:match) 5 + plug(:dispatch) 6 + 7 + get "/hello" do 8 + send_resp(conn, 200, "Hello, Joe!") 9 + end 10 + 11 + post "/echo" do 12 + {:ok, body, conn} = Plug.Conn.read_body(conn, length: 10_000_000) 13 + send_resp(conn, 200, body) 14 + end 15 + 16 + post "/echo/chunked" do 17 + conn = Plug.Conn.read_body(conn, length: 4096) |> echo_chunked(<<>>) 18 + send_resp(conn, 200, conn.assigns.body) 19 + end 20 + 21 + get "/stream" do 22 + conn = send_chunked(conn, 200) 23 + {:ok, conn} = chunk(conn, "hello, ") 24 + {:ok, conn} = chunk(conn, "Joe!") 25 + conn 26 + end 27 + 28 + get "/file/small" do 29 + conn 30 + |> put_resp_header("content-type", "application/octet-stream") 31 + |> send_file(200, "../priv/file_100kb.bin") 32 + end 33 + 34 + get "/file/big" do 35 + conn 36 + |> put_resp_header("content-type", "application/octet-stream") 37 + |> send_file(200, "../priv/file_1gb.bin") 38 + end 39 + 40 + match _ do 41 + send_resp(conn, 404, "") 42 + end 43 + 44 + defp echo_chunked({:more, partial, conn}, acc) do 45 + Plug.Conn.read_body(conn, length: 4096) |> echo_chunked(acc <> partial) 46 + end 47 + 48 + defp echo_chunked({:ok, partial, conn}, acc) do 49 + Plug.Conn.assign(conn, :body, acc <> partial) 50 + end 51 + end
+27
benchmark/bandit/mix.exs
··· 1 + defmodule BanditBench.MixProject do 2 + use Mix.Project 3 + 4 + def project do 5 + [ 6 + app: :bandit_bench, 7 + version: "0.1.0", 8 + elixir: "~> 1.13", 9 + start_permanent: Mix.env() == :prod, 10 + deps: deps() 11 + ] 12 + end 13 + 14 + def application do 15 + [ 16 + extra_applications: [:logger], 17 + mod: {BanditBench.Application, []} 18 + ] 19 + end 20 + 21 + defp deps do 22 + [ 23 + {:bandit, "~> 1.4"}, 24 + {:plug, "~> 1.13"} 25 + ] 26 + end 27 + end
+10
benchmark/bandit/mix.lock
··· 1 + %{ 2 + "bandit": {:hex, :bandit, "1.12.0", "6c5214daa2469644ac4ab0113b98abc24f75e348378e6a974c6343b3e5da22ef", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "45dac82dc86f45cf4a196dee9cc5a8b791d9c9469d996055f055e6ee36c66e20"}, 3 + "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, 4 + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 5 + "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, 6 + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 7 + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, 8 + "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, 9 + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 10 + }
+3
benchmark/bandit/run.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + MIX_ENV=prod mix run --no-halt
+337
benchmark/bench.sh
··· 1 + #!/usr/bin/env bash 2 + # 3 + # Drives wrk2 against every server in benchmark/. For each endpoint, it first 4 + # probes for the server's saturation throughput, then re-runs at fixed target 5 + # rates that are 50/75/90/95% of that saturation point, recording both corrected 6 + # and uncorrected latency percentiles at each rate. 7 + # 8 + # Requires benchmark/.wrk2/wrk2, so run ./wrk2-setup.sh once first. 9 + # 10 + # Usage: 11 + # ./bench.sh [--servers "ewe@5,mist"] [--pcts "50 75 90 95"] [--conns 50] 12 + # [--duration 20s] [--warmup 5s] [--threads 4] 13 + # [--probe-rate 500000] [--probe-duration 5s] 14 + # 15 + # --servers comma separated subset of server names (default: all) 16 + # --pcts space separated percentages of saturation to test (default: "50 75 90 95") 17 + # --conns connections held open per run (default: 50) 18 + # --duration wrk2 measured run duration (default: 20s) 19 + # --warmup untimed warmup before measuring each server (default: 5s) 20 + # --threads wrk2 threads (default: 4) 21 + # --probe-rate -R used for the saturation probe (default: 500000) 22 + # --probe-duration duration of the saturation probe (default: 5s) 23 + 24 + set -u 25 + 26 + ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 27 + cd "$ROOT" 28 + 29 + # name|port|dir|start command 30 + SERVERS=( 31 + "ewe@5|3006|ewe@5|gleam run" 32 + "ewe@4|3001|ewe@4|gleam run" 33 + "mist|3002|mist|gleam run" 34 + "elli|3003|elli|./run.sh" 35 + "bandit|3004|bandit|./run.sh" 36 + "httpd|3005|httpd|./run.sh" 37 + "roadrunner|3007|roadrunner|./run.sh" 38 + ) 39 + 40 + # name|path|wrk script relative to benchmark/wrk 41 + ENDPOINTS=( 42 + "hello|/hello|" 43 + "echo|/echo|post_echo.lua" 44 + "echo_chunked|/echo/chunked|post_echo_chunked.lua" 45 + "stream|/stream|" 46 + "file_small|/file/small|" 47 + ) 48 + 49 + # server|endpoint|reason combos to skip before probing. Two distinct reasons: 50 + # - not_implemented: the server doesn't implement the feature the endpoint is 51 + # meant to exercise, so measuring it tells us nothing. 52 + # 53 + # - unstable: the endpoint collapses under wrk2's rate-limited load for a 54 + # confirmed reproducible reason that isn't a benchmark setup mistake. 55 + SKIP=( 56 + "elli|echo_chunked|not_implemented" 57 + "httpd|echo_chunked|not_implemented" 58 + "elli|stream|unstable" 59 + "mist|stream|unstable" 60 + "ewe@4|stream|unstable" 61 + "ewe@4|file_small|unstable" 62 + ) 63 + 64 + skip_endpoint() { 65 + local name="$1" ename="$2" entry s_name s_ename 66 + for entry in "${SKIP[@]}"; do 67 + IFS='|' read -r s_name s_ename SKIP_REASON <<< "$entry" 68 + if [ "$s_name" = "$name" ] && [ "$s_ename" = "$ename" ]; then 69 + return 0 70 + fi 71 + done 72 + return 1 73 + } 74 + 75 + server_selected() { 76 + local name="$1" 77 + [ -z "${ONLY_SERVERS:-}" ] && return 0 78 + case ",$ONLY_SERVERS," in 79 + *",$name,"*) return 0 ;; 80 + *) return 1 ;; 81 + esac 82 + } 83 + 84 + # Kills whatever is already listening on $1, e.g. a server orphaned by a 85 + # previous run that got interrupted before its own cleanup ran. 86 + free_port() { 87 + local port="$1" pid 88 + pid=$(ss -tlnp 2>/dev/null | awk -v p=":$port\$" '$4 ~ p' | grep -oP 'pid=\K[0-9]+' | head -1) 89 + if [ -n "$pid" ]; then 90 + echo " port $port already in use by pid $pid, killing" >&2 91 + kill -9 "$pid" 2>/dev/null 92 + sleep 1 93 + fi 94 + } 95 + 96 + # Cleans up whichever server is currently running if the script exits early. 97 + CURRENT_PGID="" 98 + cleanup() { 99 + if [ -n "$CURRENT_PGID" ]; then 100 + kill -TERM -- -"$CURRENT_PGID" 2>/dev/null 101 + sleep 1 102 + kill -KILL -- -"$CURRENT_PGID" 2>/dev/null 103 + fi 104 + } 105 + trap cleanup EXIT INT TERM 106 + 107 + wait_ready() { 108 + local port="$1" tries=60 109 + while [ "$tries" -gt 0 ]; do 110 + if curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$port/hello"; then 111 + return 0 112 + fi 113 + sleep 0.5 114 + tries=$((tries - 1)) 115 + done 116 + return 1 117 + } 118 + 119 + # Starts $dir/$cmd in its own process group, logs to $log, and waits for $port 120 + # to answer. Must NOT be called via $(...). Sets CURRENT_PGID on success clears 121 + # it on failure. Returns 0/1. 122 + start_server() { 123 + local dir="$1" cmd="$2" port="$3" log="$4" 124 + 125 + free_port "$port" 126 + 127 + ( cd "$ROOT/$dir" && exec setsid $cmd > "$log" 2>&1 < /dev/null ) & 128 + CURRENT_PGID=$! 129 + 130 + if ! wait_ready "$port"; then 131 + echo " failed to start, see $log" >&2 132 + kill -KILL -- -"$CURRENT_PGID" 2>/dev/null 133 + wait "$CURRENT_PGID" 2>/dev/null 134 + CURRENT_PGID="" 135 + return 1 136 + fi 137 + 138 + return 0 139 + } 140 + 141 + stop_server() { 142 + local pgid="$1" 143 + kill -TERM -- -"$pgid" 2>/dev/null 144 + sleep 1 145 + kill -KILL -- -"$pgid" 2>/dev/null 146 + wait "$pgid" 2>/dev/null 147 + CURRENT_PGID="" 148 + sleep 1 149 + } 150 + 151 + WRK2_BIN="${WRK2_BIN:-$ROOT/.wrk2/wrk2}" 152 + if ! [ -x "$WRK2_BIN" ]; then 153 + WRK2_BIN="$(command -v wrk2 || true)" 154 + fi 155 + if [ -z "$WRK2_BIN" ] || ! [ -x "$WRK2_BIN" ]; then 156 + echo "wrk2 not found. Run ./wrk2-setup.sh first (or set WRK2_BIN)." >&2 157 + exit 1 158 + fi 159 + 160 + PCTS="50 75 90 95" 161 + CONNS=50 162 + DURATION="20s" 163 + WARMUP_DURATION="5s" 164 + THREADS=4 165 + PROBE_RATE=500000 166 + PROBE_DURATION="5s" 167 + ONLY_SERVERS="" 168 + 169 + while [ $# -gt 0 ]; do 170 + case "$1" in 171 + --servers) ONLY_SERVERS="$2"; shift 2 ;; 172 + --pcts) PCTS="$2"; shift 2 ;; 173 + --conns) CONNS="$2"; shift 2 ;; 174 + --duration) DURATION="$2"; shift 2 ;; 175 + --warmup) WARMUP_DURATION="$2"; shift 2 ;; 176 + --threads) THREADS="$2"; shift 2 ;; 177 + --probe-rate) PROBE_RATE="$2"; shift 2 ;; 178 + --probe-duration) PROBE_DURATION="$2"; shift 2 ;; 179 + *) echo "Unknown argument: $1" >&2; exit 1 ;; 180 + esac 181 + done 182 + 183 + TIMESTAMP="$(date +%Y%m%d-%H%M%S)" 184 + RESULTS_DIR="$ROOT/results/$TIMESTAMP-wrk2" 185 + mkdir -p "$RESULTS_DIR" 186 + CSV="$RESULTS_DIR/results.csv" 187 + echo "server,endpoint,pct_of_saturation,target_rate,saturation_rate,connections,threads,duration,requests_per_sec,corrected_p50,corrected_p90,corrected_p99,corrected_p999,corrected_p9999,uncorrected_p50,uncorrected_p90,uncorrected_p99,uncorrected_p999,uncorrected_p9999,errors" > "$CSV" 188 + 189 + mkdir -p "$ROOT/priv" 190 + [ -f "$ROOT/priv/file_100kb.bin" ] || head -c 100K /dev/urandom > "$ROOT/priv/file_100kb.bin" 191 + 192 + wrk2_url() { 193 + local port="$1" path="$2" 194 + echo "http://127.0.0.1:$port$path" 195 + } 196 + 197 + wrk2_run() { 198 + # Runs wrk2 and prints raw output; $1=port $2=path $3=script $4=rate $5=conn 199 + # $6=duration 200 + local port="$1" path="$2" script="$3" rate="$4" conn="$5" duration="$6" 201 + if [ -n "$script" ]; then 202 + "$WRK2_BIN" -t"$THREADS" -c"$conn" -d"$duration" -R"$rate" -U \ 203 + -s "$ROOT/wrk/$script" "$(wrk2_url "$port" "$path")" 204 + else 205 + "$WRK2_BIN" -t"$THREADS" -c"$conn" -d"$duration" -R"$rate" -U \ 206 + "$(wrk2_url "$port" "$path")" 207 + fi 208 + } 209 + 210 + # Probes achievable throughput by requesting a rate far above what the server 211 + # can sustain and reading back what it actually achieved. 212 + probe_saturation() { 213 + local port="$1" path="$2" script="$3" 214 + local out rps 215 + out=$(wrk2_run "$port" "$path" "$script" "$PROBE_RATE" "$CONNS" "$PROBE_DURATION") 216 + rps=$(printf '%s\n' "$out" | awk '/Requests\/sec:/ { print $2 }') 217 + # Integer floor; falls back to 1 if parsing failed so a /0 can't happen. 218 + printf '%.0f\n' "${rps:-1}" 219 + } 220 + 221 + # Parses wrk2 -U output into pipe-separated fields. 222 + parse_wrk2() { 223 + awk ' 224 + /Latency Distribution \(HdrHistogram - Recorded Latency\)/ { section="c"; next } 225 + /Latency Distribution \(HdrHistogram - Uncorrected Latency/ { section="u"; next } 226 + section=="c" && / 50\.000%/ { c50=$2 } 227 + section=="c" && / 90\.000%/ { c90=$2 } 228 + section=="c" && / 99\.000%/ { c99=$2 } 229 + section=="c" && / 99\.900%/ { c999=$2 } 230 + section=="c" && / 99\.990%/ { c9999=$2 } 231 + section=="u" && / 50\.000%/ { u50=$2 } 232 + section=="u" && / 90\.000%/ { u90=$2 } 233 + section=="u" && / 99\.000%/ { u99=$2 } 234 + section=="u" && / 99\.900%/ { u999=$2 } 235 + section=="u" && / 99\.990%/ { u9999=$2 } 236 + /Requests\/sec:/ { rps=$2 } 237 + /Socket errors:/ { gsub(",", "", $0); err=$0 } 238 + END { 239 + printf "%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n", \ 240 + rps, c50, c90, c99, c999, c9999, u50, u90, u99, u999, u9999, err 241 + } 242 + ' 243 + } 244 + 245 + # Converts a wrk-style duration string to microseconds, for computing the 246 + # corrected/uncorrected ratio. 247 + to_us() { 248 + awk -v v="$1" ' 249 + BEGIN { 250 + if (v ~ /us$/) { sub(/us$/, "", v); printf "%.4f", v } 251 + else if (v ~ /ms$/) { sub(/ms$/, "", v); printf "%.4f", v * 1000 } 252 + else if (v ~ /s$/) { sub(/s$/, "", v); printf "%.4f", v * 1000000 } 253 + else { printf "%.4f", v } 254 + } 255 + ' 256 + } 257 + 258 + # corrected/uncorrected, as like "12.3x". 259 + ratio_of() { 260 + local c u 261 + c=$(to_us "$1") 262 + u=$(to_us "$2") 263 + awk -v c="$c" -v u="$u" 'BEGIN { if (u + 0 == 0) print "n/a"; else printf "%.1fx", c / u }' 264 + } 265 + 266 + table_header() { 267 + printf " %-5s %11s %11s %-24s %-24s %s\n" \ 268 + "load" "target/s" "actual/s" "corrected p50/p99/p99.9" "uncorrected p50/p99/p99.9" "CO ratio(p99)" 269 + } 270 + 271 + run_rate() { 272 + local server="$1" port="$2" endpoint="$3" path="$4" script="$5" pct="$6" sat="$7" 273 + local rate=$(( sat * pct / 100 )) 274 + [ "$rate" -lt 1 ] && rate=1 275 + 276 + local raw="$RESULTS_DIR/${server}__${endpoint}__p${pct}.txt" 277 + local out 278 + out=$(wrk2_run "$port" "$path" "$script" "$rate" "$CONNS" "$DURATION" | tee "$raw") 279 + 280 + local parsed rps c50 c90 c99 c999 c9999 u50 u90 u99 u999 u9999 err 281 + parsed=$(printf '%s\n' "$out" | parse_wrk2) 282 + IFS='|' read -r rps c50 c90 c99 c999 c9999 u50 u90 u99 u999 u9999 err <<< "$parsed" 283 + 284 + local err_csv="" 285 + [ -n "$err" ] && err_csv="\"$err\"" 286 + echo "$server,$endpoint,$pct,$rate,$sat,$CONNS,$THREADS,$DURATION,$rps,$c50,$c90,$c99,$c999,$c9999,$u50,$u90,$u99,$u999,$u9999,$err_csv" >> "$CSV" 287 + 288 + local ratio corrected uncorrected 289 + ratio=$(ratio_of "$c99" "$u99") 290 + corrected="$c50/$c99/$c999" 291 + uncorrected="$u50/$u99/$u999" 292 + printf " %3d%% %11s %11s %-24s %-24s %s\n" \ 293 + "$pct" "$rate" "$rps" "$corrected" "$uncorrected" "$ratio" 294 + [ -n "$err" ] && printf " %s\n" "$err" 295 + } 296 + 297 + for entry in "${SERVERS[@]}"; do 298 + IFS='|' read -r name port dir cmd <<< "$entry" 299 + server_selected "$name" || continue 300 + 301 + echo "=== $name (port $port) ===" 302 + server_log="$RESULTS_DIR/${name}.server.log" 303 + 304 + start_server "$dir" "$cmd" "$port" "$server_log" || continue 305 + pgid="$CURRENT_PGID" 306 + 307 + wrk -t2 -c20 -d"$WARMUP_DURATION" "http://127.0.0.1:$port/hello" > /dev/null 2>&1 308 + 309 + for endpoint_entry in "${ENDPOINTS[@]}"; do 310 + IFS='|' read -r ename epath escript <<< "$endpoint_entry" 311 + 312 + if skip_endpoint "$name" "$ename"; then 313 + echo 314 + case "$SKIP_REASON" in 315 + not_implemented) 316 + echo " $ename (skipped: $name doesn't implement this feature)" ;; 317 + unstable) 318 + echo " $ename (skipped: cannot measure properly)" ;; 319 + esac 320 + continue 321 + fi 322 + 323 + saturation=$(probe_saturation "$port" "$epath" "$escript") 324 + echo 325 + echo " $ename (saturation ~$saturation req/s)" 326 + table_header 327 + 328 + for pct in $PCTS; do 329 + run_rate "$name" "$port" "$ename" "$epath" "$escript" "$pct" "$saturation" 330 + done 331 + done 332 + 333 + stop_server "$pgid" 334 + done 335 + 336 + echo 337 + echo "Results: $CSV"
+3
benchmark/elli/.gitignore
··· 1 + _build 2 + rebar3.crashdump 3 + erl_crash.dump
+2
benchmark/elli/rebar.config
··· 1 + {erl_opts, [debug_info]}. 2 + {deps, [{elli, "3.3.0"}]}.
+8
benchmark/elli/rebar.lock
··· 1 + {"1.2.0", 2 + [{<<"elli">>,{pkg,<<"elli">>,<<"3.3.0">>},0}]}. 3 + [ 4 + {pkg_hash,[ 5 + {<<"elli">>, <<"089218762A7FF3D20AE81C8E911BD0F73EE4EE0ED85454226D1FC6B4FFF3B4F6">>}]}, 6 + {pkg_hash_ext,[ 7 + {<<"elli">>, <<"698B13B33D05661DB9FE7EFCBA41B84825A379CCE86E486CF6AFF9285BE0CCF8">>}]} 8 + ].
+6
benchmark/elli/run.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + rebar3 compile 4 + erl -pa _build/default/lib/*/ebin \ 5 + -eval "application:ensure_all_started(elli_bench)" \ 6 + -noshell
+16
benchmark/elli/src/elli_bench.app.src
··· 1 + {application, elli_bench, 2 + [{description, "elli benchmark server"}, 3 + {vsn, "0.1.0"}, 4 + {registered, []}, 5 + {mod, {elli_bench_app, []}}, 6 + {applications, 7 + [kernel, 8 + stdlib, 9 + elli 10 + ]}, 11 + {env,[]}, 12 + {modules, []}, 13 + 14 + {licenses, ["Apache 2.0"]}, 15 + {links, []} 16 + ]}.
+11
benchmark/elli/src/elli_bench_app.erl
··· 1 + -module(elli_bench_app). 2 + 3 + -behaviour(application). 4 + 5 + -export([start/2, stop/1]). 6 + 7 + start(_StartType, _StartArgs) -> 8 + elli_bench_sup:start_link(). 9 + 10 + stop(_State) -> 11 + ok.
+50
benchmark/elli/src/elli_bench_callback.erl
··· 1 + -module(elli_bench_callback). 2 + 3 + -behaviour(elli_handler). 4 + 5 + -export([handle/2, handle_event/3]). 6 + 7 + -include_lib("elli/include/elli.hrl"). 8 + 9 + %% elli never sets TCP_NODELAY on accepted sockets, so multi write responses 10 + %% (well chunked in particular) eat a Nagle and delayed-ACK stall between each 11 + %% write. 12 + handle(Req, _Args) -> 13 + elli_tcp:setopts(Req#req.socket, [{nodelay, true}]), 14 + handle(Req#req.method, elli_request:path(Req), Req). 15 + 16 + handle('GET', [<<"hello">>], _Req) -> 17 + {ok, [], <<"Hello, Joe!">>}; 18 + 19 + %% elli reads the full request body before calling the handler, so there is 20 + %% no incremental and chunked read to distinguish from a plain echo. 21 + handle('POST', [<<"echo">>], Req) -> 22 + {ok, [], elli_request:body(Req)}; 23 + 24 + handle('GET', [<<"stream">>], Req) -> 25 + Ref = elli_request:chunk_ref(Req), 26 + spawn(fun() -> stream_hello(Ref) end), 27 + {chunk, []}; 28 + 29 + handle('GET', [<<"file">>, <<"small">>], _Req) -> 30 + {ok, [{<<"Content-Type">>, <<"application/octet-stream">>}], 31 + {file, "../priv/file_100kb.bin"}}; 32 + handle('GET', [<<"file">>, <<"big">>], _Req) -> 33 + {ok, [{<<"Content-Type">>, <<"application/octet-stream">>}], 34 + {file, "../priv/file_1gb.bin"}}; 35 + 36 + handle(_Method, _Path, _Req) -> 37 + {404, [], <<>>}. 38 + 39 + stream_hello(Ref) -> 40 + case elli_request:send_chunk(Ref, <<"hello, ">>) of 41 + ok -> 42 + case elli_request:send_chunk(Ref, <<"Joe!">>) of 43 + ok -> elli_request:close_chunk(Ref); 44 + {error, _reason} -> ok 45 + end; 46 + {error, _reason} -> ok 47 + end. 48 + 49 + handle_event(_Event, _Args, _Config) -> 50 + ok.
+26
benchmark/elli/src/elli_bench_sup.erl
··· 1 + -module(elli_bench_sup). 2 + 3 + -behaviour(supervisor). 4 + 5 + -export([start_link/0]). 6 + -export([init/1]). 7 + 8 + -define(SERVER, ?MODULE). 9 + 10 + start_link() -> 11 + supervisor:start_link({local, ?SERVER}, ?MODULE, []). 12 + 13 + init([]) -> 14 + SupFlags = #{strategy => one_for_one, 15 + intensity => 0, 16 + period => 1}, 17 + ChildSpecs = [#{id => elli_bench, 18 + start => {elli, start_link, 19 + [[{callback, elli_bench_callback}, 20 + {callback_args, []}, 21 + {port, 3003}]]}, 22 + restart => permanent, 23 + shutdown => 5000, 24 + type => worker, 25 + modules => [elli]}], 26 + {ok, {SupFlags, ChildSpecs}}.
+77 -9
benchmark/ewe@4/src/app.gleam
··· 1 1 import ewe 2 - import gleam/erlang/process 2 + import gleam/bit_array 3 + import gleam/bytes_tree 4 + import gleam/erlang/process.{type Subject} 5 + import gleam/http 3 6 import gleam/http/request 4 7 import gleam/http/response 5 8 import gleam/option 9 + import gleam/string 6 10 import logging 7 11 8 12 pub fn main() -> Nil { ··· 20 24 fn handle_request( 21 25 request: request.Request(ewe.Connection), 22 26 ) -> response.Response(ewe.ResponseBody) { 23 - case request.path { 24 - "/hello" -> 27 + case request.method, request.path { 28 + http.Get, "/hello" -> 25 29 response.new(200) 26 30 |> response.set_body(ewe.TextData("Hello, Joe!")) 27 - "/echo" -> 31 + http.Post, "/echo" -> 28 32 case ewe.read_body(request, 10_000_000) { 29 33 Ok(req) -> 30 34 response.new(200) 31 35 |> response.set_body(ewe.BitsData(req.body)) 32 36 Error(_error) -> response.new(400) |> response.set_body(ewe.Empty) 33 37 } 34 - "/file/small" -> { 38 + http.Post, "/echo/chunked" -> echo_chunked(request) 39 + http.Get, "/stream" -> stream_hello(request) 40 + http.Get, "/file/small" -> { 35 41 // head -c 100K /dev/urandom > file_100kb.bin 36 42 let assert Ok(file) = 37 43 ewe.file( 38 - "./dev/priv/file_100kb.bin", 44 + "../priv/file_100kb.bin", 39 45 offset: option.None, 40 46 limit: option.None, 41 47 ) ··· 46 52 body: file, 47 53 ) 48 54 } 49 - "/file/big" -> { 55 + http.Get, "/file/big" -> { 50 56 // head -c 1G /dev/urandom > file_1gb.bin 51 57 let assert Ok(file) = 52 58 ewe.file( 53 - "./dev/priv/file_1gb.bin", 59 + "../priv/file_1gb.bin", 54 60 offset: option.None, 55 61 limit: option.None, 56 62 ) ··· 61 67 body: file, 62 68 ) 63 69 } 64 - _ -> 70 + _method, _path -> 65 71 response.new(404) 66 72 |> response.set_body(ewe.Empty) 67 73 } 68 74 } 75 + 76 + fn echo_chunked( 77 + request: request.Request(ewe.Connection), 78 + ) -> response.Response(ewe.ResponseBody) { 79 + let failed = response.new(400) |> response.set_body(ewe.Empty) 80 + 81 + case ewe.stream_body(request) { 82 + Ok(consumer) -> 83 + case consume_chunks(consumer, bytes_tree.new()) { 84 + Ok(body) -> response.new(200) |> response.set_body(ewe.BytesData(body)) 85 + Error(Nil) -> failed 86 + } 87 + Error(_reason) -> failed 88 + } 89 + } 90 + 91 + fn consume_chunks( 92 + consumer: ewe.Consumer, 93 + acc: bytes_tree.BytesTree, 94 + ) -> Result(bytes_tree.BytesTree, Nil) { 95 + case consumer(4096) { 96 + Ok(ewe.Consumed(data, next)) -> 97 + consume_chunks(next, bytes_tree.append(acc, data)) 98 + Ok(ewe.Done) -> Ok(acc) 99 + Error(_reason) -> Error(Nil) 100 + } 101 + } 102 + 103 + type StreamMessage { 104 + StreamChunk(data: BitArray) 105 + StreamDone 106 + } 107 + 108 + fn stream_hello( 109 + request: request.Request(ewe.Connection), 110 + ) -> response.Response(ewe.ResponseBody) { 111 + ewe.chunked_body( 112 + request, 113 + response.new(200), 114 + on_init: fn(subject: Subject(StreamMessage)) { 115 + process.send(subject, StreamChunk(bit_array.from_string("hello, "))) 116 + process.send(subject, StreamChunk(bit_array.from_string("Joe!"))) 117 + process.send(subject, StreamDone) 118 + Nil 119 + }, 120 + handler: fn(conn, state, message) { 121 + case message { 122 + StreamChunk(data) -> { 123 + case ewe.send_chunk(conn, data) { 124 + Ok(Nil) -> ewe.chunked_continue(state) 125 + Error(reason) -> 126 + ewe.chunked_stop_abnormal( 127 + "Failed to send chunk: " <> string.inspect(reason), 128 + ) 129 + } 130 + } 131 + StreamDone -> ewe.chunked_stop() 132 + } 133 + }, 134 + on_close: fn(_conn, _state) { Nil }, 135 + ) 136 + }
+29 -8
benchmark/ewe@5/src/app.gleam
··· 1 1 import ewe 2 2 import gleam/bytes_tree 3 3 import gleam/erlang/process 4 + import gleam/http 4 5 import gleam/http/request 5 6 import gleam/http/response 6 7 import gleam/option ··· 15 16 16 17 let assert Ok(_started) = 17 18 ewe.new(listener_name:, connection_factory_name:, handler: handle_request) 19 + |> ewe.listening(on: 3006) 18 20 |> ewe.start 19 21 20 22 process.sleep_forever() ··· 23 25 fn handle_request( 24 26 request: request.Request(ewe.Connection), 25 27 ) -> response.Response(ewe.Body) { 26 - case request.path { 27 - "/hello" -> 28 + case request.method, request.path { 29 + http.Get, "/hello" -> 28 30 response.new(200) 29 31 |> response.set_body(ewe.Text("Hello, Joe!")) 30 - "/echo" -> 32 + http.Post, "/echo" -> 31 33 case ewe.read_body(request, 10_000_000) { 32 34 Ok(req) -> 33 35 response.new(200) 34 36 |> response.set_body(ewe.Bytes(bytes_tree.from_bit_array(req.body))) 35 37 Error(_error) -> response.new(400) |> response.set_body(ewe.Empty) 36 38 } 37 - "/file/small" -> { 39 + http.Post, "/echo/chunked" -> echo_chunked(request, bytes_tree.new()) 40 + http.Get, "/stream" -> { 41 + use writer <- ewe.stream_response(response.new(200)) 42 + let writer = ewe.send_chunk(writer, <<"hello, ":utf8>>) 43 + ewe.finish_chunk(writer, <<"Joe!":utf8>>) 44 + } 45 + http.Get, "/file/small" -> { 38 46 // head -c 100K /dev/urandom > file_100kb.bin 39 47 let assert Ok(file) = 40 48 ewe.file( 41 - "./dev/priv/file_100kb.bin", 49 + "../priv/file_100kb.bin", 42 50 offset: option.None, 43 51 limit: option.None, 44 52 ) ··· 49 57 body: file, 50 58 ) 51 59 } 52 - "/file/big" -> { 60 + http.Get, "/file/big" -> { 53 61 // head -c 1G /dev/urandom > file_1gb.bin 54 62 let assert Ok(file) = 55 63 ewe.file( 56 - "./dev/priv/file_1gb.bin", 64 + "../priv/file_1gb.bin", 57 65 offset: option.None, 58 66 limit: option.None, 59 67 ) ··· 64 72 body: file, 65 73 ) 66 74 } 67 - _ -> 75 + _method, _path -> 68 76 response.new(404) 69 77 |> response.set_body(ewe.Empty) 70 78 } 71 79 } 80 + 81 + fn echo_chunked( 82 + request: request.Request(ewe.Connection), 83 + acc: bytes_tree.BytesTree, 84 + ) -> response.Response(ewe.Body) { 85 + case ewe.read_body_chunk(request, max_chunk_bytes: 4096, limit: 10_000_000) { 86 + Ok(ewe.Chunk(data, request)) -> 87 + echo_chunked(request, bytes_tree.append(acc, data)) 88 + Ok(ewe.Done(_request)) -> 89 + response.new(200) |> response.set_body(ewe.Bytes(acc)) 90 + Error(_error) -> response.new(400) |> response.set_body(ewe.Empty) 91 + } 92 + }
+3
benchmark/httpd/.gitignore
··· 1 + _build 2 + rebar3.crashdump 3 + erl_crash.dump
+2
benchmark/httpd/rebar.config
··· 1 + {erl_opts, [debug_info]}. 2 + {deps, []}.
+1
benchmark/httpd/rebar.lock
··· 1 + [].
+6
benchmark/httpd/run.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + rebar3 compile 4 + erl -pa _build/default/lib/*/ebin \ 5 + -eval "application:ensure_all_started(httpd_bench)" \ 6 + -noshell
+16
benchmark/httpd/src/httpd_bench.app.src
··· 1 + {application, httpd_bench, 2 + [{description, "inets httpd benchmark server"}, 3 + {vsn, "0.1.0"}, 4 + {registered, []}, 5 + {mod, {httpd_bench_app, []}}, 6 + {applications, 7 + [kernel, 8 + stdlib, 9 + inets 10 + ]}, 11 + {env,[]}, 12 + {modules, []}, 13 + 14 + {licenses, ["Apache 2.0"]}, 15 + {links, []} 16 + ]}.
+11
benchmark/httpd/src/httpd_bench_app.erl
··· 1 + -module(httpd_bench_app). 2 + 3 + -behaviour(application). 4 + 5 + -export([start/2, stop/1]). 6 + 7 + start(_StartType, _StartArgs) -> 8 + httpd_bench_sup:start_link(). 9 + 10 + stop(_State) -> 11 + ok.
+91
benchmark/httpd/src/httpd_bench_callback.erl
··· 1 + -module(httpd_bench_callback). 2 + 3 + -export([do/1]). 4 + 5 + -include_lib("inets/include/httpd.hrl"). 6 + -include_lib("kernel/include/file.hrl"). 7 + 8 + -define(FILE_CHUNK_SIZE, 262144). 9 + 10 + %% inets never sets TCP_NODELAY on accepted sockets and exposes no config for it, 11 + %% so every keep-alive response after the first eats a ~40ms Nagle and 12 + %% delayed-ACK stall. 13 + do(Info) -> 14 + #mod{socket_type = SocketType, socket = Socket} = Info, 15 + ensure_nodelay(SocketType, Socket), 16 + route(Info#mod.method, Info#mod.request_uri, Info). 17 + 18 + ensure_nodelay(ip_comm, Socket) -> 19 + inet:setopts(Socket, [{nodelay, true}]); 20 + ensure_nodelay(_SocketType, _Socket) -> 21 + ok. 22 + 23 + route("GET", "/hello", _Info) -> 24 + {break, [{response, {200, "Hello, Joe!"}}]}; 25 + 26 + %% httpd reads the full request body before calling the module, so there is 27 + %% no incremental and chunked read to distinguish from a plain echo. 28 + route("POST", "/echo", Info) -> 29 + {break, [{response, {200, entity_body(Info)}}]}; 30 + 31 + route("GET", "/stream", Info) -> 32 + send_stream(Info), 33 + {break, [{response, {already_sent, 200, 0}}]}; 34 + 35 + route("GET", "/file/small", Info) -> 36 + send_file(Info, "../priv/file_100kb.bin"), 37 + {break, [{response, {already_sent, 200, 0}}]}; 38 + route("GET", "/file/big", Info) -> 39 + send_file(Info, "../priv/file_1gb.bin"), 40 + {break, [{response, {already_sent, 200, 0}}]}; 41 + 42 + route(_Method, _Uri, Info) -> 43 + {proceed, Info#mod.data}. 44 + 45 + entity_body(Info) -> 46 + case Info#mod.entity_body of 47 + undefined -> ""; 48 + Body -> Body 49 + end. 50 + 51 + send_stream(Info) -> 52 + #mod{socket_type = SocketType, socket = Socket} = Info, 53 + Head = 54 + "HTTP/1.1 200 OK\r\n" 55 + "Content-Type: text/plain\r\n" 56 + "Transfer-Encoding: chunked\r\n" 57 + "Connection: keep-alive\r\n\r\n", 58 + httpd_socket:deliver(SocketType, Socket, Head), 59 + send_chunk(SocketType, Socket, "hello, "), 60 + send_chunk(SocketType, Socket, "Joe!"), 61 + httpd_socket:deliver(SocketType, Socket, "0\r\n\r\n"). 62 + 63 + send_chunk(SocketType, Socket, Data) -> 64 + Size = integer_to_list(length(Data), 16), 65 + httpd_socket:deliver(SocketType, Socket, [Size, "\r\n", Data, "\r\n"]). 66 + 67 + %% There is no built-in equivalent of the other server's sendfile helpers 68 + send_file(Info, Path) -> 69 + #mod{socket_type = SocketType, socket = Socket} = Info, 70 + {ok, #file_info{size = Size}} = file:read_file_info(Path), 71 + Head = 72 + io_lib:format( 73 + "HTTP/1.1 200 OK\r\n" 74 + "Content-Type: application/octet-stream\r\n" 75 + "Content-Length: ~b\r\n" 76 + "Connection: keep-alive\r\n\r\n", 77 + [Size] 78 + ), 79 + httpd_socket:deliver(SocketType, Socket, Head), 80 + {ok, Fd} = file:open(Path, [read, raw, binary]), 81 + deliver_file(SocketType, Socket, Fd), 82 + file:close(Fd). 83 + 84 + deliver_file(SocketType, Socket, Fd) -> 85 + case file:read(Fd, ?FILE_CHUNK_SIZE) of 86 + {ok, Data} -> 87 + httpd_socket:deliver(SocketType, Socket, Data), 88 + deliver_file(SocketType, Socket, Fd); 89 + eof -> 90 + ok 91 + end.
+31
benchmark/httpd/src/httpd_bench_sup.erl
··· 1 + -module(httpd_bench_sup). 2 + 3 + -behaviour(supervisor). 4 + 5 + -export([start_link/0]). 6 + -export([init/1]). 7 + 8 + -define(SERVER, ?MODULE). 9 + 10 + start_link() -> 11 + supervisor:start_link({local, ?SERVER}, ?MODULE, []). 12 + 13 + init([]) -> 14 + Root = filename:absname("."), 15 + Config = [ 16 + {port, 3005}, 17 + {server_name, "httpd_bench"}, 18 + {server_root, Root}, 19 + {document_root, filename:join(Root, "www")}, 20 + {modules, [httpd_bench_callback, mod_get]} 21 + ], 22 + SupFlags = #{strategy => one_for_one, 23 + intensity => 0, 24 + period => 1}, 25 + ChildSpecs = [#{id => httpd_bench, 26 + start => {httpd, start_standalone, [Config]}, 27 + restart => permanent, 28 + shutdown => 5000, 29 + type => worker, 30 + modules => [httpd]}], 31 + {ok, {SupFlags, ChildSpecs}}.
benchmark/httpd/www/.gitkeep

This is a binary file and will not be displayed.

+70 -21
benchmark/mist/src/app.gleam
··· 1 + import gleam/bit_array 1 2 import gleam/bytes_tree 2 - import gleam/erlang/process 3 + import gleam/erlang/process.{type Subject} 3 4 import gleam/http 4 5 import gleam/http/request 5 6 import gleam/http/response 6 7 import gleam/option 7 - import gleam/string 8 8 import logging 9 9 import mist 10 10 ··· 23 23 fn handle_request( 24 24 request: request.Request(mist.Connection), 25 25 ) -> response.Response(mist.ResponseData) { 26 - case request.path { 27 - "/hello" -> 26 + case request.method, request.path { 27 + http.Get, "/hello" -> 28 28 response.new(200) 29 29 |> response.set_body(mist.Bytes(bytes_tree.from_string("Hello, Joe!"))) 30 - "/whoami" -> 31 - response.new(200) 32 - |> response.set_body( 33 - mist.Bytes(bytes_tree.from_string( 34 - "method=" <> method_to_string(request.method), 35 - )), 36 - ) 37 - "/echo" -> 30 + http.Post, "/echo" -> 38 31 case mist.read_body(request, 10_000_000) { 39 32 Ok(req) -> 40 33 response.new(200) ··· 42 35 Error(_error) -> 43 36 response.new(400) |> response.set_body(mist.Bytes(bytes_tree.new())) 44 37 } 45 - "/file/small" -> { 38 + http.Post, "/echo/chunked" -> echo_chunked(request) 39 + http.Get, "/stream" -> stream_hello(request) 40 + http.Get, "/file/small" -> { 46 41 // head -c 100K /dev/urandom > file_100kb.bin 47 42 let assert Ok(file) = 48 43 mist.send_file( 49 - "./dev/priv/file_100kb.bin", 44 + "../priv/file_100kb.bin", 50 45 offset: 0, 51 46 limit: option.None, 52 47 ) ··· 57 52 body: file, 58 53 ) 59 54 } 60 - "/file/big" -> { 55 + http.Get, "/file/big" -> { 61 56 // head -c 1G /dev/urandom > file_1gb.bin 62 57 let assert Ok(file) = 63 - mist.send_file("./dev/priv/file_1gb.bin", offset: 0, limit: option.None) 58 + mist.send_file( 59 + "../priv/file_1gb.bin", 60 + offset: 0, 61 + limit: option.None, 62 + ) 64 63 65 64 response.Response( 66 65 status: 200, ··· 68 67 body: file, 69 68 ) 70 69 } 71 - _ -> 70 + _method, _path -> 72 71 response.new(404) 73 72 |> response.set_body(mist.Bytes(bytes_tree.new())) 74 73 } 75 74 } 76 75 77 - fn method_to_string(method: http.Method) -> String { 78 - case method { 79 - http.Other(name) -> "Other(" <> name <> ")" 80 - _other -> string.inspect(method) 76 + fn echo_chunked( 77 + request: request.Request(mist.Connection), 78 + ) -> response.Response(mist.ResponseData) { 79 + let failed = 80 + response.new(400) |> response.set_body(mist.Bytes(bytes_tree.new())) 81 + 82 + case mist.stream(request) { 83 + Ok(consume) -> 84 + case consume_chunks(consume, bytes_tree.new()) { 85 + Ok(body) -> response.new(200) |> response.set_body(mist.Bytes(body)) 86 + Error(Nil) -> failed 87 + } 88 + Error(_reason) -> failed 89 + } 90 + } 91 + 92 + fn consume_chunks( 93 + consume: fn(Int) -> Result(mist.Chunk, mist.ReadError), 94 + acc: bytes_tree.BytesTree, 95 + ) -> Result(bytes_tree.BytesTree, Nil) { 96 + case consume(4096) { 97 + Ok(mist.Chunk(data, consume)) -> 98 + consume_chunks(consume, bytes_tree.append(acc, data)) 99 + Ok(mist.Done) -> Ok(acc) 100 + Error(_reason) -> Error(Nil) 81 101 } 82 102 } 103 + 104 + type StreamMessage { 105 + StreamChunk(data: BitArray) 106 + StreamDone 107 + } 108 + 109 + fn stream_hello( 110 + request: request.Request(mist.Connection), 111 + ) -> response.Response(mist.ResponseData) { 112 + mist.chunked( 113 + request:, 114 + response: response.new(200), 115 + init: fn(subject: Subject(StreamMessage)) { 116 + process.send(subject, StreamChunk(bit_array.from_string("hello, "))) 117 + process.send(subject, StreamChunk(bit_array.from_string("Joe!"))) 118 + process.send(subject, StreamDone) 119 + Nil 120 + }, 121 + loop: fn(state, message, connection) { 122 + case message { 123 + StreamChunk(data) -> { 124 + let assert Ok(Nil) = mist.send_chunk(connection, data) 125 + mist.chunk_continue(state) 126 + } 127 + StreamDone -> mist.chunk_stop() 128 + } 129 + }, 130 + ) 131 + }
benchmark/priv/.gitkeep

This is a binary file and will not be displayed.

+3
benchmark/roadrunner/.gitignore
··· 1 + _build 2 + rebar3.crashdump 3 + erl_crash.dump
+2
benchmark/roadrunner/rebar.config
··· 1 + {erl_opts, [debug_info]}. 2 + {deps, [roadrunner]}.
+11
benchmark/roadrunner/rebar.lock
··· 1 + {"1.2.0", 2 + [{<<"roadrunner">>,{pkg,<<"roadrunner">>,<<"0.8.0">>},0}, 3 + {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.4.2">>},1}]}. 4 + [ 5 + {pkg_hash,[ 6 + {<<"roadrunner">>, <<"7BBC9DA8E2085EA94FE0A06DB51DBAD762AA016E70972F2A533C3914438C96F6">>}, 7 + {<<"telemetry">>, <<"A0CB522801DFFB1C49FE6E30561BADFFC7B6D0E180DB1300DF759FAA22062855">>}]}, 8 + {pkg_hash_ext,[ 9 + {<<"roadrunner">>, <<"76F3389A27CE084FF380F80C6DE73C2448A6702017A2C22ACF34DE606C8239D1">>}, 10 + {<<"telemetry">>, <<"928F6495066506077862C0D1646609EED891A4326BEE3126BA54B60AF61FEBB1">>}]} 11 + ].
+9
benchmark/roadrunner/run.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + # A packaged relx release changes its working directory to the release 4 + # root, which would break the ../priv relative file paths, so this runs 5 + # straight from source instead. 6 + rebar3 compile 7 + erl -pa _build/default/lib/*/ebin \ 8 + -eval "application:ensure_all_started(roadrunner_bench)" \ 9 + -noshell
+16
benchmark/roadrunner/src/roadrunner_bench.app.src
··· 1 + {application, roadrunner_bench, 2 + [{description, "roadrunner benchmark server"}, 3 + {vsn, "0.1.0"}, 4 + {registered, []}, 5 + {mod, {roadrunner_bench_app, []}}, 6 + {applications, 7 + [kernel, 8 + stdlib, 9 + roadrunner 10 + ]}, 11 + {env,[]}, 12 + {modules, []}, 13 + 14 + {licenses, ["Apache 2.0"]}, 15 + {links, []} 16 + ]}.
+11
benchmark/roadrunner/src/roadrunner_bench_app.erl
··· 1 + -module(roadrunner_bench_app). 2 + 3 + -behaviour(application). 4 + 5 + -export([start/2, stop/1]). 6 + 7 + start(_StartType, _StartArgs) -> 8 + roadrunner_bench_sup:start_link(). 9 + 10 + stop(_State) -> 11 + ok.
+54
benchmark/roadrunner/src/roadrunner_bench_callback.erl
··· 1 + -module(roadrunner_bench_callback). 2 + 3 + -behaviour(roadrunner_handler). 4 + 5 + -export([handle/1]). 6 + 7 + -include_lib("kernel/include/file.hrl"). 8 + 9 + handle(Req) -> 10 + route(roadrunner_req:method(Req), roadrunner_req:path(Req), Req). 11 + 12 + route(~"GET", ~"/hello", Req) -> 13 + {roadrunner_resp:text(200, ~"Hello, Joe!"), Req}; 14 + 15 + route(~"POST", ~"/echo", Req) -> 16 + {ok, Body, Req2} = roadrunner_req:read_body(Req), 17 + {roadrunner_resp:text(200, Body), Req2}; 18 + 19 + route(~"POST", ~"/echo/chunked", Req) -> 20 + {Body, Req2} = read_all_chunks(Req, []), 21 + {roadrunner_resp:text(200, Body), Req2}; 22 + 23 + route(~"GET", ~"/stream", Req) -> 24 + Resp = {stream, 200, [], fun(Send) -> 25 + Send(~"hello, ", nofin), 26 + Send(~"Joe!", fin) 27 + end}, 28 + {Resp, Req}; 29 + 30 + route(~"GET", ~"/file/small", Req) -> 31 + send_file(Req, "../priv/file_100kb.bin"); 32 + route(~"GET", ~"/file/big", Req) -> 33 + send_file(Req, "../priv/file_1gb.bin"); 34 + 35 + route(_Method, _Path, Req) -> 36 + {roadrunner_resp:not_found(), Req}. 37 + 38 + read_all_chunks(Req, Acc) -> 39 + case roadrunner_req:read_body_chunked(Req) of 40 + {more, Bytes, Req2} -> read_all_chunks(Req2, [Bytes | Acc]); 41 + {ok, Bytes, Req2} -> {lists:reverse([Bytes | Acc]), Req2} 42 + end. 43 + 44 + send_file(Req, Path) -> 45 + case file:read_file_info(Path) of 46 + {ok, #file_info{size = Size}} -> 47 + Headers = [ 48 + {~"content-type", ~"application/octet-stream"}, 49 + {~"content-length", integer_to_binary(Size)} 50 + ], 51 + {{sendfile, 200, Headers, {Path, 0, Size}}, Req}; 52 + {error, _Reason} -> 53 + {roadrunner_resp:not_found(), Req} 54 + end.
+30
benchmark/roadrunner/src/roadrunner_bench_sup.erl
··· 1 + -module(roadrunner_bench_sup). 2 + 3 + -behaviour(supervisor). 4 + 5 + -export([start_link/0]). 6 + -export([init/1]). 7 + 8 + -define(SERVER, ?MODULE). 9 + 10 + start_link() -> 11 + supervisor:start_link({local, ?SERVER}, ?MODULE, []). 12 + 13 + init([]) -> 14 + {ok, _Pid} = roadrunner:start_listener(bench_listener, #{ 15 + port => 3007, 16 + body_buffering => manual, 17 + routes => [ 18 + {~"/hello", roadrunner_bench_callback, undefined}, 19 + {~"/echo", roadrunner_bench_callback, undefined}, 20 + {~"/echo/chunked", roadrunner_bench_callback, undefined}, 21 + {~"/stream", roadrunner_bench_callback, undefined}, 22 + {~"/file/small", roadrunner_bench_callback, undefined}, 23 + {~"/file/big", roadrunner_bench_callback, undefined} 24 + ] 25 + }), 26 + 27 + SupFlags = #{strategy => one_for_one, 28 + intensity => 0, 29 + period => 1}, 30 + {ok, {SupFlags, []}}.
+6
benchmark/wrk/post_echo.lua
··· 1 + -- POST /echo with a fixed Content-Length body, exercising the buffered 2 + -- (whole-body) request read path. 3 + 4 + wrk.method = "POST" 5 + wrk.body = string.rep("a", 4096) 6 + wrk.headers["Content-Type"] = "application/octet-stream"
+27
benchmark/wrk/post_echo_chunked.lua
··· 1 + -- POST /echo/chunked with a Transfer-Encoding: chunked body, exercising the 2 + -- incremental/streamed request read path. wrk has no built-in support for 3 + -- chunked request bodies, so the raw request is hand-built here. 4 + 5 + wrk.method = "POST" 6 + 7 + local chunks = { 8 + string.rep("a", 1024), 9 + string.rep("b", 1024), 10 + string.rep("c", 1024), 11 + string.rep("d", 1024), 12 + } 13 + 14 + local encoded = "" 15 + for _, chunk in ipairs(chunks) do 16 + encoded = encoded .. string.format("%x", #chunk) .. "\r\n" .. chunk .. "\r\n" 17 + end 18 + encoded = encoded .. "0\r\n\r\n" 19 + 20 + request = function() 21 + return "POST /echo/chunked HTTP/1.1\r\n" .. 22 + "Host: 127.0.0.1\r\n" .. 23 + "Transfer-Encoding: chunked\r\n" .. 24 + "Content-Type: application/octet-stream\r\n" .. 25 + "Connection: keep-alive\r\n\r\n" .. 26 + encoded 27 + end
+32
benchmark/wrk2-setup.sh
··· 1 + #!/usr/bin/env bash 2 + # 3 + # Clones and builds wrk2 (https://github.com/giltene/wrk2) into benchmark/.wrk2/, 4 + # for use by bench.sh. 5 + # 6 + # On newer binutils 2.4x+, wrk2's vendored LuaJIT bytecode-to-object step links 7 + # fine on its own, but the final link fails with "string table is corrupt" using 8 + # the default bfd linker. I don't know if it's issue on my side, but building 9 + # with the gold linker avoids it. 10 + 11 + set -eu 12 + 13 + ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 14 + DEST="$ROOT/.wrk2" 15 + 16 + if [ -x "$DEST/wrk2" ]; then 17 + echo "Already built: $DEST/wrk2" 18 + exit 0 19 + fi 20 + 21 + rm -rf "$DEST/src" 22 + mkdir -p "$DEST" 23 + git clone --depth 1 https://github.com/giltene/wrk2.git "$DEST/src" 24 + 25 + ( 26 + cd "$DEST/src" 27 + make CC="gcc -fuse-ld=gold" 28 + ) 29 + 30 + cp "$DEST/src/wrk" "$DEST/wrk2" 31 + echo "Built: $DEST/wrk2" 32 + "$DEST/wrk2" --version || true
+11 -10
dev/serve.gleam
··· 1 1 import ewe 2 2 import gleam/bytes_tree 3 3 import gleam/erlang/process 4 + import gleam/http 4 5 import gleam/http/request 5 6 import gleam/http/response 6 7 import gleam/option ··· 23 24 fn handle_request( 24 25 request: request.Request(ewe.Connection), 25 26 ) -> response.Response(ewe.Body) { 26 - case request.path { 27 - "/hello" -> 27 + case request.method, request.path { 28 + http.Get, "/hello" -> 28 29 response.new(200) 29 30 |> response.set_body(ewe.Text("Hello, Joe!")) 30 - "/echo" -> 31 + http.Post, "/echo" -> 31 32 case ewe.read_body(request, 10_000_000) { 32 33 Ok(req) -> 33 34 response.new(200) 34 35 |> response.set_body(ewe.Bytes(bytes_tree.from_bit_array(req.body))) 35 36 Error(_error) -> response.new(400) |> response.set_body(ewe.Empty) 36 37 } 37 - "/echo/chunked" -> echo_chunked(request, bytes_tree.new()) 38 - "/stream" -> { 38 + http.Post, "/echo/chunked" -> echo_chunked(request, bytes_tree.new()) 39 + http.Get, "/stream" -> { 39 40 use writer <- ewe.stream_response(response.new(200)) 40 - let writer = ewe.send_chunk(writer, <<"hello ":utf8>>) 41 - ewe.finish_chunk(writer, <<"world":utf8>>) 41 + let writer = ewe.send_chunk(writer, <<"hello, ":utf8>>) 42 + ewe.finish_chunk(writer, <<"Joe!":utf8>>) 42 43 } 43 - "/file/small" -> { 44 + http.Get, "/file/small" -> { 44 45 // head -c 100K /dev/urandom > file_100kb.bin 45 46 let assert Ok(file) = 46 47 ewe.file( ··· 55 56 body: file, 56 57 ) 57 58 } 58 - "/file/big" -> { 59 + http.Get, "/file/big" -> { 59 60 // head -c 1G /dev/urandom > file_1gb.bin 60 61 let assert Ok(file) = 61 62 ewe.file( ··· 70 71 body: file, 71 72 ) 72 73 } 73 - _ -> 74 + _method, _path -> 74 75 response.new(404) 75 76 |> response.set_body(ewe.Empty) 76 77 }
+9 -4
src/ewe/internal/connection.gleam
··· 1 1 import gleam/bytes_tree 2 2 import gleam/erlang/process 3 - import glisten/internal/handler 4 3 import glisten/socket 5 4 import glisten/transport 6 5 ··· 8 7 Http1( 9 8 transport: transport.Transport, 10 9 socket: socket.Socket, 11 - self: process.Subject(handler.Message(Message)), 10 + self: process.Subject(Http1Signal), 12 11 buffer: BitArray, 13 12 framing: Framing, 14 13 // Body bytes delivered to the caller so far, via `read_body_chunk`. ··· 41 40 FileMetadata(path: String, offset: Int, length: Int) 42 41 } 43 42 43 + /// Out-of-band events for a connection's own actor loop. 44 44 pub type Message { 45 45 Timeout 46 + } 47 + 48 + /// Sent by `http1.gleam` to a request's own private subject, drained 49 + /// synchronously within the same `http1.handle_message` call 50 + pub type Http1Signal { 46 51 /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves 47 52 /// once the request body has been fully consumed, carrying whatever bytes 48 53 /// came after it. ··· 56 61 /// the connection can still resume draining from here instead of from the 57 62 /// start of the body. 58 63 BodyProgress(buffer: BitArray, read: Int, chunk_remaining: Int) 59 - /// Sent by `http1.finish_chunk`/`http1.finish_response` to themselves once 64 + /// Sent by `http1.finish_chunk` and `http1.finish_response` to themselves once 60 65 /// a streamed response's terminator has been written. 61 66 StreamFinished(keep_alive: Bool) 62 67 } ··· 66 71 Http1Writer( 67 72 transport: transport.Transport, 68 73 socket: socket.Socket, 69 - self: process.Subject(handler.Message(Message)), 74 + self: process.Subject(Http1Signal), 70 75 // `True` on HTTP/1.1. Frame each chunk with its hex size and a trailing 71 76 // `0\r\n\r\n` terminator. 72 77 //
-14
src/ewe/internal/handler.gleam
··· 5 5 import gleam/http/request 6 6 import gleam/http/response 7 7 import gleam/option 8 - import gleam/string 9 8 import glisten 10 9 import glisten/internal/handler 11 10 import logging ··· 60 59 glisten.User(connection.Timeout) -> { 61 60 logging.log(logging.Debug, "Connection idled for too long, closing.") 62 61 glisten.stop() 63 - } 64 - // TODO: just use other subject brah 65 - glisten.User(connection.BodyDrained(..)) 66 - | glisten.User(connection.BodyAbandoned) 67 - | glisten.User(connection.BodyProgress(..)) 68 - | glisten.User(connection.StreamFinished(..)) -> { 69 - logging.log( 70 - logging.Critical, 71 - "Web server loop received a message that should not be reached to the handler! That means there is a bug somewhere within the implementation. Please open an issue addressing the alert, thank you! https://github.com/vshakitskiy/ewe/issues/new\n\nMessage received: " 72 - <> string.inspect(message), 73 - ) 74 - 75 - glisten.continue(state) 76 62 } 77 63 glisten.Packet(data) -> 78 64 case state {
+27 -29
src/ewe/internal/http1.gleam
··· 50 50 transport.Ssl -> http.Https 51 51 } 52 52 53 + let self = process.new_subject() 54 + 53 55 let body_connection = 54 56 connection.Http1( 55 57 transport: connection.transport, 56 58 socket: connection.socket, 57 - self: connection.subject, 59 + self:, 58 60 buffer: remaining, 59 61 framing: metadata.framing, 60 62 read: 0, ··· 75 77 76 78 let response = state.handler(request) 77 79 78 - let drained = drain_messages(connection.subject) 80 + let drained = drain_messages(self) 79 81 80 82 let #(buffer, body_drained) = 81 83 resolve_body(unsafe_to_http1_connection(body_connection), drained.body) ··· 105 107 connection.Http1Writer( 106 108 transport: connection.transport, 107 109 socket: connection.socket, 108 - self: connection.subject, 110 + self:, 109 111 chunked:, 110 112 keep_alive:, 111 113 ) 112 114 |> stream_handler 113 115 114 - let stream_drained = drain_messages(connection.subject) 116 + let stream_drained = drain_messages(self) 115 117 let stream_keep_alive = case stream_drained.stream { 116 118 option.Some(connection.StreamFinished(keep_alive:)) -> 117 119 keep_alive ··· 135 137 } 136 138 False 137 139 } 138 - option.Some(connection.Timeout) 139 - | option.Some(connection.BodyDrained(..)) 140 + option.Some(connection.BodyDrained(..)) 140 141 | option.Some(connection.BodyAbandoned) 141 142 | option.Some(connection.BodyProgress(..)) -> 142 143 panic as "drain_messages routes body messages to the other slot" ··· 216 217 Http1( 217 218 transport: transport.Transport, 218 219 socket: socket.Socket, 219 - self: process.Subject(handler.Message(connection.Message)), 220 + self: process.Subject(connection.Http1Signal), 220 221 buffer: BitArray, 221 222 framing: connection.Framing, 222 223 read: Int, ··· 245 246 246 247 case framing, consume_body(transport, socket, buffer, framing, limit) { 247 248 _framing, Ok(#(body, trailers, leftover)) -> { 248 - process.send(self, handler.User(connection.BodyDrained(leftover:))) 249 + process.send(self, connection.BodyDrained(leftover:)) 249 250 Ok(#(body, trailers)) 250 251 } 251 252 connection.Fixed(_length), Error(BodyTooLarge) -> Error(BodyTooLarge) 252 253 _framing, Error(error) -> { 253 - process.send(self, handler.User(connection.BodyAbandoned)) 254 + process.send(self, connection.BodyAbandoned) 254 255 Error(error) 255 256 } 256 257 } ··· 278 279 279 280 case pull_chunk(conn, max_chunk_bytes, limit) { 280 281 Ok(PulledChunk(data, next)) -> { 281 - handler.User(connection.BodyProgress(buffer:, read:, chunk_remaining:)) 282 + connection.BodyProgress(buffer:, read:, chunk_remaining:) 282 283 |> process.send(self, _) 283 284 284 285 Ok(Chunk(data, next)) 285 286 } 286 287 Ok(PulledDone(trailers, leftover)) -> { 287 - process.send(self, handler.User(connection.BodyDrained(leftover:))) 288 + process.send(self, connection.BodyDrained(leftover:)) 288 289 289 290 Ok(Done(trailers)) 290 291 } 291 292 Error(error) -> { 292 - process.send(self, handler.User(connection.BodyAbandoned)) 293 + process.send(self, connection.BodyAbandoned) 293 294 294 295 Error(error) 295 296 } ··· 312 313 // connection just because the handler didn't care about its body. 313 314 fn resolve_body( 314 315 conn: Connection, 315 - drained: option.Option(connection.Message), 316 + drained: option.Option(connection.Http1Signal), 316 317 ) -> #(BitArray, Bool) { 317 318 case drained { 318 319 option.Some(connection.BodyDrained(leftover)) -> #(leftover, True) 319 320 option.Some(connection.BodyAbandoned) -> #(<<>>, False) 320 321 option.Some(connection.BodyProgress(buffer:, read:, chunk_remaining:)) -> 321 322 drain_remaining(Http1(..conn, buffer:, read:, chunk_remaining:)) 322 - option.Some(connection.Timeout) | option.None -> drain_remaining(conn) 323 + option.None -> drain_remaining(conn) 323 324 option.Some(connection.StreamFinished(..)) -> 324 325 panic as "drain_messages routes stream messages to the other slot" 325 326 } 326 327 } 327 328 328 - // The latest body read and response stream outcome messages currently queued 329 - // for a connection's own subject, drained in one pass so a handler that both 330 - // reads a request body and streams a response doesn't lose one outcome to the 331 - // other. 329 + // The latest body read and response stream outcome messages currently queued 330 + // for a request's own private subject, drained in one pass so a handler that 331 + // both reads a request body and streams a response doesn't lose one outcome 332 + // to the other. 332 333 type Drained { 333 334 Drained( 334 - body: option.Option(connection.Message), 335 - stream: option.Option(connection.Message), 335 + body: option.Option(connection.Http1Signal), 336 + stream: option.Option(connection.Http1Signal), 336 337 ) 337 338 } 338 339 339 340 // Selectively receives every message already queued for `self`, keeping only 340 341 // the last one of each kind. 341 - fn drain_messages( 342 - self: process.Subject(handler.Message(connection.Message)), 343 - ) -> Drained { 342 + fn drain_messages(self: process.Subject(connection.Http1Signal)) -> Drained { 344 343 do_drain_messages(self, Drained(body: option.None, stream: option.None)) 345 344 } 346 345 347 346 fn do_drain_messages( 348 - self: process.Subject(handler.Message(connection.Message)), 347 + self: process.Subject(connection.Http1Signal), 349 348 acc: Drained, 350 349 ) -> Drained { 351 350 case process.receive(self, 0) { 352 - Ok(handler.User(connection.StreamFinished(..) as message)) -> 351 + Ok(connection.StreamFinished(..) as message) -> 353 352 do_drain_messages(self, Drained(..acc, stream: option.Some(message))) 354 - Ok(handler.User(message)) -> 353 + Ok(message) -> 355 354 do_drain_messages(self, Drained(..acc, body: option.Some(message))) 356 - Ok(handler.Internal(_message)) -> do_drain_messages(self, acc) 357 355 Error(Nil) -> acc 358 356 } 359 357 } ··· 829 827 ResponseWriter( 830 828 transport: transport.Transport, 831 829 socket: socket.Socket, 832 - self: process.Subject(handler.Message(connection.Message)), 830 + self: process.Subject(connection.Http1Signal), 833 831 // `True` on HTTP/1.1: frame each chunk with its hex size and a trailing 834 832 // `0\r\n\r\n` terminator. `False` on HTTP/1.0, which has no chunked 835 833 // encoding: write raw bytes and let the body end when the connection ··· 898 896 fn finish(writer: ResponseWriter) -> Nil { 899 897 process.send( 900 898 writer.self, 901 - handler.User(connection.StreamFinished(keep_alive: writer.keep_alive)), 899 + connection.StreamFinished(keep_alive: writer.keep_alive), 902 900 ) 903 901 } 904 902