Collection was purely allocation-driven: a GC only ran once
allocation since the last collection passed a threshold of 7/4 of
the live set (floored at 8 MiB). A page that allocated garbage but
never reached that threshold held onto it indefinitely once it went
idle, so we never handed memory back to the system promptly.
Run a 4-second repeating timer while the mutator is allocating; on
each tick IdleCollectionPolicy picks one of three actions:
- Park the timer when nothing has been allocated since the last
collection. The next allocation re-arms it, so a fully idle heap
costs nothing.
- Collect when this tick's allocation rate fell below 1/4 of the
peak rate seen this episode (the mutator left an active phase),
provided at least threshold/16 of garbage has piled up, so we
don't mark the whole live heap to reclaim a trivial amount.
- Otherwise let a watchdog collect after 15 ticks (60 seconds), so
garbage cannot sit indefinitely on a heap that allocates too
steadily to show a rate drop, or too slowly to clear the gate.
The GC heap is never completely silent in practice, since event-loop
housekeeping keeps queuing small objects like HTML tasks; that is
why the trigger watches for a relative rate drop rather than for
zero allocation.
The per-tick decision lives in IdleCollectionPolicy, separate from
the timer plumbing, with a unit test covering the rate-drop trigger,
the minimum-garbage gate, the watchdog, and parking when idle.