Repository: cockroachdb/pebble

Issue:
audit and test corruption reporting for blob files
From a comment in #5321:

I don't know if ReportCorruptionFn and ReportCorruptionArg are being properly setup for blob files.

https://github.com/cockroachdb/pebble/blob/998a55aac5bb6da774c2a00b72a1551ac73c5340/blob_rewrite.go#L315
    
This is setting ReportCorruptionFn but not the arg. And the ReadEnv passed to blob.ValueFetcher.Init doesn't set either -- possibly because the ValueFetcher is shared. Is there a gap in our corruption reporting?

CC @jbowens @annrpom 

Jira issue: PEBBLE-1189

Hints from issue comments:
@RaduBerinde: It looks like `d.reportCorruption` would panic with a nil arg:
```
func (d *DB) reportCorruption(meta base.ObjectInfo, err error) error {
	if invariants.Enabled && !IsCorruptionError(err) {
		panic("not a corruption error")
	}
	fileType, fileNum := meta.FileInfo()
```@xinhaoz: Was https://github.com/cockroachdb/pebble/issues/5127 sufficient here? Looks like setting this fn on https://github.com/cockroachdb/pebble/blob/998a55aac5bb6da774c2a00b72a1551ac73c5340/blob_rewrite.go#L315 is just redundant.@RaduBerinde: Yeah, I agree.

Solution patch:
diff --git a/blob_rewrite.go b/blob_rewrite.go
index 9eceb253..a60b859d 100644
--- a/blob_rewrite.go
+++ b/blob_rewrite.go
@@ -307,7 +307,7 @@ func (d *DB) runBlobFileRewriteLocked(
 	// the compaction finishes new iterators will read the new blob file, so it
 	// would be unlikely the cached blocks would be reused.
 	var bufferPool block.BufferPool
-	bufferPool.Init(4)
+	bufferPool.Init(4, block.ForBlobFileRewrite)
 	defer bufferPool.Release()
 	env := block.ReadEnv{
 		Stats:              &c.internalIteratorStats,
diff --git a/compaction.go b/compaction.go
index ce93d4d8..02fcf3cc 100644
--- a/compaction.go
+++ b/compaction.go
@@ -2953,7 +2953,7 @@ func (d *DB) runCopyCompaction(
 			// TODO(radu): plumb a ReadEnv to CopySpan (it could use the buffer pool
 			// or update category stats).
 			wrote, err = sstable.CopySpan(ctx,
-				src, r, d.opts.MakeReaderOptions(),
+				src, r, c.startLevel.level,
 				w, d.opts.MakeWriterOptions(c.outputLevel.level, d.TableFormat()),
 				start, end,
 			)
@@ -3379,7 +3379,7 @@ func (d *DB) compactAndWrite(
 	// a 18-buffer pool is expected to be within reason, even if all the buffers
 	// grow to the typical size of an index block (256 KiB) which would
 	// translate to 4.5 MiB per compaction.
-	c.iterationState.bufferPool.Init(18 + suggestedCacheReaders*2)
+	c.iterationState.bufferPool.Init(18+suggestedCacheReaders*2, block.ForCompaction)
 	defer c.iterationState.bufferPool.Release()
 	blockReadEnv := block.ReadEnv{
 		BufferPool: &c.iterationState.bufferPool,
diff --git a/external_iterator.go b/external_iterator.go
index b18b2960..125f7666 100644
--- a/external_iterator.go
+++ b/external_iterator.go
@@ -92,7 +92,7 @@ func NewExternalIterWithContext(
 		},
 		seqNum: base.SeqNumMax,
 	}
-	dbi.externalIter.bufferPool.Init(2)
+	dbi.externalIter.bufferPool.Init(2, block.ForExternalIter)
 
 	if iterOpts != nil {
 		dbi.opts = *iterOpts
diff --git a/file_cache.go b/file_cache.go
index 20b41dd5..4e698e2c 100644
--- a/file_cache.go
+++ b/file_cache.go
@@ -590,6 +590,9 @@ func (h *fileCacheHandle) newIters(
 	internalOpts.readEnv.Virtual = env.Virtual
 	internalOpts.readEnv.IsSharedIngested = env.IsSharedIngested
 	internalOpts.readEnv.InternalBounds = env.InternalBounds
+	if opts != nil && opts.layer.IsSet() && !opts.layer.IsFlushableIngests() {
+		internalOpts.readEnv.Block.Level = cache.MakeLevel(opts.layer.Level())
+	}
 
 	var iters iterSet
 	if kinds.RangeKey() && file.HasRangeKeys {
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index ce24774f..dd3ff098 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -17,20 +17,9 @@ import (
 
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/invariants"
+	"github.com/cockroachdb/pebble/internal/metricsutil"
 )
 
-// Metrics holds metrics for the cache.
-type Metrics struct {
-	// The number of bytes inuse by the cache.
-	Size int64
-	// The count of objects (blocks or tables) in the cache.
-	Count int64
-	// The number of cache hits.
-	Hits int64
-	// The number of cache misses.
-	Misses int64
-}
-
 // Cache implements Pebble's sharded block cache. The Clock-PRO algorithm is
 // used for page replacement
 // (http://static.usenix.org/event/usenix05/tech/general/full_papers/jiang/jiang_html/html.html). In
@@ -78,6 +67,8 @@ type Cache struct {
 	idAlloc atomic.Uint64
 	shards  []shard
 
+	metricsWindow *metricsutil.Window[HitsAndMisses]
+
 	// Traces recorded by Cache.trace. Used for debugging.
 	tr struct {
 		sync.Mutex
@@ -136,6 +127,8 @@ func NewWithShards(size int64, shards int) *Cache {
 	for i := range c.shards {
 		c.shards[i].init(size / int64(len(c.shards)))
 	}
+	c.metricsWindow = metricsutil.NewWindow[HitsAndMisses](c.hitsAndMisses)
+	c.metricsWindow.Start()
 
 	// Note: this is a no-op if invariants are disabled or race is enabled.
 	invariants.SetFinalizer(c, func(c *Cache) {
@@ -171,10 +164,19 @@ func (c *Cache) Unref() {
 	case v < 0:
 		panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v))
 	case v == 0:
-		for i := range c.shards {
-			c.shards[i].Free()
-		}
+		c.destroy()
+	}
+}
+
+func (c *Cache) destroy() {
+	if c.metricsWindow != nil {
+		c.metricsWindow.Stop()
+		c.metricsWindow = nil
 	}
+	for i := range c.shards {
+		c.shards[i].Free()
+	}
+	c.shards = nil
 }
 
 func (c *Cache) NewHandle() *Handle {
@@ -207,21 +209,6 @@ func (c *Cache) Reserve(n int) func() {
 	}
 }
 
-// Metrics returns the metrics for the cache.
-func (c *Cache) Metrics() Metrics {
-	var m Metrics
-	for i := range c.shards {
-		s := &c.shards[i]
-		s.mu.RLock()
-		m.Count += int64(s.blocks.Len())
-		m.Size += s.sizeHot + s.sizeCold
-		s.mu.RUnlock()
-		m.Hits += s.hits.Load()
-		m.Misses += s.misses.Load()
-	}
-	return m
-}
-
 // MaxSize returns the max size of the cache.
 func (c *Cache) MaxSize() int64 {
 	return c.maxSize
@@ -262,16 +249,26 @@ func (c *Handle) Cache() *Cache {
 // Peek retrieves the cache value for the specified file and offset, returning
 // nil if no value is present. Peek does not affect the state of the cache (it
 // does not "count" as an access as far as the cache replacement is concerned).
-func (c *Handle) Peek(fileNum base.DiskFileNum, offset uint64) *Value {
+//
+// Peek supports the special CategoryHidden category, in which case the hit or
+// miss is not recorded in metrics.
+func (c *Handle) Peek(
+	fileNum base.DiskFileNum, offset uint64, level Level, category Category,
+) *Value {
 	k := makeKey(c.id, fileNum, offset)
-	return c.cache.getShard(k).get(k, true /* peekOnly */)
+	return c.cache.getShard(k).get(k, level, category, true /* peekOnly */)
 }
 
+// CategoryHidden can be used with Peek to avoid recording a cache hit or miss.
+const CategoryHidden Category = -1
+
 // Get retrieves the cache value for the specified file and offset, returning
 // nil if no value is present.
-func (c *Handle) Get(fileNum base.DiskFileNum, offset uint64) *Value {
+func (c *Handle) Get(
+	fileNum base.DiskFileNum, offset uint64, level Level, category Category,
+) *Value {
 	k := makeKey(c.id, fileNum, offset)
-	return c.cache.getShard(k).get(k, false /* peekOnly */)
+	return c.cache.getShard(k).get(k, level, category, false /* peekOnly */)
 }
 
 // GetWithReadHandle retrieves the cache value for the specified handleID, fileNum
@@ -298,7 +295,7 @@ func (c *Handle) Get(fileNum base.DiskFileNum, offset uint64) *Value {
 // While waiting, someone else may successfully read the value, which results
 // in a valid Handle being returned. This is a case where cacheHit=false.
 func (c *Handle) GetWithReadHandle(
-	ctx context.Context, fileNum base.DiskFileNum, offset uint64,
+	ctx context.Context, fileNum base.DiskFileNum, offset uint64, level Level, category Category,
 ) (
 	cv *Value,
 	rh ReadHandle,
@@ -308,7 +305,7 @@ func (c *Handle) GetWithReadHandle(
 	err error,
 ) {
 	k := makeKey(c.id, fileNum, offset)
-	cv, re := c.cache.getShard(k).getWithReadEntry(k)
+	cv, re := c.cache.getShard(k).getWithReadEntry(k, level, category)
 	if cv != nil {
 		return cv, ReadHandle{}, 0, 0, true, nil
 	}
diff --git a/internal/cache/clockpro.go b/internal/cache/clockpro.go
index e5cbae6d..c707c36d 100644
--- a/internal/cache/clockpro.go
+++ b/internal/cache/clockpro.go
@@ -76,9 +76,13 @@ func (k key) String() string {
 	return fmt.Sprintf("%d/%d/%d", k.id, k.fileNum, k.offset)
 }
 
-type shard struct {
+type counters [NumLevels][NumCategories]struct {
 	hits   atomic.Int64
 	misses atomic.Int64
+}
+
+type shard struct {
+	counters counters
 
 	mu sync.RWMutex
 
@@ -134,7 +138,7 @@ func (c *shard) init(maxSize int64) {
 //
 // If peekOnly is true, the state of the cache is not modified to reflect the
 // access.
-func (c *shard) get(k key, peekOnly bool) *Value {
+func (c *shard) get(k key, level Level, category Category, peekOnly bool) *Value {
 	c.mu.RLock()
 	if e, _ := c.blocks.Get(k); e != nil {
 		if value := e.acquireValue(); value != nil {
@@ -143,12 +147,16 @@ func (c *shard) get(k key, peekOnly bool) *Value {
 				e.referenced.Store(true)
 			}
 			c.mu.RUnlock()
-			c.hits.Add(1)
+			if category != CategoryHidden {
+				c.counters[level.index()][category].hits.Add(1)
+			}
 			return value
 		}
 	}
 	c.mu.RUnlock()
-	c.misses.Add(1)
+	if category != CategoryHidden {
+		c.counters[level.index()][category].misses.Add(1)
+	}
 	return nil
 }
 
@@ -157,7 +165,7 @@ func (c *shard) get(k key, peekOnly bool) *Value {
 // is not in the cache (nil Value), a non-nil readEntry is returned (in which
 // case the caller is responsible to dereference the entry, via one of
 // unrefAndTryRemoveFromMap(), setReadValue(), setReadError()).
-func (c *shard) getWithReadEntry(k key) (*Value, *readEntry) {
+func (c *shard) getWithReadEntry(k key, level Level, category Category) (*Value, *readEntry) {
 	c.mu.RLock()
 	if e, _ := c.blocks.Get(k); e != nil {
 		if value := e.acquireValue(); value != nil {
@@ -166,13 +174,13 @@ func (c *shard) getWithReadEntry(k key) (*Value, *readEntry) {
 				e.referenced.Store(true)
 			}
 			c.mu.RUnlock()
-			c.hits.Add(1)
+			c.counters[level.index()][category].hits.Add(1)
 			return value, nil
 		}
 	}
 	re := c.readShard.acquireReadEntry(k)
 	c.mu.RUnlock()
-	c.misses.Add(1)
+	c.counters[level.index()][category].misses.Add(1)
 	return nil, re
 }
 
diff --git a/internal/cache/metrics.go b/internal/cache/metrics.go
new file mode 100644
index 00000000..d1a5a46e
--- /dev/null
+++ b/internal/cache/metrics.go
@@ -0,0 +1,207 @@
+// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package cache
+
+import (
+	"fmt"
+	"iter"
+
+	"github.com/cockroachdb/crlib/crtime"
+	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/pebble/internal/invariants"
+)
+
+// Level is the LSM level associated with an accessed block. Used to maintain
+// granular cache hit/miss statistics.
+//
+// The zero value indicates that there is no level (e.g. flushable ingests) or
+// it is unknown.
+type Level struct {
+	levelPlusOne int8
+}
+
+func (l Level) String() string {
+	if l.levelPlusOne <= 0 {
+		return "n/a"
+	}
+	return fmt.Sprintf("L%d", l.levelPlusOne-1)
+}
+
+// index returns a value between [0, NumLevels).
+func (l Level) index() int8 {
+	return l.levelPlusOne
+}
+
+func MakeLevel(l int) Level {
+	if invariants.Enabled && (l < 0 || l >= NumLevels-1) {
+		panic(errors.AssertionFailedf("invalid level: %d", l))
+	}
+	return Level{levelPlusOne: int8(l + 1)}
+}
+
+const NumLevels = 1 /* unknown level */ + 7
+
+// Levels is an iter.Seq[Level].
+func Levels(yield func(l Level) bool) {
+	for i := range NumLevels {
+		if !yield(Level{levelPlusOne: int8(i)}) {
+			return
+		}
+	}
+}
+
+var _ iter.Seq[Level] = Levels
+
+// Category is used to maintain granular cache hit/miss statistics.
+type Category int8
+
+const (
+	// CategoryBackground is used for cache accesses made by compactions or
+	// downloads.
+	CategoryBackground Category = iota
+	CategorySSTableData
+	CategorySSTableValue
+	CategoryBlobValue
+	CategoryFilter
+	// CategoryIndex includes index blocks and other metadata blocks (for both
+	// sstables and blob files).
+	CategoryIndex
+
+	// Categories can be used with the range keyword.
+	Categories
+)
+
+const NumCategories = int(Categories)
+
+func (c Category) String() string {
+	switch c {
+	case CategoryBackground:
+		return "background"
+	case CategorySSTableData:
+		return "sstdata"
+	case CategorySSTableValue:
+		return "sstval"
+	case CategoryBlobValue:
+		return "blobval"
+	case CategoryFilter:
+		return "filter"
+	case CategoryIndex:
+		return "index"
+	default:
+		return fmt.Sprintf("invalid(%d)", c)
+	}
+}
+
+// Metrics holds metrics for the cache.
+type Metrics struct {
+	// Hits and misse since the cache was created.
+	HitsAndMisses HitsAndMisses
+	// The current number of bytes inuse by the cache.
+	Size int64
+	// The current count of objects (blocks or tables) in the cache.
+	Count int64
+	// Recent contains cache hit metrics covering two recent periods (last ~10
+	// minutes and last ~1 hour).
+	Recent [2]struct {
+		HitsAndMisses
+		Since crtime.Mono
+	}
+}
+
+// HitsAndMisses contains the number of cache hits and misses across a period of
+// time.
+type HitsAndMisses [NumLevels][NumCategories]struct {
+	Hits   int64
+	Misses int64
+}
+
+func (hm *HitsAndMisses) Get(level Level, category Category) (hits, misses int64) {
+	v := hm[level.index()][category]
+	return v.Hits, v.Misses
+}
+
+func (hm *HitsAndMisses) Hits(level Level, category Category) int64 {
+	return hm[level.index()][category].Hits
+}
+
+func (hm *HitsAndMisses) Misses(level Level, category Category) int64 {
+	return hm[level.index()][category].Misses
+}
+
+// Aggregate returns the total hits and misses across all categories and levels.
+func (hm *HitsAndMisses) Aggregate() (hits, misses int64) {
+	for i := range hm {
+		for j := range hm[i] {
+			hits += hm[i][j].Hits
+			misses += hm[i][j].Misses
+		}
+	}
+	return hits, misses
+}
+
+// AggregateLevel returns the total hits and misses for a specific level (across
+// all categories).
+func (hm *HitsAndMisses) AggregateLevel(level Level) (hits, misses int64) {
+	for _, v := range hm[level.index()] {
+		hits += v.Hits
+		misses += v.Misses
+	}
+	return hits, misses
+}
+
+// AggregateCategory returns the total hits and misses for a specific category
+// (across all levels).
+func (hm *HitsAndMisses) AggregateCategory(category Category) (hits, misses int64) {
+	for i := range hm {
+		hits += hm[i][category].Hits
+		misses += hm[i][category].Misses
+	}
+	return hits, misses
+}
+
+// ToRecent changes the receiver to reflect recent hits and misses, given the
+// current metrics.
+// At a high level, hm.ToRecent(current) means hm = current - hm.
+func (hm *HitsAndMisses) ToRecent(current *HitsAndMisses) {
+	for i := range hm {
+		for j := range hm[i] {
+			hm[i][j].Hits = current[i][j].Hits - hm[i][j].Hits
+			hm[i][j].Misses = current[i][j].Misses - hm[i][j].Misses
+		}
+	}
+}
+
+// Metrics returns the current metrics for the cache.
+func (c *Cache) Metrics() Metrics {
+	var m Metrics
+	m.HitsAndMisses = c.hitsAndMisses()
+	for i := range c.shards {
+		s := &c.shards[i]
+		s.mu.RLock()
+		m.Count += int64(s.blocks.Len())
+		m.Size += s.sizeHot + s.sizeCold
+		s.mu.RUnlock()
+	}
+	m.Recent[0].HitsAndMisses, m.Recent[0].Since = c.metricsWindow.TenMinutesAgo()
+	m.Recent[1].HitsAndMisses, m.Recent[1].Since = c.metricsWindow.OneHourAgo()
+	for i := range m.Recent {
+		m.Recent[i].ToRecent(&m.HitsAndMisses)
+	}
+	return m
+}
+
+func (c *Cache) hitsAndMisses() HitsAndMisses {
+	var hm HitsAndMisses
+	for i := range c.shards {
+		shardCounters := &c.shards[i].counters
+		for j := range hm {
+			for k := range hm[j] {
+				hm[j][k].Hits += shardCounters[j][k].hits.Load()
+				hm[j][k].Misses += shardCounters[j][k].misses.Load()
+			}
+		}
+	}
+	return hm
+}
diff --git a/internal/metricsutil/window.go b/internal/metricsutil/window.go
new file mode 100644
index 00000000..e26a4497
--- /dev/null
+++ b/internal/metricsutil/window.go
@@ -0,0 +1,164 @@
+// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package metricsutil
+
+import (
+	"sync"
+	"time"
+
+	"github.com/cockroachdb/crlib/crtime"
+)
+
+// NewWindow creates a new Window instance, which maintains a sliding window of
+// metrics collected at regular intervals. It collects metrics every minute.
+//
+// Sample usage:
+//
+//	w := NewWindow[MyMetricsType](func() MyMetricsType {
+//	  return retrieveCurrentMetrics()
+//	})
+//	w.Start()
+//	defer w.Stop()
+//	..
+//	currentMetrics := retrieveCurrentMetrics()
+//	prevMetrics, prevTime := w.TenMinutesAgo()
+//	fmt.Printf("statistics over last %s: %s\n", prevTime.Elapsed(), currentMetrics.Subtract(prevMetrics))
+func NewWindow[M any](collectFn CollectFn[M]) *Window[M] {
+	return &Window[M]{collectFn: collectFn}
+}
+
+// CollectFn is a function used to collect the current metrics.
+type CollectFn[M any] func() M
+
+// Window maintains a sliding window of metrics collected at regular intervals,
+// allowing the retrieval of metrics from approximately 10 minutes and 1 hour
+// ago. These metrics can be used against the current metrics to get statistics
+// for these recent timeframes.
+type Window[M any] struct {
+	collectFn CollectFn[M]
+	mu        struct {
+		sync.Mutex
+		running   bool
+		startTime crtime.Mono
+		nextA     crtime.Mono
+		ringA     ring[M]
+		nextB     crtime.Mono
+		ringB     ring[M]
+		timer     *time.Timer
+	}
+}
+
+// TenMinutesAgo returns the metrics from ~10 minutes ago (normally ±30s) and
+// the time when they were collected.
+//
+// If no metrics are available (less than 10m passed), the zero values are
+// returned.
+func (w *Window[M]) TenMinutesAgo() (_ M, collectedAt crtime.Mono) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	return w.mu.ringA.Oldest()
+}
+
+// OneHourAgo returns the metrics from ~1h ago (normally ±3m) and the time when
+// they were collected.
+//
+// If no metrics are available (i.e. less than 1h passed), the zero values are
+// returned.
+func (w *Window[M]) OneHourAgo() (_ M, collectedAt crtime.Mono) {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	return w.mu.ringB.Oldest()
+}
+
+// Start background collection of metrics.
+func (w *Window[M]) Start() {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	if w.mu.running {
+		return
+	}
+	w.mu.ringA = ring[M]{}
+	w.mu.ringB = ring[M]{}
+	w.mu.running = true
+	// Initialize both rings. We do this in a separate goroutine in case the
+	// caller is holding a lock that the collect function uses as well.
+	go func() {
+		w.mu.Lock()
+		defer w.mu.Unlock()
+		if !w.mu.running {
+			// We were stopped before we got here.
+			return
+		}
+		w.mu.startTime = crtime.NowMono()
+		w.mu.nextA = w.mu.startTime + crtime.Mono(tickA)
+		w.mu.nextB = w.mu.startTime + crtime.Mono(tickB)
+		m := w.collectFn()
+		w.mu.ringA.Add(m, w.mu.startTime)
+		w.mu.ringB.Add(m, w.mu.startTime)
+		// We prefer to use a timer instead of a ticker and goroutine to avoid yet
+		// another goroutine showing up in goroutine dumps.
+		w.mu.timer = time.AfterFunc(tickA, w.tick)
+	}()
+}
+
+// Stop background collection of metrics and wait for any in-progress collection
+// to finish.
+func (w *Window[M]) Stop() {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	w.mu.running = false
+	if w.mu.timer != nil {
+		// If Stop fails, the timer function didn't reach the critical section yet;
+		// when it does it will notice running=false and exit.
+		w.mu.timer.Stop()
+		w.mu.timer = nil
+	}
+}
+
+func (w *Window[M]) tick() {
+	w.mu.Lock()
+	defer w.mu.Unlock()
+	if !w.mu.running {
+		return
+	}
+	m := w.collectFn()
+	now := crtime.NowMono()
+	for w.mu.nextA <= now {
+		w.mu.ringA.Add(m, now)
+		w.mu.nextA += crtime.Mono(tickA)
+	}
+	for w.mu.nextB <= now {
+		w.mu.ringB.Add(m, now)
+		w.mu.nextB += crtime.Mono(tickB)
+	}
+	w.mu.timer.Reset(min(w.mu.nextA, w.mu.nextB).Sub(now))
+}
+
+// We scale down the timeframes by 5% so that the oldest sample is within ±5% of
+// the desired target.
+const timeframeA = 10 * time.Minute * 95 / 100
+const timeframeB = time.Hour * 95 / 100
+
+const resolution = 10
+const tickA = timeframeA / time.Duration(resolution)
+const tickB = timeframeB / time.Duration(resolution)
+
+type ring[M any] struct {
+	pos int
+	buf [resolution]struct {
+		m           M
+		collectedAt crtime.Mono
+	}
+}
+
+func (r *ring[M]) Add(value M, collectedAt crtime.Mono) {
+	r.buf[r.pos].m = value
+	r.buf[r.pos].collectedAt = collectedAt
+	r.pos = (r.pos + 1) % resolution
+}
+
+func (r *ring[M]) Oldest() (_ M, collectedAt crtime.Mono) {
+	return r.buf[r.pos].m, r.buf[r.pos].collectedAt
+}
diff --git a/level_checker.go b/level_checker.go
index 998b906f..77323758 100644
--- a/level_checker.go
+++ b/level_checker.go
@@ -540,6 +540,10 @@ func (d *DB) CheckLevels(stats *CheckLevelsStats) error {
 	// memtables) above.
 	seqNum := d.mu.versions.visibleSeqNum.Load()
 
+	bufferPool := new(block.BufferPool)
+	bufferPool.Init(10, block.ForLevelChecking)
+	defer bufferPool.Release()
+
 	checkConfig := &checkConfig{
 		logger:    d.opts.Logger,
 		comparer:  d.opts.Comparer,
@@ -549,7 +553,8 @@ func (d *DB) CheckLevels(stats *CheckLevelsStats) error {
 		stats:     stats,
 		merge:     d.merge,
 		formatKey: d.opts.Comparer.FormatKey,
-		readEnv:   block.ReadEnv{
+		readEnv: block.ReadEnv{
+			BufferPool: bufferPool,
 			// TODO(jackson): Add categorized stats.
 		},
 		fileCache: d.fileCache,
@@ -689,11 +694,15 @@ type valuesInfo struct {
 // each blob.BlockID's referenced blob.BlockValueID for the `i`th blob reference.
 func gatherBlobHandles(
 	ctx context.Context,
+	readEnv block.ReadEnv,
 	r *sstable.Reader,
 	blobRefs manifest.BlobReferences,
 	valueFetcher base.ValueFetcher,
 ) ([]map[blob.BlockID]valuesInfo, error) {
 	iter, err := r.NewPointIter(ctx, sstable.IterOptions{
+		Env: sstable.ReadEnv{
+			Block: readEnv,
+		},
 		BlobContext: sstable.TableBlobContext{
 			ValueFetcher: valueFetcher,
 			References:   &blobRefs,
@@ -792,7 +801,7 @@ func validateBlobValueLiveness(
 			// For this sstable, gather all the blob handles -- tracking
 			// each base.BlobReferenceID + blob.BlockID's referenced
 			// blob.BlockValueIDs.
-			referenced, err := gatherBlobHandles(ctx, r, t.BlobReferences, valueFetcher)
+			referenced, err := gatherBlobHandles(ctx, readEnv.Block, r, t.BlobReferences, valueFetcher)
 			if err != nil {
 				return err
 			}
diff --git a/metrics.go b/metrics.go
index 841fd5c1..dd899b24 100644
--- a/metrics.go
+++ b/metrics.go
@@ -755,22 +755,33 @@ var (
 		table.Div(),
 		table.String("flushable", 11, table.AlignRight, func(i commitPipelineInfo) string { return i.flushable }),
 	)
+	blockCacheInfoTableTopHeader = `BLOCK CACHE`
+	blockCacheInfoTable          = table.Define[blockCacheInfo](
+		func() []table.Element {
+			e := make([]table.Element, 0, 3+cache.NumCategories)
+			e = append(e, table.String("level", 6, table.AlignCenter, func(i blockCacheInfo) string { return i.level }))
+			e = append(e, table.String("all", 11, table.AlignCenter, func(i blockCacheInfo) string { return i.missRate }))
+			e = append(e, table.Div())
+			for c := range cache.Categories {
+				e = append(e, table.String(c.String(), 12, table.AlignCenter, func(i blockCacheInfo) string {
+					return i.perCategory[c]
+				}))
+			}
+			return e
+		}()...,
+	)
 	iteratorInfoTableTopHeader = `ITERATORS`
-	iteratorInfoTableSubHeader = `        block cache        |         file cache         |    filter    |  sst iters  |  snapshots`
+	iteratorInfoTableSubHeader = `        file cache        |    filter   |    open     |    open`
 	iteratorInfoTable          = table.Define[iteratorInfo](
-		table.String("entries", 12, table.AlignRight, func(i iteratorInfo) string { return i.bcEntries }),
-		table.Div(),
-		table.String("hit rate", 11, table.AlignRight, func(i iteratorInfo) string { return i.bcHitRate }),
-		table.Div(),
 		table.String("entries", 12, table.AlignRight, func(i iteratorInfo) string { return i.fcEntries }),
 		table.Div(),
-		table.String("hit rate", 11, table.AlignRight, func(i iteratorInfo) string { return i.fcHitRate }),
+		table.String("hit rate", 10, table.AlignRight, func(i iteratorInfo) string { return i.fcHitRate }),
 		table.Div(),
-		table.String("util", 12, table.AlignRight, func(i iteratorInfo) string { return i.bloomFilterUtil }),
+		table.String("utilization", 11, table.AlignRight, func(i iteratorInfo) string { return i.bloomFilterUtil }),
 		table.Div(),
-		table.String("open", 11, table.AlignRight, func(i iteratorInfo) string { return i.sstableItersOpen }),
+		table.String("sst iters ", 11, table.AlignRight, func(i iteratorInfo) string { return i.sstableItersOpen }),
 		table.Div(),
-		table.String("open", 11, table.AlignRight, func(i iteratorInfo) string { return i.snapshotsOpen }),
+		table.String("snapshots ", 11, table.AlignRight, func(i iteratorInfo) string { return i.snapshotsOpen }),
 	)
 	fileInfoTableHeader = `FILES                 tables                       |       blob files        |     blob values`
 	fileInfoTable       = table.Define[tableAndBlobInfo](
@@ -847,9 +858,55 @@ type commitPipelineInfo struct {
 	flushable string
 }
 
+type blockCacheInfo struct {
+	level       string
+	missRate    string
+	perCategory [cache.NumCategories]string
+}
+
+func makeBlockCacheInfo(hm *cache.HitsAndMisses) []blockCacheInfo {
+	// missRateAndPercentage returns a string that shows misses/(hits+misses) and
+	// misses/totalMisses as percentages; for example: "12% [4.5%]".
+	missRateAndPercentage := func(hits, misses, totalMisses int64) string {
+		if hits == 0 && misses == 0 {
+			return ""
+		}
+		return fmt.Sprintf("%s [%s]",
+			crhumanize.Percent(misses, hits+misses),
+			crhumanize.Percent(misses, totalMisses),
+		)
+	}
+
+	totalHits, totalMisses := hm.Aggregate()
+	res := make([]blockCacheInfo, 0, cache.NumLevels+1)
+	for level := range cache.Levels {
+		levelHits, levelMisses := hm.AggregateLevel(level)
+		if levelHits == 0 && levelMisses == 0 {
+			// Skip levels with no activity.
+			continue
+		}
+		bci := blockCacheInfo{
+			level:    level.String(),
+			missRate: missRateAndPercentage(levelHits, levelMisses, totalMisses),
+		}
+		for category := range cache.Categories {
+			hits, misses := hm.Get(level, category)
+			bci.perCategory[category] = missRateAndPercentage(hits, misses, totalMisses)
+		}
+		res = append(res, bci)
+	}
+	last := blockCacheInfo{
+		level:    "total",
+		missRate: fmt.Sprintf("%s%%", crhumanize.Float(percent(totalMisses, totalHits+totalMisses), 1)),
+	}
+	for category := range cache.Categories {
+		catHits, catMisses := hm.AggregateCategory(category)
+		last.perCategory[category] = missRateAndPercentage(catHits, catMisses, totalMisses)
+	}
+	return append(res, last)
+}
+
 type iteratorInfo struct {
-	bcEntries        string
-	bcHitRate        string
 	fcEntries        string
 	fcHitRate        string
 	bloomFilterUtil  string
@@ -973,9 +1030,36 @@ func (m *Metrics) String() string {
 	cur = commitPipelineInfoTable.Render(cur, table.RenderOptions{}, commitPipelineInfoContents)
 	cur = cur.NewlineReturn()
 
+	cur = cur.WriteString(blockCacheInfoTableTopHeader)
+	cur = cur.Printf(": %s entries (%s)", humanizeCount(m.BlockCache.Count), humanizeBytes(m.BlockCache.Size))
+	cur = cur.NewlineReturn()
+
+	cur = cur.WriteString("                 miss rate [percentage of total misses] since start\n")
+	bci := makeBlockCacheInfo(&m.BlockCache.HitsAndMisses)
+	cur = blockCacheInfoTable.Render(cur, table.RenderOptions{
+		HorizontalDividers: table.MakeHorizontalDividers(0, len(bci)-1),
+	}, bci...)
+	cur = cur.NewlineReturn()
+
+	if m.BlockCache.Recent[0].Since != 0 {
+		cur = cur.WriteString("                 miss rate [percentage of total misses] over last ~10m\n") // TODO(radu): print exact timeframe
+		bci = makeBlockCacheInfo(&m.BlockCache.Recent[0].HitsAndMisses)
+		cur = blockCacheInfoTable.Render(cur, table.RenderOptions{
+			HorizontalDividers: table.MakeHorizontalDividers(0, len(bci)-1),
+		}, bci...)
+		cur = cur.NewlineReturn()
+	}
+
+	if m.BlockCache.Recent[1].Since != 0 {
+		cur = cur.WriteString("                 miss rate [percentage of total misses] over last ~1h\n") // TODO(radu): print exact timeframe
+		bci = makeBlockCacheInfo(&m.BlockCache.Recent[1].HitsAndMisses)
+		cur = blockCacheInfoTable.Render(cur, table.RenderOptions{
+			HorizontalDividers: table.MakeHorizontalDividers(0, len(bci)-1),
+		}, bci...)
+		cur = cur.NewlineReturn()
+	}
+
 	iteratorInfoContents := iteratorInfo{
-		bcEntries:        fmt.Sprintf("%s (%s)", humanizeCount(m.BlockCache.Count), humanizeBytes(m.BlockCache.Size)),
-		bcHitRate:        fmt.Sprintf("%.1f%%", hitRate(m.BlockCache.Hits, m.BlockCache.Misses)),
 		fcEntries:        fmt.Sprintf("%s (%s)", humanizeCount(m.FileCache.TableCount), humanizeBytes(m.FileCache.Size)),
 		fcHitRate:        fmt.Sprintf("%.1f%%", hitRate(m.FileCache.Hits, m.FileCache.Misses)),
 		bloomFilterUtil:  fmt.Sprintf("%.1f%%", hitRate(m.Filter.Hits, m.Filter.Misses)),
@@ -1121,6 +1205,12 @@ func (m *Metrics) StringForTests() string {
 	// Don't show cgo memory statistics as they can vary based on architecture,
 	// invariants tag, etc.
 	mCopy.manualMemory = manual.Metrics{}
+
+	// Clear the recent block cache stats as they can vary based on timing.
+	for i := range mCopy.BlockCache.Recent {
+		mCopy.BlockCache.Recent[i].HitsAndMisses = cache.HitsAndMisses{}
+		mCopy.BlockCache.Recent[i].Since = 0
+	}
 	return redact.StringWithoutMarkers(&mCopy)
 }
 
diff --git a/sstable/blob/blob.go b/sstable/blob/blob.go
index f635e8cd..c1769b85 100644
--- a/sstable/blob/blob.go
+++ b/sstable/blob/blob.go
@@ -666,7 +666,7 @@ func (r *FileReader) ReadProperties(ctx context.Context) (FileProperties, error)
 	// We don't want the property block to go into the block cache, so we use a
 	// buffer pool.
 	var bufferPool block.BufferPool
-	bufferPool.Init(1)
+	bufferPool.Init(1, block.ForBlobFileMetadata)
 	defer bufferPool.Release()
 	b, err := r.r.Read(
 		ctx, block.ReadEnv{BufferPool: &bufferPool}, nil /* readHandle */, r.footer.propertiesHandle,
diff --git a/sstable/block/block.go b/sstable/block/block.go
index b2fd6792..f515874b 100644
--- a/sstable/block/block.go
+++ b/sstable/block/block.go
@@ -255,6 +255,9 @@ var NoReadEnv = ReadEnv{}
 
 // ReadEnv contains arguments used when reading a block which apply to all
 // the block reads performed by a higher-level operation.
+//
+// ReadEnv can be copied by value; any copy will use the same statistics and
+// buffer pool.
 type ReadEnv struct {
 	// stats and iterStats are slightly different. stats is a shared struct
 	// supplied from the outside, and represents stats for the whole iterator
@@ -268,8 +271,12 @@ type ReadEnv struct {
 	IterStats *CategoryStatsShard
 
 	// BufferPool is not-nil if we read blocks into a buffer pool and not into the
-	// cache. This is used during compactions.
+	// cache. This is used during compactions and by the level checker.
 	BufferPool *BufferPool
+	// Level is the LSM level associated with the operation, when the operation
+	// applies to a (possibly virtual) sstable. It is used when interacting with
+	// the block cache.
+	Level cache.Level
 
 	// ReportCorruptionFn is called with ReportCorruptionArg and the error
 	// whenever an SSTable corruption is detected. The argument is used to avoid
@@ -351,6 +358,19 @@ func (r *Reader) ChecksumType() ChecksumType {
 	return r.checksumType
 }
 
+var kindToCacheCategory = [blockkind.NumKinds]cache.Category{
+	blockkind.Unknown:                         cache.CategoryBackground,
+	blockkind.SSTableData:                     cache.CategorySSTableData,
+	blockkind.SSTableIndex:                    cache.CategoryIndex,
+	blockkind.SSTableValue:                    cache.CategorySSTableValue,
+	blockkind.BlobValue:                       cache.CategoryBlobValue,
+	blockkind.BlobReferenceValueLivenessIndex: cache.CategoryIndex,
+	blockkind.Filter:                          cache.CategoryFilter,
+	blockkind.RangeDel:                        cache.CategorySSTableData,
+	blockkind.RangeKey:                        cache.CategorySSTableData,
+	blockkind.Metadata:                        cache.CategoryIndex,
+}
+
 // Read reads the block referenced by the provided handle. The readHandle is
 // optional.
 func (r *Reader) Read(
@@ -366,7 +386,20 @@ func (r *Reader) Read(
 	// reading a block.
 	if r.opts.CacheOpts.CacheHandle == nil || env.BufferPool != nil {
 		if r.opts.CacheOpts.CacheHandle != nil {
-			if cv := r.opts.CacheOpts.CacheHandle.Peek(r.opts.CacheOpts.FileNum, bh.Offset); cv != nil {
+			var category cache.Category
+			switch env.BufferPool.Reason() {
+			case ForLevelChecking:
+				category = cache.CategoryHidden
+			case ForCompaction, ForBlobFileRewrite:
+				// We don't want background work to obscure the hit/miss metrics for
+				// "foreground" traffic; and having two variants for each category would
+				// be overkill, so compaction-related metrics all go into one
+				// "background" category.
+				category = cache.CategoryBackground
+			default:
+				category = kindToCacheCategory[kind]
+			}
+			if cv := r.opts.CacheOpts.CacheHandle.Peek(r.opts.CacheOpts.FileNum, bh.Offset, env.Level, category); cv != nil {
 				recordCacheHit(ctx, env, readHandle, bh, kind)
 				return CacheBufferHandle(cv), nil
 			}
@@ -379,7 +412,7 @@ func (r *Reader) Read(
 	}
 
 	cv, crh, errorDuration, waitDuration, hit, err := r.opts.CacheOpts.CacheHandle.GetWithReadHandle(
-		ctx, r.opts.CacheOpts.FileNum, bh.Offset)
+		ctx, r.opts.CacheOpts.FileNum, bh.Offset, env.Level, kindToCacheCategory[kind])
 	const slowDur = 5 * time.Millisecond
 	if waitDuration > slowDur && r.opts.LoggerAndTracer.IsTracingEnabled(ctx) {
 		r.opts.LoggerAndTracer.Eventf(
@@ -533,8 +566,8 @@ func (r *Reader) Readable() objstorage.Readable {
 //
 // Users should prefer using Read, which handles reading from object storage on
 // a cache miss.
-func (r *Reader) GetFromCache(bh Handle) *cache.Value {
-	return r.opts.CacheOpts.CacheHandle.Peek(r.opts.CacheOpts.FileNum, bh.Offset)
+func (r *Reader) GetFromCache(bh Handle, level cache.Level) *cache.Value {
+	return r.opts.CacheOpts.CacheHandle.Peek(r.opts.CacheOpts.FileNum, bh.Offset, level, cache.CategoryBackground)
 }
 
 // UsePreallocatedReadHandle returns a ReadHandle that reads from the reader and
diff --git a/sstable/block/buffer_pool.go b/sstable/block/buffer_pool.go
index 017b9da4..e514d2a6 100644
--- a/sstable/block/buffer_pool.go
+++ b/sstable/block/buffer_pool.go
@@ -139,9 +139,26 @@ func (bh BufferHandle) Release() {
 type BufferPool struct {
 	// pool contains all the buffers held by the pool, including buffers that
 	// are in-use. For every i < len(pool): pool[i].v is non-nil.
-	pool []AllocedBuffer
+	pool   []AllocedBuffer
+	reason ReasonForBufferPool
 }
 
+// ReasonForBufferPool identifies the use case for th BufferPool. These are
+// exceptional use cases, as using a buffer pool does not populate the block
+// cache.
+//
+// The reason determines how the cache hit/miss metrics are updated.
+type ReasonForBufferPool int8
+
+const (
+	ForCompaction ReasonForBufferPool = iota
+	ForBlobFileRewrite
+	ForSSTableMetadata
+	ForBlobFileMetadata
+	ForExternalIter
+	ForLevelChecking
+)
+
 // AllocedBuffer is an allocated memory buffer.
 type AllocedBuffer struct {
 	v *cache.Value
@@ -155,19 +172,16 @@ type AllocedBuffer struct {
 
 // Init initializes the pool with an initial working set buffer size of
 // `initialSize`.
-func (p *BufferPool) Init(initialSize int) {
+func (p *BufferPool) Init(initialSize int, reason ReasonForBufferPool) {
 	*p = BufferPool{
-		pool: make([]AllocedBuffer, 0, initialSize),
+		pool:   make([]AllocedBuffer, 0, initialSize),
+		reason: reason,
 	}
 }
 
-// InitPreallocated is like Init but for internal sstable package use in
-// instances where a pre-allocated slice of []allocedBuffer already exists. It's
-// used to avoid an extra allocation initializing BufferPool.pool.
-func (p *BufferPool) InitPreallocated(pool []AllocedBuffer) {
-	*p = BufferPool{
-		pool: pool[:0],
-	}
+// Reason returns the reason passed to Init.
+func (p *BufferPool) Reason() ReasonForBufferPool {
+	return p.reason
 }
 
 // Release releases all buffers held by the pool and resets the pool to an
diff --git a/sstable/copier.go b/sstable/copier.go
index 1b0be520..3075ccad 100644
--- a/sstable/copier.go
+++ b/sstable/copier.go
@@ -12,6 +12,7 @@ import (
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/bytealloc"
+	"github.com/cockroachdb/pebble/internal/cache"
 	"github.com/cockroachdb/pebble/objstorage"
 	"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
 	"github.com/cockroachdb/pebble/sstable/block"
@@ -46,7 +47,7 @@ func CopySpan(
 	ctx context.Context,
 	input objstorage.Readable,
 	r *Reader,
-	rOpts ReaderOptions,
+	level int,
 	output objstorage.Writable,
 	o WriterOptions,
 	start, end InternalKey,
@@ -162,7 +163,7 @@ func CopySpan(
 	var blocksNotInCache []indexEntry
 
 	for i := range blocks {
-		cv := r.blockReader.GetFromCache(blocks[i].bh.Handle)
+		cv := r.blockReader.GetFromCache(blocks[i].bh.Handle, cache.MakeLevel(level))
 		if cv == nil {
 			// Cache miss. Add this block to the list of blocks that are not in cache.
 			blocksNotInCache = blocks[i-len(blocksNotInCache) : i+1]
diff --git a/sstable/reader.go b/sstable/reader.go
index 1e5db147..cc17d628 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -471,7 +471,7 @@ var metaBufferPools = sync.Pool{
 		// New pools are initialized with a capacity of 3 to accommodate the
 		// meta block (1), and both the compressed properties block (1) and
 		// decompressed properties block (1) simultaneously.
-		bp.Init(3)
+		bp.Init(3, block.ForSSTableMetadata)
 		return bp
 	},
 }
@@ -578,7 +578,7 @@ var propertiesBlockBufPools = sync.Pool{
 		// New pools are initialized with a capacity of 2 to accommodate
 		// both the compressed properties block (1) and decompressed
 		// properties block (1).
-		bp.Init(2)
+		bp.Init(2, block.ForSSTableMetadata)
 		return bp
 	},
 }
diff --git a/table_stats.go b/table_stats.go
index 121aba1f..0d939758 100644
--- a/table_stats.go
+++ b/table_stats.go
@@ -14,6 +14,7 @@ import (
 	"github.com/cockroachdb/crlib/crtime"
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/cache"
 	"github.com/cockroachdb/pebble/internal/invariants"
 	"github.com/cockroachdb/pebble/internal/keyspan"
 	"github.com/cockroachdb/pebble/internal/keyspan/keyspanimpl"
@@ -349,12 +350,15 @@ func (d *DB) loadTableStats(
 
 	backingProps, backingPropsOk := meta.TableBacking.Properties()
 
+	blockReadEnv := block.ReadEnv{
+		Level: cache.MakeLevel(level),
+	}
 	// If the stats are already available (always the case other than after
 	// initial startup), and there are no range deletions or range key deletions,
 	// we avoid opening the table.
 	if !backingPropsOk || backingProps.NumRangeDeletions > 0 || backingProps.NumRangeKeyDels > 0 {
 		err := d.fileCache.withReader(
-			ctx, block.NoReadEnv, meta, func(r *sstable.Reader, env sstable.ReadEnv) (err error) {
+			ctx, blockReadEnv, meta, func(r *sstable.Reader, env sstable.ReadEnv) (err error) {
 				if !backingPropsOk {
 					loadedProps, err := r.ReadPropertiesBlock(ctx, nil /* buffer pool */)
 					if err != nil {



Test patch:
diff --git a/blob_rewrite_test.go b/blob_rewrite_test.go
index f49fe3ed..823c272d 100644
--- a/blob_rewrite_test.go
+++ b/blob_rewrite_test.go
@@ -334,7 +334,7 @@ func TestBlobRewriteRandomized(t *testing.T) {
 	fch := fc.newHandle(ch, objStore, base.NoopLoggerAndTracer{}, sstable.ReaderOptions{}, nil)
 	defer fch.Close()
 	var bufferPool block.BufferPool
-	bufferPool.Init(4)
+	bufferPool.Init(4, block.ForBlobFileRewrite)
 	defer bufferPool.Release()
 	readEnv := block.ReadEnv{BufferPool: &bufferPool}
 
diff --git a/db_test.go b/db_test.go
index 454c88e0..b2d086dc 100644
--- a/db_test.go
+++ b/db_test.go
@@ -858,7 +858,7 @@ func TestMemTableReservation(t *testing.T) {
 		t.Fatalf("expected 2 refs, but found %d", refs)
 	}
 	// Verify the memtable reservation has caused our test block to be evicted.
-	if cv := tmpHandle.Peek(base.DiskFileNum(0), 0); cv != nil {
+	if cv := tmpHandle.Peek(base.DiskFileNum(0), 0, cache.MakeLevel(0), cache.CategoryBackground); cv != nil {
 		t.Fatalf("expected failure, but found success: %#v", cv)
 	}
 
diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go
index 278cfb35..0abdb2ee 100644
--- a/internal/cache/cache_test.go
+++ b/internal/cache/cache_test.go
@@ -14,7 +14,6 @@ import (
 	"strconv"
 	"sync"
 	"testing"
-	"time"
 
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/stretchr/testify/require"
@@ -42,7 +41,7 @@ func TestCache(t *testing.T) {
 		wantHit := fields[1][0] == 'h'
 
 		var hit bool
-		cv := h.Get(base.DiskFileNum(key), 0)
+		cv := h.Get(base.DiskFileNum(key), 0, MakeLevel(0), CategorySSTableData)
 		if cv == nil {
 			cv = Alloc(1)
 			cv.RawBuffer()[0] = fields[0][0]
@@ -82,14 +81,14 @@ func TestCachePeek(t *testing.T) {
 		setTestValue(h, 0, uint64(i), "a", 1)
 	}
 	for i := range size / 2 {
-		v := h.Get(base.DiskFileNum(0), uint64(i))
+		v := h.Get(base.DiskFileNum(0), uint64(i), MakeLevel(0), CategoryBackground)
 		if v == nil {
 			t.Fatalf("expected to find block %d", i)
 		}
 		v.Release()
 	}
 	for i := size / 2; i < size; i++ {
-		v := h.Peek(base.DiskFileNum(0), uint64(i))
+		v := h.Peek(base.DiskFileNum(0), uint64(i), MakeLevel(0), CategoryBackground)
 		if v == nil {
 			t.Fatalf("expected to find block %d", i)
 		}
@@ -101,7 +100,7 @@ func TestCachePeek(t *testing.T) {
 	}
 	// Verify that the Gets still find their values, despite the Peeks.
 	for i := range size / 2 {
-		v := h.Get(base.DiskFileNum(0), uint64(i))
+		v := h.Get(base.DiskFileNum(0), uint64(i), MakeLevel(0), CategoryBackground)
 		if v == nil {
 			t.Fatalf("expected to find block %d", i)
 		}
@@ -125,12 +124,12 @@ func TestCacheDelete(t *testing.T) {
 	if expected, size := int64(10), cache.Size(); expected != size {
 		t.Fatalf("expected cache size %d, but found %d", expected, size)
 	}
-	if v := h.Get(base.DiskFileNum(0), 0); v == nil {
+	if v := h.Get(base.DiskFileNum(0), 0, MakeLevel(0), CategorySSTableData); v == nil {
 		t.Fatalf("expected to find block 0/0")
 	} else {
 		v.Release()
 	}
-	if v := h.Get(base.DiskFileNum(1), 0); v != nil {
+	if v := h.Get(base.DiskFileNum(1), 0, MakeLevel(0), CategorySSTableData); v != nil {
 		t.Fatalf("expected to not find block 1/0")
 	}
 	// Deleting a non-existing block does nothing.
@@ -197,11 +196,11 @@ func TestMultipleDBs(t *testing.T) {
 	if expected, size := int64(5), cache.Size(); expected != size {
 		t.Fatalf("expected cache size %d, but found %d", expected, size)
 	}
-	v := h1.Get(base.DiskFileNum(0), 0)
+	v := h1.Get(base.DiskFileNum(0), 0, MakeLevel(0), CategorySSTableData)
 	if v != nil {
 		t.Fatalf("expected not present, but found %#v", v)
 	}
-	v = h2.Get(base.DiskFileNum(0), 0)
+	v = h2.Get(base.DiskFileNum(0), 0, MakeLevel(0), CategorySSTableData)
 	if v := v.RawBuffer(); string(v) != "bbbbb" {
 		t.Fatalf("expected bbbbb, but found %s", v)
 	}
@@ -289,10 +288,12 @@ func TestCacheStressSetExisting(t *testing.T) {
 }
 
 func BenchmarkCacheGet(b *testing.B) {
-	const size = 100000
+	const size = 1_000_000
 
-	n := runtime.GOMAXPROCS(0)
-	cache := NewWithShards(size*int64(n), n)
+	// We double the size to allow for shard imbalances. With many objects and
+	// relatively few shards, the probability that any bucket is more than double
+	// the expected size is vanishingly small.
+	cache := New(2 * size)
 	defer cache.Unref()
 	h := cache.NewHandle()
 	defer h.Close()
@@ -303,12 +304,15 @@ func BenchmarkCacheGet(b *testing.B) {
 
 	b.ResetTimer()
 	b.RunParallel(func(pb *testing.PB) {
-		rng := rand.New(rand.NewPCG(0, uint64(time.Now().UnixNano())))
-
+		pcg := rand.NewPCG(rand.Uint64(), rand.Uint64())
 		for pb.Next() {
-			v := h.Get(base.DiskFileNum(0), uint64(rng.IntN(size)))
+			randVal := pcg.Uint64()
+			offset := randVal % size
+			level := Level{levelPlusOne: int8((randVal >> 32) % NumLevels)}
+			category := Category((randVal >> 48) % uint64(NumCategories))
+			v := h.Get(base.DiskFileNum(0), offset, level, category)
 			if v == nil {
-				b.Fatal("failed to lookup value")
+				b.Fatal("failed to look up value")
 			}
 			v.Release()
 		}
diff --git a/internal/cache/read_shard_test.go b/internal/cache/read_shard_test.go
index 1d6b9b83..d9753489 100644
--- a/internal/cache/read_shard_test.go
+++ b/internal/cache/read_shard_test.go
@@ -50,7 +50,7 @@ func newTestReader(
 }
 
 func (r *testReader) getAsync(shard *shard) *string {
-	v, re := shard.getWithReadEntry(r.key)
+	v, re := shard.getWithReadEntry(r.key, MakeLevel(0), CategorySSTableData)
 	if v != nil {
 		str := string(v.RawBuffer())
 		v.Release()
@@ -285,7 +285,7 @@ func TestReadShardConcurrent(t *testing.T) {
 	for _, r := range differentReaders {
 		for j := 0; j < r.numReaders; j++ {
 			go func(r *testSyncReaders, index int) {
-				v, rh, _, _, _, err := r.handle.GetWithReadHandle(context.Background(), r.fileNum, r.offset)
+				v, rh, _, _, _, err := r.handle.GetWithReadHandle(context.Background(), r.fileNum, r.offset, MakeLevel(0), CategorySSTableData)
 				require.NoError(t, err)
 				if v != nil {
 					require.Equal(t, r.val, v.RawBuffer())
diff --git a/internal/metricsutil/window_test.go b/internal/metricsutil/window_test.go
new file mode 100644
index 00000000..e7e22f40
--- /dev/null
+++ b/internal/metricsutil/window_test.go
@@ -0,0 +1,55 @@
+// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+//go:build go1.25
+
+package metricsutil
+
+import (
+	"testing"
+	"testing/synctest"
+	"time"
+
+	"github.com/cockroachdb/crlib/crtime"
+)
+
+func TestWindow(t *testing.T) {
+	synctest.Test(t, func(t *testing.T) {
+		startTime := crtime.NowMono()
+		w := NewWindow[time.Duration](startTime.Elapsed)
+		w.Start()
+		defer w.Stop()
+		<-time.After(5 * time.Minute)
+		v, x := w.TenMinutesAgo()
+		if v != 0 || x != 0 {
+			t.Fatalf("expected empty, got %v %v", v, x)
+		}
+		<-time.After(5*time.Minute + time.Second)
+		v, x = w.TenMinutesAgo()
+		if x := x.Elapsed(); x < 9*time.Minute || x > 11*time.Minute {
+			t.Fatalf("expected ~10md, got %s", x)
+		}
+		if v < 0 || v > time.Minute {
+			t.Fatalf("expected ~0, got %s", v)
+		}
+
+		<-time.After(20*time.Minute + time.Second)
+		v, x = w.TenMinutesAgo()
+		if x := x.Elapsed(); x < 9*time.Minute || x > 11*time.Minute {
+			t.Fatalf("expected ~10m, got %s", x)
+		}
+		if v < 19*time.Minute || v > 21*time.Minute {
+			t.Fatalf("expected ~0, got %s", v)
+		}
+
+		<-time.After(30*time.Minute + time.Second)
+		v, x = w.OneHourAgo()
+		if x := x.Elapsed(); x < 50*time.Minute || x > 70*time.Minute {
+			t.Fatalf("expected ~1h, got %s", x)
+		}
+		if v < 0 || v > 10*time.Minute {
+			t.Fatalf("expected 0-10m, got %s", v)
+		}
+	})
+}
diff --git a/metrics_test.go b/metrics_test.go
index 67785464..7d299355 100644
--- a/metrics_test.go
+++ b/metrics_test.go
@@ -39,8 +39,17 @@ func exampleMetrics() Metrics {
 	var m Metrics
 	m.BlockCache.Size = 1 * GB
 	m.BlockCache.Count = 100
-	m.BlockCache.Hits = 10000
-	m.BlockCache.Misses = 5000
+	for i := range m.BlockCache.HitsAndMisses {
+		for j := range m.BlockCache.HitsAndMisses[i] {
+			x := int64(i)*1000 + int64(j)*100
+			m.BlockCache.HitsAndMisses[i][j].Hits = 10000 + x
+			m.BlockCache.HitsAndMisses[i][j].Misses = 1000 + x
+			m.BlockCache.Recent[0].HitsAndMisses[i][j].Hits = 20000 + x
+			m.BlockCache.Recent[0].HitsAndMisses[i][j].Misses = 4000 + x
+			m.BlockCache.Recent[1].HitsAndMisses[i][j].Hits = 30000 + x
+			m.BlockCache.Recent[1].HitsAndMisses[i][j].Misses = 9000 + x
+		}
+	}
 
 	m.Compact.Count = 1000
 	m.Compact.DefaultCount = 10
diff --git a/sstable/block/buffer_pool_test.go b/sstable/block/buffer_pool_test.go
index a9c07892..cc90c9c1 100644
--- a/sstable/block/buffer_pool_test.go
+++ b/sstable/block/buffer_pool_test.go
@@ -52,7 +52,7 @@ func TestBufferPool(t *testing.T) {
 			}
 			var initialSize int
 			td.ScanArgs(t, "size", &initialSize)
-			bp.Init(initialSize)
+			bp.Init(initialSize, ForCompaction)
 			writeBufferPool(&buf, &bp)
 			return buf.String()
 		case "alloc":
diff --git a/sstable/colblk/index_block_test.go b/sstable/colblk/index_block_test.go
index 12336061..b56fd942 100644
--- a/sstable/colblk/index_block_test.go
+++ b/sstable/colblk/index_block_test.go
@@ -132,7 +132,7 @@ func TestIndexIterInitHandle(t *testing.T) {
 	}
 
 	getBlockAndIterate := func(it *IndexIter) {
-		cv := ch.Get(base.DiskFileNum(1), 0)
+		cv := ch.Get(base.DiskFileNum(1), 0, cache.MakeLevel(0), cache.CategorySSTableData)
 		require.NotNil(t, cv)
 		require.NoError(t, it.InitHandle(testkeys.Comparer, block.CacheBufferHandle(cv), blockiter.NoTransforms))
 		defer it.Close()
diff --git a/sstable/colblk/keyspan_test.go b/sstable/colblk/keyspan_test.go
index 312324c4..f8b1048a 100644
--- a/sstable/colblk/keyspan_test.go
+++ b/sstable/colblk/keyspan_test.go
@@ -94,7 +94,7 @@ func TestKeyspanBlockPooling(t *testing.T) {
 	v.SetInCacheForTesting(ch, base.DiskFileNum(1), 0)
 
 	getBlockAndIterate := func() {
-		cv := ch.Get(base.DiskFileNum(1), 0)
+		cv := ch.Get(base.DiskFileNum(1), 0, cache.MakeLevel(0), cache.CategorySSTableData)
 		require.NotNil(t, cv)
 		it := NewKeyspanIter(testkeys.Comparer.Compare, block.CacheBufferHandle(cv), blockiter.NoFragmentTransforms)
 		defer it.Close()
diff --git a/sstable/copier_test.go b/sstable/copier_test.go
index 7ff7b903..0401dc96 100644
--- a/sstable/copier_test.go
+++ b/sstable/copier_test.go
@@ -154,19 +154,6 @@ func TestCopySpan(t *testing.T) {
 			fileNameToNum[outputFile] = nextFileNum
 			nextFileNum++
 
-			rOpts := ReaderOptions{
-				Filters: map[string]FilterPolicy{
-					"rocksdb.BuiltinBloomFilter": bloom.FilterPolicy(10),
-				},
-				ReaderOptions: block.ReaderOptions{
-					CacheOpts: sstableinternal.CacheOptions{
-						CacheHandle: cacheHandle,
-						FileNum:     base.DiskFileNum(fileNameToNum[d.CmdArgs[0].Key]),
-					},
-				},
-				Comparer:   testkeys.Comparer,
-				KeySchemas: KeySchemas{keySchema.Name: &keySchema},
-			}
 			r, err := getReader(d)
 			if err != nil {
 				return err.Error()
@@ -182,7 +169,7 @@ func TestCopySpan(t *testing.T) {
 			if err != nil {
 				return err.Error()
 			}
-			size, err := CopySpan(context.TODO(), readable2, r, rOpts, writable, wOpts, start, end)
+			size, err := CopySpan(context.TODO(), readable2, r, 0 /* level */, writable, wOpts, start, end)
 			if err != nil {
 				return err.Error()
 			}
diff --git a/sstable/reader_iter_test.go b/sstable/reader_iter_test.go
index ac47fa6e..69461882 100644
--- a/sstable/reader_iter_test.go
+++ b/sstable/reader_iter_test.go
@@ -53,7 +53,7 @@ func TestIteratorErrorOnInit(t *testing.T) {
 	defer r.Close()
 
 	var pool block.BufferPool
-	pool.Init(5)
+	pool.Init(5, block.ForCompaction)
 	defer pool.Release()
 
 	toggle.On()
@@ -812,7 +812,7 @@ func TestBloomFilterOptimizationSingleLevel(t *testing.T) {
 		defer cleanup()
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
@@ -856,7 +856,7 @@ func TestBloomFilterOptimizationSingleLevel(t *testing.T) {
 		defer cleanup()
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
@@ -914,7 +914,7 @@ func TestBloomFilterOptimizationTwoLevel(t *testing.T) {
 			"Test requires two-level index structure to be created")
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
@@ -965,7 +965,7 @@ func TestBloomFilterOptimizationTwoLevel(t *testing.T) {
 			"Test requires two-level index structure to be created")
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
@@ -1013,7 +1013,7 @@ func TestBloomFilterOptimizationEdgeCases(t *testing.T) {
 		defer cleanup()
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
@@ -1049,7 +1049,7 @@ func TestBloomFilterOptimizationEdgeCases(t *testing.T) {
 		defer cleanup()
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
@@ -1089,7 +1089,7 @@ func TestBloomFilterOptimizationEdgeCases(t *testing.T) {
 		defer cleanup()
 
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 
 		var stats base.InternalIteratorStats
diff --git a/sstable/reader_test.go b/sstable/reader_test.go
index fdc94a6c..be877862 100644
--- a/sstable/reader_test.go
+++ b/sstable/reader_test.go
@@ -170,7 +170,7 @@ func runVirtualReaderTest(t *testing.T, path string, blockSize, indexBlockSize i
 			if err != nil {
 				return err.Error()
 			}
-			bp.Init(5)
+			bp.Init(5, block.ForCompaction)
 			return formatWriterMetadata(td, wMeta)
 
 		case "virtualize":
@@ -941,7 +941,7 @@ func TestCompactionIteratorSetupForCompaction(t *testing.T) {
 				ch := c.NewHandle()
 				r := buildTestTableWithProvider(t, provider, numEntries, blockSize, indexBlockSize, block.DefaultCompression, nil, ch)
 				var pool block.BufferPool
-				pool.Init(5)
+				pool.Init(5, block.ForCompaction)
 				citer, err := r.NewCompactionIter(
 					NoTransforms, ReadEnv{Block: block.ReadEnv{BufferPool: &pool}},
 					MakeTrivialReaderProvider(r), AssertNoBlobHandles)
@@ -1001,7 +1001,7 @@ func TestReadaheadSetupForV3TablesWithMultipleVersions(t *testing.T) {
 	defer r.Close()
 	{
 		var pool block.BufferPool
-		pool.Init(5)
+		pool.Init(5, block.ForCompaction)
 		defer pool.Release()
 		citer, err := r.NewCompactionIter(
 			NoTransforms, ReadEnv{Block: block.ReadEnv{BufferPool: &pool}},
diff --git a/sstable/rowblk/rowblk_fragment_iter_test.go b/sstable/rowblk/rowblk_fragment_iter_test.go
index b1c30558..ec58ec47 100644
--- a/sstable/rowblk/rowblk_fragment_iter_test.go
+++ b/sstable/rowblk/rowblk_fragment_iter_test.go
@@ -96,7 +96,7 @@ func TestBlockFragmentIterator(t *testing.T) {
 				return d.Expected
 			}
 
-			blockHandle := block.CacheBufferHandle(cacheHandle.Get(0, 0))
+			blockHandle := block.CacheBufferHandle(cacheHandle.Get(0, 0, cache.MakeLevel(0), cache.CategorySSTableData))
 			require.True(t, blockHandle.Valid())
 			i, err := NewFragmentIter(0, comparer, blockHandle, transforms)
 			defer i.Close()
diff --git a/sstable/single_lvl_iter_benchmark_test.go b/sstable/single_lvl_iter_benchmark_test.go
index b0bb799a..4ee06f76 100644
--- a/sstable/single_lvl_iter_benchmark_test.go
+++ b/sstable/single_lvl_iter_benchmark_test.go
@@ -64,7 +64,7 @@ func init() {
 
 	var stats base.InternalIteratorStats
 	var bufferPool block.BufferPool
-	bufferPool.Init(5)
+	bufferPool.Init(5, block.ForCompaction)
 
 	benchIterOpts = IterOptions{
 		Transforms:           NoTransforms,
@@ -128,7 +128,7 @@ func BenchmarkIteratorSeekPrefixGE_Hit(b *testing.B) {
 	// Setup iterator options with bloom filter enabled
 	var stats base.InternalIteratorStats
 	var bufferPool block.BufferPool
-	bufferPool.Init(5)
+	bufferPool.Init(5, block.ForCompaction)
 	defer bufferPool.Release()
 
 	iterOpts := IterOptions{
@@ -160,7 +160,7 @@ func BenchmarkIteratorSeekPrefixGE_Hit(b *testing.B) {
 func BenchmarkIteratorSeekPrefixGE_NoHit(b *testing.B) {
 	var stats base.InternalIteratorStats
 	var bufferPool block.BufferPool
-	bufferPool.Init(5)
+	bufferPool.Init(5, block.ForCompaction)
 	defer bufferPool.Release()
 
 	iterOpts := IterOptions{
diff --git a/sstable/writer_test.go b/sstable/writer_test.go
index 5bc00360..1d1dcfda 100644
--- a/sstable/writer_test.go
+++ b/sstable/writer_test.go
@@ -835,7 +835,7 @@ func TestWriterClearCache(t *testing.T) {
 
 	// Verify that the written blocks have been cleared from the cache.
 	check := func(bh block.Handle) {
-		cv := cacheOpts.CacheHandle.Get(cacheOpts.FileNum, bh.Offset)
+		cv := cacheOpts.CacheHandle.Get(cacheOpts.FileNum, bh.Offset, cache.MakeLevel(0), cache.CategorySSTableData)
 		if cv != nil {
 			t.Fatalf("%d: expected cache to be cleared, but found %#v", bh.Offset, cv)
 		}
diff --git a/testdata/compaction/l0_to_lbase_compaction b/testdata/compaction/l0_to_lbase_compaction
index d2bbf3c9..defe2c8a 100644
--- a/testdata/compaction/l0_to_lbase_compaction
+++ b/testdata/compaction/l0_to_lbase_compaction
@@ -60,11 +60,19 @@ COMMIT PIPELINE
 ----------+--------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 160KB: 160KB |      0.0% |         1 |  1 (512KB) |  1 (512KB) |     328 K |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [100%] |                                                                  100% [100%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |                                                                  100% [100%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-  63 (251KB) |       78.0% |    4 (1.1KB) |       90.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+   4 (1.1KB) |      90.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
diff --git a/testdata/compaction/value_separation b/testdata/compaction/value_separation
index a8449ce8..a6decb62 100644
--- a/testdata/compaction/value_separation
+++ b/testdata/compaction/value_separation
@@ -151,11 +151,21 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   30B: 41B |     36.7% |         1 |  1 (256KB) |  1 (256KB) |        71 |      0 (0B)
 
+BLOCK CACHE: 4 entries (1.3KB)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [73%]  |  100% [36%]                            100% [4.5%]                100% [32%]
+  L5   100% [9.1%] | 100% [9.1%]
+  L6   100% [18%]  | 100% [9.1%]  100% [4.5%]                                         100% [4.5%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [55%]  100% [4.5%]               100% [4.5%]                100% [36%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-   5 (1.6KB) |       81.8% |     1 (392B) |       89.6% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    1 (392B) |      89.6% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -419,11 +429,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 126B: 156B |     23.8% |         2 |  1 (256KB) |  1 (256KB) |       282 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [69%]  |  100% [23%]                                                       100% [46%]
+  L0   100% [31%]  |  100% [31%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [54%]                                                       100% [46%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-   7 (2.3KB) |       59.6% |     1 (504B) |       78.6% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    1 (504B) |      78.6% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -695,32 +714,32 @@ stats
 a: (hello, .)
 b: (helloworld, .)
 c: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 158B cached; points: 3 (3B keys, 26B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 0B cached, 158B not cached (read time: 0s); points: 3 (3B keys, 26B values)
 d: (hello, .)
 e: (helloworld, .)
 f: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 5 times (5 internal); blocks: 421B cached; points: 6 (6B keys, 43B values); separated: 1 (11B, 11B fetched)
+stats: seeked 1 times (1 internal); stepped 5 times (5 internal); blocks: 0B cached, 421B not cached (read time: 0s); points: 6 (6B keys, 43B values); separated: 1 (11B, 11B fetched)
 g: (hello, .)
 h: (helloworld, .)
 i: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 8 times (8 internal); blocks: 421B cached; points: 9 (9B keys, 52B values); separated: 3 (32B, 32B fetched)
+stats: seeked 1 times (1 internal); stepped 8 times (8 internal); blocks: 0B cached, 421B not cached (read time: 0s); points: 9 (9B keys, 52B values); separated: 3 (32B, 32B fetched)
 j: (hello, .)
 k: (helloworld, .)
 l: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 11 times (11 internal); blocks: 421B cached; points: 12 (12B keys, 61B values); separated: 5 (53B, 53B fetched)
+stats: seeked 1 times (1 internal); stepped 11 times (11 internal); blocks: 0B cached, 421B not cached (read time: 0s); points: 12 (12B keys, 61B values); separated: 5 (53B, 53B fetched)
 m: (hello, .)
 n: (helloworld, .)
 o: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 14 times (14 internal); blocks: 615B cached; points: 15 (15B keys, 79B values); separated: 6 (63B, 63B fetched)
+stats: seeked 1 times (1 internal); stepped 14 times (14 internal); blocks: 0B cached, 615B not cached (read time: 0s); points: 15 (15B keys, 79B values); separated: 6 (63B, 63B fetched)
 p: (hello, .)
 q: (helloworld, .)
 r: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 17 times (17 internal); blocks: 615B cached; points: 18 (18B keys, 105B values); separated: 6 (63B, 63B fetched)
+stats: seeked 1 times (1 internal); stepped 17 times (17 internal); blocks: 0B cached, 615B not cached (read time: 0s); points: 18 (18B keys, 105B values); separated: 6 (63B, 63B fetched)
 s: (hello, .)
 t: (helloworld, .)
 u: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 20 times (20 internal); blocks: 615B cached; points: 21 (21B keys, 131B values); separated: 6 (63B, 63B fetched)
+stats: seeked 1 times (1 internal); stepped 20 times (20 internal); blocks: 0B cached, 615B not cached (read time: 0s); points: 21 (21B keys, 131B values); separated: 6 (63B, 63B fetched)
 v: (hello, .)
 w: (helloworld, .)
 x: (helloworld!, .)
-stats: seeked 1 times (1 internal); stepped 23 times (23 internal); blocks: 615B cached; points: 24 (24B keys, 157B values); separated: 6 (63B, 63B fetched)
+stats: seeked 1 times (1 internal); stepped 23 times (23 internal); blocks: 0B cached, 615B not cached (read time: 0s); points: 24 (24B keys, 157B values); separated: 6 (63B, 63B fetched)
diff --git a/testdata/compaction/virtual_rewrite b/testdata/compaction/virtual_rewrite
index 680d4c9c..66204e90 100644
--- a/testdata/compaction/virtual_rewrite
+++ b/testdata/compaction/virtual_rewrite
@@ -162,11 +162,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 259B: 289B |     11.6% |         2 |  1 (256KB) |  1 (256KB) |       548 |      0 (0B)
 
+BLOCK CACHE: 2 entries (797B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a    78% [78%]  |                                                                   78% [78%]
+  L6    40% [22%]  |   0% [0%]     100% [11%]                                          100% [11%]
+-------------------+------------------------------------------------------------------------------
+total     64.3%    |   0% [0%]     100% [11%]                                          80% [89%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-   8 (2.8KB) |       69.2% |    4 (1.1KB) |       91.9% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+   4 (1.1KB) |      91.9% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
diff --git a/testdata/event_listener b/testdata/event_listener
index 0ac043d7..d45bc946 100644
--- a/testdata/event_listener
+++ b/testdata/event_listener
@@ -283,11 +283,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   48B: 97B |    102.1% |         3 |  1 (256KB) |  1 (256KB) |       145 |      0 (0B)
 
+BLOCK CACHE: 2 entries (672B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [69%]  |              100% [7.7%]                                          100% [62%]
+  L0   100% [31%]  |  100% [31%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [31%]  100% [7.7%]                                          100% [62%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (672B) |        0.0% |       0 (0B) |       50.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      50.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -416,11 +425,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |  82B: 132B |     61.0% |         6 |  1 (512KB) |  1 (512KB) |       214 |   1 (1.3KB)
 
+BLOCK CACHE: 6 entries (2KB)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [83%]  |               100% [13%]                                          100% [70%]
+  L0   100% [17%]  |  100% [17%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [17%]   100% [13%]                                          100% [70%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-     6 (2KB) |        0.0% |       0 (0B) |       50.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      50.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
diff --git a/testdata/ingest b/testdata/ingest
index 5f40a8c9..637c2faf 100644
--- a/testdata/ingest
+++ b/testdata/ingest
@@ -55,11 +55,19 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |     0B: 0B |      0.0% |         0 |  1 (256KB) |     0 (0B) |         0 |      0 (0B)
 
+BLOCK CACHE: 3 entries (924B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [100%] |              100% [9.1%]                                          100% [91%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |              100% [9.1%]                                          100% [91%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    3 (924B) |       15.4% |     1 (280B) |       50.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    1 (280B) |      50.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
diff --git a/testdata/metrics b/testdata/metrics
index a2c136b8..99097bb5 100644
--- a/testdata/metrics
+++ b/testdata/metrics
@@ -36,11 +36,26 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
  22 (24B) |   25B: 26B |      4.0% |         8 |   12 (2GB) |   5 (13MB) |        51 |      4 (6B)
 
+BLOCK CACHE: 100 entries (1GB)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   11% [3.3%]  | 9.1% [0.4%]  9.8% [0.5%]   11% [0.5%]   11% [0.6%]   12% [0.6%]   12% [0.7%]
+  L0   17% [5.9%]  |  15% [0.9%]   16% [0.9%]    16% [1%]     17% [1%]    17% [1.1%]   18% [1.1%]
+  L1   21% [8.6%]  |  20% [1.3%]   20% [1.4%]   21% [1.4%]   21% [1.4%]   22% [1.5%]   22% [1.5%]
+  L2    24% [11%]  |  24% [1.8%]   24% [1.8%]   24% [1.8%]   24% [1.9%]   25% [1.9%]    25% [2%]
+  L3    27% [14%]  |  26% [2.2%]   27% [2.2%]   27% [2.3%]   27% [2.3%]   27% [2.4%]   28% [2.4%]
+  L4    29% [16%]  |  29% [2.6%]   29% [2.7%]   29% [2.7%]   29% [2.8%]   29% [2.8%]   30% [2.9%]
+  L5    31% [19%]  |  30% [3.1%]   31% [3.1%]   31% [3.2%]   31% [3.2%]   31% [3.2%]   31% [3.3%]
+  L6    32% [22%]  |  32% [3.5%]   32% [3.6%]   32% [3.6%]   32% [3.6%]   33% [3.7%]   33% [3.7%]
+-------------------+------------------------------------------------------------------------------
+total     25.7%    |  25% [16%]    25% [16%]    26% [16%]    26% [17%]    26% [17%]    26% [18%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-   100 (1GB) |       66.7% |    180 (1MB) |       48.7% |        47.4% |          21 |           4
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+   180 (1MB) |      48.7% |       47.4% |          21 |           4
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |        live |     zombie |  total |      refed
@@ -119,11 +134,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   17B: 28B |     64.7% |         1 |  1 (256KB) |  1 (256KB) |        45 |      0 (0B)
 
+BLOCK CACHE: 2 entries (683B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0   100% [50%]  |               100% [25%]                                          100% [25%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |               100% [25%]                                          100% [75%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (683B) |        0.0% |     1 (280B) |        0.0% |         0.0% |           1 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    1 (280B) |       0.0% |        0.0% |           1 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -213,11 +237,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   34B: 64B |     88.2% |         2 |  1 (256KB) |  2 (512KB) |        98 |      0 (0B)
 
+BLOCK CACHE: 2 entries (683B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0    50% [50%]  |  50% [25%]    50% [12%]                                           50% [12%]
+-------------------+------------------------------------------------------------------------------
+total     66.7%    |  50% [25%]    50% [12%]                                           83% [62%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (683B) |       33.3% |     2 (560B) |       66.7% |         0.0% |           2 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    2 (560B) |      66.7% |        0.0% |           2 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -289,11 +322,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   34B: 64B |     88.2% |         2 |  1 (256KB) |  2 (512KB) |        98 |      0 (0B)
 
+BLOCK CACHE: 2 entries (683B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0    50% [50%]  |  50% [25%]    50% [12%]                                           50% [12%]
+-------------------+------------------------------------------------------------------------------
+total     66.7%    |  50% [25%]    50% [12%]                                           83% [62%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (683B) |       33.3% |     2 (560B) |       66.7% |         0.0% |           2 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    2 (560B) |      66.7% |        0.0% |           2 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -362,11 +404,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   34B: 64B |     88.2% |         2 |  1 (256KB) |  2 (512KB) |        98 |      0 (0B)
 
+BLOCK CACHE: 2 entries (683B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0    50% [50%]  |  50% [25%]    50% [12%]                                           50% [12%]
+-------------------+------------------------------------------------------------------------------
+total     66.7%    |  50% [25%]    50% [12%]                                           83% [62%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (683B) |       33.3% |     1 (280B) |       66.7% |         0.0% |           1 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    1 (280B) |      66.7% |        0.0% |           1 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -438,11 +489,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   34B: 64B |     88.2% |         2 |  1 (256KB) |  1 (256KB) |        98 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0    50% [50%]  |  50% [25%]    50% [12%]                                           50% [12%]
+-------------------+------------------------------------------------------------------------------
+total     66.7%    |  50% [25%]    50% [12%]                                           83% [62%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |       33.3% |       0 (0B) |       66.7% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      66.7% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -553,11 +613,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 116B: 165B |     42.2% |         3 |  1 (256KB) |  1 (256KB) |       281 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0    50% [50%]  |  50% [25%]    50% [12%]                                           50% [12%]
+-------------------+------------------------------------------------------------------------------
+total     66.7%    |  50% [25%]    50% [12%]                                           83% [62%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |       33.3% |       0 (0B) |       66.7% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      66.7% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -654,11 +723,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 116B: 165B |     42.2% |         3 |  1 (256KB) |  1 (256KB) |       281 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0    82% [50%]  |  89% [44%]    50% [2.8%]                                          50% [2.8%]
+-------------------+------------------------------------------------------------------------------
+total      90%     |  89% [44%]    50% [2.8%]                                          95% [53%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |       10.0% |       0 (0B) |       55.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      55.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -806,11 +884,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 176B: 211B |     19.9% |         8 |    1 (1MB) |    1 (1MB) |       387 |   2 (1.9KB)
 
+BLOCK CACHE: 6 entries (2KB)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [65%]  |              100% [5.9%]                                          100% [59%]
+  L0    82% [35%]  |  89% [31%]     50% [2%]                                            50% [2%]
+-------------------+------------------------------------------------------------------------------
+total     92.7%    |  89% [31%]    80% [7.8%]                                          97% [61%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-     6 (2KB) |        7.3% |       0 (0B) |       55.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      55.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -920,11 +1007,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 223B: 277B |     24.2% |         9 |    1 (1MB) |    1 (1MB) |       500 |   2 (1.9KB)
 
+BLOCK CACHE: 6 entries (2KB)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [65%]  |              100% [5.9%]                                          100% [59%]
+  L0    82% [35%]  |  89% [31%]     50% [2%]                                            50% [2%]
+-------------------+------------------------------------------------------------------------------
+total     92.7%    |  89% [31%]    80% [7.8%]                                          97% [61%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-     6 (2KB) |        7.3% |       0 (0B) |       55.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      55.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1046,11 +1142,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 223B: 277B |     24.2% |         9 |    1 (1MB) |    1 (1MB) |       500 |   2 (1.9KB)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1216,11 +1318,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) | 223B: 277B |     24.2% |         9 |    1 (1MB) |    1 (1MB) |       500 |   2 (1.9KB)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1301,11 +1409,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   27B: 38B |     40.7% |         1 |  1 (256KB) |  1 (256KB) |        65 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1374,11 +1488,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   27B: 38B |     40.7% |         1 |  1 (256KB) |  1 (256KB) |        65 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [50%]  |                                                                   100% [50%]
+  L0   100% [50%]  |  100% [50%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [50%]                                                       100% [50%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |       50.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      50.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1462,11 +1585,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   27B: 38B |     40.7% |         1 |  1 (256KB) |  1 (256KB) |        65 |      0 (0B)
 
+BLOCK CACHE: 2 entries (675B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [65%]  |              100% [5.9%]                                          100% [59%]
+  L0   100% [35%]  |  100% [35%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [35%]  100% [5.9%]                                          100% [59%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (675B) |        0.0% |       0 (0B) |       50.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      50.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1542,11 +1674,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |   44B: 74B |     68.2% |         2 |  1 (256KB) |  1 (256KB) |       118 |      0 (0B)
 
+BLOCK CACHE: 2 entries (675B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [65%]  |              100% [5.9%]                                          100% [59%]
+  L0   100% [35%]  |  100% [35%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |  100% [35%]  100% [5.9%]                                          100% [59%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (675B) |        0.0% |       0 (0B) |       50.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |      50.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1612,11 +1753,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |     0B: 0B |      0.0% |         1 |  1 (512KB) |  1 (256KB) |         0 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1683,11 +1830,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |     0B: 0B |      0.0% |         1 |  1 (512KB) |  1 (256KB) |         0 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1793,11 +1946,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |  57B: 106B |     86.0% |         3 |  1 (256KB) |  1 (256KB) |       163 |      0 (0B)
 
+BLOCK CACHE: 2 entries (662B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [67%]  |                                                                   100% [67%]
+  L0   100% [33%]  |               100% [33%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |               100% [33%]                                          100% [67%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (662B) |        0.0% |     2 (560B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    2 (560B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -1863,11 +2025,20 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    1 (0B) |  57B: 106B |     86.0% |         3 |  1 (256KB) |  1 (256KB) |       163 |      0 (0B)
 
+BLOCK CACHE: 2 entries (662B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+ n/a   100% [67%]  |                                                                   100% [67%]
+  L0   100% [33%]  |               100% [33%]
+-------------------+------------------------------------------------------------------------------
+total     100%     |               100% [33%]                                          100% [67%]
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-    2 (662B) |        0.0% |     2 (560B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+    2 (560B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
diff --git a/tool/testdata/db_lsm b/tool/testdata/db_lsm
index 6f934597..3b44ea6d 100644
--- a/tool/testdata/db_lsm
+++ b/tool/testdata/db_lsm
@@ -34,11 +34,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    0 (0B) |     0B: 0B |      0.0% |         0 |  1 (256KB) |     0 (0B) |         0 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed
@@ -94,11 +100,17 @@ COMMIT PIPELINE
 ----------+------------+-----------+-----------+------------+------------+-----------+------------
    0 (0B) |     0B: 0B |      0.0% |         0 |  1 (256KB) |     0 (0B) |         0 |      0 (0B)
 
+BLOCK CACHE: 0 entries (0B)
+                 miss rate [percentage of total misses] since start
+level      all     |  background    sstdata       sstval      blobval       filter       index
+-------------------+------------------------------------------------------------------------------
+total      0%      |
+
 ITERATORS
-        block cache        |         file cache         |    filter    |  sst iters  |  snapshots
-     entries |    hit rate |      entries |    hit rate |         util |        open |        open
--------------+-------------+--------------+-------------+--------------+-------------+------------
-      0 (0B) |        0.0% |       0 (0B) |        0.0% |         0.0% |           0 |           0
+        file cache        |    filter   |    open     |    open
+     entries |   hit rate | utilization |  sst iters  |  snapshots
+-------------+------------+-------------+-------------+------------
+      0 (0B) |       0.0% |        0.0% |           0 |           0
 
 FILES                 tables                       |       blob files        |     blob values
    stats prog |    backing |                zombie |       live |     zombie |  total |      refed