An async function called inside LifetimeRegion.run now keeps its region
after every await: its fiber records the region that was current at spawn,
and the event loop makes that region current while it resumes the fiber.
A region is not freed (or attempted) while a fiber that carries it exists,
so a region released while its async work is pending keeps taking that
work's objects and is freed once the work ends. Before, objects created
after an await went to the default region, which is never freed, and kept
the request's region allocated for good, so per-request regions in async
servers grew without bound.
Freed region chunks now go to a per-thread cache (at most 8 MiB resident,
the first 256 KiB of each kept cleared, fresh pages past it, no huge-page
chunks) instead of being unmapped, so a region per request or per batch no
longer maps and faults in new memory every time.
Adds the requests.ts region benchmark (concurrent async handlers), corpus
5023 and its region-stats checks, and a chunk-reuse runtime unit check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every release of a LifetimeRegion, and every retry at region creation, ran
scr_collect_cycles(): full cycle-collector passes over the thread's heap, to
a fixpoint, before checking any region, in case dead cycles of ordinary
objects held counted references into it. Freeing a region ran them again
whenever the owned ordinary objects left counts behind after the region
released its fields. Next to a large heap of candidates (a type checker
beside per-file emit regions) those passes cost far more than the regions.
A retry now checks the due regions first. A region whose references are all
explained frees without a pass; only when one would be kept does the heap
get the full pass, after which every due region is judged as before. The
first check leaves a region it cannot free as it was, so the backoff, the
kept report and SCRIPTC_REGION_DEBUG see only the check after the pass. It
keeps what its walk found: a region whose counted references the pass did not
change is judged from that, without walking it again. A pass that freed a dead
cycle referring only to an owned object leaves the count unchanged; such a
region waits for its next attempt.
After a region releases its instances' fields, what can still hold counts
is the owned ordinary objects' cycles. Each has a member that lost a
reference in those releases and is buffered as a candidate, and the release
walk entered every cycle member. The cycle collector gains a pass limited to
a set of candidates (scr_cyc_collect_set): it takes the set's members out of
the buffers up to a given generation, reading only the buffers' pointers, so
the candidates it leaves are not touched, and it walks at full depth without
moving the schedule's baselines. A dead object such a pass reaches through a
candidate it did not take leaves its buffer before it is freed. Freeing a
region runs these passes over the walk's objects from the youngest buffer
out, and the whole heap only if counts remain.
SCRIPTC_REGION_STATS also prints, per thread at exit, how many full passes
over the heap regions needed.