From b3b336db76f853d555e613f991c78d11a5f8cdd0 Mon Sep 17 00:00:00 2001 From: Gabriel Baraldi Date: Tue, 4 Aug 2026 16:39:43 +0000 Subject: [PATCH] Add webserver: loopback HTTP server under load Start an HTTP.jl server on 127.0.0.1 and hammer it with 4 client tasks per thread, with the handler building a JSON-ish response per request and retaining a fixed-size cache of response objects as an old generation. Server workloads are the motivating case for concurrent-GC work: many tasks, a high rate of short-lived request/response garbage, and long-lived state behind it. No existing benchmark in the suite is task-heavy and IO-bound. 8000 requests run in ~6s measured with ~10% GC time on 4 threads. Co-Authored-By: Claude Fable 5 --- benches/multithreaded/http/Project.toml | 5 ++ benches/multithreaded/http/webserver.jl | 82 +++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 benches/multithreaded/http/Project.toml create mode 100644 benches/multithreaded/http/webserver.jl diff --git a/benches/multithreaded/http/Project.toml b/benches/multithreaded/http/Project.toml new file mode 100644 index 0000000..c32f1d1 --- /dev/null +++ b/benches/multithreaded/http/Project.toml @@ -0,0 +1,5 @@ +[deps] +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" + +[compat] +HTTP = "1" diff --git a/benches/multithreaded/http/webserver.jl b/benches/multithreaded/http/webserver.jl new file mode 100644 index 0000000..2998a5a --- /dev/null +++ b/benches/multithreaded/http/webserver.jl @@ -0,0 +1,82 @@ +include(joinpath("..", "..", "..", "util", "utils.jl")) + +# A loopback HTTP server under load, as a GC benchmark. +# +# Server workloads are the motivating case for concurrent-GC work (Go's garbage +# benchmark and Green Tea were both driven by them): many tasks, a high rate of +# short-lived request/response garbage (strings, buffers, headers), and a +# long-lived response cache behind it acting as an old generation. None of the +# existing benchmarks has this profile - task-heavy, IO-bound, heterogeneous. +# +# The server and the clients run in the same process, 4 client tasks per thread, +# over 127.0.0.1. + +module WebServerBench + +using HTTP +using Sockets + +const NREQ = 8000 # total requests +const CACHE_SIZE = 512 # responses kept alive server-side (the old generation) + +# Build a JSON-ish payload of nested Dicts/Vectors, then render it. The returned +# pair is (retained object graph, response body). +function build_item(id::Int) + item = Dict{String,Any}( + "id" => id, + "name" => "item-$id", + "tags" => [string("tag", (id + k) % 37) for k in 1:8], + "attrs" => Dict{String,Any}(string("k", k) => id * k for k in 1:12), + "history" => [(seq = k, note = "rev $k of $id") for k in 1:6], + ) + io = IOBuffer() + show(io, item) + return item, String(take!(io)) +end + +function serve_and_hammer(nreq::Int = NREQ) + cache = Vector{Any}(undef, CACHE_SIZE) + fill!(cache, nothing) + lk = ReentrantLock() + + function handler(req::HTTP.Request) + id = parse(Int, last(split(req.target, '/'))) + item, body = build_item(id) + @lock lk begin + cache[id % CACHE_SIZE + 1] = item + end + return HTTP.Response(200, ["Content-Type" => "application/json"], body) + end + + port, sock = Sockets.listenany(ip"127.0.0.1", 8797) + server = HTTP.serve!(handler, "127.0.0.1", port; server = sock) + + nbytes = Threads.Atomic{Int}(0) + next = Threads.Atomic{Int}(0) + try + tasks = map(1:(4 * Threads.nthreads())) do _ + Threads.@spawn begin + got = 0 + while true + i = Threads.atomic_add!(next, 1) + i >= nreq && break + resp = HTTP.get("http://127.0.0.1:$port/item/$i"; + retry = false, status_exception = true) + got += length(resp.body) + end + Threads.atomic_add!(nbytes, got) + end + end + foreach(wait, tasks) + finally + close(server) + end + return nbytes[] +end + +end # module WebServerBench + +# compile server + client paths outside the measured region +WebServerBench.serve_and_hammer(32) + +@gctime WebServerBench.serve_and_hammer()