sqlite: add DBStatus wrapper for sqlite3_db_status
Follow-up to the !130 (merged) review, where we agreed to expose sqlite3_db_status as a public API so the pcache pool's I/O can be measured against pcache1 with the real pager counters instead of the EasyRefusals proxy.
API (the part to review)
New dbstatus.go, mirroring the FileControl surface you pointed at:
type DBStatusOp int32
type DBStatus interface {
Status(op DBStatusOp, reset bool) (current, high int, err error)
}
var _ DBStatus = (*conn)(nil)DBStatusOpis a distinct typed enum of theSQLITE_DBSTATUS_*verbs, the same wayFetchModeis typed, so a constant from another op family will not compile in its place.- All 14 ops the transpiled
lib/defines are exposed (DBStatusLookasideUsed,DBStatusCacheUsed,DBStatusSchemaUsed,DBStatusStmtUsed,DBStatusLookasideHit,DBStatusLookasideMissSize,DBStatusLookasideMissFull,DBStatusCacheHit,DBStatusCacheMiss,DBStatusCacheWrite,DBStatusDeferredFKs,DBStatusCacheUsedShared,DBStatusCacheSpill,DBStatusTempbufSpill). Statusis implemented on the unexported*connand reached through(*sql.Conn).Raw(), exactly likeFileControl. It uses thetls.Alloc(8)two-int32pattern from your skeleton and surfaces an out-of-range op as an error viac.errstr(rc).- The doc comments call out the three counter families that report their value differently: memory high-water ops (
*Used), running-counter ops (CacheHit/Miss/Write/Spill,high == 0, reset zeroes current), and the lookaside event ops (LookasideHit/MissSize/MissFull) whose count is reported inhigh, notcurrent.
Usage:
err := sqlConn.Raw(func(dc any) error {
cur, _, err := dc.(sqlite.DBStatus).Status(sqlite.DBStatusCacheSpill, false)
...
})Tests
dbstatus_test.go drives all three families through Raw(): SchemaUsed grows after DDL; CacheHit resets; LookasideHit reports its value in high not current; an out-of-range op errors.
Benchmark (the application that motivated the API)
pcache/bench_test.go gains BenchmarkPoolSpillIO, which reads the pager-level CACHE_SPILL / CACHE_WRITE / CACHE_HIT / CACHE_MISS counters through the new API instead of the in-package EasyRefusals proxy. These counters are maintained identically by the pager for pcache1 and the pool, so the comparison is apples-to-apples, which is the measurement you asked for on the !127 (merged) review. As with the existing pcache benchmarks the pcache1 baseline comes from running the same workload in a tree that does not import pcache (registration is process-global and one-shot).
Numbers on the rotating-residue eviction-churn workload at cache_size=16, darwin/arm64, flat across 25x/50x/100x:
| metric | pcache1 | pool |
|---|---|---|
| cache-spill/op | 8.96 | 31.96 |
| cache-write/op | 436 | 450 |
| cache-hit/op | 1021 | 1021 |
| cache-miss/op | 61.9 | 62.3 |
The pool spills ~3.5x more than pcache1 for ~3% more actual page writes at identical hit/miss. That quantifies the I/O cost of the strict Easy contract (refusing to recycle at cap) that EasyRefusals only proxied: most extra spill decisions are absorbed, but the spill-decision rate itself is materially higher. Honest trade-off for the off-Go-heap page memory the pool buys.
Verification (darwin/arm64)
gofmt,go vet(only the pre-existing Buf/Extra notices),staticcheckall clean on the new filesgo test -run TestDBStatusandgo test -race -short ./pcache/pass- cross-build clean across linux/{amd64,386,arm,arm64} + darwin/{amd64,arm64} + windows/amd64 + freebsd/amd64
CHANGELOG added to the pending v1.53.0 section.
The BenchmarkPoolSpillIO numbers are also a candidate to fold back into the pcache benchmark comment now that the real counters supersede the EasyRefusals proxy framing, but I left the proxy in place for now since it is cheap and in-package; happy to do that as a follow-up if you prefer.