turbo-persistence: prefetch multi-page SST blocks with MADV_WILLNEED (#100283)
### What?
turbo-persistence SST lookup mappings keep `MADV_RANDOM`, and also
prefetch whole multi-page blocks:
- **Block hint:** before a block's first full read in an SST open (CRC
check and decompression), the block's page-aligned range gets
`MADV_WILLNEED`. The hint is issued before any byte of the block is
read, header included, and only when the block spans more than one page
(using the system page size).
- Medium value blocks bypass the block cache, so they get the hint on
every read.
- Already-verified blocks and single-page blocks get no syscall.
- **Block-offset table:** gets `MADV_WILLNEED` at open, instead of
`MADV_SEQUENTIAL`.
- **Unknown page size:** if `sysconf(_SC_PAGESIZE)` fails, block hints
can't be computed, so `MADV_RANDOM` isn't applied either and the mapping
keeps default advice (read-around still prefetches). The offset-table
hint is applied in both cases.
- **Unchanged:** compaction iteration (`MADV_SEQUENTIAL`), meta/blob
files, File access mode, non-Unix platforms and the on-disk format.
### Why?
See #100250.
- With `MADV_RANDOM` alone, a cold multi-page block is faulted in one
page at a time.
- Dropping the advice (the first version of this PR) lets kernel
read-around fix that, but it also reads neighboring blocks that are
often never used.
The two variants were compared on a real workload:
- **Setup:** warm `next build`s of
`bench/nested-deps-app-router-many-pages`, with a new
`NEXT_DEPLOYMENT_ID` each build. The cache snapshot contained fresh,
compacted, hot_bottom and cold_bottom SSTs.
- **Runs:** cold guest page cache, Linux, 4 KiB pages, 5 rotated trials
per variant.
| Variant | Median build | Major faults | Cache resident after |
| --- | --- | --- | --- |
| canary (`Random`) | 40.0 s | ~101k | 527 MiB |
| default advice | 39.6 s | ~14.2k | 545 MiB |
| `Random` + `WillNeed` everywhere (this PR) | 38.2 s | ~13.4k | 527 MiB
|
| `WillNeed` for cold_bottom only, default for the rest | 38.8 s |
~13.8k | 533 MiB |
- Build times are within run-to-run noise. Cache reads are a small part
of this build, so this is not a claimed wall-time win.
- The targeted hints match default advice's fault reduction, with less
data pulled into the page cache.
- Reading unneeded pages costs more on slow or compressed backing
storage such as squashfs. That is another reason to avoid read-around,
though it wasn't benchmarked here.
- Not measured: memory pressure, macOS and 16 KiB pages. With 16 KiB
pages more blocks fit in one page and get no hint.
### How?
- `get_raw_block_slice` takes a `will_need` flag and issues the hint
after locating the block, before reading its header.
- Lookups pass `true` when the block's verified-CRC bit is unset; the
index block and medium values always pass `true`; iteration passes
`false`.
- Adds a `libc` dependency on Unix for `sysconf(_SC_PAGESIZE)`, queried
once and cached. If it can't be determined, both the block hints and
`MADV_RANDOM` are skipped.
- Tests:
- Linux `/proc/self/smaps` check that lookup mappings carry `rr` and not
`sr`;
- a test-only hint log verifying the offset-table hint, multi-page-only
first-read hints, no hints on re-reads, and per-read medium-value hints;
- pure page-boundary tests for 4 KiB and 16 KiB.
- README documents the advice policy.
<!-- NEXT_JS_LLM -->
<!-- fleet 5f39f2b2-0882-4566-ae42-cf64574c5747 -->
---------
Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Co-authored-by: Luke Sandberg <210140+lukesandberg@users.noreply.github.com>