pcache: add pool-backed PageCache implementation + #204 memory benchmark
Motivation
Follow-up to !126 (merged). Issue #204 (closed) asks for a pluggable cross-connection page cache; !126 (merged) landed the API surface, and this MR delivers the production pool-backed implementation that exercises it end-to-end and produces the memory-utilization numbers the issue is ultimately about.
The implementation lives in a new `pcache/` subpackage so the parent package keeps its vec-init constraint (the e2e harness opens a real database through the wrapper, which is impossible in-package because `vec_test.go`'s `init()` fires `sqlite3_initialize` before `sqlite3_config(PCACHE2)` runs).
What this MR delivers
-
`pcache/pool.go` (402 lines): `Pool` factory minting per-database `Cache` instances backed by `libc.Xmalloc` page buffers and `libc.Xcalloc` extra buffers. Each `Cache` honours `PRAGMA cache_size` strictly through LRU-tail eviction, with `FetchCreateForce` overcommitting only when every page is pinned (pcache1 parity). `Page` identity is `*page` so the binding's identity comparison reuses or replaces the stub exactly as the !126 (merged) design intends. The `Pool` aggregates hit/miss/alloc/eviction/rekey/truncate/caches counters via atomics so a process can observe behaviour through `Pool.Stats()` without instrumenting individual caches.
-
`pcache/pool_test.go` (260 lines): 16 unit tests directly against `Pool`, without `database/sql` round-trip. Covers empty state, retain across Fetch cycles, `FetchCreateEasy` refusing at cap, `FetchCreateForce` evicting LRU tail, `SetSize` shrink + overcommit-on-pinned, `Rekey` with colliding eviction, `Truncate` pinned eviction (the only callback permitted to evict pinned per spec), `Shrink`, `Destroy` no-panic on subsequent calls.
-
`pcache/e2e_test.go` (215 lines): real-DB harness mirroring the validation workload you ran for !126 (merged): `cache_size=16`, 4000 BLOB rows + DELETE half + `PRAGMA incremental_vacuum`, `integrity_check=ok` under `-race`. Local stats over the workload: 1188 allocs / 1173 evictions / 1 Cache, integrity OK. Multi-DB test opens 4 separate `sql.DB` and asserts 4 fresh `Cache.Create` calls.
-
`pcache/bench_test.go` (140 lines): `BenchmarkPoolBoundedCache` reports per-insert `page-allocs/op` + `page-evictions/op` + `go-heap-inuse-delta-bytes` for the #204 (closed) memory-utilization measurement. `BenchmarkPoolEvictionChurn` drives a DELETE/vacuum/INSERT cycle and reports steady-state churn (local: 133 allocs/op : 133 evictions/op at `cache_size=16`, demonstrating strict bounding).
What this MR explicitly does NOT deliver
- Cross-connection sharing (one Pool serving multiple databases from a shared page set). The `Pool` deliberately returns a fresh `Cache` per database; the shared-cache hook lives at the binding level and was noted in the !126 (merged) thread as a future revisit. Deferred to MR-C.
- Lifecycle / error-recovery hardening for the production impl (OOM-during-vacuum, mid-truncate panic recovery). Deferred to MR-D.
- DSN flag to make pool the default page cache. Currently opt-in: users call `sqlite.MustRegisterPageCache(pcache.New())` in their `init()`. Deferred to MR-E.
Verification
- `go test -race -short -count=1 ./pcache/` clean on darwin/arm64 (16 unit + 2 e2e tests, 2.2s).
- `go test -race -short -count=1 ./` parent package clean (no regressions).
- Cross-build clean for linux/amd64, linux/arm64, linux/386, darwin/amd64, darwin/arm64, windows/amd64, freebsd/amd64, openbsd/amd64.
- `gofmt -l pcache/` clean.
- `go vet ./pcache/` flags only the two `unsafe.Pointer(uintptr)` conversions on `page.Buf()`/`page.Extra()`, identical pattern to and tolerated at `pagecache.go:347` and `pagecache_trampolines.go:272` in the parent package.
Page identity comparability is satisfied by `*page` (pointer-backed natural shape per the !126 (merged) Page docstring).
Side-by-side memory comparison vs default pcache1 requires running the benchmark in a sibling tree without `pcache.New()` registered; can produce those numbers in a follow-up comment if useful.
Addresses #204 (closed); final closure rides on which release you tag this with.