glibc hands each new thread its own 64 MiB arena (up to 8x cores). With one
thread per connection that arena is the dominant per-connection memory term,
and freed chunks stay reserved in it rather than returning to the OS: resident
memory tracks the *peak* number of concurrent connections and never comes back
down, which reads as a leak although nothing is leaked and no thread is stuck.
Measured with reliability/conn-scale.py in obscura-benchmark. 100 connections
each running JS reserve 90.0 GiB of address space uncapped versus 83.5 GiB
capped; per idle connection, 65 MiB of reserved address space drops to 2.0 MiB.
`main` at that same 100-connection JS workload reserves 83.6 GiB, so with the
cap this server is level with it -- most of the total is V8's process-wide
sandbox, which `main` pays too the moment it runs any JS.
The resident-set effect is the one that bites in production. Measured in the
container image against Google Maps, four concurrent connections per round:
350 / 619 / 826 MiB over three rounds uncapped and still climbing linearly,
versus 166 / 235 / 269 MiB capped, on a decelerating curve.
Two arenas cost no measurable throughput here -- 8 concurrent connections x 12
navigations, 1.53s uncapped against 1.50s capped over three runs. V8 allocates
the JS heap through its own allocator and the Rust side is dominated by network
I/O rather than malloc traffic. Applied only from `serve`, which owns the
process, and skipped when the caller has already set MALLOC_ARENA_MAX.
Refs #430.