Repository: cockroachdb/pebble
Pull request #3646 (2024-06-05 19:01:41)

sstable: clean up TODO in fragmentBlockIter
The way we use `Decode` to append to an existing span is pretty
subtle. This change adds a separate `DecodeIntoSpan` which is used to
add keys to an existing span. Checks for consistency of the start/end
keys are moved here.

cockroach-teamcity: 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cockroachdb/pebble/3646)
<!-- Reviewable:end -->


Patch:
diff --git a/go.mod b/go.mod
index 8c4ec8f6..579a4f0d 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0
 	github.com/cockroachdb/datadriven v1.0.3-0.20240530155848-7682d40af056
 	github.com/cockroachdb/errors v1.11.1
+	github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce
 	github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895
 	github.com/cockroachdb/redact v1.1.5
 	github.com/cockroachdb/swiss v0.0.0-20240605133600-232b93a2b829
@@ -20,10 +21,10 @@ require (
 	github.com/prometheus/client_golang v1.12.0
 	github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a
 	github.com/spf13/cobra v1.0.0
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
 	golang.org/x/perf v0.0.0-20230113213139-801c7ef9e5c5
-	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
+	golang.org/x/sync v0.7.0
 	golang.org/x/sys v0.17.0
 )
 
diff --git a/go.sum b/go.sum
index 9f8d4858..541e76e3 100644
--- a/go.sum
+++ b/go.sum
@@ -75,6 +75,8 @@ github.com/cockroachdb/datadriven v1.0.3-0.20240530155848-7682d40af056 h1:slXych
 github.com/cockroachdb/datadriven v1.0.3-0.20240530155848-7682d40af056/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
 github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=
 github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
+github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
+github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
 github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
 github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
 github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPYtvELQ/h4IrmPAohXqe2pWA8Bwhejr3VQoZsA=
@@ -323,8 +325,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@@ -444,8 +446,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/internal/base/iterator.go b/internal/base/iterator.go
index bea935eb..17b3b1b3 100644
--- a/internal/base/iterator.go
+++ b/internal/base/iterator.go
@@ -436,17 +436,21 @@ func (s *InternalIteratorStats) String() string {
 
 // SafeFormat implements the redact.SafeFormatter interface.
 func (s *InternalIteratorStats) SafeFormat(p redact.SafePrinter, verb rune) {
-	var tombstoned humanize.FormattedString
+	p.Printf("blocks: %s cached",
+		humanize.Bytes.Uint64(s.BlockBytesInCache),
+	)
+	if s.BlockBytes != s.BlockBytesInCache || s.BlockReadDuration != 0 {
+		p.Printf(", %s not cached (read time: %s)",
+			humanize.Bytes.Uint64(s.BlockBytes-s.BlockBytesInCache),
+			humanize.FormattedString(s.BlockReadDuration.String()),
+		)
+	}
+	p.Printf("; points: %s", humanize.Count.Uint64(s.PointCount))
+
 	if s.PointsCoveredByRangeTombstones != 0 {
-		tombstoned = "(" + humanize.Count.Uint64(s.PointsCoveredByRangeTombstones) + " tombstoned)"
+		p.Printf("(%s tombstoned)", humanize.Count.Uint64(s.PointsCoveredByRangeTombstones))
 	}
-	p.Printf("blocks: %s (%s cached), read time %s; "+
-		"points: %s%s (%s keys, %s values)",
-		humanize.Bytes.Uint64(s.BlockBytes),
-		humanize.Bytes.Uint64(s.BlockBytesInCache),
-		humanize.FormattedString(s.BlockReadDuration.String()),
-		humanize.Count.Uint64(s.PointCount),
-		tombstoned,
+	p.Printf(" (%s keys, %s values)",
 		humanize.Bytes.Uint64(s.KeyBytes),
 		humanize.Bytes.Uint64(s.ValueBytes),
 	)
diff --git a/internal/rangedel/rangedel.go b/internal/rangedel/rangedel.go
index f8504bb7..f5ea96e7 100644
--- a/internal/rangedel/rangedel.go
+++ b/internal/rangedel/rangedel.go
@@ -6,6 +6,7 @@ package rangedel
 
 import (
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/invariants"
 	"github.com/cockroachdb/pebble/internal/keyspan"
 )
 
@@ -41,3 +42,22 @@ func Decode(ik base.InternalKey, v []byte, keysDst []keyspan.Key) keyspan.Span {
 		}),
 	}
 }
+
+// DecodeIntoSpan decodes an internal key pair encoding a range deletion and
+// appends a key to the given span. The start and end keys must match those in
+// the span.
+func DecodeIntoSpan(cmp base.Compare, ik base.InternalKey, v []byte, s *keyspan.Span) error {
+	// This function should only be called when ik.UserKey matches the Start of
+	// the span we already have. If this is not the case, it is a bug in the
+	// calling code.
+	if invariants.Enabled && cmp(s.Start, ik.UserKey) != 0 {
+		return base.AssertionFailedf("DecodeIntoSpan called with different start key")
+	}
+	// The value can come from disk or from the user, so we want to check the end
+	// key in all builds.
+	if cmp(s.End, v) != 0 {
+		return base.CorruptionErrorf("pebble: corrupt range key fragmentation")
+	}
+	s.Keys = append(s.Keys, keyspan.Key{Trailer: ik.Trailer})
+	return nil
+}
diff --git a/internal/rangekey/rangekey.go b/internal/rangekey/rangekey.go
index 2a99834a..1ac71059 100644
--- a/internal/rangekey/rangekey.go
+++ b/internal/rangekey/rangekey.go
@@ -52,8 +52,8 @@ package rangekey
 import (
 	"encoding/binary"
 
-	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/invariants"
 	"github.com/cockroachdb/pebble/internal/keyspan"
 )
 
@@ -161,30 +161,59 @@ func (e *Encoder) flush(s *keyspan.Span, seqNum uint64, del bool) error {
 }
 
 // Decode takes an internal key pair encoding range key(s) and returns a decoded
-// keyspan containing the keys. If keysDst is provided, keys will be appended to
-// keysDst.
-func Decode(ik base.InternalKey, v []byte, keysDst []keyspan.Key) (keyspan.Span, error) {
+// keyspan containing the keys. If keysBuf is provided, keys will be appended to
+// it.
+func Decode(ik base.InternalKey, v []byte, keysBuf []keyspan.Key) (keyspan.Span, error) {
 	var s keyspan.Span
+	s.Start = ik.UserKey
+	var err error
+	s.End, v, err = DecodeEndKey(ik.Kind(), v)
+	if err != nil {
+		return keyspan.Span{}, err
+	}
+	s.Keys, err = appendKeys(keysBuf, ik, v)
+	if err != nil {
+		return keyspan.Span{}, err
+	}
+	return s, nil
+}
 
+// DecodeIntoSpan decodes an internal key pair encoding range key(s) and appends
+// them to the given span. The start and end keys must match those in the span.
+func DecodeIntoSpan(cmp base.Compare, ik base.InternalKey, v []byte, s *keyspan.Span) error {
 	// Hydrate the user key bounds.
-	s.Start = ik.UserKey
-	var ok bool
-	s.End, v, ok = DecodeEndKey(ik.Kind(), v)
-	if !ok {
-		return keyspan.Span{}, base.CorruptionErrorf("pebble: unable to decode range key end from %s", ik.Kind())
+	startKey := ik.UserKey
+	endKey, v, err := DecodeEndKey(ik.Kind(), v)
+	if err != nil {
+		return err
 	}
-	s.Keys = keysDst
+	// This function should only be called when ik.UserKey matches the Start of
+	// the span we already have. If this is not the case, it is a bug in the
+	// calling code.
+	if invariants.Enabled && cmp(s.Start, startKey) != 0 {
+		return base.AssertionFailedf("DecodeIntoSpan called with different start key")
+	}
+	// The value can come from disk or from the user, so we want to check the end
+	// key in all builds.
+	if cmp(s.End, endKey) != 0 {
+		return base.CorruptionErrorf("pebble: corrupt range key fragmentation")
+	}
+	s.Keys, err = appendKeys(s.Keys, ik, v)
+	return err
+}
 
+func appendKeys(buf []keyspan.Key, ik base.InternalKey, v []byte) ([]keyspan.Key, error) {
 	// Hydrate the contents of the range key(s).
 	switch ik.Kind() {
 	case base.InternalKeyKindRangeKeySet:
 		for len(v) > 0 {
 			var sv SuffixValue
+			var ok bool
 			sv, v, ok = decodeSuffixValue(v)
 			if !ok {
-				return keyspan.Span{}, base.CorruptionErrorf("pebble: unable to decode range key suffix-value tuple")
+				return nil, base.CorruptionErrorf("pebble: unable to decode range key suffix-value tuple")
 			}
-			s.Keys = append(s.Keys, keyspan.Key{
+			buf = append(buf, keyspan.Key{
 				Trailer: ik.Trailer,
 				Suffix:  sv.Suffix,
 				Value:   sv.Value,
@@ -193,24 +222,25 @@ func Decode(ik base.InternalKey, v []byte, keysDst []keyspan.Key) (keyspan.Span,
 	case base.InternalKeyKindRangeKeyUnset:
 		for len(v) > 0 {
 			var suffix []byte
+			var ok bool
 			suffix, v, ok = decodeSuffix(v)
 			if !ok {
-				return keyspan.Span{}, base.CorruptionErrorf("pebble: unable to decode range key unset suffix")
+				return nil, base.CorruptionErrorf("pebble: unable to decode range key unset suffix")
 			}
-			s.Keys = append(s.Keys, keyspan.Key{
+			buf = append(buf, keyspan.Key{
 				Trailer: ik.Trailer,
 				Suffix:  suffix,
 			})
 		}
 	case base.InternalKeyKindRangeKeyDelete:
 		if len(v) > 0 {
-			return keyspan.Span{}, base.CorruptionErrorf("pebble: RANGEKEYDELs must not contain additional data")
+			return nil, base.CorruptionErrorf("pebble: RANGEKEYDELs must not contain additional data")
 		}
-		s.Keys = append(s.Keys, keyspan.Key{Trailer: ik.Trailer})
+		buf = append(buf, keyspan.Key{Trailer: ik.Trailer})
 	default:
-		return keyspan.Span{}, base.CorruptionErrorf("pebble: %s is not a range key", ik.Kind())
+		return nil, base.CorruptionErrorf("pebble: %s is not a range key", ik.Kind())
 	}
-	return s, nil
+	return buf, nil
 }
 
 // SuffixValue represents a tuple of a suffix and a corresponding value. A
@@ -284,21 +314,23 @@ func EncodeSetValue(dst []byte, endKey []byte, suffixValues []SuffixValue) int {
 // DecodeEndKey reads the end key from the beginning of a range key (RANGEKEYSET,
 // RANGEKEYUNSET or RANGEKEYDEL)'s physical encoded value. Both sets and unsets
 // encode the range key, plus additional data in the value.
-func DecodeEndKey(kind base.InternalKeyKind, data []byte) (endKey, value []byte, ok bool) {
+func DecodeEndKey(kind base.InternalKeyKind, data []byte) (endKey, value []byte, _ error) {
 	switch kind {
 	case base.InternalKeyKindRangeKeyDelete:
 		// No splitting is necessary for range key deletes. The value is the end
 		// key, and there is no additional associated value.
-		return data, nil, true
+		return data, nil, nil
+
 	case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset:
 		v, n := binary.Uvarint(data)
 		if n <= 0 || uint64(n)+v >= uint64(len(data)) {
-			return nil, nil, false
+			return nil, nil, base.CorruptionErrorf("pebble: unable to decode range key end from %s", kind)
 		}
 		endKey, value = data[n:n+int(v)], data[n+int(v):]
-		return endKey, value, true
+		return endKey, value, nil
+
 	default:
-		panic(errors.Newf("key kind %s is not a range key kind", kind))
+		return nil, nil, base.AssertionFailedf("key kind %s is not a range key kind", kind)
 	}
 }
 
diff --git a/objstorage/objstorageprovider/provider.go b/objstorage/objstorageprovider/provider.go
index e24f5431..729f61a5 100644
--- a/objstorage/objstorageprovider/provider.go
+++ b/objstorage/objstorageprovider/provider.go
@@ -87,6 +87,17 @@ type Settings struct {
 	// out a large chunk of dirty filesystem buffers.
 	BytesPerSync int
 
+	// Local contains fields that are only relevant for files stored on the local
+	// filesystem.
+	Local struct {
+		// TODO(radu): move FSCleaner, NoSyncOnClose, BytesPerSync here.
+
+		// ReadaheadConfigFn is a function used to retrieve the current readahead
+		// mode. This function is run whenever a local object is open for reading.
+		// If it is nil, DefaultReadaheadConfig is used.
+		ReadaheadConfigFn func() ReadaheadConfig
+	}
+
 	// Fields here are set only if the provider is to support remote objects
 	// (experimental).
 	Remote struct {
@@ -124,6 +135,43 @@ type Settings struct {
 	}
 }
 
+// ReadaheadConfig controls the use of read-ahead.
+type ReadaheadConfig struct {
+	// Informed is the type of read-ahead for operations that are known to read a
+	// large consecutive chunk of a file.
+	Informed ReadaheadMode
+
+	// Speculative is the type of read-ahead used automatically, when consecutive
+	// reads are detected.
+	Speculative ReadaheadMode
+}
+
+// DefaultReadaheadConfig is the readahead config used when ReadaheadConfigFn is
+// not specified.
+var DefaultReadaheadConfig = ReadaheadConfig{
+	Informed:    FadviseSequential,
+	Speculative: FadviseSequential,
+}
+
+// ReadaheadMode indicates the type of read-ahead to use, either for informed
+// read-ahead (e.g. compactions) or speculative read-ahead.
+type ReadaheadMode uint8
+
+const (
+	// NoReadahead disables readahead altogether.
+	NoReadahead ReadaheadMode = iota
+
+	// SysReadahead enables the use of SYS_READAHEAD call to prefetch data.
+	// The prefetch window grows dynamically as consecutive writes are detected.
+	SysReadahead
+
+	// FadviseSequential enables the use of FADV_SEQUENTIAL. For informed
+	// read-ahead, FADV_SEQUENTIAL is used from the beginning. For speculative
+	// read-ahead, SYS_READAHEAD is first used until the window reaches the
+	// maximum size, then we switch to FADV_SEQUENTIAL.
+	FadviseSequential
+)
+
 // DefaultSettings initializes default settings (with no remote storage),
 // suitable for tests and tools.
 func DefaultSettings(fs vfs.FS, dirName string) Settings {
diff --git a/objstorage/objstorageprovider/readahead.go b/objstorage/objstorageprovider/readahead.go
index e29155d8..70376591 100644
--- a/objstorage/objstorageprovider/readahead.go
+++ b/objstorage/objstorageprovider/readahead.go
@@ -4,6 +4,8 @@
 
 package objstorageprovider
 
+import "github.com/cockroachdb/pebble/internal/invariants"
+
 const (
 	// Constants for dynamic readahead of data blocks. Note that the size values
 	// make sense as some multiple of the default block size; and they should
@@ -55,6 +57,9 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
 func (rs *readaheadState) maybeReadaheadOrCacheHit(
 	offset, blockLength int64, readahead bool,
 ) int64 {
+	if invariants.Enabled && rs.maxReadaheadSize == 0 {
+		panic("readaheadState not initialized")
+	}
 	currentReadEnd := offset + blockLength
 	if rs.numReads >= minFileReadsForReadahead {
 		// The minimum threshold of sequential reads to justify reading ahead
diff --git a/objstorage/objstorageprovider/vfs.go b/objstorage/objstorageprovider/vfs.go
index 3350c354..2ce4175c 100644
--- a/objstorage/objstorageprovider/vfs.go
+++ b/objstorage/objstorageprovider/vfs.go
@@ -31,7 +31,11 @@ func (p *provider) vfsOpenForReading(
 		}
 		return nil, err
 	}
-	return newFileReadable(file, p.st.FS, filename)
+	readaheadConfig := DefaultReadaheadConfig
+	if f := p.st.Local.ReadaheadConfigFn; f != nil {
+		readaheadConfig = f()
+	}
+	return newFileReadable(file, p.st.FS, readaheadConfig, filename)
 }
 
 func (p *provider) vfsCreate(
diff --git a/objstorage/objstorageprovider/vfs_readable.go b/objstorage/objstorageprovider/vfs_readable.go
index ecf73838..ddc0786e 100644
--- a/objstorage/objstorageprovider/vfs_readable.go
+++ b/objstorage/objstorageprovider/vfs_readable.go
@@ -27,22 +27,26 @@ type fileReadable struct {
 
 	// The following fields are used to possibly open the file again using the
 	// sequential reads option (see vfsReadHandle).
-	filename string
-	fs       vfs.FS
+	filename        string
+	fs              vfs.FS
+	readaheadConfig ReadaheadConfig
 }
 
 var _ objstorage.Readable = (*fileReadable)(nil)
 
-func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, error) {
+func newFileReadable(
+	file vfs.File, fs vfs.FS, readaheadConfig ReadaheadConfig, filename string,
+) (*fileReadable, error) {
 	info, err := file.Stat()
 	if err != nil {
 		return nil, err
 	}
 	r := &fileReadable{
-		file:     file,
-		size:     info.Size(),
-		filename: filename,
-		fs:       fs,
+		file:            file,
+		size:            info.Size(),
+		filename:        filename,
+		fs:              fs,
+		readaheadConfig: readaheadConfig,
 	}
 	invariants.SetFinalizer(r, func(obj interface{}) {
 		if obj.(*fileReadable).file != nil {
@@ -78,14 +82,14 @@ func (r *fileReadable) NewReadHandle(
 	ctx context.Context, readBeforeSize objstorage.ReadBeforeSize,
 ) objstorage.ReadHandle {
 	rh := readHandlePool.Get().(*vfsReadHandle)
-	rh.r = r
-	rh.rs = makeReadaheadState(fileMaxReadaheadSize)
+	rh.init(r)
 	return rh
 }
 
 type vfsReadHandle struct {
-	r  *fileReadable
-	rs readaheadState
+	r             *fileReadable
+	rs            readaheadState
+	readaheadMode ReadaheadMode
 
 	// sequentialFile holds a file descriptor to the same underlying File,
 	// except with fadvise(FADV_SEQUENTIAL) called on it to take advantage of
@@ -110,6 +114,14 @@ var readHandlePool = sync.Pool{
 	},
 }
 
+func (rh *vfsReadHandle) init(r *fileReadable) {
+	*rh = vfsReadHandle{
+		r:             r,
+		rs:            makeReadaheadState(fileMaxReadaheadSize),
+		readaheadMode: r.readaheadConfig.Speculative,
+	}
+}
+
 // Close is part of the objstorage.ReadHandle interface.
 func (rh *vfsReadHandle) Close() error {
 	var err error
@@ -123,14 +135,17 @@ func (rh *vfsReadHandle) Close() error {
 
 // ReadAt is part of the objstorage.ReadHandle interface.
 func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error {
-	var n int
-	var err error
 	if rh.sequentialFile != nil {
 		// Use OS-level read-ahead.
-		n, err = rh.sequentialFile.ReadAt(p, offset)
-	} else {
+		n, err := rh.sequentialFile.ReadAt(p, offset)
+		if invariants.Enabled && err == nil && n != len(p) {
+			panic("short read")
+		}
+		return err
+	}
+	if rh.readaheadMode != NoReadahead {
 		if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 {
-			if readaheadSize >= fileMaxReadaheadSize {
+			if rh.readaheadMode == FadviseSequential && readaheadSize >= fileMaxReadaheadSize {
 				// We've reached the maximum readahead size. Beyond this point, rely on
 				// OS-level readahead.
 				rh.switchToOSReadahead()
@@ -138,8 +153,8 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
 				_ = rh.r.file.Prefetch(offset, readaheadSize)
 			}
 		}
-		n, err = rh.r.file.ReadAt(p, offset)
 	}
+	n, err := rh.r.file.ReadAt(p, offset)
 	if invariants.Enabled && err == nil && n != len(p) {
 		panic("short read")
 	}
@@ -148,10 +163,16 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
 
 // SetupForCompaction is part of the objstorage.ReadHandle interface.
 func (rh *vfsReadHandle) SetupForCompaction() {
-	rh.switchToOSReadahead()
+	rh.readaheadMode = rh.r.readaheadConfig.Informed
+	if rh.readaheadMode == FadviseSequential {
+		rh.switchToOSReadahead()
+	}
 }
 
 func (rh *vfsReadHandle) switchToOSReadahead() {
+	if invariants.Enabled && rh.readaheadMode != FadviseSequential {
+		panic("readheadMode not respected")
+	}
 	if rh.sequentialFile != nil {
 		return
 	}
@@ -166,8 +187,8 @@ func (rh *vfsReadHandle) switchToOSReadahead() {
 
 // RecordCacheHit is part of the objstorage.ReadHandle interface.
 func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {
-	if rh.sequentialFile != nil {
-		// Using OS-level readahead, so do nothing.
+	if rh.sequentialFile != nil || rh.readaheadMode == NoReadahead {
+		// Using OS-level or no readahead, so do nothing.
 		return
 	}
 	rh.rs.recordCacheHit(offset, size)
@@ -213,8 +234,7 @@ func UsePreallocatedReadHandle(
 	rh *PreallocatedReadHandle,
 ) objstorage.ReadHandle {
 	if r, ok := readable.(*fileReadable); ok {
-		// See fileReadable.NewReadHandle.
-		rh.vfsReadHandle = vfsReadHandle{r: r}
+		rh.init(r)
 		return rh
 	}
 	return readable.NewReadHandle(ctx, readBeforeSize)
diff --git a/open.go b/open.go
index cab6196e..cb986299 100644
--- a/open.go
+++ b/open.go
@@ -297,6 +297,7 @@ func Open(dirname string, opts *Options) (db *DB, err error) {
 		NoSyncOnClose:       opts.NoSyncOnClose,
 		BytesPerSync:        opts.BytesPerSync,
 	}
+	providerSettings.Local.ReadaheadConfigFn = opts.Local.ReadaheadConfigFn
 	providerSettings.Remote.StorageFactory = opts.Experimental.RemoteStorage
 	providerSettings.Remote.CreateOnShared = opts.Experimental.CreateOnShared
 	providerSettings.Remote.CreateOnSharedLocator = opts.Experimental.CreateOnSharedLocator
diff --git a/options.go b/options.go
index 6fbb7d1b..c2e1140d 100644
--- a/options.go
+++ b/options.go
@@ -15,11 +15,13 @@ import (
 	"unicode"
 
 	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/fifo"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/cache"
 	"github.com/cockroachdb/pebble/internal/humanize"
 	"github.com/cockroachdb/pebble/internal/keyspan"
 	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
 	"github.com/cockroachdb/pebble/objstorage/remote"
 	"github.com/cockroachdb/pebble/rangekey"
 	"github.com/cockroachdb/pebble/sstable"
@@ -483,11 +485,25 @@ type Options struct {
 	// The default cache size is 8 MB.
 	Cache *cache.Cache
 
+	// LoadBlockSema, if set, is used to limit the number of blocks that can be
+	// loaded (i.e. read from the filesystem) in parallel. Each load acquires one
+	// unit from the semaphore for the duration of the read.
+	LoadBlockSema *fifo.Semaphore
+
 	// Cleaner cleans obsolete files.
 	//
 	// The default cleaner uses the DeleteCleaner.
 	Cleaner Cleaner
 
+	// Local contains option that pertain to files stored on the local filesystem.
+	Local struct {
+		// ReadaheadConfigFn is a function used to retrieve the current readahead
+		// mode. This function is consulted when a table enters the table cache.
+		ReadaheadConfigFn func() ReadaheadConfig
+
+		// TODO(radu): move BytesPerSync, LoadBlockSema, Cleaner here.
+	}
+
 	// Comparer defines a total ordering over the space of []byte keys: a 'less
 	// than' relationship. The same comparison algorithm must be used for reads
 	// and writes over the lifetime of the DB.
@@ -1111,6 +1127,9 @@ type WALFailoverOptions struct {
 	wal.FailoverOptions
 }
 
+// ReadaheadConfig controls the use of read-ahead.
+type ReadaheadConfig = objstorageprovider.ReadaheadConfig
+
 // DebugCheckLevels calls CheckLevels on the provided database.
 // It may be set in the DebugCheck field of Options to check
 // level invariants whenever a new version is installed.
@@ -1934,6 +1953,7 @@ func (o *Options) MakeReaderOptions() sstable.ReaderOptions {
 	var readerOpts sstable.ReaderOptions
 	if o != nil {
 		readerOpts.Cache = o.Cache
+		readerOpts.LoadBlockSema = o.LoadBlockSema
 		readerOpts.Comparer = o.Comparer
 		readerOpts.Filters = o.Filters
 		if o.Merger != nil {
diff --git a/sstable/block_fragment_iter.go b/sstable/block_fragment_iter.go
index 9d1ff919..42106367 100644
--- a/sstable/block_fragment_iter.go
+++ b/sstable/block_fragment_iter.go
@@ -43,29 +43,42 @@ type fragmentBlockIter struct {
 	elideSameSeqnum bool
 }
 
-func (i *fragmentBlockIter) resetForReuse() fragmentBlockIter {
-	return fragmentBlockIter{blockIter: i.blockIter.resetForReuse()}
+func (i *fragmentBlockIter) Init(elideSameSeqnum bool) {
+	// Use the i.keyBuf array to back the Keys slice to prevent an allocation
+	// when the spans contain few keys.
+	i.span.Keys = i.keyBuf[:0]
+	i.elideSameSeqnum = elideSameSeqnum
+}
+
+func (i *fragmentBlockIter) ResetForReuse() {
+	*i = fragmentBlockIter{blockIter: i.blockIter.resetForReuse()}
 }
 
-func (i *fragmentBlockIter) decodeSpanKeys(kv *base.InternalKV, internalValue []byte) error {
-	// TODO(jackson): The use of i.span.Keys to accumulate keys across multiple
-	// calls to Decode is too confusing and subtle. Refactor to make it
-	// explicit.
+// initSpan initializes the span with a single fragment.
+// Note that the span start and end keys and range key contents are aliased to
+// the key or value. This is ok because the range del/key block doesn't use
+// prefix compression (and we don't perform any transforms), so the key/value
+// will be pointing directly into the buffer data.
+func (i *fragmentBlockIter) initSpan(ik base.InternalKey, internalValue []byte) error {
+	var err error
+	if ik.Kind() == base.InternalKeyKindRangeDelete {
+		i.span = rangedel.Decode(ik, internalValue, i.span.Keys[:0])
+	} else {
+		i.span, err = rangekey.Decode(ik, internalValue, i.span.Keys[:0])
+	}
+	return err
+}
 
-	// decode the contents of the fragment's value. This always includes at
-	// least the end key: RANGEDELs store the end key directly as the value,
-	// whereas the various range key kinds store are more complicated.  The
-	// details of the range key internal value format are documented within the
-	// internal/rangekey package.
+// addToSpan adds a fragment to the existing span. The fragment must be for the
+// same start/end keys.
+func (i *fragmentBlockIter) addToSpan(
+	cmp base.Compare, ik base.InternalKey, internalValue []byte,
+) error {
 	var err error
-	switch kv.Kind() {
-	case base.InternalKeyKindRangeDelete:
-		i.span = rangedel.Decode(kv.K, internalValue, i.span.Keys)
-	case base.InternalKeyKindRangeKeySet, base.InternalKeyKindRangeKeyUnset, base.InternalKeyKindRangeKeyDelete:
-		i.span, err = rangekey.Decode(kv.K, internalValue, i.span.Keys)
-	default:
-		i.span = keyspan.Span{}
-		err = base.CorruptionErrorf("pebble: corrupt keyspan fragment of kind %d", kv.Kind())
+	if ik.Kind() == base.InternalKeyKindRangeDelete {
+		err = rangedel.DecodeIntoSpan(cmp, ik, internalValue, &i.span)
+	} else {
+		err = rangekey.DecodeIntoSpan(cmp, ik, internalValue, &i.span)
 	}
 	return err
 }
@@ -106,31 +119,20 @@ func (i *fragmentBlockIter) gatherForward(kv *base.InternalKV) (*keyspan.Span, e
 	i.span.Keys = i.keyBuf[:0]
 
 	// Decode the span's end key and individual keys from the value.
-	internalValue := kv.V.InPlaceValue()
-	if err := i.decodeSpanKeys(kv, internalValue); err != nil {
+	if err := i.initSpan(kv.K, kv.InPlaceValue()); err != nil {
 		return nil, err
 	}
-	prevEnd := i.span.End
 
 	// There might exist additional internal keys with identical bounds encoded
 	// within the block. Iterate forward, accumulating all the keys with
 	// identical bounds to s.
-	kv = i.blockIter.Next()
-	for kv != nil && i.blockIter.cmp(kv.K.UserKey, i.span.Start) == 0 {
-		internalValue = kv.InPlaceValue()
-		if err := i.decodeSpanKeys(kv, internalValue); err != nil {
-			return nil, err
-		}
 
-		// Since k indicates an equal start key, the encoded end key must
-		// exactly equal the original end key from the first internal key.
-		// Overlapping fragments are required to have exactly equal start and
-		// end bounds.
-		if i.blockIter.cmp(prevEnd, i.span.End) != 0 {
-			i.span = keyspan.Span{}
-			return nil, base.CorruptionErrorf("pebble: corrupt keyspan fragmentation")
+	// Overlapping fragments are required to have exactly equal start and
+	// end bounds.
+	for kv = i.blockIter.Next(); kv != nil && i.blockIter.cmp(kv.K.UserKey, i.span.Start) == 0; kv = i.blockIter.Next() {
+		if err := i.addToSpan(i.blockIter.cmp, kv.K, kv.InPlaceValue()); err != nil {
+			return nil, err
 		}
-		kv = i.blockIter.Next()
 	}
 	if i.elideSameSeqnum && len(i.span.Keys) > 0 {
 		i.elideKeysOfSameSeqNum()
@@ -152,36 +154,22 @@ func (i *fragmentBlockIter) gatherBackward(kv *base.InternalKV) (*keyspan.Span,
 	if kv == nil || !i.blockIter.valid() {
 		return nil, nil
 	}
-	// Use the i.keyBuf array to back the Keys slice to prevent an allocation
-	// when a span contains few keys.
-	i.span.Keys = i.keyBuf[:0]
 
 	// Decode the span's end key and individual keys from the value.
-	internalValue := kv.V.InPlaceValue()
-	if err := i.decodeSpanKeys(kv, internalValue); err != nil {
+	if err := i.initSpan(kv.K, kv.InPlaceValue()); err != nil {
 		return nil, err
 	}
-	prevEnd := i.span.End
 
 	// There might exist additional internal keys with identical bounds encoded
 	// within the block. Iterate backward, accumulating all the keys with
 	// identical bounds to s.
-	kv = i.blockIter.Prev()
-	for kv != nil && i.blockIter.cmp(kv.K.UserKey, i.span.Start) == 0 {
-		internalValue = kv.V.InPlaceValue()
-		if err := i.decodeSpanKeys(kv, internalValue); err != nil {
+	//
+	// Overlapping fragments are required to have exactly equal start and
+	// end bounds.
+	for kv = i.blockIter.Prev(); kv != nil && i.blockIter.cmp(kv.K.UserKey, i.span.Start) == 0; kv = i.blockIter.Prev() {
+		if err := i.addToSpan(i.blockIter.cmp, kv.K, kv.InPlaceValue()); err != nil {
 			return nil, err
 		}
-
-		// Since k indicates an equal start key, the encoded end key must
-		// exactly equal the original end key from the first internal key.
-		// Overlapping fragments are required to have exactly equal start and
-		// end bounds.
-		if i.blockIter.cmp(prevEnd, i.span.End) != 0 {
-			i.span = keyspan.Span{}
-			return nil, base.CorruptionErrorf("pebble: corrupt keyspan fragmentation")
-		}
-		kv = i.blockIter.Prev()
 	}
 	// i.blockIter is positioned over the last internal key for the previous
 	// span.
diff --git a/sstable/options.go b/sstable/options.go
index f94ec995..98013739 100644
--- a/sstable/options.go
+++ b/sstable/options.go
@@ -5,6 +5,7 @@
 package sstable
 
 import (
+	"github.com/cockroachdb/fifo"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/cache"
 )
@@ -85,6 +86,11 @@ type ReaderOptions struct {
 	// The default cache size is a zero-size cache.
 	Cache *cache.Cache
 
+	// LoadBlockSema, if set, is used to limit the number of blocks that can be
+	// loaded (i.e. read from the filesystem) in parallel. Each load acquires one
+	// unit from the semaphore for the duration of the read.
+	LoadBlockSema *fifo.Semaphore
+
 	// User properties specified in this map will not be added to sst.Properties.UserProperties.
 	DeniedUserProperties map[string]struct{}
 
diff --git a/sstable/reader.go b/sstable/reader.go
index d1e047c4..bbb6792c 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -498,7 +498,7 @@ type rangeKeyFragmentBlockIter struct {
 
 func (i *rangeKeyFragmentBlockIter) Close() error {
 	err := i.fragmentBlockIter.Close()
-	i.fragmentBlockIter = i.fragmentBlockIter.resetForReuse()
+	i.fragmentBlockIter.ResetForReuse()
 	rangeKeyFragmentBlockIterPool.Put(i)
 	return err
 }
@@ -629,6 +629,15 @@ func (r *Reader) readBlock(
 	}
 
 	// Cache miss.
+
+	if sema := r.opts.LoadBlockSema; sema != nil {
+		if err := sema.Acquire(ctx, 1); err != nil {
+			// An error here can only come from the context.
+			return bufferHandle{}, err
+		}
+		defer sema.Release(1)
+	}
+
 	var compressed cacheValueOrBuf
 	if bufferPool != nil {
 		compressed = cacheValueOrBuf{
diff --git a/sstable/writer.go b/sstable/writer.go
index 7f29dc66..acf98b56 100644
--- a/sstable/writer.go
+++ b/sstable/writer.go
@@ -1263,19 +1263,17 @@ func (w *Writer) encodeRangeKeySpan(span keyspan.Span) {
 func (w *Writer) addRangeKey(key InternalKey, value []byte) error {
 	if !w.disableKeyOrderChecks && w.rangeKeyBlock.nEntries > 0 {
 		prevStartKey := w.rangeKeyBlock.getCurKey()
-		prevEndKey, _, ok := rangekey.DecodeEndKey(prevStartKey.Kind(), w.rangeKeyBlock.curValue)
-		if !ok {
+		prevEndKey, _, err := rangekey.DecodeEndKey(prevStartKey.Kind(), w.rangeKeyBlock.curValue)
+		if err != nil {
 			// We panic here as we should have previously decoded and validated this
 			// key and value when it was first added to the range key block.
-			panic(errors.Errorf("pebble: invalid end key for span: %s",
-				prevStartKey.Pretty(w.formatKey)))
+			panic(err)
 		}
 
 		curStartKey := key
-		curEndKey, _, ok := rangekey.DecodeEndKey(curStartKey.Kind(), value)
-		if !ok {
-			w.err = errors.Errorf("pebble: invalid end key for span: %s",
-				curStartKey.Pretty(w.formatKey))
+		curEndKey, _, err := rangekey.DecodeEndKey(curStartKey.Kind(), value)
+		if err != nil {
+			w.err = err
 			return w.err
 		}
 
@@ -2103,9 +2101,9 @@ func (w *Writer) Close() (err error) {
 	if w.props.NumRangeKeys() > 0 {
 		key := w.rangeKeyBlock.getCurKey()
 		kind := key.Kind()
-		endKey, _, ok := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue)
-		if !ok {
-			return errors.Newf("invalid end key: %s", w.rangeKeyBlock.curValue)
+		endKey, _, err := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue)
+		if err != nil {
+			return err
 		}
 		k := base.MakeExclusiveSentinelKey(kind, endKey).Clone()
 		w.meta.SetLargestRangeKey(k)
diff --git a/vfs/logging_fs.go b/vfs/logging_fs.go
index aa52436c..b341fdb2 100644
--- a/vfs/logging_fs.go
+++ b/vfs/logging_fs.go
@@ -5,6 +5,7 @@
 package vfs
 
 import (
+	"fmt"
 	"io"
 	"os"
 )
@@ -38,7 +39,15 @@ func (fs *loggingFS) Create(name string, category DiskWriteCategory) (File, erro
 }
 
 func (fs *loggingFS) Open(name string, opts ...OpenOption) (File, error) {
-	fs.logFn("open: %s", name)
+	var optsStr string
+	if len(opts) > 0 {
+		optsStr = " (options:"
+		for i := range opts {
+			optsStr += fmt.Sprintf(" %T", opts[i])
+		}
+		optsStr += ")"
+	}
+	fs.logFn("open: %s%s", name, optsStr)
 	f, err := fs.FS.Open(name, opts...)
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/db_test.go b/db_test.go
index ecb02305..79dd337a 100644
--- a/db_test.go
+++ b/db_test.go
@@ -10,15 +10,18 @@ import (
 	"fmt"
 	"io"
 	"path/filepath"
+	"runtime"
 	"slices"
 	"strconv"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"testing"
 	"time"
 
 	"github.com/cockroachdb/datadriven"
 	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/fifo"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/cache"
 	"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
@@ -2340,3 +2343,104 @@ func parseOrderingTokens(tokens []string) (orderingNode, int) {
 		return leaf(v), 1
 	}
 }
+
+type readTrackFS struct {
+	vfs.FS
+
+	currReadCount atomic.Int32
+	maxReadCount  atomic.Int32
+}
+
+type readTrackFile struct {
+	vfs.File
+	fs *readTrackFS
+}
+
+func (fs *readTrackFS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) {
+	file, err := fs.FS.Open(name, opts...)
+	if err != nil || !strings.HasSuffix(name, ".sst") {
+		return file, err
+	}
+	return &readTrackFile{
+		File: file,
+		fs:   fs,
+	}, nil
+}
+
+func (f *readTrackFile) ReadAt(p []byte, off int64) (n int, err error) {
+	val := f.fs.currReadCount.Add(1)
+	defer f.fs.currReadCount.Add(-1)
+	for maxVal := f.fs.maxReadCount.Load(); val > maxVal; maxVal = f.fs.maxReadCount.Load() {
+		if f.fs.maxReadCount.CompareAndSwap(maxVal, val) {
+			break
+		}
+	}
+	return f.File.ReadAt(p, off)
+}
+
+func TestLoadBlockSema(t *testing.T) {
+	fs := &readTrackFS{FS: vfs.NewMem()}
+	sema := fifo.NewSemaphore(100)
+	db, err := Open("", testingRandomized(t, &Options{
+		Cache:         cache.New(1),
+		FS:            fs,
+		LoadBlockSema: sema,
+	}))
+	require.NoError(t, err)
+
+	key := func(i, j int) []byte {
+		return []byte(fmt.Sprintf("%02d/%02d", i, j))
+	}
+
+	// Create 20 regions and compact them separately, so we end up with 20
+	// disjoint tables.
+	const numRegions = 20
+	const numKeys = 20
+	for i := 0; i < numRegions; i++ {
+		for j := 0; j < numKeys; j++ {
+			require.NoError(t, db.Set(key(i, j), []byte("value"), nil))
+		}
+		require.NoError(t, db.Compact(key(i, 0), key(i, numKeys-1), false))
+	}
+
+	// Read all regions to warm up the table cache.
+	for i := 0; i < numRegions; i++ {
+		val, closer, err := db.Get(key(i, 1))
+		require.NoError(t, err)
+		require.Equal(t, []byte("value"), val)
+		if closer != nil {
+			closer.Close()
+		}
+	}
+
+	for _, n := range []int64{1, 2, 4} {
+		t.Run(fmt.Sprintf("%d", n), func(t *testing.T) {
+			sema.UpdateCapacity(n)
+			fs.maxReadCount.Store(0)
+			var wg sync.WaitGroup
+			// Spin up workers that perform random reads.
+			const numWorkers = 20
+			for i := 0; i < numWorkers; i++ {
+				wg.Add(1)
+				go func() {
+					defer wg.Done()
+					const numQueries = 100
+					for i := 0; i < numQueries; i++ {
+						val, closer, err := db.Get(key(rand.Intn(numRegions), rand.Intn(numKeys)))
+						require.NoError(t, err)
+						require.Equal(t, []byte("value"), val)
+						if closer != nil {
+							closer.Close()
+						}
+						runtime.Gosched()
+					}
+				}()
+			}
+			wg.Wait()
+			// Verify the maximum read count did not exceed the limit.
+			maxReadCount := fs.maxReadCount.Load()
+			require.Greater(t, maxReadCount, int32(0))
+			require.LessOrEqual(t, maxReadCount, int32(n))
+		})
+	}
+}
diff --git a/internal/rangekey/rangekey_test.go b/internal/rangekey/rangekey_test.go
index deec970d..b4cb3efb 100644
--- a/internal/rangekey/rangekey_test.go
+++ b/internal/rangekey/rangekey_test.go
@@ -90,10 +90,8 @@ func TestSetValue_Roundtrip(t *testing.T) {
 		n := EncodeSetValue(b, tc.endKey, tc.suffixValues)
 		require.Equal(t, l, n)
 
-		var endKey, rest []byte
-		var ok bool
-		endKey, rest, ok = DecodeEndKey(base.InternalKeyKindRangeKeySet, b[:n])
-		require.True(t, ok)
+		endKey, rest, err := DecodeEndKey(base.InternalKeyKindRangeKeySet, b[:n])
+		require.NoError(t, err)
 
 		var suffixValues []SuffixValue
 		for len(rest) > 0 {
@@ -184,10 +182,8 @@ func TestUnsetValue_Roundtrip(t *testing.T) {
 		n := EncodeUnsetValue(b, tc.endKey, tc.suffixes)
 		require.Equal(t, l, n)
 
-		var ok bool
-		var endKey, rest []byte
-		endKey, rest, ok = DecodeEndKey(base.InternalKeyKindRangeKeyUnset, b[:n])
-		require.True(t, ok)
+		endKey, rest, err := DecodeEndKey(base.InternalKeyKindRangeKeyUnset, b[:n])
+		require.NoError(t, err)
 		var suffixes [][]byte
 		for len(rest) > 0 {
 			var ok bool
diff --git a/objstorage/objstorageprovider/provider_test.go b/objstorage/objstorageprovider/provider_test.go
index 1999ec63..1e795791 100644
--- a/objstorage/objstorageprovider/provider_test.go
+++ b/objstorage/objstorageprovider/provider_test.go
@@ -40,7 +40,9 @@ func TestProvider(t *testing.T) {
 		backings := make(map[string]objstorage.RemoteObjectBacking)
 		backingHandles := make(map[string]objstorage.RemoteObjectBackingHandle)
 		var curProvider objstorage.Provider
+		readaheadConfig := DefaultReadaheadConfig
 		datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string {
+			readaheadConfig = DefaultReadaheadConfig
 			scanArgs := func(desc string, args ...interface{}) {
 				t.Helper()
 				if len(d.CmdArgs) != len(args) {
@@ -68,6 +70,9 @@ func TestProvider(t *testing.T) {
 					st.Remote.CreateOnShared = remote.CreateOnSharedAll
 					st.Remote.CreateOnSharedLocator = ""
 				}
+				st.Local.ReadaheadConfigFn = func() ReadaheadConfig {
+					return readaheadConfig
+				}
 				require.NoError(t, fs.MkdirAll(fsDir, 0755))
 				p, err := Open(st)
 				require.NoError(t, err)
@@ -170,18 +175,41 @@ func TestProvider(t *testing.T) {
 				return log.String()
 
 			case "read":
-				forCompaction := false
-				if len(d.CmdArgs) == 2 && d.CmdArgs[1].Key == "for-compaction" {
-					d.CmdArgs = d.CmdArgs[:1]
-					forCompaction = true
+				forCompaction := d.HasArg("for-compaction")
+				if arg, ok := d.Arg("readahead"); ok {
+					var mode ReadaheadMode
+					switch arg.SingleVal(t) {
+					case "off":
+						mode = NoReadahead
+					case "sys-readahead":
+						mode = SysReadahead
+					case "fadvise-sequential":
+						mode = FadviseSequential
+					default:
+						d.Fatalf(t, "unknown readahead mode %s", arg.SingleVal(t))
+					}
+					if forCompaction {
+						readaheadConfig.Informed = mode
+					} else {
+						readaheadConfig.Speculative = mode
+					}
 				}
+
+				d.CmdArgs = d.CmdArgs[:1]
 				var fileNum base.DiskFileNum
-				scanArgs("<file-num> [for-compaction]", &fileNum)
+				scanArgs("<file-num> [for-compaction] [readahead|speculative-overhead=off|sys-readahead|fadvise-sequential]", &fileNum)
 				r, err := curProvider.OpenForReading(ctx, base.FileTypeTable, fileNum, objstorage.OpenOptions{})
 				if err != nil {
 					return err.Error()
 				}
-				rh := r.NewReadHandle(ctx, objstorage.NoReadBefore)
+				var rh objstorage.ReadHandle
+				// Test both ways of getting a handle.
+				if rand.Intn(2) == 0 {
+					rh = r.NewReadHandle(ctx, objstorage.NoReadBefore)
+				} else {
+					var prealloc PreallocatedReadHandle
+					rh = UsePreallocatedReadHandle(ctx, r, objstorage.NoReadBefore, &prealloc)
+				}
 				if forCompaction {
 					rh.SetupForCompaction()
 				}
diff --git a/objstorage/objstorageprovider/testdata/provider/local b/objstorage/objstorageprovider/testdata/provider/local
index a2f4ce37..c2bba671 100644
--- a/objstorage/objstorageprovider/testdata/provider/local
+++ b/objstorage/objstorageprovider/testdata/provider/local
@@ -17,7 +17,7 @@ read 1
 0 1024
 512 1024
 ----
-<local fs> open: p0/000001.sst
+<local fs> open: p0/000001.sst (options: *vfs.randomReadsOption)
 size: 1024
 <local fs> read-at(0, 512): p0/000001.sst
 0 512: ok (salt 1)
@@ -41,7 +41,7 @@ read 2
 0 1024
 512 1024
 ----
-<local fs> open: p0/000002.sst
+<local fs> open: p0/000002.sst (options: *vfs.randomReadsOption)
 size: 1024
 <local fs> read-at(0, 512): p0/000002.sst
 0 512: ok (salt 2)
@@ -73,7 +73,7 @@ link-or-copy 3 local 3 100
 read 3
 0 100
 ----
-<local fs> open: p0/000003.sst
+<local fs> open: p0/000003.sst (options: *vfs.randomReadsOption)
 size: 100
 <local fs> read-at(0, 100): p0/000003.sst
 0 100: ok (salt 3)
@@ -88,7 +88,7 @@ link-or-copy 4 shared 4 1234
 read 4
 0 1234
 ----
-<local fs> open: p0/000004.sst
+<local fs> open: p0/000004.sst (options: *vfs.randomReadsOption)
 size: 1234
 <local fs> read-at(0, 1234): p0/000004.sst
 0 1234: ok (salt 4)
diff --git a/objstorage/objstorageprovider/testdata/provider/local_readahead b/objstorage/objstorageprovider/testdata/provider/local_readahead
index 87266cc4..8ad754ed 100644
--- a/objstorage/objstorageprovider/testdata/provider/local_readahead
+++ b/objstorage/objstorageprovider/testdata/provider/local_readahead
@@ -17,8 +17,8 @@ create 1 local 1 2000000
 <local fs> sync-data: p1/000001.sst
 <local fs> close: p1/000001.sst
 
-# We should see prefetch calls, and eventually a reopen
-# (with sequential reads option).
+# We should see prefetch calls, and eventually a reopen with sequential reads
+# option.
 read 1
 0 1000
 1000 15000
@@ -28,7 +28,7 @@ read 1
 106000 30000
 140000 80000
 ----
-<local fs> open: p1/000001.sst
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
 size: 2000000
 <local fs> read-at(0, 1000): p1/000001.sst
 0 1000: ok (salt 1)
@@ -44,8 +44,141 @@ size: 2000000
 56000 50000: ok (salt 1)
 <local fs> read-at(106000, 30000): p1/000001.sst
 106000 30000: ok (salt 1)
-<local fs> open: p1/000001.sst
+<local fs> open: p1/000001.sst (options: *vfs.sequentialReadsOption)
 <local fs> read-at(140000, 80000): p1/000001.sst
 140000 80000: ok (salt 1)
 <local fs> close: p1/000001.sst
 <local fs> close: p1/000001.sst
+
+# We should directly see a reopen with sequential reads option.
+read 1 for-compaction
+0 1000
+1000 15000
+----
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
+<local fs> open: p1/000001.sst (options: *vfs.sequentialReadsOption)
+size: 2000000
+<local fs> read-at(0, 1000): p1/000001.sst
+0 1000: ok (salt 1)
+<local fs> read-at(1000, 15000): p1/000001.sst
+1000 15000: ok (salt 1)
+<local fs> close: p1/000001.sst
+<local fs> close: p1/000001.sst
+
+# Test non-default readahead modes.
+
+read 1 readahead=off
+0 1000
+1000 15000
+16000 30000
+46000 10000
+56000 50000
+106000 30000
+140000 80000
+----
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
+size: 2000000
+<local fs> read-at(0, 1000): p1/000001.sst
+0 1000: ok (salt 1)
+<local fs> read-at(1000, 15000): p1/000001.sst
+1000 15000: ok (salt 1)
+<local fs> read-at(16000, 30000): p1/000001.sst
+16000 30000: ok (salt 1)
+<local fs> read-at(46000, 10000): p1/000001.sst
+46000 10000: ok (salt 1)
+<local fs> read-at(56000, 50000): p1/000001.sst
+56000 50000: ok (salt 1)
+<local fs> read-at(106000, 30000): p1/000001.sst
+106000 30000: ok (salt 1)
+<local fs> read-at(140000, 80000): p1/000001.sst
+140000 80000: ok (salt 1)
+<local fs> close: p1/000001.sst
+
+read 1 for-compaction readahead=off
+0 1000
+1000 15000
+16000 30000
+46000 10000
+56000 50000
+106000 30000
+140000 80000
+----
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
+size: 2000000
+<local fs> read-at(0, 1000): p1/000001.sst
+0 1000: ok (salt 1)
+<local fs> read-at(1000, 15000): p1/000001.sst
+1000 15000: ok (salt 1)
+<local fs> read-at(16000, 30000): p1/000001.sst
+16000 30000: ok (salt 1)
+<local fs> read-at(46000, 10000): p1/000001.sst
+46000 10000: ok (salt 1)
+<local fs> read-at(56000, 50000): p1/000001.sst
+56000 50000: ok (salt 1)
+<local fs> read-at(106000, 30000): p1/000001.sst
+106000 30000: ok (salt 1)
+<local fs> read-at(140000, 80000): p1/000001.sst
+140000 80000: ok (salt 1)
+<local fs> close: p1/000001.sst
+
+read 1 readahead=sys-readahead
+0 1000
+1000 15000
+16000 30000
+46000 10000
+56000 50000
+106000 30000
+140000 80000
+----
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
+size: 2000000
+<local fs> read-at(0, 1000): p1/000001.sst
+0 1000: ok (salt 1)
+<local fs> read-at(1000, 15000): p1/000001.sst
+1000 15000: ok (salt 1)
+<local fs> prefetch(16000, 65536): p1/000001.sst
+<local fs> read-at(16000, 30000): p1/000001.sst
+16000 30000: ok (salt 1)
+<local fs> read-at(46000, 10000): p1/000001.sst
+46000 10000: ok (salt 1)
+<local fs> prefetch(56000, 131072): p1/000001.sst
+<local fs> read-at(56000, 50000): p1/000001.sst
+56000 50000: ok (salt 1)
+<local fs> read-at(106000, 30000): p1/000001.sst
+106000 30000: ok (salt 1)
+<local fs> prefetch(140000, 262144): p1/000001.sst
+<local fs> read-at(140000, 80000): p1/000001.sst
+140000 80000: ok (salt 1)
+<local fs> close: p1/000001.sst
+
+# TODO(radu): for informed/sys-readahead, we should start with the maximum
+# prefetch window.
+read 1 for-compaction readahead=sys-readahead
+0 1000
+1000 15000
+16000 30000
+46000 10000
+56000 50000
+106000 30000
+140000 80000
+----
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
+size: 2000000
+<local fs> read-at(0, 1000): p1/000001.sst
+0 1000: ok (salt 1)
+<local fs> read-at(1000, 15000): p1/000001.sst
+1000 15000: ok (salt 1)
+<local fs> prefetch(16000, 65536): p1/000001.sst
+<local fs> read-at(16000, 30000): p1/000001.sst
+16000 30000: ok (salt 1)
+<local fs> read-at(46000, 10000): p1/000001.sst
+46000 10000: ok (salt 1)
+<local fs> prefetch(56000, 131072): p1/000001.sst
+<local fs> read-at(56000, 50000): p1/000001.sst
+56000 50000: ok (salt 1)
+<local fs> read-at(106000, 30000): p1/000001.sst
+106000 30000: ok (salt 1)
+<local fs> prefetch(140000, 262144): p1/000001.sst
+<local fs> read-at(140000, 80000): p1/000001.sst
+140000 80000: ok (salt 1)
+<local fs> close: p1/000001.sst
diff --git a/objstorage/objstorageprovider/testdata/provider/shared_basic b/objstorage/objstorageprovider/testdata/provider/shared_basic
index 2882d498..8ebbaa17 100644
--- a/objstorage/objstorageprovider/testdata/provider/shared_basic
+++ b/objstorage/objstorageprovider/testdata/provider/shared_basic
@@ -22,7 +22,7 @@ create 1 local 1 100
 read 1
 0 100
 ----
-<local fs> open: p1/000001.sst
+<local fs> open: p1/000001.sst (options: *vfs.randomReadsOption)
 size: 100
 <local fs> read-at(0, 100): p1/000001.sst
 0 100: ok (salt 1)
@@ -91,7 +91,7 @@ link-or-copy 3 local 3 100
 read 3
 0 100
 ----
-<local fs> open: p1/000003.sst
+<local fs> open: p1/000003.sst (options: *vfs.randomReadsOption)
 size: 100
 <local fs> read-at(0, 100): p1/000003.sst
 0 100: ok (salt 3)
@@ -102,7 +102,7 @@ link-or-copy 4 shared 4 100
 <local fs> create: temp-file-2
 <local fs> close: temp-file-2
 <remote> create object "2f2f-1-000004.sst"
-<local fs> open: temp-file-2
+<local fs> open: temp-file-2 (options: *vfs.sequentialReadsOption)
 <remote> close writer for "2f2f-1-000004.sst" after 100 bytes
 <remote> create object "2f2f-1-000004.sst.ref.1.000004"
 <remote> close writer for "2f2f-1-000004.sst.ref.1.000004" after 0 bytes
diff --git a/objstorage/objstorageprovider/testdata/provider/shared_no_ref b/objstorage/objstorageprovider/testdata/provider/shared_no_ref
index 538dde8b..3a9ed5d4 100644
--- a/objstorage/objstorageprovider/testdata/provider/shared_no_ref
+++ b/objstorage/objstorageprovider/testdata/provider/shared_no_ref
@@ -51,7 +51,7 @@ link-or-copy 3 shared 3 100 no-ref-tracking
 <local fs> create: temp-file-1
 <local fs> close: temp-file-1
 <remote> create object "eaac-1-000003.sst"
-<local fs> open: temp-file-1
+<local fs> open: temp-file-1 (options: *vfs.sequentialReadsOption)
 <remote> close writer for "eaac-1-000003.sst" after 100 bytes
 <local fs> close: temp-file-1
 
diff --git a/testdata/checkpoint b/testdata/checkpoint
index d6993d53..b9ae37c2 100644
--- a/testdata/checkpoint
+++ b/testdata/checkpoint
@@ -105,7 +105,7 @@ sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
 link: db/000005.sst -> checkpoints/checkpoint1/000005.sst
 link: db/000007.sst -> checkpoints/checkpoint1/000007.sst
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint1/MANIFEST-000001
 sync-data: checkpoints/checkpoint1/MANIFEST-000001
 close: checkpoints/checkpoint1/MANIFEST-000001
@@ -116,7 +116,7 @@ sync-data: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
-open: db/000006.log
+open: db/000006.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint1/000006.log
 sync-data: checkpoints/checkpoint1/000006.log
 close: checkpoints/checkpoint1/000006.log
@@ -144,7 +144,7 @@ close: checkpoints/checkpoint2/marker.format-version.000001.017
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
 link: db/000007.sst -> checkpoints/checkpoint2/000007.sst
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint2/MANIFEST-000001
 sync-data: checkpoints/checkpoint2/MANIFEST-000001
 close: checkpoints/checkpoint2/MANIFEST-000001
@@ -155,7 +155,7 @@ sync-data: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
-open: db/000006.log
+open: db/000006.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint2/000006.log
 sync-data: checkpoints/checkpoint2/000006.log
 close: checkpoints/checkpoint2/000006.log
@@ -180,7 +180,7 @@ sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
 link: db/000005.sst -> checkpoints/checkpoint3/000005.sst
 link: db/000007.sst -> checkpoints/checkpoint3/000007.sst
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint3/MANIFEST-000001
 sync-data: checkpoints/checkpoint3/MANIFEST-000001
 close: checkpoints/checkpoint3/MANIFEST-000001
@@ -191,7 +191,7 @@ sync-data: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
-open: db/000006.log
+open: db/000006.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint3/000006.log
 sync-data: checkpoints/checkpoint3/000006.log
 close: checkpoints/checkpoint3/000006.log
@@ -210,28 +210,28 @@ sync-data: db/000009.sst
 close: db/000009.sst
 sync: db
 sync: db/MANIFEST-000001
-open: db/000005.sst
+open: db/000005.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): db/000005.sst
 read-at(521, 37): db/000005.sst
 read-at(74, 447): db/000005.sst
-open: db/000009.sst
+open: db/000009.sst (options: *vfs.randomReadsOption)
 read-at(553, 53): db/000009.sst
 read-at(516, 37): db/000009.sst
 read-at(69, 447): db/000009.sst
-open: db/000007.sst
+open: db/000007.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): db/000007.sst
 read-at(521, 37): db/000007.sst
 read-at(74, 447): db/000007.sst
 read-at(47, 27): db/000005.sst
-open: db/000005.sst
+open: db/000005.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 47): db/000005.sst
 read-at(47, 27): db/000007.sst
-open: db/000007.sst
+open: db/000007.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 47): db/000007.sst
 create: db/000010.sst
 close: db/000005.sst
 read-at(42, 27): db/000009.sst
-open: db/000009.sst
+open: db/000009.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 42): db/000009.sst
 close: db/000007.sst
 close: db/000009.sst
@@ -290,13 +290,13 @@ close: checkpoints/checkpoint1/000006.log
 
 scan checkpoints/checkpoint1
 ----
-open: checkpoints/checkpoint1/000007.sst
+open: checkpoints/checkpoint1/000007.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): checkpoints/checkpoint1/000007.sst
 read-at(521, 37): checkpoints/checkpoint1/000007.sst
 read-at(74, 447): checkpoints/checkpoint1/000007.sst
 read-at(47, 27): checkpoints/checkpoint1/000007.sst
 read-at(0, 47): checkpoints/checkpoint1/000007.sst
-open: checkpoints/checkpoint1/000005.sst
+open: checkpoints/checkpoint1/000005.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): checkpoints/checkpoint1/000005.sst
 read-at(521, 37): checkpoints/checkpoint1/000005.sst
 read-at(74, 447): checkpoints/checkpoint1/000005.sst
@@ -313,7 +313,7 @@ g 10
 
 scan db
 ----
-open: db/000010.sst
+open: db/000010.sst (options: *vfs.randomReadsOption)
 read-at(585, 53): db/000010.sst
 read-at(548, 37): db/000010.sst
 read-at(101, 447): db/000010.sst
@@ -357,7 +357,7 @@ close: checkpoints/checkpoint2/000006.log
 
 scan checkpoints/checkpoint2
 ----
-open: checkpoints/checkpoint2/000007.sst
+open: checkpoints/checkpoint2/000007.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): checkpoints/checkpoint2/000007.sst
 read-at(521, 37): checkpoints/checkpoint2/000007.sst
 read-at(74, 447): checkpoints/checkpoint2/000007.sst
@@ -399,13 +399,13 @@ close: checkpoints/checkpoint3/000006.log
 
 scan checkpoints/checkpoint3
 ----
-open: checkpoints/checkpoint3/000007.sst
+open: checkpoints/checkpoint3/000007.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): checkpoints/checkpoint3/000007.sst
 read-at(521, 37): checkpoints/checkpoint3/000007.sst
 read-at(74, 447): checkpoints/checkpoint3/000007.sst
 read-at(47, 27): checkpoints/checkpoint3/000007.sst
 read-at(0, 47): checkpoints/checkpoint3/000007.sst
-open: checkpoints/checkpoint3/000005.sst
+open: checkpoints/checkpoint3/000005.sst (options: *vfs.randomReadsOption)
 read-at(558, 53): checkpoints/checkpoint3/000005.sst
 read-at(521, 37): checkpoints/checkpoint3/000005.sst
 read-at(74, 447): checkpoints/checkpoint3/000005.sst
@@ -476,7 +476,7 @@ g 10
 h 11
 i i
 k k
-open: db/000014.sst
+open: db/000014.sst (options: *vfs.randomReadsOption)
 read-at(509, 53): db/000014.sst
 read-at(472, 37): db/000014.sst
 z z
@@ -500,7 +500,7 @@ close: checkpoints/checkpoint4
 link: db/000010.sst -> checkpoints/checkpoint4/000010.sst
 link: db/000011.sst -> checkpoints/checkpoint4/000011.sst
 link: db/000014.sst -> checkpoints/checkpoint4/000014.sst
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint4/MANIFEST-000001
 sync-data: checkpoints/checkpoint4/MANIFEST-000001
 close: checkpoints/checkpoint4/MANIFEST-000001
@@ -511,7 +511,7 @@ sync-data: checkpoints/checkpoint4/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint4/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint4
 close: checkpoints/checkpoint4
-open: db/000008.log
+open: db/000008.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint4/000008.log
 sync-data: checkpoints/checkpoint4/000008.log
 close: checkpoints/checkpoint4/000008.log
@@ -537,7 +537,7 @@ close: checkpoints/checkpoint4/000008.log
 
 scan checkpoints/checkpoint4
 ----
-open: checkpoints/checkpoint4/000010.sst
+open: checkpoints/checkpoint4/000010.sst (options: *vfs.randomReadsOption)
 read-at(585, 53): checkpoints/checkpoint4/000010.sst
 read-at(548, 37): checkpoints/checkpoint4/000010.sst
 read-at(101, 447): checkpoints/checkpoint4/000010.sst
@@ -549,7 +549,7 @@ d 7
 e 8
 f 9
 g 10
-open: checkpoints/checkpoint4/000011.sst
+open: checkpoints/checkpoint4/000011.sst (options: *vfs.randomReadsOption)
 read-at(526, 53): checkpoints/checkpoint4/000011.sst
 read-at(489, 37): checkpoints/checkpoint4/000011.sst
 read-at(70, 419): checkpoints/checkpoint4/000011.sst
@@ -558,7 +558,7 @@ read-at(0, 43): checkpoints/checkpoint4/000011.sst
 h 11
 i i
 k k
-open: checkpoints/checkpoint4/000014.sst
+open: checkpoints/checkpoint4/000014.sst (options: *vfs.randomReadsOption)
 read-at(509, 53): checkpoints/checkpoint4/000014.sst
 read-at(472, 37): checkpoints/checkpoint4/000014.sst
 read-at(53, 419): checkpoints/checkpoint4/000014.sst
@@ -605,7 +605,7 @@ close: checkpoints/checkpoint5
 link: db/000010.sst -> checkpoints/checkpoint5/000010.sst
 link: db/000011.sst -> checkpoints/checkpoint5/000011.sst
 link: db/000014.sst -> checkpoints/checkpoint5/000014.sst
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint5/MANIFEST-000001
 sync-data: checkpoints/checkpoint5/MANIFEST-000001
 close: checkpoints/checkpoint5/MANIFEST-000001
@@ -616,7 +616,7 @@ sync-data: checkpoints/checkpoint5/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint5/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint5
 close: checkpoints/checkpoint5
-open: db/000008.log
+open: db/000008.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint5/000008.log
 sync-data: checkpoints/checkpoint5/000008.log
 close: checkpoints/checkpoint5/000008.log
@@ -702,7 +702,7 @@ sync: checkpoints/checkpoint6
 close: checkpoints/checkpoint6
 link: db/000011.sst -> checkpoints/checkpoint6/000011.sst
 link: db/000014.sst -> checkpoints/checkpoint6/000014.sst
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint6/MANIFEST-000001
 sync-data: checkpoints/checkpoint6/MANIFEST-000001
 close: checkpoints/checkpoint6/MANIFEST-000001
@@ -713,7 +713,7 @@ sync-data: checkpoints/checkpoint6/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint6/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint6
 close: checkpoints/checkpoint6
-open: db/000008.log
+open: db/000008.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint6/000008.log
 sync-data: checkpoints/checkpoint6/000008.log
 close: checkpoints/checkpoint6/000008.log
diff --git a/testdata/checkpoint_shared b/testdata/checkpoint_shared
index 24929737..eda39dc9 100644
--- a/testdata/checkpoint_shared
+++ b/testdata/checkpoint_shared
@@ -91,7 +91,7 @@ sync-data: checkpoints/checkpoint1/marker.format-version.000001.017
 close: checkpoints/checkpoint1/marker.format-version.000001.017
 sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint1/MANIFEST-000001
 sync-data: checkpoints/checkpoint1/MANIFEST-000001
 close: checkpoints/checkpoint1/MANIFEST-000001
@@ -102,7 +102,7 @@ sync-data: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint1/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
-open: db/REMOTE-OBJ-CATALOG-000001
+open: db/REMOTE-OBJ-CATALOG-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint1/REMOTE-OBJ-CATALOG-000001
 sync-data: checkpoints/checkpoint1/REMOTE-OBJ-CATALOG-000001
 close: checkpoints/checkpoint1/REMOTE-OBJ-CATALOG-000001
@@ -113,7 +113,7 @@ sync-data: checkpoints/checkpoint1/marker.remote-obj-catalog.000001.REMOTE-OBJ-C
 close: checkpoints/checkpoint1/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001
 sync: checkpoints/checkpoint1
 close: checkpoints/checkpoint1
-open: db/000006.log
+open: db/000006.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint1/000006.log
 sync-data: checkpoints/checkpoint1/000006.log
 close: checkpoints/checkpoint1/000006.log
@@ -140,7 +140,7 @@ sync-data: checkpoints/checkpoint2/marker.format-version.000001.017
 close: checkpoints/checkpoint2/marker.format-version.000001.017
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint2/MANIFEST-000001
 sync-data: checkpoints/checkpoint2/MANIFEST-000001
 close: checkpoints/checkpoint2/MANIFEST-000001
@@ -151,7 +151,7 @@ sync-data: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint2/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
-open: db/REMOTE-OBJ-CATALOG-000001
+open: db/REMOTE-OBJ-CATALOG-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint2/REMOTE-OBJ-CATALOG-000001
 sync-data: checkpoints/checkpoint2/REMOTE-OBJ-CATALOG-000001
 close: checkpoints/checkpoint2/REMOTE-OBJ-CATALOG-000001
@@ -162,7 +162,7 @@ sync-data: checkpoints/checkpoint2/marker.remote-obj-catalog.000001.REMOTE-OBJ-C
 close: checkpoints/checkpoint2/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001
 sync: checkpoints/checkpoint2
 close: checkpoints/checkpoint2
-open: db/000006.log
+open: db/000006.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint2/000006.log
 sync-data: checkpoints/checkpoint2/000006.log
 close: checkpoints/checkpoint2/000006.log
@@ -185,7 +185,7 @@ sync-data: checkpoints/checkpoint3/marker.format-version.000001.017
 close: checkpoints/checkpoint3/marker.format-version.000001.017
 sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
-open: db/MANIFEST-000001
+open: db/MANIFEST-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint3/MANIFEST-000001
 sync-data: checkpoints/checkpoint3/MANIFEST-000001
 close: checkpoints/checkpoint3/MANIFEST-000001
@@ -196,7 +196,7 @@ sync-data: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001
 close: checkpoints/checkpoint3/marker.manifest.000001.MANIFEST-000001
 sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
-open: db/REMOTE-OBJ-CATALOG-000001
+open: db/REMOTE-OBJ-CATALOG-000001 (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint3/REMOTE-OBJ-CATALOG-000001
 sync-data: checkpoints/checkpoint3/REMOTE-OBJ-CATALOG-000001
 close: checkpoints/checkpoint3/REMOTE-OBJ-CATALOG-000001
@@ -207,7 +207,7 @@ sync-data: checkpoints/checkpoint3/marker.remote-obj-catalog.000001.REMOTE-OBJ-C
 close: checkpoints/checkpoint3/marker.remote-obj-catalog.000001.REMOTE-OBJ-CATALOG-000001
 sync: checkpoints/checkpoint3
 close: checkpoints/checkpoint3
-open: db/000006.log
+open: db/000006.log (options: *vfs.sequentialReadsOption)
 create: checkpoints/checkpoint3/000006.log
 sync-data: checkpoints/checkpoint3/000006.log
 close: checkpoints/checkpoint3/000006.log
diff --git a/testdata/cleaner b/testdata/cleaner
index d233e921..0bafa369 100644
--- a/testdata/cleaner
+++ b/testdata/cleaner
@@ -75,21 +75,21 @@ sync: db
 sync: db/MANIFEST-000001
 mkdir-all: db_wal/archive 0755
 rename: db_wal/000004.log -> db_wal/archive/000004.log
-open: db/000005.sst
+open: db/000005.sst (options: *vfs.randomReadsOption)
 read-at(535, 53): db/000005.sst
 read-at(498, 37): db/000005.sst
 read-at(79, 419): db/000005.sst
-open: db/000007.sst
+open: db/000007.sst (options: *vfs.randomReadsOption)
 read-at(509, 53): db/000007.sst
 read-at(472, 37): db/000007.sst
 read-at(53, 419): db/000007.sst
 read-at(52, 27): db/000005.sst
-open: db/000005.sst
+open: db/000005.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 52): db/000005.sst
 create: db/000008.sst
 close: db/000005.sst
 read-at(26, 27): db/000007.sst
-open: db/000007.sst
+open: db/000007.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 26): db/000007.sst
 close: db/000007.sst
 sync-data: db/000008.sst
diff --git a/testdata/event_listener b/testdata/event_listener
index 2d6650c9..34b61403 100644
--- a/testdata/event_listener
+++ b/testdata/event_listener
@@ -103,19 +103,19 @@ sync: db
 remove: db/MANIFEST-000001
 [JOB 5] MANIFEST deleted 000001
 [JOB 6] compacting(default) L0 [000005 000008] (1.2KB) Score=0.00 + L6 [] (0B) Score=0.00; OverlappingRatio: Single 0.00, Multi 0.00
-open: db/000005.sst
+open: db/000005.sst (options: *vfs.randomReadsOption)
 read-at(537, 53): db/000005.sst
 read-at(500, 37): db/000005.sst
 read-at(53, 447): db/000005.sst
-open: db/000008.sst
+open: db/000008.sst (options: *vfs.randomReadsOption)
 read-at(537, 53): db/000008.sst
 read-at(500, 37): db/000008.sst
 read-at(53, 447): db/000008.sst
 read-at(26, 27): db/000005.sst
-open: db/000005.sst
+open: db/000005.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 26): db/000005.sst
 read-at(26, 27): db/000008.sst
-open: db/000008.sst
+open: db/000008.sst (options: *vfs.sequentialReadsOption)
 read-at(0, 26): db/000008.sst
 close: db/000008.sst
 close: db/000005.sst
@@ -363,7 +363,7 @@ link: db/000022.sst -> checkpoint/000022.sst
 link: db/000017.sst -> checkpoint/000017.sst
 link: db/000010.sst -> checkpoint/000010.sst
 link: db/000018.sst -> checkpoint/000018.sst
-open: db/MANIFEST-000023
+open: db/MANIFEST-000023 (options: *vfs.sequentialReadsOption)
 create: checkpoint/MANIFEST-000023
 sync-data: checkpoint/MANIFEST-000023
 close: checkpoint/MANIFEST-000023
@@ -374,7 +374,7 @@ sync-data: checkpoint/marker.manifest.000001.MANIFEST-000023
 close: checkpoint/marker.manifest.000001.MANIFEST-000023
 sync: checkpoint
 close: checkpoint
-open: wal/000021.log
+open: wal/000021.log (options: *vfs.sequentialReadsOption)
 create: checkpoint/000021.log
 sync-data: checkpoint/000021.log
 close: checkpoint/000021.log
diff --git a/testdata/external_iterator b/testdata/external_iterator
index 238b81b4..87664996 100644
--- a/testdata/external_iterator
+++ b/testdata/external_iterator
@@ -245,4 +245,4 @@ aaaa@3: (aaaa@3, .)
 aaaa@1: (aaaa@1, .)
 aaaaa@3: (aaaaa@3, .)
 aaaaa@1: (aaaaa@1, .)
-stats: seeked 5 times (5 internal); stepped 5 times (5 internal); blocks: 547B (0B cached), read time 0s; points: 10 (50B keys, 35B values); separated: 15 (65B, 25B fetched)
+stats: seeked 5 times (5 internal); stepped 5 times (5 internal); blocks: 0B cached, 547B not cached (read time: 0s); points: 10 (50B keys, 35B values); separated: 15 (65B, 25B fetched)
diff --git a/testdata/ingest b/testdata/ingest
index 6675dee0..165fe26c 100644
--- a/testdata/ingest
+++ b/testdata/ingest
@@ -54,7 +54,7 @@ Virtual tables: 0 (0B)
 Local tables size: 569B
 Compression types: snappy: 1
 Block cache: 6 entries (945B)  hit rate: 30.8%
-Table cache: 1 entries (760B)  hit rate: 50.0%
+Table cache: 1 entries (768B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
diff --git a/testdata/iter_histories/iter_optimizations b/testdata/iter_histories/iter_optimizations
index 5c3513ae..8d2b6ebe 100644
--- a/testdata/iter_histories/iter_optimizations
+++ b/testdata/iter_histories/iter_optimizations
@@ -199,7 +199,7 @@ stats
 ----
 lastPositioningOp="unknown"
 b@5: (b@5, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 119B (0B cached), read time 0s; points: 1 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached, 119B not cached (read time: 0s); points: 1 (3B keys, 3B values)
 
 mutate batch=foo
 set h@2 h@2
@@ -214,7 +214,7 @@ stats
 .
 lastPositioningOp="seekprefixge"
 c@3: (c@3, .)
-stats: seeked 2 times (2 internal); stepped 0 times (0 internal); blocks: 119B (0B cached), read time 0s; points: 2 (6B keys, 6B values)
+stats: seeked 2 times (2 internal); stepped 0 times (0 internal); blocks: 0B cached, 119B not cached (read time: 0s); points: 2 (6B keys, 6B values)
 
 mutate batch=foo
 set i@1 i@1
@@ -229,7 +229,7 @@ stats
 .
 lastPositioningOp="seekprefixge"
 d@9: (d@9, .)
-stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 119B (0B cached), read time 0s; points: 3 (9B keys, 9B values)
+stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B cached, 119B not cached (read time: 0s); points: 3 (9B keys, 9B values)
 
 mutate batch=foo
 set j@6 j@6
@@ -244,7 +244,7 @@ stats
 .
 lastPositioningOp="seekprefixge"
 e@8: (e@8, .)
-stats: seeked 4 times (4 internal); stepped 0 times (0 internal); blocks: 119B (0B cached), read time 0s; points: 4 (12B keys, 12B values)
+stats: seeked 4 times (4 internal); stepped 0 times (0 internal); blocks: 0B cached, 119B not cached (read time: 0s); points: 4 (12B keys, 12B values)
 
 # Ensure that a case eligible for TrySeekUsingNext across a SetOptions correctly
 # sees new batch mutations. The batch iterator should ignore the
diff --git a/testdata/iter_histories/range_key_masking b/testdata/iter_histories/range_key_masking
index 5fcccee9..2252c46a 100644
--- a/testdata/iter_histories/range_key_masking
+++ b/testdata/iter_histories/range_key_masking
@@ -153,7 +153,7 @@ stats
 ----
 a: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 1.1KB (0B cached), read time 0s; points: 25 (75B keys, 75B values), range keys: 1, contained points: 25 (25 skipped)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached, 1.1KB not cached (read time: 0s); points: 25 (75B keys, 75B values), range keys: 1, contained points: 25 (25 skipped)
 
 # Repeat the above test, but with an iterator that uses a block-property filter
 # mask. The internal stats should reflect fewer bytes read and fewer points
@@ -166,7 +166,7 @@ stats
 ----
 a: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 514B (514B cached), read time 0s; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 514B cached; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
 
 # Perform a similar comparison in reverse.
 
@@ -177,7 +177,7 @@ stats
 ----
 a: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 1.1KB (1.1KB cached), read time 0s; points: 25 (75B keys, 75B values), range keys: 1, contained points: 25 (25 skipped)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 1.1KB cached; points: 25 (75B keys, 75B values), range keys: 1, contained points: 25 (25 skipped)
 
 combined-iter mask-suffix=@9 mask-filter
 last
@@ -186,7 +186,7 @@ stats
 ----
 a: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 514B (514B cached), read time 0s; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 514B cached; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
 
 # Perform similar comparisons with seeks.
 
@@ -197,7 +197,7 @@ stats
 ----
 m: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 789B (789B cached), read time 0s; points: 13 (39B keys, 39B values), range keys: 1, contained points: 13 (13 skipped)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 789B cached; points: 13 (39B keys, 39B values), range keys: 1, contained points: 13 (13 skipped)
 
 combined-iter mask-suffix=@9 mask-filter
 seek-ge m
@@ -206,7 +206,7 @@ stats
 ----
 m: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 514B (514B cached), read time 0s; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 514B cached; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
 
 combined-iter mask-suffix=@9
 seek-lt m
@@ -215,7 +215,7 @@ stats
 ----
 a: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 789B (789B cached), read time 0s; points: 12 (36B keys, 36B values), range keys: 1, contained points: 12 (12 skipped)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 789B cached; points: 12 (36B keys, 36B values), range keys: 1, contained points: 12 (12 skipped)
 
 combined-iter mask-suffix=@9 mask-filter
 seek-lt m
@@ -224,4 +224,4 @@ stats
 ----
 a: (., [a-z) @5=boop UPDATED)
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 539B (539B cached), read time 0s; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 539B cached; points: 2 (6B keys, 6B values), range keys: 1, contained points: 2 (2 skipped)
diff --git a/testdata/iter_histories/stats_no_invariants b/testdata/iter_histories/stats_no_invariants
index dd73915f..ac1cf0de 100644
--- a/testdata/iter_histories/stats_no_invariants
+++ b/testdata/iter_histories/stats_no_invariants
@@ -33,12 +33,12 @@ stats
 stats: seeked 0 times (0 internal); stepped 0 times (0 internal)
 a: (a, .)
 b: (b, [b-c) @5=boop UPDATED)
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 89B (0B cached), read time 0s; points: 2 (2B keys, 2B values), range keys: 1, contained points: 1 (0 skipped)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached, 89B not cached (read time: 0s); points: 2 (2B keys, 2B values), range keys: 1, contained points: 1 (0 skipped)
 c: (c, . UPDATED)
 cat: (., [cat-dog) @3=beep UPDATED)
 d: (d, [cat-dog) @3=beep)
 .
-stats: seeked 1 times (1 internal); stepped 5 times (6 internal); blocks: 89B (0B cached), read time 0s; points: 4 (4B keys, 4B values), range keys: 2, contained points: 2 (0 skipped)
+stats: seeked 1 times (1 internal); stepped 5 times (6 internal); blocks: 0B cached, 89B not cached (read time: 0s); points: 4 (4B keys, 4B values), range keys: 2, contained points: 2 (0 skipped)
 
 # Do the above forward iteration but with a mask suffix. The results should be
 # identical despite range keys serving as masks, because none of the point keys
@@ -59,7 +59,7 @@ c: (c, . UPDATED)
 cat: (., [cat-dog) @3=beep UPDATED)
 d: (d, [cat-dog) @3=beep)
 .
-stats: seeked 1 times (1 internal); stepped 5 times (6 internal); blocks: 89B (89B cached), read time 0s; points: 4 (4B keys, 4B values), range keys: 2, contained points: 2 (0 skipped)
+stats: seeked 1 times (1 internal); stepped 5 times (6 internal); blocks: 89B cached; points: 4 (4B keys, 4B values), range keys: 2, contained points: 2 (0 skipped)
 
 # Scan backward
 
@@ -78,7 +78,7 @@ c: (c, . UPDATED)
 b: (b, [b-c) @5=boop UPDATED)
 a: (a, . UPDATED)
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 5 times (0 fwd/5 rev, internal: 0 fwd/6 rev); blocks: 89B (89B cached), read time 0s; points: 4 (4B keys, 4B values), range keys: 2, contained points: 2 (0 skipped)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 5 times (0 fwd/5 rev, internal: 0 fwd/6 rev); blocks: 89B cached; points: 4 (4B keys, 4B values), range keys: 2, contained points: 2 (0 skipped)
 
 combined-iter
 seek-ge ace
@@ -99,7 +99,7 @@ cat: (., [cat-dog) @3=beep)
 d: (d, [cat-dog) @3=beep)
 day: (., [cat-dog) @3=beep)
 .
-stats: seeked 8 times (6 internal); stepped 0 times (4 internal); blocks: 89B (89B cached), read time 0s; points: 4 (4B keys, 4B values), range keys: 2, contained points: 3 (0 skipped)
+stats: seeked 8 times (6 internal); stepped 0 times (4 internal); blocks: 89B cached; points: 4 (4B keys, 4B values), range keys: 2, contained points: 3 (0 skipped)
 
 combined-iter
 seek-lt 1
@@ -124,7 +124,7 @@ cat: (., [cat-dog) @3=beep UPDATED)
 d: (d, [cat-dog) @3=beep)
 d: (d, [cat-dog) @3=beep)
 d: (d, [cat-dog) @3=beep)
-stats: seeked 10 times (0 fwd/10 rev, internal: 0 fwd/10 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/10 rev); blocks: 267B (267B cached), read time 0s; points: 15 (15B keys, 15B values), range keys: 2, contained points: 6 (0 skipped)
+stats: seeked 10 times (0 fwd/10 rev, internal: 0 fwd/10 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/10 rev); blocks: 267B cached; points: 15 (15B keys, 15B values), range keys: 2, contained points: 6 (0 skipped)
 
 rangekey-iter
 first
diff --git a/testdata/iterator b/testdata/iterator
index e5d1aad0..7a58aefc 100644
--- a/testdata/iterator
+++ b/testdata/iterator
@@ -8,7 +8,7 @@ iter seq=4
 seek-prefix-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 define
 a.SET.1:b
@@ -22,7 +22,7 @@ prev
 a: (b, .)
 .
 a: (b, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 seek-ge b
@@ -50,7 +50,7 @@ prev
 a: (b, .)
 .
 a: (b, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 4B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); blocks: 0B cached; points: 4 (4B keys, 4B values)
 
 iter seq=3
 seek-ge a
@@ -60,7 +60,7 @@ prev
 a: (c, .)
 .
 a: (c, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 2 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 4B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 2 fwd/2 rev); blocks: 0B cached; points: 4 (4B keys, 4B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -72,7 +72,7 @@ a: (b, .)
 .
 err=pebble: unsupported reverse prefix iteration
 err=pebble: unsupported reverse prefix iteration
-stats: seeked 1 times (1 internal); stepped 3 times (2 fwd/1 rev, internal: 1 fwd/0 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 3 times (2 fwd/1 rev, internal: 1 fwd/0 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=3
 seek-prefix-ge a
@@ -80,7 +80,7 @@ next
 ----
 a: (c, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 define
 a.DEL.2:
@@ -91,7 +91,7 @@ iter seq=3
 seek-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=2
 seek-ge 1
@@ -99,13 +99,13 @@ next
 ----
 a: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=3
 seek-lt b
 ----
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/2 rev); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=2
 seek-lt b
@@ -115,13 +115,13 @@ next
 a: (b, .)
 .
 a: (b, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 2B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 4 (4B keys, 2B values)
 
 iter seq=3
 seek-prefix-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=2
 seek-prefix-ge 1
@@ -141,37 +141,37 @@ next
 ----
 b: (c, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B cached; points: 3 (3B keys, 2B values)
 
 iter seq=3
 seek-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 3 (3B keys, 2B values)
 
 iter seq=2
 seek-ge a
 ----
 a: (b, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=4
 seek-prefix-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=3
 seek-prefix-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=2
 seek-prefix-ge a
 ----
 a: (b, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -179,7 +179,7 @@ seek-prefix-ge b
 ----
 a: (b, .)
 .
-stats: seeked 2 times (2 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 3 (3B keys, 2B values)
 
 define
 a.DEL.3:
@@ -197,7 +197,7 @@ seek-prefix-ge c
 .
 .
 c: (d, .)
-stats: seeked 3 times (3 internal); stepped 0 times (4 internal); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 3B values)
+stats: seeked 3 times (3 internal); stepped 0 times (4 internal); blocks: 0B cached; points: 5 (5B keys, 3B values)
 
 iter seq=3
 seek-prefix-ge a
@@ -207,7 +207,7 @@ seek-prefix-ge c
 a: (b, .)
 b: (c, .)
 .
-stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 3B values)
+stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 5 (5B keys, 3B values)
 
 iter seq=3
 seek-ge a
@@ -217,7 +217,7 @@ seek-ge c
 a: (b, .)
 b: (c, .)
 .
-stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 3B values)
+stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 5 (5B keys, 3B values)
 
 define
 a.SET.1:a
@@ -235,7 +235,7 @@ a: (a, .)
 b: (b, .)
 c: (c, .)
 .
-stats: seeked 1 times (1 internal); stepped 3 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 3 times (3 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=4
 seek-ge b
@@ -243,13 +243,13 @@ next
 ----
 b: (b, .)
 c: (c, .)
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=4
 seek-ge c
 ----
 c: (c, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=4
 seek-lt a
@@ -265,7 +265,7 @@ next
 a: (a, .)
 .
 a: (a, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=4
 seek-lt c
@@ -277,7 +277,7 @@ b: (b, .)
 a: (a, .)
 .
 a: (a, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 3 times (1 fwd/2 rev, internal: 0 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 3 times (1 fwd/2 rev, internal: 0 fwd/2 rev); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 
 iter seq=4
@@ -292,7 +292,7 @@ b: (b, .)
 a: (a, .)
 .
 a: (a, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 4 times (1 fwd/3 rev, internal: 0 fwd/3 rev); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 4B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 4 times (1 fwd/3 rev, internal: 0 fwd/3 rev); blocks: 0B cached; points: 4 (4B keys, 4B values)
 
 iter seq=4
 seek-prefix-ge a
@@ -300,7 +300,7 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=4
 seek-prefix-ge b
@@ -308,7 +308,7 @@ next
 ----
 b: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 
 iter seq=4
@@ -317,7 +317,7 @@ next
 ----
 c: (c, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 
 iter seq=4
@@ -334,7 +334,7 @@ seek-prefix-ge b
 a: (a, .)
 c: (c, .)
 b: (b, .)
-stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 3 times (3 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 define
 a.SET.b2:b
@@ -349,13 +349,13 @@ prev
 a: (b, .)
 .
 a: (b, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 4B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); blocks: 0B cached; points: 4 (4B keys, 4B values)
 
 iter seq=2
 seek-ge b
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 seek-lt a
@@ -371,7 +371,7 @@ next
 a: (b, .)
 .
 a: (b, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 seek-lt c
@@ -381,7 +381,7 @@ next
 a: (b, .)
 .
 a: (b, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -389,13 +389,13 @@ next
 ----
 a: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 seek-prefix-ge b
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 
 define
@@ -411,7 +411,7 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=5
 seek-prefix-ge a
@@ -419,13 +419,13 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=5
 seek-prefix-ge aa
 ----
 aa: (aa, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (2B keys, 2B values)
 
 iter seq=5
 seek-prefix-ge aa
@@ -433,7 +433,7 @@ next
 ----
 aa: (aa, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (2B keys, 2B values)
 
 iter seq=5
 seek-prefix-ge aa
@@ -441,7 +441,7 @@ next
 ----
 aa: (aa, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (2B keys, 2B values)
 
 iter seq=5
 seek-prefix-ge aaa
@@ -449,13 +449,13 @@ next
 ----
 aaa: (aaa, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (3B keys, 3B values)
 
 iter seq=5
 seek-prefix-ge aaa
 ----
 aaa: (aaa, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (3B keys, 3B values)
 
 iter seq=5
 seek-prefix-ge b
@@ -463,7 +463,7 @@ next
 ----
 b: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=5
 seek-prefix-ge aa
@@ -479,7 +479,7 @@ aaa: (aaa, .)
 aa: (aa, .)
 a: (a, .)
 .
-stats: seeked 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); stepped 4 times (0 fwd/4 rev, internal: 0 fwd/4 rev); blocks: 0B (0B cached), read time 0s; points: 5 (9B keys, 9B values)
+stats: seeked 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); stepped 4 times (0 fwd/4 rev, internal: 0 fwd/4 rev); blocks: 0B cached; points: 5 (9B keys, 9B values)
 
 iter seq=5
 seek-prefix-ge aa
@@ -495,7 +495,7 @@ aa: (aa, .)
 aaa: (aaa, .)
 b: (b, .)
 .
-stats: seeked 2 times (2 internal); stepped 4 times (4 internal); blocks: 0B (0B cached), read time 0s; points: 5 (9B keys, 9B values)
+stats: seeked 2 times (2 internal); stepped 4 times (4 internal); blocks: 0B cached; points: 5 (9B keys, 9B values)
 
 iter seq=5
 seek-prefix-ge aaa
@@ -509,7 +509,7 @@ aa: (aa, .)
 aaa: (aaa, .)
 b: (b, .)
 .
-stats: seeked 2 times (2 internal); stepped 3 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 4 (9B keys, 9B values)
+stats: seeked 2 times (2 internal); stepped 3 times (3 internal); blocks: 0B cached; points: 4 (9B keys, 9B values)
 
 iter seq=5
 seek-prefix-ge aaa
@@ -521,7 +521,7 @@ aaa: (aaa, .)
 aaa: (aaa, .)
 b: (b, .)
 .
-stats: seeked 2 times (2 internal); stepped 2 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 3 (7B keys, 7B values)
+stats: seeked 2 times (2 internal); stepped 2 times (2 internal); blocks: 0B cached; points: 3 (7B keys, 7B values)
 
 iter seq=5
 seek-prefix-ge aaa
@@ -537,7 +537,7 @@ aa: (aa, .)
 aaa: (aaa, .)
 b: (b, .)
 .
-stats: seeked 2 times (1 fwd/1 rev, internal: 2 fwd/1 rev); stepped 4 times (4 fwd/0 rev, internal: 4 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 6 (11B keys, 11B values)
+stats: seeked 2 times (1 fwd/1 rev, internal: 2 fwd/1 rev); stepped 4 times (4 fwd/0 rev, internal: 4 fwd/1 rev); blocks: 0B cached; points: 6 (11B keys, 11B values)
 
 
 iter seq=5
@@ -550,7 +550,7 @@ aaa: (aaa, .)
 aaa: (aaa, .)
 b: (b, .)
 .
-stats: seeked 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (2 fwd/0 rev, internal: 3 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 5 (12B keys, 12B values)
+stats: seeked 2 times (1 fwd/1 rev, internal: 1 fwd/1 rev); stepped 2 times (2 fwd/0 rev, internal: 3 fwd/1 rev); blocks: 0B cached; points: 5 (12B keys, 12B values)
 
 iter seq=4
 seek-prefix-ge a
@@ -562,7 +562,7 @@ a: (a, .)
 aa: (aa, .)
 aaa: (aaa, .)
 .
-stats: seeked 4 times (4 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 4 (7B keys, 7B values)
+stats: seeked 4 times (4 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 4 (7B keys, 7B values)
 
 iter seq=3
 seek-prefix-ge aaa
@@ -576,7 +576,7 @@ seek-prefix-ge aaa
 a: (a, .)
 aa: (aa, .)
 .
-stats: seeked 5 times (5 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 5 (10B keys, 10B values)
+stats: seeked 5 times (5 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 5 (10B keys, 10B values)
 
 define
 bb.DEL.2:
@@ -588,7 +588,7 @@ iter seq=4
 seek-prefix-ge bb
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (4B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (4B keys, 1B values)
 
 
 define
@@ -609,7 +609,7 @@ a: (bcd, .)
 b: (ab, .)
 .
 b: (ab, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 5 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 8 (8B keys, 8B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 5 fwd/2 rev); blocks: 0B cached; points: 8 (8B keys, 8B values)
 
 iter seq=3
 seek-ge a
@@ -617,7 +617,7 @@ next
 ----
 a: (bc, .)
 b: (ab, .)
-stats: seeked 1 times (1 internal); stepped 1 times (4 internal); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 1 times (1 internal); stepped 1 times (4 internal); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=2
 seek-ge a
@@ -625,7 +625,7 @@ next
 ----
 a: (b, .)
 b: (a, .)
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=4
 seek-lt c
@@ -637,7 +637,7 @@ b: (ab, .)
 a: (bcd, .)
 .
 a: (bcd, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 3 times (1 fwd/2 rev, internal: 2 fwd/5 rev); blocks: 0B (0B cached), read time 0s; points: 8 (8B keys, 8B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 3 times (1 fwd/2 rev, internal: 2 fwd/5 rev); blocks: 0B cached; points: 8 (8B keys, 8B values)
 
 iter seq=3
 seek-lt c
@@ -645,7 +645,7 @@ prev
 ----
 b: (ab, .)
 a: (bc, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/4 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/4 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=2
 seek-lt c
@@ -653,7 +653,7 @@ prev
 ----
 b: (a, .)
 a: (b, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/2 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=4
 seek-ge a
@@ -665,7 +665,7 @@ a: (bcd, .)
 b: (ab, .)
 a: (bcd, .)
 b: (ab, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 2 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 10 fwd/5 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 15B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 2 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 10 fwd/5 rev); blocks: 0B cached; points: 15 (15B keys, 15B values)
 
 iter seq=3
 seek-ge a
@@ -677,7 +677,7 @@ a: (bc, .)
 b: (ab, .)
 a: (bc, .)
 b: (ab, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 2 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 8 fwd/4 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 15B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 2 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 8 fwd/4 rev); blocks: 0B cached; points: 15 (15B keys, 15B values)
 
 iter seq=2
 seek-ge a
@@ -689,7 +689,7 @@ a: (b, .)
 b: (a, .)
 a: (b, .)
 b: (a, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 2 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 4 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 15B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 2 fwd/1 rev); stepped 3 times (2 fwd/1 rev, internal: 4 fwd/2 rev); blocks: 0B cached; points: 15 (15B keys, 15B values)
 
 iter seq=4
 seek-lt c
@@ -701,7 +701,7 @@ b: (ab, .)
 a: (bcd, .)
 b: (ab, .)
 a: (bcd, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/2 rev); stepped 3 times (1 fwd/2 rev, internal: 5 fwd/10 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 15B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/2 rev); stepped 3 times (1 fwd/2 rev, internal: 5 fwd/10 rev); blocks: 0B cached; points: 15 (15B keys, 15B values)
 
 iter seq=3
 seek-lt c
@@ -713,7 +713,7 @@ b: (ab, .)
 a: (bc, .)
 b: (ab, .)
 a: (bc, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/2 rev); stepped 3 times (1 fwd/2 rev, internal: 4 fwd/8 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 15B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/2 rev); stepped 3 times (1 fwd/2 rev, internal: 4 fwd/8 rev); blocks: 0B cached; points: 15 (15B keys, 15B values)
 
 iter seq=2
 seek-lt c
@@ -725,7 +725,7 @@ b: (a, .)
 a: (b, .)
 b: (a, .)
 a: (b, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/2 rev); stepped 3 times (1 fwd/2 rev, internal: 2 fwd/4 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 15B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/2 rev); stepped 3 times (1 fwd/2 rev, internal: 2 fwd/4 rev); blocks: 0B cached; points: 15 (15B keys, 15B values)
 
 iter seq=3
 seek-prefix-ge a
@@ -733,7 +733,7 @@ next
 ----
 a: (bc, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -741,7 +741,7 @@ next
 ----
 a: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=4
 seek-prefix-ge a
@@ -749,7 +749,7 @@ next
 ----
 a: (bcd, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -757,7 +757,7 @@ next
 ----
 a: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=3
 seek-prefix-ge a
@@ -765,7 +765,7 @@ next
 ----
 a: (bc, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=3
 seek-prefix-ge c
@@ -783,7 +783,7 @@ iter seq=3
 seek-prefix-ge a
 ----
 a: (bc, .)
-stats: seeked 1 times (1 internal); stepped 0 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 0 times (1 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 
 define
@@ -804,7 +804,7 @@ next
 a: (bc, .)
 .
 .
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -814,7 +814,7 @@ next
 a: (b, .)
 .
 .
-stats: seeked 1 times (1 internal); stepped 2 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 2 times (1 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=4
 seek-prefix-ge a
@@ -822,7 +822,7 @@ next
 ----
 a: (bcd, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 seek-prefix-ge a
@@ -830,7 +830,7 @@ next
 ----
 a: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=3
 seek-prefix-ge aa
@@ -838,13 +838,13 @@ next
 ----
 aa: (ab, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (4B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached; points: 2 (4B keys, 2B values)
 
 iter seq=4
 seek-prefix-ge aa
 ----
 aa: (ab, .)
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (4B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (4B keys, 2B values)
 
 define
 a.SET.1:a
@@ -861,7 +861,7 @@ prev
 a: (a, .)
 a: (a, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2 lower=b
 seek-ge a
@@ -871,7 +871,7 @@ prev
 b: (b, .)
 b: (b, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2 lower=c
 seek-ge a
@@ -881,7 +881,7 @@ prev
 c: (c, .)
 c: (c, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2 lower=d
 seek-ge a
@@ -891,7 +891,7 @@ prev
 d: (d, .)
 d: (d, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2 lower=e
 seek-ge a
@@ -909,7 +909,7 @@ next
 c: (c, .)
 c: (c, .)
 .
-stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=2 upper=c
 seek-lt d
@@ -919,7 +919,7 @@ next
 b: (b, .)
 b: (b, .)
 .
-stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=2 upper=b
 seek-lt d
@@ -929,7 +929,7 @@ next
 a: (a, .)
 a: (a, .)
 .
-stats: seeked 2 times (0 fwd/2 rev, internal: 1 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 1 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 2 times (0 fwd/2 rev, internal: 1 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 1 fwd/2 rev); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2 upper=a
 seek-lt d
@@ -945,7 +945,7 @@ next
 ----
 b: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 set-bounds lower=a
@@ -957,7 +957,7 @@ prev
 a: (a, .)
 a: (a, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 set-bounds lower=b
@@ -969,7 +969,7 @@ prev
 b: (b, .)
 b: (b, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 set-bounds lower=c
@@ -981,7 +981,7 @@ prev
 c: (c, .)
 c: (c, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 set-bounds lower=d
@@ -993,7 +993,7 @@ prev
 d: (d, .)
 d: (d, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 set-bounds lower=e
@@ -1015,7 +1015,7 @@ next
 c: (c, .)
 c: (c, .)
 .
-stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=2
 set-bounds upper=c
@@ -1027,7 +1027,7 @@ next
 b: (b, .)
 b: (b, .)
 .
-stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 2 times (0 fwd/2 rev, internal: 0 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 2 fwd/2 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 iter seq=2
 set-bounds upper=b
@@ -1039,7 +1039,7 @@ next
 a: (a, .)
 a: (a, .)
 .
-stats: seeked 2 times (0 fwd/2 rev, internal: 1 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 1 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 2 times (0 fwd/2 rev, internal: 1 fwd/2 rev); stepped 1 times (1 fwd/0 rev, internal: 1 fwd/2 rev); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 set-bounds upper=a
@@ -1061,7 +1061,7 @@ next
 c: (c, .)
 d: (d, .)
 .
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 2 times (2 fwd/0 rev, internal: 3 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 4B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 2 times (2 fwd/0 rev, internal: 3 fwd/1 rev); blocks: 0B cached; points: 4 (4B keys, 4B values)
 
 iter seq=2
 set-bounds lower=b upper=c
@@ -1071,7 +1071,7 @@ next
 .
 b: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 set-bounds lower=b
@@ -1083,7 +1083,7 @@ seek-ge a
 b: (b, .)
 .
 b: (b, .)
-stats: seeked 2 times (2 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 seek-ge a
@@ -1091,7 +1091,7 @@ set-bounds upper=e
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 set-bounds lower=b
@@ -1101,7 +1101,7 @@ set-bounds upper=e
 .
 b: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 set-bounds lower=b
@@ -1109,7 +1109,7 @@ first
 ----
 .
 b: (b, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 set-bounds upper=b
@@ -1117,7 +1117,7 @@ first
 ----
 .
 a: (a, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2
 set-bounds lower=b
@@ -1125,7 +1125,7 @@ last
 ----
 .
 d: (d, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 set-bounds upper=b
@@ -1133,7 +1133,7 @@ last
 ----
 .
 a: (a, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); stepped 0 times (0 fwd/0 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 # The prev call after "set-bounds upper=c" will assume that the iterator
 # is exhausted due to having stepped up to c. Which means prev should step
@@ -1148,7 +1148,7 @@ d: (d, .)
 .
 .
 b: (b, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/2 rev); stepped 2 times (1 fwd/1 rev, internal: 2 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 5 (5B keys, 5B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 0 fwd/2 rev); stepped 2 times (1 fwd/1 rev, internal: 2 fwd/2 rev); blocks: 0B cached; points: 5 (5B keys, 5B values)
 
 # The next call after "set-bounds lower=b" will assume that the iterator
 # is exhausted due to having stepped below b. Which means next should step
@@ -1163,7 +1163,7 @@ a: (a, .)
 .
 .
 b: (b, .)
-stats: seeked 1 times (2 internal); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (2 internal); stepped 2 times (1 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 iter seq=2
 set-bounds lower=b
@@ -1173,7 +1173,7 @@ next
 .
 b: (b, .)
 c: (c, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 iter seq=2
 set-bounds upper=d
@@ -1183,7 +1183,7 @@ prev
 .
 c: (c, .)
 b: (b, .)
-stats: seeked 1 times (1 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/2 rev); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/2 rev); blocks: 0B cached; points: 3 (3B keys, 3B values)
 
 define
 a.SET.1:a
@@ -1200,7 +1200,7 @@ prev
 a: (a, .)
 a: (a, .)
 .
-stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 2 times (2 internal); stepped 1 times (0 fwd/1 rev, internal: 0 fwd/1 rev); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 
 iter seq=2 lower=aa
@@ -1215,7 +1215,7 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2 lower=a upper=aaa
 seek-prefix-ge a
@@ -1223,7 +1223,7 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2 lower=a upper=b
 seek-prefix-ge a
@@ -1231,7 +1231,7 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2 lower=a upper=c
 seek-prefix-ge a
@@ -1239,13 +1239,13 @@ next
 ----
 a: (a, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 iter seq=2 lower=a upper=aaa
 seek-prefix-ge aa
 ----
 aa: (aa, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (2B keys, 2B values)
 
 iter seq=2 lower=a upper=aaa
 seek-prefix-ge aa
@@ -1253,7 +1253,7 @@ next
 ----
 aa: (aa, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (1 internal); blocks: 0B cached; points: 1 (2B keys, 2B values)
 
 define
 a.SET.1:a
@@ -1268,7 +1268,7 @@ next
 a: (a, .)
 b: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 2B values)
 
 define
 a.SET.2:a
@@ -1279,7 +1279,7 @@ iter seq=2
 seek-prefix-ge a
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 1 (1B keys, 1B values)
 
 define
 a.SINGLEDEL.1:
@@ -1289,7 +1289,7 @@ iter seq=2
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (1 internal); blocks: 0B (0B cached), read time 0s; points: 1 (1B keys, 0B values)
+stats: seeked 1 times (1 internal); stepped 0 times (1 internal); blocks: 0B cached; points: 1 (1B keys, 0B values)
 
 define
 a.SINGLEDEL.2:
@@ -1300,7 +1300,7 @@ iter seq=3
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 0B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 0B values)
 
 define
 a.SINGLEDEL.2:
@@ -1311,7 +1311,7 @@ iter seq=3
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 0B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 0B values)
 
 define
 a.SINGLEDEL.2:
@@ -1322,7 +1322,7 @@ iter seq=3
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 0B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 0B values)
 
 define
 a.SINGLEDEL.2:
@@ -1333,7 +1333,7 @@ iter seq=3
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 define
 a.SET.2:b
@@ -1346,7 +1346,7 @@ next
 ----
 a: (b, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 1 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 1B values)
 
 define
 a.SINGLEDEL.2:
@@ -1360,7 +1360,7 @@ next
 ----
 b: (c, .)
 .
-stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 1 times (3 internal); blocks: 0B cached; points: 3 (3B keys, 2B values)
 
 define
 a.SINGLEDEL.3:
@@ -1372,7 +1372,7 @@ iter
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 0B cached; points: 3 (3B keys, 2B values)
 
 define
 a.SINGLEDEL.3:
@@ -1384,7 +1384,7 @@ iter seq=4
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (3 internal); blocks: 0B (0B cached), read time 0s; points: 3 (3B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 0 times (3 internal); blocks: 0B cached; points: 3 (3B keys, 2B values)
 
 define
 a.SINGLEDEL.4:
@@ -1397,7 +1397,7 @@ iter seq=5
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (4 internal); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 6B values)
+stats: seeked 1 times (1 internal); stepped 0 times (4 internal); blocks: 0B cached; points: 4 (4B keys, 6B values)
 
 define
 a.SINGLEDEL.4:
@@ -1410,7 +1410,7 @@ iter seq=5
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (4 internal); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 6B values)
+stats: seeked 1 times (1 internal); stepped 0 times (4 internal); blocks: 0B cached; points: 4 (4B keys, 6B values)
 
 define
 a.SINGLEDEL.4:
@@ -1423,7 +1423,7 @@ iter seq=5
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (4 internal); blocks: 0B (0B cached), read time 0s; points: 4 (4B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 0 times (4 internal); blocks: 0B cached; points: 4 (4B keys, 3B values)
 
 define
 a.SINGLEDEL.3:
@@ -1434,7 +1434,7 @@ iter seq=4
 first
 ----
 .
-stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B (0B cached), read time 0s; points: 2 (2B keys, 3B values)
+stats: seeked 1 times (1 internal); stepped 0 times (2 internal); blocks: 0B cached; points: 2 (2B keys, 3B values)
 
 # Exercise iteration with limits, when there are no deletes.
 define
@@ -1476,7 +1476,7 @@ a: valid (a, .)
 . exhausted
 . at-limit
 a: valid (a, .)
-stats: seeked 2 times (1 fwd/1 rev, internal: 3 fwd/1 rev); stepped 13 times (6 fwd/7 rev, internal: 3 fwd/6 rev); blocks: 0B (0B cached), read time 0s; points: 11 (11B keys, 11B values)
+stats: seeked 2 times (1 fwd/1 rev, internal: 3 fwd/1 rev); stepped 13 times (6 fwd/7 rev, internal: 3 fwd/6 rev); blocks: 0B cached; points: 11 (11B keys, 11B values)
 
 # Exercise iteration with limits when we have deletes.
 
@@ -1523,7 +1523,7 @@ d: valid (d, .)
 . exhausted
 d: valid (d, .)
 . exhausted
-stats: seeked 1 times (1 fwd/0 rev, internal: 3 fwd/1 rev); stepped 15 times (10 fwd/5 rev, internal: 13 fwd/8 rev); blocks: 0B (0B cached), read time 0s; points: 21 (21B keys, 14B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 3 fwd/1 rev); stepped 15 times (10 fwd/5 rev, internal: 13 fwd/8 rev); blocks: 0B cached; points: 21 (21B keys, 14B values)
 
 iter seq=4
 seek-ge-limit b d
@@ -1535,7 +1535,7 @@ next-limit e
 . at-limit
 . at-limit
 d: valid (d, .)
-stats: seeked 1 times (1 internal); stepped 3 times (2 fwd/1 rev, internal: 9 fwd/5 rev); blocks: 0B (0B cached), read time 0s; points: 15 (15B keys, 9B values)
+stats: seeked 1 times (1 internal); stepped 3 times (2 fwd/1 rev, internal: 9 fwd/5 rev); blocks: 0B cached; points: 15 (15B keys, 9B values)
 
 iter seq=4
 seek-lt-limit d c
@@ -1551,7 +1551,7 @@ next-limit b
 a: valid (a, .)
 . exhausted
 a: valid (a, .)
-stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 5 times (1 fwd/4 rev, internal: 0 fwd/5 rev); blocks: 0B (0B cached), read time 0s; points: 6 (6B keys, 4B values)
+stats: seeked 1 times (0 fwd/1 rev, internal: 1 fwd/1 rev); stepped 5 times (1 fwd/4 rev, internal: 0 fwd/5 rev); blocks: 0B cached; points: 6 (6B keys, 4B values)
 
 # NB: Zero values are skipped by deletable merger.
 define merger=deletable
@@ -1577,7 +1577,7 @@ prev
 .
 .
 .
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 4 times (2 fwd/2 rev, internal: 8 fwd/8 rev); blocks: 0B (0B cached), read time 0s; points: 16 (16B keys, 24B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 4 times (2 fwd/2 rev, internal: 8 fwd/8 rev); blocks: 0B cached; points: 16 (16B keys, 24B values)
 
 iter seq=4
 seek-ge a
@@ -1591,4 +1591,4 @@ b: (3, .)
 .
 b: (3, .)
 a: (2, .)
-stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 4 times (2 fwd/2 rev, internal: 6 fwd/6 rev); blocks: 0B (0B cached), read time 0s; points: 16 (16B keys, 24B values)
+stats: seeked 1 times (1 fwd/0 rev, internal: 1 fwd/1 rev); stepped 4 times (2 fwd/2 rev, internal: 6 fwd/6 rev); blocks: 0B cached; points: 16 (16B keys, 24B values)
diff --git a/testdata/iterator_stats b/testdata/iterator_stats
index 5579b4d8..7c7b9347 100644
--- a/testdata/iterator_stats
+++ b/testdata/iterator_stats
@@ -17,7 +17,7 @@ stats
 a: (1, .)
 c: (2, .)
 .
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 57B (57B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 57B cached; points: 2 (2B keys, 2B values)
 
 # Perform the same operation again with a new iterator. It should yield
 # identical statistics.
@@ -31,7 +31,7 @@ stats
 a: (1, .)
 c: (2, .)
 .
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 57B (57B cached), read time 0s; points: 2 (2B keys, 2B values)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 57B cached; points: 2 (2B keys, 2B values)
 
 build ext2
 set d@10 d10
@@ -61,13 +61,13 @@ next
 stats
 ----
 c: (2, .)
-stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 57B (57B cached), read time 0s; points: 1 (1B keys, 1B values)
+stats: seeked 1 times (1 internal); stepped 0 times (0 internal); blocks: 57B cached; points: 1 (1B keys, 1B values)
 d@10: (d10, .)
 d@9: (d9, .)
-stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 157B (147B cached), read time 0s; points: 3 (8B keys, 6B values); separated: 1 (2B, 2B fetched)
+stats: seeked 1 times (1 internal); stepped 2 times (2 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 3 (8B keys, 6B values); separated: 1 (2B, 2B fetched)
 d@8: (d8, .)
-stats: seeked 1 times (1 internal); stepped 3 times (3 internal); blocks: 157B (147B cached), read time 0s; points: 4 (11B keys, 8B values); separated: 2 (4B, 4B fetched)
+stats: seeked 1 times (1 internal); stepped 3 times (3 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 4 (11B keys, 8B values); separated: 2 (4B, 4B fetched)
 e@20: (e20, .)
-stats: seeked 1 times (1 internal); stepped 4 times (4 internal); blocks: 157B (147B cached), read time 0s; points: 5 (15B keys, 11B values); separated: 2 (4B, 4B fetched)
+stats: seeked 1 times (1 internal); stepped 4 times (4 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 5 (15B keys, 11B values); separated: 2 (4B, 4B fetched)
 e@18: (e18, .)
-stats: seeked 1 times (1 internal); stepped 5 times (5 internal); blocks: 157B (147B cached), read time 0s; points: 6 (19B keys, 13B values); separated: 3 (7B, 7B fetched)
+stats: seeked 1 times (1 internal); stepped 5 times (5 internal); blocks: 147B cached, 10B not cached (read time: 0s); points: 6 (19B keys, 13B values); separated: 3 (7B, 7B fetched)
diff --git a/testdata/metrics b/testdata/metrics
index 6776db1c..ebd7787a 100644
--- a/testdata/metrics
+++ b/testdata/metrics
@@ -75,7 +75,7 @@ Virtual tables: 0 (0B)
 Local tables size: 589B
 Compression types: snappy: 1
 Block cache: 3 entries (484B)  hit rate: 0.0%
-Table cache: 1 entries (760B)  hit rate: 0.0%
+Table cache: 1 entries (768B)  hit rate: 0.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 1
@@ -214,7 +214,7 @@ Virtual tables: 0 (0B)
 Local tables size: 595B
 Compression types: snappy: 1
 Block cache: 3 entries (484B)  hit rate: 33.3%
-Table cache: 1 entries (760B)  hit rate: 66.7%
+Table cache: 1 entries (768B)  hit rate: 66.7%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 1
@@ -488,7 +488,7 @@ Virtual tables: 0 (0B)
 Local tables size: 4.3KB
 Compression types: snappy: 7
 Block cache: 12 entries (1.9KB)  hit rate: 9.1%
-Table cache: 1 entries (760B)  hit rate: 53.8%
+Table cache: 1 entries (768B)  hit rate: 53.8%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
@@ -551,7 +551,7 @@ Virtual tables: 0 (0B)
 Local tables size: 6.1KB
 Compression types: snappy: 10
 Block cache: 12 entries (1.9KB)  hit rate: 9.1%
-Table cache: 1 entries (760B)  hit rate: 53.8%
+Table cache: 1 entries (768B)  hit rate: 53.8%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
@@ -822,7 +822,7 @@ Virtual tables: 0 (0B)
 Local tables size: 0B
 Compression types: snappy: 1
 Block cache: 1 entries (440B)  hit rate: 0.0%
-Table cache: 1 entries (760B)  hit rate: 0.0%
+Table cache: 1 entries (768B)  hit rate: 0.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
@@ -869,7 +869,7 @@ Virtual tables: 0 (0B)
 Local tables size: 0B
 Compression types: snappy: 2
 Block cache: 6 entries (996B)  hit rate: 0.0%
-Table cache: 1 entries (760B)  hit rate: 50.0%
+Table cache: 1 entries (768B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
@@ -917,7 +917,7 @@ Virtual tables: 0 (0B)
 Local tables size: 589B
 Compression types: snappy: 3
 Block cache: 6 entries (996B)  hit rate: 0.0%
-Table cache: 1 entries (760B)  hit rate: 50.0%
+Table cache: 1 entries (768B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0