[turbopack] Don't evict when there is little memory to save (#95213)
### What?
Adds a new `'auto'` mode to the experimental `turbopackMemoryEviction`
config option and makes it the default. In `'auto'` mode, Turbopack only
evicts in-memory cache after a snapshot once enough memory has been
allocated since the last eviction to make the work worthwhile.
The option now accepts three values:
- `false`: never evict.
- `'auto'` (new default): evict after a snapshot only once a memory
threshold has been crossed.
- `'full'`: evict all evictable data after every snapshot (the previous
behavior).
### Why?
When the persistent (FileSystem) cache is enabled, Turbopack snapshots
its in-memory state to disk and can then evict those in-memory copies to
reclaim memory, reloading them from disk on demand.
Previously, with eviction enabled (`'full'`), we evicted after *every*
snapshot. This is too aggressive: tasks get restored from disk and then
immediately evicted again, cycle after cycle, wasting work for little
memory benefit.
`'auto'` mirrors the existing persistence-threshold model (we already
skip a snapshot when too little compilation time has accumulated to
justify its cost). Here the proxy is memory instead of time: it isn't
worth paying the restore-then-re-evict churn to reclaim a small amount
of memory.
This was motivated by an example in v0.app where the client would poll
the server every 5 seconds leading to a pathological behavior
* client poll ->
* next.js ensurePage ->
* turbopack writeEndpointToDisk
* recompute and restore settings for that endpoint (3-10ms of io work)
* realize there is nothing to do
* respond to client
* 2 seconds later..... persist and evict everything saving 0.5M of ram
(100ms of work! writes out 2 SSTs and then compacts them!)
* 3 seconds after that restart the loop
This is silly, #95137 will prevent the persistence loop from occurring ,
this PR will also just skip the 'restore and recompute' work using a
similar strategy.
### How?
We can't measure exactly how much memory a sweep would reclaim, so we
use `TurboMalloc::memory_usage()` (process-global net live bytes) as a
proxy. In `'auto'` mode an eviction sweep runs only once the net bytes
allocated since the last eviction exceed a threshold (default 128 MiB,
overridable via `TURBO_ENGINE_EVICT_MIN_BYTES`). The first eviction
after startup always runs. The threshold scales down under OS memory
pressure (`TurboMalloc::memory_pressure()`) so we evict more eagerly
when memory is tight.
**Backend (`turbo-tasks-backend`):**
- `BackendOptions.evict_after_snapshot: bool` → `eviction_mode:
EvictionMode` (`Off` / `Full` / `Auto`).
- New `EvictionControl` type owns the policy: the mode plus the
threshold bookkeeping. The background snapshot loop calls
`should_evict(snapshot_had_new_data)` once per cycle and
`record_eviction()` after a sweep, so the loop no longer branches on the
mode. `'full'` and `false` behave exactly as before.
<!-- NEXT_JS_LLM_PR -->