Repository: cockroachdb/pebble
Pull request #3636 (2024-05-26 11:44:41)

objstorageprovider: reuse buf from pooled remoteHandle
During heavy reads of remote files, the `r.buffered.data = make([]byte, length)` line was observed to account for more than half of CRDB's total space allocations.

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/3636)
<!-- Reviewable:end -->


Patch:
diff --git a/cmd/pebble/db.go b/cmd/pebble/db.go
index 41c6e594..21aed4b6 100644
--- a/cmd/pebble/db.go
+++ b/cmd/pebble/db.go
@@ -10,7 +10,9 @@ import (
 	"github.com/cockroachdb/pebble"
 	"github.com/cockroachdb/pebble/bloom"
 	"github.com/cockroachdb/pebble/internal/bytealloc"
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 	"github.com/cockroachdb/pebble/objstorage/remote"
+	"github.com/cockroachdb/pebble/sstable"
 	"github.com/cockroachdb/pebble/vfs"
 )
 
@@ -57,7 +59,7 @@ func newPebbleDB(dir string) DB {
 	defer cache.Unref()
 	opts := &pebble.Options{
 		Cache:                       cache,
-		Comparer:                    mvccComparer,
+		Comparer:                    &crdbtest.Comparer,
 		DisableWAL:                  disableWAL,
 		FormatMajorVersion:          pebble.FormatNewest,
 		L0CompactionThreshold:       2,
@@ -77,7 +79,8 @@ func newPebbleDB(dir string) DB {
 
 	for i := 0; i < len(opts.Levels); i++ {
 		l := &opts.Levels[i]
-		l.BlockSize = 32 << 10       // 32 KB
+		l.BlockSize = 32 << 10 // 32 KB
+		l.Compression = func() sstable.Compression { return pebble.ZstdCompression }
 		l.IndexBlockSize = 256 << 10 // 256 KB
 		l.FilterPolicy = bloom.FilterPolicy(10)
 		l.FilterType = pebble.TableFilter
diff --git a/cmd/pebble/main.go b/cmd/pebble/main.go
index 9417bfb8..688a8687 100644
--- a/cmd/pebble/main.go
+++ b/cmd/pebble/main.go
@@ -9,6 +9,7 @@ import (
 	"os"
 	"time"
 
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 	"github.com/cockroachdb/pebble/internal/testkeys"
 	"github.com/cockroachdb/pebble/tool"
 	"github.com/spf13/cobra"
@@ -57,7 +58,7 @@ func main() {
 	}
 	rootCmd.AddCommand(benchCmd)
 
-	t := tool.New(tool.Comparers(mvccComparer, testkeys.Comparer), tool.Mergers(fauxMVCCMerger))
+	t := tool.New(tool.Comparers(&crdbtest.Comparer, testkeys.Comparer), tool.Mergers(fauxMVCCMerger))
 	rootCmd.AddCommand(t.Commands...)
 
 	for _, cmd := range []*cobra.Command{replayCmd, scanCmd, syncCmd, tombstoneCmd, writeBenchCmd, ycsbCmd} {
diff --git a/cmd/pebble/mvcc.go b/cmd/pebble/mvcc.go
index 0e388de1..ef4cdd50 100644
--- a/cmd/pebble/mvcc.go
+++ b/cmd/pebble/mvcc.go
@@ -9,167 +9,16 @@ import (
 
 	"github.com/cockroachdb/pebble"
 	"github.com/cockroachdb/pebble/internal/bytealloc"
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 )
 
-// MVCC encoding and decoding routines adapted from CockroachDB sources. Used
-// to perform apples-to-apples benchmarking for CockroachDB's usage of RocksDB.
-
-var mvccComparer = &pebble.Comparer{
-	Compare: mvccCompare,
-
-	AbbreviatedKey: func(k []byte) uint64 {
-		key, _, ok := mvccSplitKey(k)
-		if !ok {
-			return 0
-		}
-		return pebble.DefaultComparer.AbbreviatedKey(key)
-	},
-
-	Equal: func(a, b []byte) bool {
-		return mvccCompare(a, b) == 0
-	},
-
-	Separator: func(dst, a, b []byte) []byte {
-		aKey, _, ok := mvccSplitKey(a)
-		if !ok {
-			return append(dst, a...)
-		}
-		bKey, _, ok := mvccSplitKey(b)
-		if !ok {
-			return append(dst, a...)
-		}
-		// If the keys are the same just return a.
-		if bytes.Equal(aKey, bKey) {
-			return append(dst, a...)
-		}
-		n := len(dst)
-		// MVCC key comparison uses bytes.Compare on the roachpb.Key, which is the same semantics as
-		// pebble.DefaultComparer, so reuse the latter's Separator implementation.
-		dst = pebble.DefaultComparer.Separator(dst, aKey, bKey)
-		// Did it pick a separator different than aKey -- if it did not we can't do better than a.
-		buf := dst[n:]
-		if bytes.Equal(aKey, buf) {
-			return append(dst[:n], a...)
-		}
-		// The separator is > aKey, so we only need to add the timestamp sentinel.
-		return append(dst, 0)
-	},
-
-	Successor: func(dst, a []byte) []byte {
-		aKey, _, ok := mvccSplitKey(a)
-		if !ok {
-			return append(dst, a...)
-		}
-		n := len(dst)
-		// MVCC key comparison uses bytes.Compare on the roachpb.Key, which is the same semantics as
-		// pebble.DefaultComparer, so reuse the latter's Successor implementation.
-		dst = pebble.DefaultComparer.Successor(dst, aKey)
-		// Did it pick a successor different than aKey -- if it did not we can't do better than a.
-		buf := dst[n:]
-		if bytes.Equal(aKey, buf) {
-			return append(dst[:n], a...)
-		}
-		// The successor is > aKey, so we only need to add the timestamp sentinel.
-		return append(dst, 0)
-	},
-
-	Split: func(k []byte) int {
-		key, _, ok := mvccSplitKey(k)
-		if !ok {
-			return len(k)
-		}
-		// This matches the behavior of libroach/KeyPrefix. RocksDB requires that
-		// keys generated via a SliceTransform be comparable with normal encoded
-		// MVCC keys. Encoded MVCC keys have a suffix indicating the number of
-		// bytes of timestamp data. MVCC keys without a timestamp have a suffix of
-		// 0. We're careful in EncodeKey to make sure that the user-key always has
-		// a trailing 0. If there is no timestamp this falls out naturally. If
-		// there is a timestamp we prepend a 0 to the encoded timestamp data.
-		return len(key) + 1
-	},
-
-	Name: "cockroach_comparator",
-}
-
-func mvccSplitKey(mvccKey []byte) (key []byte, ts []byte, ok bool) {
-	if len(mvccKey) == 0 {
-		return nil, nil, false
-	}
-	n := len(mvccKey) - 1
-	tsLen := int(mvccKey[n])
-	if n < tsLen {
-		return nil, nil, false
-	}
-	key = mvccKey[:n-tsLen]
-	if tsLen > 0 {
-		ts = mvccKey[n-tsLen+1 : len(mvccKey)-1]
-	}
-	return key, ts, true
-}
-
-func mvccCompare(a, b []byte) int {
-	// NB: For performance, this routine manually splits the key into the
-	// user-key and timestamp components rather than using SplitMVCCKey. Don't
-	// try this at home kids: use SplitMVCCKey.
-
-	aEnd := len(a) - 1
-	bEnd := len(b) - 1
-	if aEnd < 0 || bEnd < 0 {
-		// This should never happen unless there is some sort of corruption of
-		// the keys. This is a little bizarre, but the behavior exactly matches
-		// engine/db.cc:DBComparator.
-		return bytes.Compare(a, b)
-	}
-
-	// Compute the index of the separator between the key and the timestamp.
-	aSep := aEnd - int(a[aEnd])
-	bSep := bEnd - int(b[bEnd])
-	if aSep < 0 || bSep < 0 {
-		// This should never happen unless there is some sort of corruption of
-		// the keys. This is a little bizarre, but the behavior exactly matches
-		// engine/db.cc:DBComparator.
-		return bytes.Compare(a, b)
-	}
-
-	// Compare the "user key" part of the key.
-	if c := bytes.Compare(a[:aSep], b[:bSep]); c != 0 {
-		return c
-	}
-
-	// Compare the timestamp part of the key.
-	aTS := a[aSep:aEnd]
-	bTS := b[bSep:bEnd]
-	if len(aTS) == 0 {
-		if len(bTS) == 0 {
-			return 0
-		}
-		return -1
-	} else if len(bTS) == 0 {
-		return 1
-	}
-	return bytes.Compare(bTS, aTS)
-}
-
-// <key>\x00[<wall_time>[<logical>]]<#timestamp-bytes>
-func mvccEncode(dst, key []byte, walltime uint64, logical uint32) []byte {
-	dst = append(dst, key...)
-	dst = append(dst, 0)
-	if walltime != 0 || logical != 0 {
-		extra := byte(1 + 8)
-		dst = encodeUint64Ascending(dst, walltime)
-		if logical != 0 {
-			dst = encodeUint32Ascending(dst, logical)
-			extra += 4
-		}
-		dst = append(dst, extra)
-	}
-	return dst
-}
+// MVCC routines adapted from CockroachDB sources. Used to perform
+// apples-to-apples benchmarking for CockroachDB's usage of RocksDB.
 
 func mvccForwardScan(d DB, start, end, ts []byte) (int, int64) {
 	it := d.NewIter(&pebble.IterOptions{
-		LowerBound: mvccEncode(nil, start, 0, 0),
-		UpperBound: mvccEncode(nil, end, 0, 0),
+		LowerBound: crdbtest.EncodeMVCCKey(nil, start, 0, 0),
+		UpperBound: crdbtest.EncodeMVCCKey(nil, end, 0, 0),
 	})
 	defer it.Close()
 
@@ -178,9 +27,10 @@ func mvccForwardScan(d DB, start, end, ts []byte) (int, int64) {
 	var nbytes int64
 
 	for valid := it.First(); valid; valid = it.Next() {
-		key, keyTS, _ := mvccSplitKey(it.Key())
-		if bytes.Compare(keyTS, ts) <= 0 {
-			data, _ = data.Copy(key)
+		k := it.Key()
+		si := crdbtest.Split(k)
+		if bytes.Compare(k[si:], ts) <= 0 {
+			data, _ = data.Copy(k[:si])
 			data, _ = data.Copy(it.Value())
 		}
 		count++
@@ -191,8 +41,8 @@ func mvccForwardScan(d DB, start, end, ts []byte) (int, int64) {
 
 func mvccReverseScan(d DB, start, end, ts []byte) (int, int64) {
 	it := d.NewIter(&pebble.IterOptions{
-		LowerBound: mvccEncode(nil, start, 0, 0),
-		UpperBound: mvccEncode(nil, end, 0, 0),
+		LowerBound: crdbtest.EncodeMVCCKey(nil, start, 0, 0),
+		UpperBound: crdbtest.EncodeMVCCKey(nil, end, 0, 0),
 	})
 	defer it.Close()
 
@@ -201,9 +51,10 @@ func mvccReverseScan(d DB, start, end, ts []byte) (int, int64) {
 	var nbytes int64
 
 	for valid := it.Last(); valid; valid = it.Prev() {
-		key, keyTS, _ := mvccSplitKey(it.Key())
-		if bytes.Compare(keyTS, ts) <= 0 {
-			data, _ = data.Copy(key)
+		k := it.Key()
+		si := crdbtest.Split(k)
+		if bytes.Compare(k[si:], ts) <= 0 {
+			data, _ = data.Copy(k[:si])
 			data, _ = data.Copy(it.Value())
 		}
 		count++
diff --git a/cmd/pebble/queue.go b/cmd/pebble/queue.go
index 71937411..8c3de68a 100644
--- a/cmd/pebble/queue.go
+++ b/cmd/pebble/queue.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"github.com/cockroachdb/pebble"
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 	"github.com/cockroachdb/pebble/internal/randvar"
 	"github.com/spf13/cobra"
 	"golang.org/x/exp/rand"
@@ -47,7 +48,7 @@ func queueTest() (test, *atomic.Int64) {
 			)
 			for i := 0; i < queueConfig.size; i++ {
 				b := d.NewBatch()
-				queue[i] = mvccEncode(nil, encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0)
+				queue[i] = crdbtest.EncodeMVCCKey(nil, encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0)
 				value = queueConfig.values.Bytes(rng, value)
 				b.Set(queue[i], value, pebble.NoSync)
 				if err := b.Commit(pebble.NoSync); err != nil {
@@ -79,7 +80,7 @@ func queueTest() (test, *atomic.Int64) {
 
 					// Append to the tail.
 					b = d.NewBatch()
-					queue[idx] = mvccEncode(queue[idx][:0], encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0)
+					queue[idx] = crdbtest.EncodeMVCCKey(queue[idx][:0], encodeUint32Ascending([]byte("queue-"), uint32(i)), uint64(i+1), 0)
 					value = queueConfig.values.Bytes(rng, value)
 					b.Set(queue[idx], value, nil)
 					if err := b.Commit(pebble.Sync); err != nil {
diff --git a/cmd/pebble/replay.go b/cmd/pebble/replay.go
index 7479769b..07fa5e85 100644
--- a/cmd/pebble/replay.go
+++ b/cmd/pebble/replay.go
@@ -24,6 +24,7 @@ import (
 	"github.com/cockroachdb/pebble/bloom"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/cache"
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 	"github.com/cockroachdb/pebble/replay"
 	"github.com/cockroachdb/pebble/vfs"
 	"github.com/spf13/cobra"
@@ -371,7 +372,7 @@ func makeComparer(name string) (*pebble.Comparer, error) {
 	case base.DefaultComparer.Name:
 		return base.DefaultComparer, nil
 	case "cockroach_comparator":
-		return mvccComparer, nil
+		return &crdbtest.Comparer, nil
 	default:
 		return nil, errors.Newf("unrecognized comparer %q", name)
 	}
diff --git a/cmd/pebble/scan.go b/cmd/pebble/scan.go
index 0803501f..d30a314d 100644
--- a/cmd/pebble/scan.go
+++ b/cmd/pebble/scan.go
@@ -13,6 +13,7 @@ import (
 	"time"
 
 	"github.com/cockroachdb/pebble"
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 	"github.com/cockroachdb/pebble/internal/randvar"
 	"github.com/spf13/cobra"
 	"golang.org/x/exp/rand"
@@ -72,7 +73,7 @@ func runScan(cmd *cobra.Command, args []string) {
 				b := d.NewBatch()
 				var value []byte
 				for end := i + batch; i < end; i++ {
-					keys[i] = mvccEncode(nil, encodeUint32Ascending([]byte("key-"), uint32(i)), uint64(i+1), 0)
+					keys[i] = crdbtest.EncodeMVCCKey(nil, encodeUint32Ascending([]byte("key-"), uint32(i)), uint64(i+1), 0)
 					value = scanConfig.values.Bytes(rng, value)
 					if err := b.Set(keys[i], value, nil); err != nil {
 						log.Fatal(err)
diff --git a/cmd/pebble/sync.go b/cmd/pebble/sync.go
index e2add263..33e79dc0 100644
--- a/cmd/pebble/sync.go
+++ b/cmd/pebble/sync.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"github.com/cockroachdb/pebble"
+	"github.com/cockroachdb/pebble/internal/crdbtest"
 	"github.com/cockroachdb/pebble/internal/randvar"
 	"github.com/spf13/cobra"
 	"golang.org/x/exp/rand"
@@ -86,7 +87,7 @@ func runSync(cmd *cobra.Command, args []string) {
 								}
 							} else {
 								raw = encodeUint32Ascending(raw[:0], rand.Uint32())
-								key := mvccEncode(buf[:0], raw, 0, 0)
+								key := crdbtest.EncodeMVCCKey(buf[:0], raw, 0, 0)
 								buf = key[:0]
 								if err := b.Set(key, block, nil); err != nil {
 									log.Fatal(err)
diff --git a/compaction.go b/compaction.go
index 94ec4871..536fb0f5 100644
--- a/compaction.go
+++ b/compaction.go
@@ -8,7 +8,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io"
 	"math"
 	"runtime/pprof"
 	"slices"
@@ -77,9 +76,7 @@ type noCloseIter struct {
 	keyspan.FragmentIterator
 }
 
-func (i noCloseIter) Close() error {
-	return nil
-}
+func (i *noCloseIter) Close() {}
 
 type compactionLevel struct {
 	level int
@@ -228,10 +225,10 @@ type compaction struct {
 	smallest InternalKey
 	largest  InternalKey
 
-	// A list of objects to close when the compaction finishes. Used by input
-	// iteration to keep rangeDelIters open for the lifetime of the compaction,
-	// and only close them when the compaction finishes.
-	closers []io.Closer
+	// A list of fragment iterators to close when the compaction finishes. Used by
+	// input iteration to keep rangeDelIters open for the lifetime of the
+	// compaction, and only close them when the compaction finishes.
+	closers []*noCloseIter
 
 	// grandparents are the tables in level+2 that overlap with the files being
 	// compacted. Used to determine output table boundaries. Do not assume that the actual files
@@ -788,8 +785,7 @@ func (c *compaction) newInputIters(
 			// mergingIter.
 			iter := level.files.Iter()
 			for f := iter.First(); f != nil; f = iter.Next() {
-				rangeDelIter, closer, err := c.newRangeDelIter(
-					newIters, iter.Take(), iterOpts, l)
+				rangeDelIter, err := c.newRangeDelIter(newIters, iter.Take(), iterOpts, l)
 				if err != nil {
 					// The error will already be annotated with the BackingFileNum, so
 					// we annotate it with the FileNum.
@@ -799,7 +795,7 @@ func (c *compaction) newInputIters(
 					continue
 				}
 				rangeDelIters = append(rangeDelIters, rangeDelIter)
-				c.closers = append(c.closers, closer)
+				c.closers = append(c.closers, rangeDelIter)
 			}
 
 			// Check if this level has any range keys.
@@ -813,25 +809,25 @@ func (c *compaction) newInputIters(
 			if hasRangeKeys {
 				li := &keyspanimpl.LevelIter{}
 				newRangeKeyIterWrapper := func(file *manifest.FileMetadata, iterOptions keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) {
-					iter, err := newRangeKeyIter(file, iterOptions)
+					rangeKeyIter, err := newRangeKeyIter(file, iterOptions)
 					if err != nil {
 						return nil, err
-					} else if iter == nil {
+					} else if rangeKeyIter == nil {
 						return emptyKeyspanIter, nil
 					}
 					// Ensure that the range key iter is not closed until the compaction is
 					// finished. This is necessary because range key processing
 					// requires the range keys to be held in memory for up to the
 					// lifetime of the compaction.
-					c.closers = append(c.closers, iter)
-					iter = noCloseIter{iter}
+					noCloseIter := &noCloseIter{rangeKeyIter}
+					c.closers = append(c.closers, noCloseIter)
 
 					// We do not need to truncate range keys to sstable boundaries, or
 					// only read within the file's atomic compaction units, unlike with
 					// range tombstones. This is because range keys were added after we
 					// stopped splitting user keys across sstables, so all the range keys
 					// in this sstable must wholly lie within the file's bounds.
-					return iter, err
+					return noCloseIter, err
 				}
 				li.Init(keyspan.SpanIterOptions{}, c.cmp, newRangeKeyIterWrapper, level.files.Iter(), l, manifest.KeyTypeRange)
 				rangeKeyIters = append(rangeKeyIters, li)
@@ -902,7 +898,7 @@ func (c *compaction) newInputIters(
 
 func (c *compaction) newRangeDelIter(
 	newIters tableNewIters, f manifest.LevelFile, opts IterOptions, l manifest.Level,
-) (keyspan.FragmentIterator, io.Closer, error) {
+) (*noCloseIter, error) {
 	opts.level = l
 	iterSet, err := newIters(context.Background(), f.FileMetadata, &opts,
 		internalIterOpts{
@@ -910,16 +906,16 @@ func (c *compaction) newRangeDelIter(
 			bufferPool: &c.bufferPool,
 		}, iterRangeDeletions)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	} else if iterSet.rangeDeletion == nil {
 		// The file doesn't contain any range deletions.
-		return nil, nil, nil
+		return nil, nil
 	}
 	// Ensure that rangeDelIter is not closed until the compaction is
 	// finished. This is necessary because range tombstone processing
 	// requires the range tombstones to be held in memory for up to the
 	// lifetime of the compaction.
-	return noCloseIter{iterSet.rangeDeletion}, iterSet.rangeDeletion, nil
+	return &noCloseIter{iterSet.rangeDeletion}, nil
 }
 
 func (c *compaction) String() string {
@@ -2497,7 +2493,7 @@ func (d *DB) compactAndWrite(
 	pointIter, rangeDelIter, rangeKeyIter, err := c.newInputIters(d.newIters, d.tableNewRangeKeyIter)
 	defer func() {
 		for _, closer := range c.closers {
-			result.Err = firstError(result.Err, closer.Close())
+			closer.FragmentIterator.Close()
 		}
 	}()
 	if err != nil {
diff --git a/db.go b/db.go
index f61e90fd..7a8cf541 100644
--- a/db.go
+++ b/db.go
@@ -3007,7 +3007,13 @@ func (d *DB) checkVirtualBounds(m *fileMetadata) {
 
 // DebugString returns a debugging string describing the LSM.
 func (d *DB) DebugString() string {
+	return d.DebugCurrentVersion().DebugString()
+}
+
+// DebugCurrentVersion returns the current LSM tree metadata. Should only be
+// used for testing/debugging.
+func (d *DB) DebugCurrentVersion() *manifest.Version {
 	d.mu.Lock()
 	defer d.mu.Unlock()
-	return d.mu.versions.currentVersion().DebugString()
+	return d.mu.versions.currentVersion()
 }
diff --git a/error_iter.go b/error_iter.go
index 4ae02811..b9f4c5ba 100644
--- a/error_iter.go
+++ b/error_iter.go
@@ -85,6 +85,6 @@ func (i *errorKeyspanIter) First() (*keyspan.Span, error)            { return ni
 func (i *errorKeyspanIter) Last() (*keyspan.Span, error)             { return nil, i.err }
 func (i *errorKeyspanIter) Next() (*keyspan.Span, error)             { return nil, i.err }
 func (i *errorKeyspanIter) Prev() (*keyspan.Span, error)             { return nil, i.err }
-func (i *errorKeyspanIter) Close() error                             { return i.err }
+func (i *errorKeyspanIter) Close()                                   {}
 func (*errorKeyspanIter) String() string                             { return "error" }
 func (*errorKeyspanIter) WrapChildren(wrap keyspan.WrapFn)           {}
diff --git a/flushable.go b/flushable.go
index a2ed68c4..241ddcfc 100644
--- a/flushable.go
+++ b/flushable.go
@@ -7,7 +7,6 @@ package pebble
 import (
 	"context"
 	"fmt"
-	"io"
 	"sync/atomic"
 	"time"
 
@@ -347,9 +346,9 @@ func computePossibleOverlapsGenericImpl[F flushable](
 ) {
 	iter := f.newIter(nil)
 	rangeDelIter := f.newRangeDelIter(nil)
-	rkeyIter := f.newRangeKeyIter(nil)
+	rangeKeyIter := f.newRangeKeyIter(nil)
 	for _, b := range bounded {
-		overlap, err := determineOverlapAllIters(cmp, b.UserKeyBounds(), iter, rangeDelIter, rkeyIter)
+		overlap, err := determineOverlapAllIters(cmp, b.UserKeyBounds(), iter, rangeDelIter, rangeKeyIter)
 		if invariants.Enabled && err != nil {
 			panic(errors.AssertionFailedf("expected iterator to be infallible: %v", err))
 		}
@@ -360,15 +359,19 @@ func computePossibleOverlapsGenericImpl[F flushable](
 		}
 	}
 
-	for _, c := range [3]io.Closer{iter, rangeDelIter, rkeyIter} {
-		if c != nil {
-			if err := c.Close(); err != nil {
-				// This implementation must be used in circumstances where
-				// reading through the iterator is infallible.
-				panic(err)
-			}
+	if iter != nil {
+		if err := iter.Close(); err != nil {
+			// This implementation must be used in circumstances where
+			// reading through the iterator is infallible.
+			panic(err)
 		}
 	}
+	if rangeDelIter != nil {
+		rangeDelIter.Close()
+	}
+	if rangeKeyIter != nil {
+		rangeKeyIter.Close()
+	}
 }
 
 // determineOverlapAllIters checks for overlap in a point iterator, range
@@ -394,3 +397,33 @@ func determineOverlapAllIters(
 	}
 	return false, nil
 }
+
+func determineOverlapPointIterator(
+	cmp base.Compare, bounds base.UserKeyBounds, iter internalIterator,
+) (bool, error) {
+	kv := iter.SeekGE(bounds.Start, base.SeekGEFlagsNone)
+	if kv == nil {
+		return false, iter.Error()
+	}
+	return bounds.End.IsUpperBoundForInternalKey(cmp, kv.K), nil
+}
+
+func determineOverlapKeyspanIterator(
+	cmp base.Compare, bounds base.UserKeyBounds, iter keyspan.FragmentIterator,
+) (bool, error) {
+	// NB: The spans surfaced by the fragment iterator are non-overlapping.
+	span, err := iter.SeekGE(bounds.Start)
+	if err != nil {
+		return false, err
+	}
+	for ; span != nil; span, err = iter.Next() {
+		if !bounds.End.IsUpperBoundFor(cmp, span.Start) {
+			// The span starts after our bounds.
+			return false, nil
+		}
+		if !span.Empty() {
+			return true, nil
+		}
+	}
+	return false, err
+}
diff --git a/get_iter.go b/get_iter.go
index ee2b4044..44a4c890 100644
--- a/get_iter.go
+++ b/get_iter.go
@@ -288,8 +288,6 @@ func (g *getIter) maybeSetTombstone(rangeDelIter keyspan.FragmentIterator) (ok b
 	// care about the most recent range deletion that's visible because it's the
 	// "most powerful."
 	g.tombstonedSeqNum, g.tombstoned = t.LargestVisibleSeqNum(g.snapshot)
-	if g.err = firstError(g.err, rangeDelIter.Close()); g.err != nil {
-		return false
-	}
+	rangeDelIter.Close()
 	return true
 }
diff --git a/go.mod b/go.mod
index 2d294169..45bc605d 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,12 @@ require (
 	github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e
 	github.com/HdrHistogram/hdrhistogram-go v1.1.2
 	github.com/cespare/xxhash/v2 v2.2.0
-	github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f
-	github.com/cockroachdb/errors v1.11.1
+	github.com/cockroachdb/datadriven v1.0.3-0.20240530155848-7682d40af056
+	github.com/cockroachdb/errors v1.11.3
+	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-20240303172742-c161743eb608
+	github.com/cockroachdb/swiss v0.0.0-20240612210725-f4de07ae6964
 	github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06
 	github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9
 	github.com/golang/snappy v0.0.4
@@ -20,11 +21,11 @@ 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/sys v0.17.0
+	golang.org/x/sync v0.7.0
+	golang.org/x/sys v0.18.0
 )
 
 require (
@@ -32,9 +33,9 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/getsentry/sentry-go v0.18.0 // indirect
+	github.com/getsentry/sentry-go v0.27.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
@@ -42,8 +43,8 @@ require (
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/rogpeppe/go-internal v1.9.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	golang.org/x/text v0.7.0 // indirect
-	google.golang.org/protobuf v1.28.1 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	google.golang.org/protobuf v1.33.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/go.sum b/go.sum
index 9824d7f1..7e76bb7e 100644
--- a/go.sum
+++ b/go.sum
@@ -71,18 +71,20 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
-github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/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/datadriven v1.0.3-0.20240530155848-7682d40af056 h1:slXychO2uDM6hYRu4c0pD0udNI8uObfeKN6UInWViS8=
+github.com/cockroachdb/datadriven v1.0.3-0.20240530155848-7682d40af056/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
+github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
+github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
+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=
 github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895/go.mod h1:aPd7gM9ov9M8v32Yy5NJrDyOcD8z642dqs+F0CeNXfA=
 github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
 github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
-github.com/cockroachdb/swiss v0.0.0-20240303172742-c161743eb608 h1:GXGrLNSC5+LGrwVgtczB6JCITxB9WGaLv7XilPkBDvc=
-github.com/cockroachdb/swiss v0.0.0-20240303172742-c161743eb608/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
+github.com/cockroachdb/swiss v0.0.0-20240612210725-f4de07ae6964 h1:Ew0znI2JatzKy52N1iS5muUsHkf2UJuhocH7uFW7jjs=
+github.com/cockroachdb/swiss v0.0.0-20240612210725-f4de07ae6964/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
 github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
 github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -104,8 +106,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
-github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
+github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
+github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
 github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM=
 github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -162,8 +164,9 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=
@@ -323,8 +326,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 +447,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=
@@ -485,8 +488,8 @@ golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -495,8 +498,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -636,8 +639,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/ingest.go b/ingest.go
index c2df8301..7f4c9f31 100644
--- a/ingest.go
+++ b/ingest.go
@@ -15,6 +15,7 @@ import (
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/invariants"
 	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/cockroachdb/pebble/internal/overlap"
 	"github.com/cockroachdb/pebble/internal/private"
 	"github.com/cockroachdb/pebble/objstorage"
 	"github.com/cockroachdb/pebble/objstorage/remote"
@@ -832,7 +833,7 @@ func ingestUpdateSeqNum(
 func ingestTargetLevel(
 	ctx context.Context,
 	cmp base.Compare,
-	lsmOverlap lsmOverlap,
+	lsmOverlap overlap.WithLSM,
 	baseLevel int,
 	compactions map[*compaction]struct{},
 	meta *fileMetadata,
@@ -904,32 +905,32 @@ func ingestTargetLevel(
 	// existing point that falls within the ingested table bounds as being "data
 	// overlap".
 
-	if lsmOverlap[0].result == dataOverlap {
+	if lsmOverlap[0].Result == overlap.Data {
 		return 0, nil, nil
 	}
 	targetLevel = 0
 	splitFile = nil
 	for level := baseLevel; level < numLevels; level++ {
 		var candidateSplitFile *fileMetadata
-		switch lsmOverlap[level].result {
-		case dataOverlap:
+		switch lsmOverlap[level].Result {
+		case overlap.Data:
 			// We cannot ingest into or under this level; return the best target level
 			// so far.
 			return targetLevel, splitFile, nil
 
-		case noDataOverlap:
-			if !suggestSplit || lsmOverlap[level].splitFile == nil {
+		case overlap.OnlyBoundary:
+			if !suggestSplit || lsmOverlap[level].SplitFile == nil {
 				// We can ingest under this level, but not into this level.
 				continue
 			}
 			// We can ingest into this level if we split this file.
-			candidateSplitFile = lsmOverlap[level].splitFile
+			candidateSplitFile = lsmOverlap[level].SplitFile
 
-		case noBoundaryOverlap:
+		case overlap.None:
 		// We can ingest into this level.
 
 		default:
-			return 0, nil, base.AssertionFailedf("unexpected lsmOverlap result: %v", lsmOverlap[level].result)
+			return 0, nil, base.AssertionFailedf("unexpected WithLevel.Result: %v", lsmOverlap[level].Result)
 		}
 
 		// Check boundary overlap with any ongoing compactions. We consider an
@@ -2192,15 +2193,15 @@ func (d *DB) ingestApply(
 				// We check overlap against the LSM without holding DB.mu. Note that we
 				// are still holding the log lock, so the version cannot change.
 				// TODO(radu): perform this check optimistically outside of the log lock.
-				var overlap lsmOverlap
-				overlap, err = func() (lsmOverlap, error) {
+				var lsmOverlap overlap.WithLSM
+				lsmOverlap, err = func() (overlap.WithLSM, error) {
 					d.mu.Unlock()
 					defer d.mu.Lock()
 					return overlapChecker.DetermineLSMOverlap(ctx, m.UserKeyBounds())
 				}()
 				if err == nil {
 					f.Level, splitFile, err = ingestTargetLevel(
-						ctx, d.cmp, overlap, baseLevel, d.mu.compact.inProgress, m, shouldIngestSplit,
+						ctx, d.cmp, lsmOverlap, baseLevel, d.mu.compact.inProgress, m, shouldIngestSplit,
 					)
 				}
 			}
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/base/key_bounds.go b/internal/base/key_bounds.go
index 9151451c..7a41f786 100644
--- a/internal/base/key_bounds.go
+++ b/internal/base/key_bounds.go
@@ -73,6 +73,23 @@ func (eb UserKeyBoundary) IsUpperBoundForInternalKey(cmp Compare, key InternalKe
 	return c < 0 || (c == 0 && (eb.Kind == Inclusive || key.IsExclusiveSentinel()))
 }
 
+// CompareUpperBounds compares two UserKeyBoundaries as upper bounds (e.g. when
+// they are used for UserKeyBounds.End).
+func (eb UserKeyBoundary) CompareUpperBounds(cmp Compare, other UserKeyBoundary) int {
+	switch c := cmp(eb.Key, other.Key); {
+	case c != 0:
+		return c
+	case eb.Kind == other.Kind:
+		return 0
+	case eb.Kind == Inclusive:
+		// eb is inclusive, other is exclusive.
+		return 1
+	default:
+		// eb is exclusive, other is inclusive.
+		return -1
+	}
+}
+
 // UserKeyBounds is a user key interval with an inclusive start boundary and
 // with an end boundary that can be either inclusive or exclusive.
 type UserKeyBounds struct {
@@ -132,8 +149,7 @@ func (b *UserKeyBounds) ContainsBounds(cmp Compare, other *UserKeyBounds) bool {
 	if cmp(b.Start, other.Start) > 0 {
 		return false
 	}
-	c := cmp(other.End.Key, b.End.Key)
-	return c < 0 || (c == 0 && (b.End.Kind == Inclusive || other.End.Kind == Exclusive))
+	return other.End.CompareUpperBounds(cmp, b.End) <= 0
 }
 
 // ContainsUserKey returns true if the user key is within the bounds.
diff --git a/internal/compact/iterator.go b/internal/compact/iterator.go
index 6f3d7d1d..375f56a4 100644
--- a/internal/compact/iterator.go
+++ b/internal/compact/iterator.go
@@ -239,6 +239,9 @@ type Iter struct {
 	// lastRangeDelSpan stores the last, not compacted tombstone span. It is used
 	// to elide points or mark them as snapshot-pinned.
 	lastRangeDelSpan keyspan.Span
+	// lastRangeDelSpanFrontier is the frontier used to clear out lastRangeDelSpan
+	// when we move beyond its end key.
+	lastRangeDelSpanFrontier frontier
 
 	// span stores the last, compacted tombstone or range key span. It is provided
 	// to the caller via Span().
@@ -327,6 +330,7 @@ func NewIter(
 	i.delElider.Init(i.cmp, cfg.TombstoneElision)
 	i.rangeDelCompactor = MakeRangeDelSpanCompactor(i.cmp, i.cfg.Comparer.Equal, cfg.Snapshots, cfg.TombstoneElision)
 	i.rangeKeyCompactor = MakeRangeKeySpanCompactor(i.cmp, i.cfg.Comparer.Equal, cfg.Snapshots, cfg.RangeKeyElision)
+	i.lastRangeDelSpanFrontier.Init(&i.frontiers, nil, i.lastRangeDelSpanFrontierReached)
 	return i
 }
 
@@ -408,6 +412,8 @@ func (i *Iter) Next() (*base.InternalKey, []byte) {
 	i.pos = iterPosCurForward
 
 	for i.iterKV != nil {
+		i.frontiers.Advance(i.iterKV.K.UserKey)
+
 		// If we entered a new snapshot stripe with the same key, any key we
 		// return on this iteration is only returned because the open snapshot
 		// prevented it from being elided or merged with the key returned for
@@ -437,7 +443,7 @@ func (i *Iter) Next() (*base.InternalKey, []byte) {
 
 			if i.iterKV.Kind() == base.InternalKeyKindRangeDelete {
 				span := i.rangeDelInterleaving.Span()
-				i.lastRangeDelSpan.CopyFrom(span)
+				i.setLastRangeDelSpan(span)
 				i.rangeDelCompactor.Compact(span, &i.span)
 				if i.span.Empty() {
 					// The range del span was elided entirely; don't return this key to the caller.
@@ -1254,7 +1260,6 @@ func (i *Iter) saveKey() {
 		Trailer: i.iterKV.K.Trailer,
 	}
 	i.keyTrailer = i.key.Trailer
-	i.frontiers.Advance(i.key.UserKey)
 }
 
 // Error returns any error encountered.
@@ -1311,13 +1316,9 @@ func (i *Iter) tombstoneCovers(key base.InternalKey, snapshot uint64) cover {
 	if i.lastRangeDelSpan.Empty() {
 		return noCover
 	}
-	if invariants.Enabled && i.cmp(key.UserKey, i.lastRangeDelSpan.Start) < 0 {
+	if invariants.Enabled && (i.cmp(key.UserKey, i.lastRangeDelSpan.Start) < 0 || i.cmp(key.UserKey, i.lastRangeDelSpan.End) >= 0) {
 		panic(errors.AssertionFailedf("invalid key %q, last span %s", key, i.lastRangeDelSpan))
 	}
-	if i.cmp(key.UserKey, i.lastRangeDelSpan.End) >= 0 {
-		i.lastRangeDelSpan.Reset()
-		return noCover
-	}
 	// The Covers() check is very cheap, so we want to do that first.
 	switch {
 	case !i.lastRangeDelSpan.Covers(key.SeqNum()):
@@ -1329,6 +1330,19 @@ func (i *Iter) tombstoneCovers(key base.InternalKey, snapshot uint64) cover {
 	}
 }
 
+func (i *Iter) setLastRangeDelSpan(span *keyspan.Span) {
+	if invariants.Enabled && !i.lastRangeDelSpan.Empty() {
+		panic("last range del span overwritten")
+	}
+	i.lastRangeDelSpan.CopyFrom(span)
+	i.lastRangeDelSpanFrontier.Update(i.lastRangeDelSpan.End)
+}
+
+func (i *Iter) lastRangeDelSpanFrontierReached(key []byte) []byte {
+	i.lastRangeDelSpan.Reset()
+	return nil
+}
+
 // maybeZeroSeqnum attempts to set the seqnum for the current key to 0. Doing
 // so improves compression and enables an optimization during forward iteration
 // to skip some key comparisons. The seqnum for an entry can be zeroed if the
diff --git a/internal/keyspan/assert_iter.go b/internal/keyspan/assert_iter.go
index 3a56b680..6fb6b59b 100644
--- a/internal/keyspan/assert_iter.go
+++ b/internal/keyspan/assert_iter.go
@@ -163,8 +163,8 @@ func (i *assertIter) Prev() (*Span, error) {
 }
 
 // Close implements FragmentIterator.
-func (i *assertIter) Close() error {
-	return i.iter.Close()
+func (i *assertIter) Close() {
+	i.iter.Close()
 }
 
 // WrapChildren implements FragmentIterator.
diff --git a/internal/keyspan/bounded.go b/internal/keyspan/bounded.go
index 3ca452ea..6b0e88a2 100644
--- a/internal/keyspan/bounded.go
+++ b/internal/keyspan/bounded.go
@@ -202,8 +202,8 @@ func (i *BoundedIter) Prev() (*Span, error) {
 }
 
 // Close implements FragmentIterator.
-func (i *BoundedIter) Close() error {
-	return i.iter.Close()
+func (i *BoundedIter) Close() {
+	i.iter.Close()
 }
 
 // SetBounds modifies the FragmentIterator's bounds.
diff --git a/internal/keyspan/defragment.go b/internal/keyspan/defragment.go
index 4e05e204..5f6402ce 100644
--- a/internal/keyspan/defragment.go
+++ b/internal/keyspan/defragment.go
@@ -196,8 +196,8 @@ func (i *DefragmentingIter) Init(
 }
 
 // Close closes the underlying iterators.
-func (i *DefragmentingIter) Close() error {
-	return i.iter.Close()
+func (i *DefragmentingIter) Close() {
+	i.iter.Close()
 }
 
 // SeekGE moves the iterator to the first span covering a key greater than or
diff --git a/internal/keyspan/filter.go b/internal/keyspan/filter.go
index 436750a1..55a8d6f0 100644
--- a/internal/keyspan/filter.go
+++ b/internal/keyspan/filter.go
@@ -91,8 +91,8 @@ func (i *filteringIter) Prev() (*Span, error) {
 }
 
 // Close implements FragmentIterator.
-func (i *filteringIter) Close() error {
-	return i.iter.Close()
+func (i *filteringIter) Close() {
+	i.iter.Close()
 }
 
 // filter uses the filterFn (if configured) to filter and possibly mutate the
diff --git a/internal/keyspan/interleaving_iter.go b/internal/keyspan/interleaving_iter.go
index a6eee31e..dda0da6d 100644
--- a/internal/keyspan/interleaving_iter.go
+++ b/internal/keyspan/interleaving_iter.go
@@ -1152,9 +1152,9 @@ func (i *InterleavingIter) Error() error {
 
 // Close implements (base.InternalIterator).Close.
 func (i *InterleavingIter) Close() error {
-	perr := i.pointIter.Close()
-	rerr := i.keyspanIter.Close()
-	return firstError(perr, rerr)
+	err := i.pointIter.Close()
+	i.keyspanIter.Close()
+	return err
 }
 
 // String implements (base.InternalIterator).String.
diff --git a/internal/keyspan/iter.go b/internal/keyspan/iter.go
index 325fb377..3ff971d6 100644
--- a/internal/keyspan/iter.go
+++ b/internal/keyspan/iter.go
@@ -55,11 +55,9 @@ type FragmentIterator interface {
 	// previous call to SeekLT or Prev returned an invalid span.
 	Prev() (*Span, error)
 
-	// Close closes the iterator and returns any accumulated error. Exhausting
-	// the iterator is not considered to be an error. It is valid to call Close
-	// multiple times. Other methods should not be called after the iterator has
-	// been closed.
-	Close() error
+	// Close closes the iterator. It is valid to call Close multiple times. Other
+	// methods should not be called after the iterator has been closed.
+	Close()
 
 	// WrapChildren wraps any child iterators using the given function. The
 	// function can call WrapChildren to recursively wrap an entire iterator
@@ -207,9 +205,7 @@ func (i *Iter) Prev() (*Span, error) {
 }
 
 // Close implements FragmentIterator.Close.
-func (i *Iter) Close() error {
-	return nil
-}
+func (i *Iter) Close() {}
 
 func (i *Iter) String() string {
 	return "fragmented-spans"
diff --git a/internal/keyspan/keyspanimpl/level_iter.go b/internal/keyspan/keyspanimpl/level_iter.go
index b45c4340..5be37b9b 100644
--- a/internal/keyspan/keyspanimpl/level_iter.go
+++ b/internal/keyspan/keyspanimpl/level_iter.go
@@ -146,9 +146,7 @@ func (l *LevelIter) loadFile(file *manifest.FileMetadata, dir int) loadFileRetur
 	}
 
 	// Note that LevelIter.Close() can be called multiple times.
-	if err := l.Close(); err != nil {
-		return noFileLoaded
-	}
+	l.Close()
 
 	l.iterFile = file
 	l.iter = nil
@@ -197,9 +195,7 @@ func (l *LevelIter) SeekGE(key []byte) (*keyspan.Span, error) {
 			// cases similar to the above, while still retaining correctness.
 			// Return a straddling key instead of loading the file.
 			l.iterFile = f
-			if l.err = l.Close(); l.err != nil {
-				return l.verify(nil, l.err)
-			}
+			l.Close()
 			l.straddleDir = +1
 			l.straddle = keyspan.Span{
 				Start: prevFile.LargestRangeKey.UserKey,
@@ -250,9 +246,7 @@ func (l *LevelIter) SeekLT(key []byte) (*keyspan.Span, error) {
 			// cases similar to the above, while still retaining correctness.
 			// Return a straddling key instead of loading the file.
 			l.iterFile = f
-			if l.err = l.Close(); l.err != nil {
-				return l.verify(nil, l.err)
-			}
+			l.Close()
 			l.straddleDir = -1
 			l.straddle = keyspan.Span{
 				Start: f.LargestRangeKey.UserKey,
@@ -360,9 +354,7 @@ func (l *LevelIter) skipEmptyFileForward() (*keyspan.Span, error) {
 		// a "straddle span" in l.straddle and return that.
 		//
 		// Straddle spans are not created in rangedel mode.
-		if l.err = l.Close(); l.err != nil {
-			return l.verify(nil, l.err)
-		}
+		l.Close()
 		startKey := l.iterFile.LargestRangeKey.UserKey
 		// Resetting l.iterFile without loading the file into l.iter is okay and
 		// does not change the logic in loadFile() as long as l.iter is also nil;
@@ -423,9 +415,7 @@ func (l *LevelIter) skipEmptyFileBackward() (*keyspan.Span, error) {
 	// Straddle spans are not created in rangedel mode.
 	if l.straddleDir == 0 && l.keyType == manifest.KeyTypeRange &&
 		l.iterFile != nil && l.iter != nil {
-		if l.err = l.Close(); l.err != nil {
-			return l.verify(nil, l.err)
-		}
+		l.Close()
 		endKey := l.iterFile.SmallestRangeKey.UserKey
 		// Resetting l.iterFile without loading the file into l.iter is okay and
 		// does not change the logic in loadFile() as long as l.iter is also nil;
@@ -503,12 +493,11 @@ func (l *LevelIter) Error() error {
 }
 
 // Close implements keyspan.FragmentIterator.
-func (l *LevelIter) Close() error {
+func (l *LevelIter) Close() {
 	if l.iter != nil {
-		l.err = l.iter.Close()
+		l.iter.Close()
 		l.iter = nil
 	}
-	return l.err
 }
 
 // String implements keyspan.FragmentIterator.
diff --git a/internal/keyspan/keyspanimpl/merging_iter.go b/internal/keyspan/keyspanimpl/merging_iter.go
index 8840754e..ad54e725 100644
--- a/internal/keyspan/keyspanimpl/merging_iter.go
+++ b/internal/keyspan/keyspanimpl/merging_iter.go
@@ -676,14 +676,12 @@ func (m *MergingIter) Prev() (*keyspan.Span, error) {
 }
 
 // Close closes the iterator, releasing all acquired resources.
-func (m *MergingIter) Close() error {
-	var err error
+func (m *MergingIter) Close() {
 	for i := range m.levels {
-		err = firstError(err, m.levels[i].iter.Close())
+		m.levels[i].iter.Close()
 	}
 	m.levels = nil
 	m.heap.items = m.heap.items[:0]
-	return err
 }
 
 // String implements fmt.Stringer.
@@ -1240,10 +1238,3 @@ func (k boundKey) String() string {
 	fmt.Fprint(&buf, "]")
 	return buf.String()
 }
-
-func firstError(e1, e2 error) error {
-	if e1 == nil {
-		return e2
-	}
-	return e1
-}
diff --git a/internal/keyspan/logging_iter.go b/internal/keyspan/logging_iter.go
index 0536f578..219fd825 100644
--- a/internal/keyspan/logging_iter.go
+++ b/internal/keyspan/logging_iter.go
@@ -133,15 +133,10 @@ func (i *loggingIter) Prev() (*Span, error) {
 }
 
 // Close implements FragmentIterator.
-func (i *loggingIter) Close() error {
+func (i *loggingIter) Close() {
 	opEnd := i.opStartf("Close()")
-	err := i.iter.Close()
-	if err != nil {
-		opEnd(err)
-	} else {
-		opEnd()
-	}
-	return err
+	i.iter.Close()
+	opEnd()
 }
 
 // WrapChildren implements FragmentIterator.
diff --git a/internal/keyspan/transformer.go b/internal/keyspan/transformer.go
index b85d29fe..fe1f1fd0 100644
--- a/internal/keyspan/transformer.go
+++ b/internal/keyspan/transformer.go
@@ -134,6 +134,6 @@ func (t *TransformerIter) Prev() (*Span, error) {
 }
 
 // Close implements the FragmentIterator interface.
-func (t *TransformerIter) Close() error {
-	return t.FragmentIterator.Close()
+func (t *TransformerIter) Close() {
+	t.FragmentIterator.Close()
 }
diff --git a/internal/keyspan/truncate.go b/internal/keyspan/truncate.go
index 63aa0ea4..898c51a8 100644
--- a/internal/keyspan/truncate.go
+++ b/internal/keyspan/truncate.go
@@ -112,8 +112,8 @@ func (i *truncatingIter) Prev() (*Span, error) {
 }
 
 // Close implements FragmentIterator.
-func (i *truncatingIter) Close() error {
-	return i.iter.Close()
+func (i *truncatingIter) Close() {
+	i.iter.Close()
 }
 
 // nextSpanWithinBounds returns the first span (starting with the given span and
diff --git a/internal/manifest/level_metadata.go b/internal/manifest/level_metadata.go
index d0be0f8b..af7b012f 100644
--- a/internal/manifest/level_metadata.go
+++ b/internal/manifest/level_metadata.go
@@ -41,7 +41,8 @@ func (lm *LevelMetadata) release() (obsolete []*FileBacking) {
 	return lm.tree.Release()
 }
 
-func makeLevelMetadata(cmp Compare, level int, files []*FileMetadata) LevelMetadata {
+// MakeLevelMetadata creates a LevelMetadata with the given files.
+func MakeLevelMetadata(cmp Compare, level int, files []*FileMetadata) LevelMetadata {
 	bcmp := btreeCmpSeqNum
 	if level > 0 {
 		bcmp = btreeCmpSmallestKey(cmp)
diff --git a/internal/manifest/version.go b/internal/manifest/version.go
index af2c9de0..b584899d 100644
--- a/internal/manifest/version.go
+++ b/internal/manifest/version.go
@@ -301,6 +301,21 @@ func (m *FileMetadata) UserKeyBounds() base.UserKeyBounds {
 	return base.UserKeyBoundsFromInternal(m.Smallest, m.Largest)
 }
 
+// UserKeyBoundsByType returns the user key bounds for the given key types.
+// Note that the returned bounds are invalid when requesting KeyTypePoint but
+// HasPointKeys is false, or when requesting KeyTypeRange and HasRangeKeys is
+// false.
+func (m *FileMetadata) UserKeyBoundsByType(keyType KeyType) base.UserKeyBounds {
+	switch keyType {
+	case KeyTypePoint:
+		return base.UserKeyBoundsFromInternal(m.SmallestPointKey, m.LargestPointKey)
+	case KeyTypeRange:
+		return base.UserKeyBoundsFromInternal(m.SmallestRangeKey, m.LargestRangeKey)
+	default:
+		return base.UserKeyBoundsFromInternal(m.Smallest, m.Largest)
+	}
+}
+
 // SyntheticSeqNum returns a SyntheticSeqNum which is set when SmallestSeqNum
 // equals LargestSeqNum.
 func (m *FileMetadata) SyntheticSeqNum() sstable.SyntheticSeqNum {
diff --git a/internal/manifest/version_edit.go b/internal/manifest/version_edit.go
index 8e41280d..adb6315f 100644
--- a/internal/manifest/version_edit.go
+++ b/internal/manifest/version_edit.go
@@ -963,12 +963,12 @@ func (b *BulkVersionEdit) Apply(
 
 	for level := range v.Levels {
 		if curr == nil || curr.Levels[level].tree.root == nil {
-			v.Levels[level] = makeLevelMetadata(comparer.Compare, level, nil /* files */)
+			v.Levels[level] = MakeLevelMetadata(comparer.Compare, level, nil /* files */)
 		} else {
 			v.Levels[level] = curr.Levels[level].clone()
 		}
 		if curr == nil || curr.RangeKeyLevels[level].tree.root == nil {
-			v.RangeKeyLevels[level] = makeLevelMetadata(comparer.Compare, level, nil /* files */)
+			v.RangeKeyLevels[level] = MakeLevelMetadata(comparer.Compare, level, nil /* files */)
 		} else {
 			v.RangeKeyLevels[level] = curr.RangeKeyLevels[level].clone()
 		}
diff --git a/internal/overlap/checker.go b/internal/overlap/checker.go
new file mode 100644
index 00000000..c219b553
--- /dev/null
+++ b/internal/overlap/checker.go
@@ -0,0 +1,263 @@
+// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+// Package overlap provides facilities for checking whether tables have data
+// overlap.
+package overlap
+
+import (
+	"context"
+
+	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/keyspan"
+	"github.com/cockroachdb/pebble/internal/manifest"
+)
+
+// WithLSM stores the result of checking for boundary and data overlap between a
+// region of key space and the LSM levels, starting from the top (L0) and
+// stopping at the highest level with data overlap.
+type WithLSM [manifest.NumLevels]WithLevel
+
+// WithLevel is the result of checking overlap against an LSM level.
+type WithLevel struct {
+	Result Kind
+	// SplitFile can be set only when result is OnlyBoundary. If it is set, this
+	// file can be split to free up the range of interest.
+	SplitFile *manifest.FileMetadata
+}
+
+// Kind indicates the kind of overlap detected between a key range and a level.
+// We check two types of overlap:
+//
+//   - file boundary overlap: whether the key range overlaps any of the level's
+//     user key boundaries;
+//
+//   - data overlap: whether the key range overlaps any keys or ranges in the
+//     level. Data overlap implies file boundary overlap.
+type Kind uint8
+
+const (
+	// None indicates that the key range of interest doesn't overlap any tables on
+	// the level.
+	None Kind = iota + 1
+	// OnlyBoundary indicates that there is boundary overlap but no data overlap.
+	OnlyBoundary
+	// Data indicates that at least a key or range in the level overlaps with the
+	// key range of interest. Note that the data overlap check is best-effort and
+	// there could be false positives.
+	Data
+)
+
+// Checker is used to check for data overlap between tables in the LSM and a
+// user key region of interest.
+type Checker struct {
+	cmp             base.Compare
+	iteratorFactory IteratorFactory
+}
+
+// IteratorFactory is an interface that is used by the Checker to create
+// iterators for a given table. All methods can return nil as an empty iterator.
+type IteratorFactory interface {
+	Points(ctx context.Context, m *manifest.FileMetadata) (base.InternalIterator, error)
+	RangeDels(ctx context.Context, m *manifest.FileMetadata) (keyspan.FragmentIterator, error)
+	RangeKeys(ctx context.Context, m *manifest.FileMetadata) (keyspan.FragmentIterator, error)
+}
+
+// MakeChecker initializes a new Checker.
+func MakeChecker(cmp base.Compare, iteratorFactory IteratorFactory) Checker {
+	return Checker{
+		cmp:             cmp,
+		iteratorFactory: iteratorFactory,
+	}
+}
+
+// LSMOverlap calculates the LSM overlap for the given region.
+func (c *Checker) LSMOverlap(
+	ctx context.Context, region base.UserKeyBounds, v *manifest.Version,
+) (WithLSM, error) {
+	var result WithLSM
+	result[0].Result = None
+	for sublevel := 0; sublevel < len(v.L0Sublevels.Levels); sublevel++ {
+		res, err := c.LevelOverlap(ctx, region, v.L0Sublevels.Levels[sublevel])
+		if err != nil {
+			return WithLSM{}, err
+		}
+		if res.Result == Data {
+			result[0].Result = Data
+			return result, nil
+		}
+		if res.Result == OnlyBoundary {
+			result[0].Result = OnlyBoundary
+		}
+	}
+	for level := 1; level < manifest.NumLevels; level++ {
+		var err error
+		result[level], err = c.LevelOverlap(ctx, region, v.Levels[level].Slice())
+		if err != nil {
+			return WithLSM{}, err
+		}
+		if result[level].Result == Data {
+			return result, err
+		}
+	}
+	return result, nil
+}
+
+// LevelOverlap returns true if there is possible data overlap between a user
+// key region and an L0 sublevel or L1+ level.
+func (c *Checker) LevelOverlap(
+	ctx context.Context, region base.UserKeyBounds, ls manifest.LevelSlice,
+) (WithLevel, error) {
+	// Quick check: if the target region contains any file boundaries, we assume
+	// data overlap. This is a correct assumption in most cases; it is pessimistic
+	// only for external ingestions which could have "loose" boundaries. External
+	// ingestions are also the most expensive to look at, so we don't want to do
+	// that just in the off chance that we'll find a significant empty region at
+	// the boundary.
+	//
+	// This check is important because the region can be very large in the key
+	// space and encompass many files, and we don't want to open any of them in
+	// that case.
+	startIter := ls.Iter()
+	file := startIter.SeekGE(c.cmp, region.Start)
+	if file == nil {
+		// No overlapping files.
+		return WithLevel{Result: None}, nil
+	}
+	fileBounds := file.UserKeyBounds()
+	if !region.End.IsUpperBoundFor(c.cmp, fileBounds.Start) {
+		// No overlapping files.
+		return WithLevel{Result: None}, nil
+	}
+	if c.cmp(fileBounds.Start, region.Start) >= 0 || region.End.CompareUpperBounds(c.cmp, fileBounds.End) >= 0 {
+		// The file ends or starts inside our region; we assume data overlap.
+		return WithLevel{Result: Data}, nil
+	}
+	// We have a single file to look at; its boundaries enclose our region.
+	empty, err := c.EmptyRegion(ctx, region, file)
+	if err != nil {
+		return WithLevel{}, err
+	}
+	if !empty {
+		return WithLevel{Result: Data}, nil
+	}
+	return WithLevel{
+		Result:    OnlyBoundary,
+		SplitFile: file,
+	}, nil
+}
+
+// EmptyRegion returns true if the given region doesn't overlap with any keys or
+// ranges in the given table.
+func (c *Checker) EmptyRegion(
+	ctx context.Context, region base.UserKeyBounds, m *manifest.FileMetadata,
+) (bool, error) {
+	empty, err := c.emptyRegionPointsAndRangeDels(ctx, region, m)
+	if err != nil || !empty {
+		return empty, err
+	}
+	return c.emptyRegionRangeKeys(ctx, region, m)
+}
+
+// emptyRegionPointsAndRangeDels returns true if the file doesn't contain any
+// point keys or range del spans that overlap with region.
+func (c *Checker) emptyRegionPointsAndRangeDels(
+	ctx context.Context, region base.UserKeyBounds, m *manifest.FileMetadata,
+) (bool, error) {
+	if !m.HasPointKeys {
+		return true, nil
+	}
+	pointBounds := m.UserKeyBoundsByType(manifest.KeyTypePoint)
+	if !pointBounds.Overlaps(c.cmp, &region) {
+		return true, nil
+	}
+	points, err := c.iteratorFactory.Points(ctx, m)
+	if err != nil {
+		return false, err
+	}
+	if points != nil {
+		defer points.Close()
+		var kv *base.InternalKV
+		if c.cmp(region.Start, pointBounds.Start) <= 0 {
+			kv = points.First()
+		} else {
+			kv = points.SeekGE(region.Start, base.SeekGEFlagsNone)
+		}
+		if kv == nil && points.Error() != nil {
+			return false, points.Error()
+		}
+		if kv != nil && region.End.IsUpperBoundForInternalKey(c.cmp, kv.K) {
+			// Found overlap.
+			return false, nil
+		}
+	}
+	rangeDels, err := c.iteratorFactory.RangeDels(ctx, m)
+	if err != nil {
+		return false, err
+	}
+	if rangeDels != nil {
+		defer rangeDels.Close()
+		empty, err := c.emptyFragmentRegion(region, pointBounds.Start, rangeDels)
+		if err != nil || !empty {
+			return empty, err
+		}
+	}
+	// Found no overlap.
+	return true, nil
+}
+
+// emptyRegionRangeKeys returns true if the file doesn't contain any range key
+// spans that overlap with region.
+func (c *Checker) emptyRegionRangeKeys(
+	ctx context.Context, region base.UserKeyBounds, m *manifest.FileMetadata,
+) (bool, error) {
+	if !m.HasRangeKeys {
+		return true, nil
+	}
+	rangeKeyBounds := m.UserKeyBoundsByType(manifest.KeyTypeRange)
+	if !rangeKeyBounds.Overlaps(c.cmp, &region) {
+		return true, nil
+	}
+	rangeKeys, err := c.iteratorFactory.RangeKeys(ctx, m)
+	if err != nil {
+		return false, err
+	}
+	if rangeKeys != nil {
+		defer rangeKeys.Close()
+		empty, err := c.emptyFragmentRegion(region, rangeKeyBounds.Start, rangeKeys)
+		if err != nil || !empty {
+			return empty, err
+		}
+	}
+	// Found no overlap.
+	return true, nil
+}
+
+// emptyFragmentRegion returns true if the given iterator doesn't contain any
+// spans that overlap with region. The fragmentLowerBounds is a known lower
+// bound for all the spans.
+func (c *Checker) emptyFragmentRegion(
+	region base.UserKeyBounds, fragmentLowerBound []byte, fragments keyspan.FragmentIterator,
+) (bool, error) {
+	var span *keyspan.Span
+	var err error
+	if c.cmp(region.Start, fragmentLowerBound) <= 0 {
+		// This is an optimization: we know there are no spans before region.Start,
+		// so we can use First.
+		span, err = fragments.First()
+	} else {
+		span, err = fragments.SeekGE(region.Start)
+	}
+	if err != nil {
+		return false, err
+	}
+	if span != nil && span.Empty() {
+		return false, base.AssertionFailedf("fragment iterator produced empty span")
+	}
+	if span != nil && region.End.IsUpperBoundFor(c.cmp, span.Start) {
+		// Found overlap.
+		return false, nil
+	}
+	return true, nil
+}
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/iterator.go b/iterator.go
index 2957ecf1..76e45b06 100644
--- a/iterator.go
+++ b/iterator.go
@@ -2347,7 +2347,7 @@ func (i *Iterator) Close() error {
 			i.err = firstError(i.err, i.pointIter.Close())
 		}
 		if i.rangeKey != nil && i.rangeKey.rangeKeyIter != nil {
-			i.err = firstError(i.err, i.rangeKey.rangeKeyIter.Close())
+			i.rangeKey.rangeKeyIter.Close()
 		}
 	}
 	err := i.err
@@ -2591,7 +2591,7 @@ func (i *Iterator) SetOptions(o *IterOptions) {
 	}
 	if i.rangeKey != nil {
 		if closeBoth || len(o.RangeKeyFilters) > 0 || len(i.opts.RangeKeyFilters) > 0 {
-			i.err = firstError(i.err, i.rangeKey.rangeKeyIter.Close())
+			i.rangeKey.rangeKeyIter.Close()
 			i.rangeKey = nil
 		} else {
 			// If there's still a range key iterator stack, invalidate the
@@ -2649,7 +2649,7 @@ func (i *Iterator) SetOptions(o *IterOptions) {
 					// iterator stack. We need to reconstruct the range key
 					// iterator to add i.batchRangeKeyIter into the iterator
 					// stack.
-					i.err = firstError(i.err, i.rangeKey.rangeKeyIter.Close())
+					i.rangeKey.rangeKeyIter.Close()
 					i.rangeKey = nil
 				} else {
 					// There are range keys in the batch and we already
diff --git a/level_checker.go b/level_checker.go
index b2f0936b..28692e8f 100644
--- a/level_checker.go
+++ b/level_checker.go
@@ -449,9 +449,9 @@ func addTombstonesFromIter(
 	seqNum uint64,
 	cmp Compare,
 	formatKey base.FormatKey,
-) (_ []tombstoneWithLevel, err error) {
+) ([]tombstoneWithLevel, error) {
 	defer func() {
-		err = firstError(err, iter.Close())
+		iter.Close()
 	}()
 
 	var prevTombstone keyspan.Span
@@ -594,7 +594,7 @@ func checkLevelsInternal(c *checkConfig) (err error) {
 				l.iter = nil
 			}
 			if l.rangeDelIter != nil {
-				err = firstError(err, l.rangeDelIter.Close())
+				l.rangeDelIter.Close()
 				l.rangeDelIter = nil
 			}
 		}
diff --git a/level_iter.go b/level_iter.go
index 568e38e5..f461800a 100644
--- a/level_iter.go
+++ b/level_iter.go
@@ -921,7 +921,7 @@ func (l *levelIter) Close() error {
 	}
 	if l.rangeDelIterPtr != nil {
 		if t := l.rangeDelIterCopy; t != nil {
-			l.err = firstError(l.err, t.Close())
+			t.Close()
 		}
 		*l.rangeDelIterPtr = nil
 		l.rangeDelIterCopy = nil
diff --git a/merging_iter.go b/merging_iter.go
index 2a1781bf..aef58565 100644
--- a/merging_iter.go
+++ b/merging_iter.go
@@ -1291,9 +1291,7 @@ func (m *mergingIter) Close() error {
 			m.err = err
 		}
 		if rangeDelIter := m.levels[i].rangeDelIter; rangeDelIter != nil {
-			if err := rangeDelIter.Close(); err != nil && m.err == nil {
-				m.err = err
-			}
+			rangeDelIter.Close()
 		}
 	}
 	m.levels = nil
diff --git a/metamorphic/build.go b/metamorphic/build.go
index 623c6267..24a87e0d 100644
--- a/metamorphic/build.go
+++ b/metamorphic/build.go
@@ -41,11 +41,15 @@ func writeSSTForIngestion(
 	}
 	w := sstable.NewWriter(writable, writerOpts)
 	pointIterCloser := base.CloseHelper(pointIter)
-	defer pointIterCloser.Close()
-	rangeDelIterCloser := base.CloseHelper(rangeDelIter)
-	defer rangeDelIterCloser.Close()
-	rangeKeyIterCloser := base.CloseHelper(rangeKeyIter)
-	defer rangeKeyIterCloser.Close()
+	defer func() {
+		_ = pointIterCloser.Close()
+		if rangeDelIter != nil {
+			rangeDelIter.Close()
+		}
+		if rangeKeyIter != nil {
+			rangeKeyIter.Close()
+		}
+	}()
 
 	outputKey := func(key []byte) []byte {
 		if !syntheticPrefix.IsSet() && !syntheticSuffix.IsSet() {
@@ -110,9 +114,8 @@ func writeSSTForIngestion(
 		if err != nil {
 			return nil, err
 		}
-		if err := rangeDelIterCloser.Close(); err != nil {
-			return nil, err
-		}
+		rangeDelIter.Close()
+		rangeDelIter = nil
 	}
 
 	if rangeKeyIter != nil {
@@ -146,9 +149,8 @@ func writeSSTForIngestion(
 		if err != nil {
 			return nil, err
 		}
-		if err := rangeKeyIterCloser.Close(); err != nil {
-			return nil, err
-		}
+		rangeKeyIter.Close()
+		rangeKeyIter = nil
 	}
 
 	if err := w.Close(); err != nil {
diff --git a/metamorphic/ops.go b/metamorphic/ops.go
index 0db675fc..daf132f2 100644
--- a/metamorphic/ops.go
+++ b/metamorphic/ops.go
@@ -772,9 +772,7 @@ func (o *ingestOp) collapseBatch(
 		if err != nil {
 			return nil, err
 		}
-		if err := rangeDelIter.Close(); err != nil {
-			return nil, err
-		}
+		rangeDelIter.Close()
 		rangeDelIter = nil
 	}
 
@@ -856,9 +854,7 @@ func (o *ingestOp) collapseBatch(
 				return nil, err
 			}
 		}
-		if err := rangeKeyIter.Close(); err != nil {
-			return nil, err
-		}
+		rangeKeyIter.Close()
 		rangeKeyIter = nil
 	}
 
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/remote_readable.go b/objstorage/objstorageprovider/remote_readable.go
index 1780ba51..cfa79c4d 100644
--- a/objstorage/objstorageprovider/remote_readable.go
+++ b/objstorage/objstorageprovider/remote_readable.go
@@ -93,7 +93,7 @@ func (r *remoteReadable) NewReadHandle(
 	ctx context.Context, readBeforeSize objstorage.ReadBeforeSize,
 ) objstorage.ReadHandle {
 	rh := remoteReadHandlePool.Get().(*remoteReadHandle)
-	*rh = remoteReadHandle{readable: r, readBeforeSize: readBeforeSize}
+	*rh = remoteReadHandle{readable: r, readBeforeSize: readBeforeSize, buffered: rh.buffered}
 	rh.readAheadState = makeReadaheadState(remoteMaxReadaheadSize)
 	return rh
 }
@@ -250,7 +250,9 @@ func (r *remoteReadHandle) readToBuffer(ctx context.Context, offset int64, lengt
 
 // Close is part of the objstorage.ReadHandle interface.
 func (r *remoteReadHandle) Close() error {
+	buf := r.buffered.data[:0]
 	*r = remoteReadHandle{}
+	r.buffered.data = buf
 	remoteReadHandlePool.Put(r)
 	return nil
 }
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/overlap.go b/overlap.go
index 561ea660..23cd4e8d 100644
--- a/overlap.go
+++ b/overlap.go
@@ -7,229 +7,61 @@ package pebble
 import (
 	"context"
 
-	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/keyspan"
-	"github.com/cockroachdb/pebble/internal/keyspan/keyspanimpl"
 	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/cockroachdb/pebble/internal/overlap"
 )
 
 // An overlapChecker provides facilities for checking whether any keys within a
-// particular LSM version overlap a set of bounds.
+// particular LSM version overlap a set of bounds. It is a thin wrapper for
+// dataoverlap.Checker.
 type overlapChecker struct {
 	comparer *base.Comparer
 	newIters tableNewIters
 	opts     IterOptions
 	v        *version
-
-	// bufs; reused across calls to avoid allocations.
-	upperBoundBuf    []byte
-	pointLevelIter   levelIter
-	keyspanLevelIter keyspanimpl.LevelIter
 }
 
-// levelOverlapResult indicates the result of checking data overlap between a
-// key range and a level. We check two types of overlap:
-//
-//   - file boundary overlap: whether the key range overlaps any of the level's
-//     user key boundaries;
-//
-//   - data overlap: whether the key range overlaps any keys or ranges in the
-//     level. Data overlap implies file boundary overlap.
-type levelOverlapResult uint8
-
-const (
-	// The key range of interest doesn't overlap any tables on the level.
-	noBoundaryOverlap levelOverlapResult = iota + 1
-	// The key range of interest overlaps some tables on the level in terms of
-	// boundary, but the tables contain no data within that range.
-	noDataOverlap
-	// At least a key or range in the level overlaps with the key range of
-	// interest. Note that the data overlap check is best-effort and there could
-	// be false positives.
-	dataOverlap
-)
-
-// levelOverlap is the result of checking overlap against an LSM level.
-type levelOverlap struct {
-	result levelOverlapResult
-	// splitFile can be set only when result is noDataOverlap. If it is set, this
-	// file can be split to free up the range of interest.
-	splitFile *fileMetadata
-}
-
-// lsmOverlap stores the result of checking for data overlap for the LSM levels,
-// starting from the top (L0). The overlap checks are only populated up to the
-// first level where we find data overlap.
-//
-// This is akin to a tetris game where a horizontal bar is falling and we want
-// to determine where it lands.
-type lsmOverlap [numLevels]levelOverlap
-
-// DetermineLSMOverlap calculates the lsmOverlap for the given bounds.
+// DetermineLSMOverlap calculates the overlap.WithLSM for the given bounds.
 func (c *overlapChecker) DetermineLSMOverlap(
 	ctx context.Context, bounds base.UserKeyBounds,
-) (lsmOverlap, error) {
-	var result lsmOverlap
-	for level := 0; level < numLevels; level++ {
-		var err error
-		result[level], err = c.DetermineLevelOverlap(ctx, bounds, level)
-		if err != nil || result[level].result == dataOverlap {
-			return result, err
-		}
-	}
-	return result, nil
-}
-
-func (c *overlapChecker) DetermineLevelOverlap(
-	ctx context.Context, bounds base.UserKeyBounds, level int,
-) (levelOverlap, error) {
-	// First, check boundary overlap.
-	boundaryOverlaps := c.v.Overlaps(level, bounds)
-	if boundaryOverlaps.Empty() {
-		return levelOverlap{result: noBoundaryOverlap}, nil
-	}
-
-	overlap, err := c.DetermineAnyDataOverlapInLevel(ctx, bounds, level)
-	if err != nil || overlap {
-		return levelOverlap{result: dataOverlap}, err
-	}
-	var splitFile *fileMetadata
-	if boundaryOverlaps.Len() == 1 {
-		iter := boundaryOverlaps.Iter()
-		splitFile = iter.First()
-	}
-	return levelOverlap{
-		result:    noDataOverlap,
-		splitFile: splitFile,
-	}, nil
+) (overlap.WithLSM, error) {
+	checker := overlap.MakeChecker(c.comparer.Compare, c)
+	return checker.LSMOverlap(ctx, bounds, c.v)
 }
 
-// DetermineAnyDataOverlapInLevel checks whether any keys within the provided
-// bounds and level exist within the checker's associated LSM version. This
-// function checks for actual keys within the bounds, performing I/O if
-// necessary. It may return false when files within the level overlaps the
-// bounds, but the files do not contain any keys within the bounds.
-func (c *overlapChecker) DetermineAnyDataOverlapInLevel(
-	ctx context.Context, bounds base.UserKeyBounds, level int,
-) (bool, error) {
-	// Propagating an upper bound can prevent a levelIter from unnecessarily
-	// opening files that fall outside bounds if no files within a level overlap
-	// the provided bounds.
-	c.opts.UpperBound = nil
-	if bounds.End.Kind == base.Exclusive {
-		c.opts.UpperBound = bounds.End.Key
-	} else if c.comparer.ImmediateSuccessor != nil {
-		si := c.comparer.Split(bounds.End.Key)
-		c.upperBoundBuf = c.comparer.ImmediateSuccessor(c.upperBoundBuf[:0], bounds.End.Key[:si])
-		c.opts.UpperBound = c.upperBoundBuf
-	}
-
-	// Check for overlap over the keys of L0 by iterating over the sublevels.
-	// NB: sublevel 0 contains the newest keys, whereas sublevel n contains the
-	// oldest keys.
-	if level == 0 {
-		if c.v.L0Sublevels == nil {
-			return true, base.AssertionFailedf("could not read L0 sublevels")
-		}
-		for subLevel := 0; subLevel < len(c.v.L0SublevelFiles); subLevel++ {
-			manifestIter := c.v.L0Sublevels.Levels[subLevel].Iter()
-			pointOverlap, err := c.determinePointKeyOverlapInLevel(
-				ctx, bounds, manifest.Level(0), manifestIter)
-			if err != nil || pointOverlap {
-				return pointOverlap, err
-			}
-			rangeOverlap, err := c.determineRangeKeyOverlapInLevel(
-				ctx, bounds, manifest.Level(0), manifestIter)
-			if err != nil || rangeOverlap {
-				return rangeOverlap, err
-			}
-		}
-		return false, nil
-	}
-
-	pointManifestIter := c.v.Levels[level].Iter()
-	pointOverlap, err := c.determinePointKeyOverlapInLevel(
-		ctx, bounds, manifest.Level(level), pointManifestIter)
-	if pointOverlap || err != nil {
-		return pointOverlap, err
-	}
-	rangeManifestIter := c.v.RangeKeyLevels[level].Iter()
-	return c.determineRangeKeyOverlapInLevel(
-		ctx, bounds, manifest.Level(level), rangeManifestIter)
-}
+var _ overlap.IteratorFactory = (*overlapChecker)(nil)
 
-func (c *overlapChecker) determinePointKeyOverlapInLevel(
-	ctx context.Context,
-	bounds base.UserKeyBounds,
-	level manifest.Level,
-	metadataIter manifest.LevelIterator,
-) (bool, error) {
-	// Check for overlapping point keys.
-	{
-		c.pointLevelIter.init(ctx, c.opts, c.comparer, c.newIters, metadataIter, level, internalIterOpts{})
-		pointOverlap, err := determineOverlapPointIterator(c.comparer.Compare, bounds, &c.pointLevelIter)
-		err = errors.CombineErrors(err, c.pointLevelIter.Close())
-		if pointOverlap || err != nil {
-			return pointOverlap, err
-		}
-	}
-	// Check for overlapping range deletions.
-	{
-		c.keyspanLevelIter.Init(
-			keyspan.SpanIterOptions{}, c.comparer.Compare, tableNewRangeDelIter(ctx, c.newIters),
-			metadataIter, level, manifest.KeyTypePoint,
-		)
-		rangeDeletionOverlap, err := determineOverlapKeyspanIterator(c.comparer.Compare, bounds, &c.keyspanLevelIter)
-		err = errors.CombineErrors(err, c.keyspanLevelIter.Close())
-		if rangeDeletionOverlap || err != nil {
-			return rangeDeletionOverlap, err
-		}
+// Points is part of the overlap.IteratorFactory implementation.
+func (c *overlapChecker) Points(
+	ctx context.Context, m *manifest.FileMetadata,
+) (base.InternalIterator, error) {
+	iters, err := c.newIters(ctx, m, &c.opts, internalIterOpts{}, iterPointKeys)
+	if err != nil {
+		return nil, err
 	}
-	return false, nil
-}
-
-func (c *overlapChecker) determineRangeKeyOverlapInLevel(
-	ctx context.Context,
-	bounds base.UserKeyBounds,
-	level manifest.Level,
-	metadataIter manifest.LevelIterator,
-) (bool, error) {
-	// Check for overlapping range keys.
-	c.keyspanLevelIter.Init(
-		keyspan.SpanIterOptions{}, c.comparer.Compare, tableNewRangeKeyIter(ctx, c.newIters),
-		metadataIter, level, manifest.KeyTypeRange,
-	)
-	rangeKeyOverlap, err := determineOverlapKeyspanIterator(c.comparer.Compare, bounds, &c.keyspanLevelIter)
-	return rangeKeyOverlap, errors.CombineErrors(err, c.keyspanLevelIter.Close())
+	return iters.point, nil
 }
 
-func determineOverlapPointIterator(
-	cmp base.Compare, bounds base.UserKeyBounds, iter internalIterator,
-) (bool, error) {
-	kv := iter.SeekGE(bounds.Start, base.SeekGEFlagsNone)
-	if kv == nil {
-		return false, iter.Error()
+// RangeDels is part of the overlap.IteratorFactory implementation.
+func (c *overlapChecker) RangeDels(
+	ctx context.Context, m *manifest.FileMetadata,
+) (keyspan.FragmentIterator, error) {
+	iters, err := c.newIters(ctx, m, &c.opts, internalIterOpts{}, iterRangeDeletions)
+	if err != nil {
+		return nil, err
 	}
-	return bounds.End.IsUpperBoundForInternalKey(cmp, kv.K), nil
+	return iters.rangeDeletion, nil
 }
 
-func determineOverlapKeyspanIterator(
-	cmp base.Compare, bounds base.UserKeyBounds, iter keyspan.FragmentIterator,
-) (bool, error) {
-	// NB: The spans surfaced by the fragment iterator are non-overlapping.
-	span, err := iter.SeekGE(bounds.Start)
+// RangeKeys is part of the overlap.IteratorFactory implementation.
+func (c *overlapChecker) RangeKeys(
+	ctx context.Context, m *manifest.FileMetadata,
+) (keyspan.FragmentIterator, error) {
+	iters, err := c.newIters(ctx, m, &c.opts, internalIterOpts{}, iterRangeKeys)
 	if err != nil {
-		return false, err
-	}
-	for ; span != nil; span, err = iter.Next() {
-		if !bounds.End.IsUpperBoundFor(cmp, span.Start) {
-			// The span starts after our bounds.
-			return false, nil
-		}
-		if !span.Empty() {
-			return true, nil
-		}
+		return nil, err
 	}
-	return false, err
+	return iters.rangeKey, nil
 }
diff --git a/sstable/block_fragment_iter.go b/sstable/block_fragment_iter.go
index 9d1ff919..91948581 100644
--- a/sstable/block_fragment_iter.go
+++ b/sstable/block_fragment_iter.go
@@ -36,36 +36,50 @@ type fragmentBlockIter struct {
 	keyBuf    [2]keyspan.Key
 	span      keyspan.Span
 	dir       int8
-	closeHook func(i keyspan.FragmentIterator) error
 
 	// elideSameSeqnum, if true, returns only the first-occurring (in forward
 	// order) Key for each sequence number.
 	elideSameSeqnum bool
 }
 
-func (i *fragmentBlockIter) resetForReuse() fragmentBlockIter {
-	return fragmentBlockIter{blockIter: i.blockIter.resetForReuse()}
+var _ keyspan.FragmentIterator = (*fragmentBlockIter)(nil)
+
+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) 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.
+func (i *fragmentBlockIter) ResetForReuse() {
+	*i = fragmentBlockIter{blockIter: i.blockIter.resetForReuse()}
+}
 
-	// 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.
+// 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
-	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 {
+		i.span = rangedel.Decode(ik, internalValue, i.span.Keys[:0])
+	} else {
+		i.span, err = rangekey.Decode(ik, internalValue, i.span.Keys[:0])
+	}
+	return err
+}
+
+// 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
+	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 +120,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 +155,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.
@@ -196,13 +185,8 @@ func (i *fragmentBlockIter) gatherBackward(kv *base.InternalKV) (*keyspan.Span,
 }
 
 // Close implements (keyspan.FragmentIterator).Close.
-func (i *fragmentBlockIter) Close() error {
-	var err error
-	if i.closeHook != nil {
-		err = i.closeHook(i)
-	}
-	err = firstError(err, i.blockIter.Close())
-	return err
+func (i *fragmentBlockIter) Close() {
+	i.blockIter.Close()
 }
 
 // First implements (keyspan.FragmentIterator).First
@@ -313,10 +297,5 @@ func (i *fragmentBlockIter) String() string {
 	return "fragment-block-iter"
 }
 
-// SetCloseHook implements sstable.FragmentIterator.
-func (i *fragmentBlockIter) SetCloseHook(fn func(i keyspan.FragmentIterator) error) {
-	i.closeHook = fn
-}
-
 // WrapChildren implements FragmentIterator.
 func (i *fragmentBlockIter) WrapChildren(wrap keyspan.WrapFn) {}
diff --git a/sstable/category_stats.go b/sstable/category_stats.go
index 178f8845..d5b20029 100644
--- a/sstable/category_stats.go
+++ b/sstable/category_stats.go
@@ -63,8 +63,8 @@ func StringToQoSForTesting(s string) QoSLevel {
 
 // CategoryAndQoS specifies both the Category and the QoSLevel.
 type CategoryAndQoS struct {
-	Category
-	QoSLevel
+	Category Category
+	QoSLevel QoSLevel
 }
 
 // CategoryStats provides stats about a category of reads.
@@ -91,9 +91,9 @@ func (s *CategoryStats) aggregate(a CategoryStats) {
 
 // CategoryStatsAggregate is the aggregate for the given category.
 type CategoryStatsAggregate struct {
-	Category
-	QoSLevel
-	CategoryStats
+	Category      Category
+	QoSLevel      QoSLevel
+	CategoryStats CategoryStats
 }
 
 type categoryStatsWithMu struct {
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 084ae0a1..fb711ce8 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -496,11 +496,10 @@ type rangeKeyFragmentBlockIter struct {
 	fragmentBlockIter
 }
 
-func (i *rangeKeyFragmentBlockIter) Close() error {
-	err := i.fragmentBlockIter.Close()
-	i.fragmentBlockIter = i.fragmentBlockIter.resetForReuse()
+func (i *rangeKeyFragmentBlockIter) Close() {
+	i.fragmentBlockIter.Close()
+	i.fragmentBlockIter.ResetForReuse()
 	rangeKeyFragmentBlockIterPool.Put(i)
-	return err
 }
 
 func (r *Reader) readIndex(
@@ -629,6 +628,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{
@@ -661,6 +669,7 @@ func (r *Reader) readBlock(
 			int(bh.Length+blockTrailerLen), readDuration.String())
 	}
 	if stats != nil {
+		stats.BlockBytes += bh.Length
 		stats.BlockReadDuration += readDuration
 	}
 	if err != nil {
@@ -718,9 +727,6 @@ func (r *Reader) readBlock(
 		decompressed = transformed
 	}
 
-	if stats != nil {
-		stats.BlockBytes += bh.Length
-	}
 	if iterStats != nil {
 		iterStats.reportStats(bh.Length, 0, readDuration)
 	}
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/table_cache.go b/table_cache.go
index a8f9154b..60b64bd4 100644
--- a/table_cache.go
+++ b/table_cache.go
@@ -1272,10 +1272,10 @@ func (s *iterSet) CloseAll() error {
 		err = s.point.Close()
 	}
 	if s.rangeDeletion != nil {
-		err = firstError(err, s.rangeDeletion.Close())
+		s.rangeDeletion.Close()
 	}
 	if s.rangeKey != nil {
-		err = firstError(err, s.rangeKey.Close())
+		s.rangeKey.Close()
 	}
 	return err
 }
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/error_test.go b/error_test.go
index 066f3e72..8086ef2c 100644
--- a/error_test.go
+++ b/error_test.go
@@ -8,11 +8,14 @@ import (
 	"bytes"
 	"fmt"
 	"math"
+	"math/rand"
 	"strings"
 	"sync/atomic"
 	"testing"
+	"time"
 
 	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/pebble/internal/testkeys"
 	"github.com/cockroachdb/pebble/vfs"
 	"github.com/cockroachdb/pebble/vfs/errorfs"
 	"github.com/stretchr/testify/require"
@@ -411,3 +414,135 @@ func TestDBWALRotationCrash(t *testing.T) {
 		require.NoError(t, run(fs, k))
 	}
 }
+
+func TestDBCompactionCrash(t *testing.T) {
+	seed := time.Now().UnixNano()
+	t.Log("seed", seed)
+
+	// This test uses the strict MemFS with the error injector to simulate
+	// crashes. Each subtest runs with a crash induced at the k-th write
+	// operation. Each subsequent run increases k by +1-5 until a subtest runs
+	// to completion without performing a k-th write operation.
+	//
+	// crashIndex holds the value of k at which the crash is induced and is
+	// decremented by the errorfs on each write operation.
+	var crashIndex atomic.Int32
+	mkFS := func() (vfs.FS, *vfs.MemFS) {
+		memfs := vfs.NewStrictMem()
+		inj := errorfs.InjectorFunc(func(op errorfs.Op) error {
+			if op.Kind.ReadOrWrite() == errorfs.OpIsWrite && crashIndex.Add(-1) == -1 {
+				memfs.SetIgnoreSyncs(true)
+			}
+			return nil
+		})
+		return errorfs.Wrap(memfs, inj), memfs
+	}
+	triggered := func() bool { return crashIndex.Load() < 0 }
+
+	// run opens the store and performs some random writes, simulating a crash
+	// at the k-th write operation.
+	run := func(t *testing.T, fs vfs.FS, k int32, seed int64) (i int64, err error) {
+		rng := rand.New(rand.NewSource(seed))
+		maxConcurrentCompactions := rng.Intn(3) + 2
+		opts := &Options{
+			DisableTableStats:           true,
+			FS:                          errorfs.Wrap(fs, errorfs.RandomLatency(nil, 20*time.Microsecond, seed)),
+			Logger:                      testLogger{t: t},
+			MemTableSize:                128 << 10,
+			MaxConcurrentCompactions:    func() int { return maxConcurrentCompactions },
+			LBaseMaxBytes:               64 << 10,
+			L0CompactionThreshold:       2,
+			L0CompactionFileThreshold:   2,
+			MemTableStopWritesThreshold: 10,
+			L0StopWritesThreshold:       10,
+		}
+		if testing.Verbose() {
+			lel := MakeLoggingEventListener(opts.Logger)
+			opts.EventListener = &lel
+		}
+		d, err := Open("", opts)
+		if err != nil || triggered() {
+			return 0, err
+		}
+
+		// Set index to k so that the k-th write operation decrements it to zero
+		// and simulates a crash.
+		crashIndex.Store(k)
+
+		// Write keys in random order in batches of random sizes.
+		const maxKeyLength = 2
+		const valLength = 4 << 10
+		timestamps := []int{10, 5}
+		ks := testkeys.Alpha(maxKeyLength)
+		ks = ks.EveryN(10)
+		buf := make([]byte, ks.MaxLen()+testkeys.MaxSuffixLen)
+		vbuf := make([]byte, valLength)
+		b := d.NewBatch()
+		perm := rng.Perm(int(ks.Count()))
+	done:
+		for _, ts := range timestamps {
+			for _, i := range perm {
+				n := testkeys.WriteKeyAt(buf, ks, int64(i), int64(ts))
+				_, err = rng.Read(vbuf)
+				require.NoError(t, err)
+				require.NoError(t, b.Set(buf[:n], vbuf, nil))
+				if rng.Intn(10) == 1 {
+					if err = d.Apply(b, nil); err != nil || triggered() {
+						b = nil
+						break done
+					}
+					b.Reset()
+				}
+				if rng.Intn(100) == 1 {
+					if err = d.Flush(); err != nil || triggered() {
+						break done
+					}
+				}
+			}
+		}
+		if b != nil && b.Count() > 0 {
+			err = firstError(err, d.Apply(b, nil))
+		}
+		err = firstError(err, d.Close())
+		return i, err
+	}
+
+	// Run the test with increasing values of k until a subtest runs to
+	// completion without performing a k-th write operation.
+	done := false
+	rng := rand.New(rand.NewSource(seed))
+	for k := int32(0); !done; k += rng.Int31n(5) + 1 {
+		t.Run(fmt.Sprintf("k=%d", k), func(t *testing.T) {
+			// Run, simulating a crash by ignoring syncs after the k-th write
+			// operation after Open.
+			crashIndex.Store(math.MaxInt32)
+			fs, memfs := mkFS()
+			i, err := run(t, fs, k, seed)
+			if !triggered() {
+				// Stop when we reach a value of k greater than the number of
+				// write operations performed during `run`.
+				t.Logf("No crash at write operation %d\n", k)
+				if err != nil {
+					t.Fatalf("Filesystem did not 'crash', but error returned: %s", err)
+				}
+				done = true
+				return
+			}
+			t.Logf("Simulated crash at write operation % 2d after writing %d keys, error: %v\n", k, i, err)
+
+			// Reset the filesystem to its state right before the simulated
+			// "crash", restore syncs and run again without crashing. No errors
+			// should be encountered.
+			//
+			// TODO(jackson): Allow an arbitrary subset of synced state to
+			// survive beyond the crash point.
+			memfs.ResetToSyncedState()
+			memfs.SetIgnoreSyncs(false)
+			crashIndex.Store(math.MaxInt32)
+			_, err = run(t, fs, math.MaxInt32, seed)
+			require.False(t, triggered())
+			// TODO(jackson): Add assertions on the database keys.
+			require.NoError(t, err)
+		})
+	}
+}
diff --git a/flushable_test.go b/flushable_test.go
index 50b7420d..82b4420d 100644
--- a/flushable_test.go
+++ b/flushable_test.go
@@ -138,7 +138,7 @@ func TestIngestedSSTFlushableAPI(t *testing.T) {
 					buf.WriteString(span.String())
 					buf.WriteString("\n")
 				}
-				err = firstError(err, iter.Close())
+				iter.Close()
 				if err != nil {
 					fmt.Fprintf(&buf, "err=%q", err.Error())
 				}
@@ -153,7 +153,7 @@ func TestIngestedSSTFlushableAPI(t *testing.T) {
 					buf.WriteString(span.String())
 					buf.WriteString("\n")
 				}
-				err = firstError(err, iter.Close())
+				iter.Close()
 				if err != nil {
 					fmt.Fprintf(&buf, "err=%q", err.Error())
 				}
diff --git a/internal/base/key_bounds_test.go b/internal/base/key_bounds_test.go
index 6c7281ac..725cd903 100644
--- a/internal/base/key_bounds_test.go
+++ b/internal/base/key_bounds_test.go
@@ -10,6 +10,42 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestUserKeyBoundary(t *testing.T) {
+	cmp := DefaultComparer.Compare
+	a := []byte("a")
+	b := []byte("b")
+
+	aExclusive := UserKeyExclusive(a)
+	require.False(t, aExclusive.IsUpperBoundFor(cmp, a))
+	require.False(t, aExclusive.IsUpperBoundFor(cmp, b))
+
+	aInclusive := UserKeyInclusive(a)
+	require.True(t, aInclusive.IsUpperBoundFor(cmp, a))
+	require.False(t, aInclusive.IsUpperBoundFor(cmp, b))
+
+	bExclusive := UserKeyExclusive(b)
+	require.True(t, bExclusive.IsUpperBoundFor(cmp, a))
+	require.False(t, bExclusive.IsUpperBoundFor(cmp, b))
+
+	bInclusive := UserKeyInclusive(b)
+	require.True(t, bInclusive.IsUpperBoundFor(cmp, a))
+	require.True(t, bInclusive.IsUpperBoundFor(cmp, b))
+
+	ordered := []UserKeyBoundary{aExclusive, aInclusive, bExclusive, bInclusive}
+	for i := range ordered {
+		for j := range ordered {
+			expected := 0
+			if i < j {
+				expected = -1
+			} else if i > j {
+				expected = 1
+			}
+			require.Equalf(t, expected, ordered[i].CompareUpperBounds(cmp, ordered[j]),
+				"%v, %v", ordered[i], ordered[j])
+		}
+	}
+}
+
 func TestUserKeyBounds(t *testing.T) {
 	var ukb UserKeyBounds
 	cmp := DefaultComparer.Compare
diff --git a/internal/base/test_utils.go b/internal/base/test_utils.go
index b15001dd..08871b73 100644
--- a/internal/base/test_utils.go
+++ b/internal/base/test_utils.go
@@ -6,6 +6,7 @@ package base
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"strconv"
 	"strings"
@@ -249,3 +250,14 @@ func (f *FakeIter) SetBounds(lower, upper []byte) {
 
 // SetContext is part of the InternalIterator interface.
 func (f *FakeIter) SetContext(_ context.Context) {}
+
+// ParseUserKeyBounds parses UserKeyBounds from a string representation of the
+// form "[foo, bar]" or "[foo, bar)".
+func ParseUserKeyBounds(s string) UserKeyBounds {
+	first, last, s := s[0], s[len(s)-1], s[1:len(s)-1]
+	start, end, ok := strings.Cut(s, ", ")
+	if !ok || first != '[' || (last != ']' && last != ')') {
+		panic(fmt.Sprintf("invalid bounds %q", s))
+	}
+	return UserKeyBoundsEndExclusiveIf([]byte(start), []byte(end), last == ')')
+}
diff --git a/internal/crdbtest/crdbtest.go b/internal/crdbtest/crdbtest.go
new file mode 100644
index 00000000..0efdc719
--- /dev/null
+++ b/internal/crdbtest/crdbtest.go
@@ -0,0 +1,308 @@
+// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+// Package crdbtest provides facilities for representing keys, workloads, etc
+// representative of CockroachDB's use of Pebble.
+package crdbtest
+
+import (
+	"bytes"
+	"encoding/binary"
+
+	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/pebble/internal/base"
+)
+
+const withWall = 9
+const withLogical = withWall + 4
+const withSynthetic = withLogical + 1
+const withLockTableLen = 18
+
+// MaxSuffixLen is the maximum length of the CockroachDB key suffix.
+const MaxSuffixLen = max(withLockTableLen, withSynthetic, withLogical, withWall)
+
+// Comparer is a base.Comparer for CockroachDB keys.
+var Comparer = base.Comparer{
+	Compare: Compare,
+	Equal:   Equal,
+	AbbreviatedKey: func(k []byte) uint64 {
+		key, ok := getKeyPartFromEngineKey(k)
+		if !ok {
+			return 0
+		}
+		return base.DefaultComparer.AbbreviatedKey(key)
+	},
+	FormatKey: base.DefaultFormatter,
+	Separator: func(dst, a, b []byte) []byte {
+		aKey, ok := getKeyPartFromEngineKey(a)
+		if !ok {
+			return append(dst, a...)
+		}
+		bKey, ok := getKeyPartFromEngineKey(b)
+		if !ok {
+			return append(dst, a...)
+		}
+		// If the keys are the same just return a.
+		if bytes.Equal(aKey, bKey) {
+			return append(dst, a...)
+		}
+		n := len(dst)
+		dst = base.DefaultComparer.Separator(dst, aKey, bKey)
+		// Did it pick a separator different than aKey -- if it did not we can't do better than a.
+		buf := dst[n:]
+		if bytes.Equal(aKey, buf) {
+			return append(dst[:n], a...)
+		}
+		// The separator is > aKey, so we only need to add the sentinel.
+		return append(dst, 0)
+	},
+	Successor: func(dst, a []byte) []byte {
+		aKey, ok := getKeyPartFromEngineKey(a)
+		if !ok {
+			return append(dst, a...)
+		}
+		n := len(dst)
+		// Engine key comparison uses bytes.Compare on the roachpb.Key, which is the same semantics as
+		// pebble.DefaultComparer, so reuse the latter's Successor implementation.
+		dst = base.DefaultComparer.Successor(dst, aKey)
+		// Did it pick a successor different than aKey -- if it did not we can't do better than a.
+		buf := dst[n:]
+		if bytes.Equal(aKey, buf) {
+			return append(dst[:n], a...)
+		}
+		// The successor is > aKey, so we only need to add the sentinel.
+		return append(dst, 0)
+	},
+	ImmediateSuccessor: func(dst, a []byte) []byte {
+		// The key `a` is guaranteed to be a bare prefix: It's a
+		// `engineKeyNoVersion` key without a version—just a trailing 0-byte to
+		// signify the length of the version. For example the user key "foo" is
+		// encoded as: "foo\0". We need to encode the immediate successor to
+		// "foo", which in the natural byte ordering is "foo\0".  Append a
+		// single additional zero, to encode the user key "foo\0" with a
+		// zero-length version.
+		return append(append(dst, a...), 0)
+	},
+	Split: Split,
+	Name:  "cockroach_comparator",
+}
+
+// EncodeMVCCKey encodes a MVCC key into dst, growing dst as necessary.
+func EncodeMVCCKey(dst []byte, key []byte, walltime uint64, logical uint32) []byte {
+	if cap(dst) < len(key)+withSynthetic {
+		newKey := make([]byte, len(key), len(key)+withSynthetic)
+		copy(newKey, key)
+		key = newKey
+	}
+	dst = append(dst[:0], key...)
+	return EncodeTimestamp(dst, walltime, logical)
+}
+
+// EncodeTimestamp encodes a MVCC timestamp into a key, returning the new key.
+// The key's capacity must be sufficiently large to hold the encoded timestamp.
+func EncodeTimestamp(key []byte, walltime uint64, logical uint32) []byte {
+	pos := len(key)
+	if logical == 0 {
+		if walltime == 0 {
+			key = key[:pos+1]
+			key[pos] = 0 // sentinel byte
+			return key
+		}
+
+		key = key[:pos+1+8+1]
+		key[pos] = 0 // sentinel byte
+		key[pos+1+8] = 9
+		binary.BigEndian.PutUint64(key[pos+1:], walltime)
+		return key
+	}
+
+	key = key[:pos+1+12+1]
+	key[pos] = 0 // sentinel byte
+	key[pos+1+8+4] = 13
+	binary.BigEndian.PutUint64(key[pos+1:], walltime)
+	binary.BigEndian.PutUint32(key[pos+1+8:], logical)
+	return key
+}
+
+// DecodeTimestamp decodes a MVCC timestamp from a serialized MVCC key.
+func DecodeTimestamp(mvccKey []byte) ([]byte, []byte, uint64, uint32) {
+	tsLen := int(mvccKey[len(mvccKey)-1])
+	keyPartEnd := len(mvccKey) - 1 - tsLen
+	if keyPartEnd < 0 {
+		return nil, nil, 0, 0
+	}
+
+	key := mvccKey[:keyPartEnd]
+	if tsLen > 0 {
+		ts := mvccKey[keyPartEnd+1 : len(mvccKey)-1]
+		switch len(ts) {
+		case 8:
+			return key, nil, binary.BigEndian.Uint64(ts[:8]), 0
+		case 12, 13:
+			return key, nil, binary.BigEndian.Uint64(ts[:8]), binary.BigEndian.Uint32(ts[8:12])
+		default:
+			return key, ts, 0, 0
+		}
+	}
+	return key, nil, 0, 0
+}
+
+// Split implements base.Split for CockroachDB keys.
+func Split(key []byte) int {
+	keyLen := len(key)
+	if keyLen == 0 {
+		return 0
+	}
+	// Last byte is the version length + 1 when there is a version, else it is
+	// 0.
+	versionLen := int(key[keyLen-1])
+	keyPartEnd := keyLen - 1 - versionLen
+	if keyPartEnd < 0 {
+		return keyLen
+	}
+	return keyPartEnd + 1
+}
+
+// Compare compares cockroach keys, including the version (which could be MVCC
+// timestamps).
+func Compare(a, b []byte) int {
+	// NB: For performance, this routine manually splits the key into the
+	// user-key and version components rather than using DecodeEngineKey. In
+	// most situations, use DecodeEngineKey or GetKeyPartFromEngineKey or
+	// SplitMVCCKey instead of doing this.
+	aEnd := len(a) - 1
+	bEnd := len(b) - 1
+	if aEnd < 0 || bEnd < 0 {
+		// This should never happen unless there is some sort of corruption of
+		// the keys.
+		panic(errors.AssertionFailedf("malformed key: %x, %x", a, b))
+	}
+
+	// Compute the index of the separator between the key and the version. If the
+	// separator is found to be at -1 for both keys, then we are comparing bare
+	// suffixes without a user key part. Pebble requires bare suffixes to be
+	// comparable with the same ordering as if they had a common user key.
+	aSep := aEnd - int(a[aEnd])
+	bSep := bEnd - int(b[bEnd])
+	if aSep == -1 && bSep == -1 {
+		aSep, bSep = 0, 0 // comparing bare suffixes
+	}
+	if aSep < 0 || bSep < 0 {
+		// This should never happen unless there is some sort of corruption of
+		// the keys.
+		return bytes.Compare(a, b)
+	}
+	// Compare the "user key" part of the key.
+	if c := bytes.Compare(a[:aSep], b[:bSep]); c != 0 {
+		return c
+	}
+
+	// Compare the version part of the key. Note that when the version is a
+	// timestamp, the timestamp encoding causes byte comparison to be equivalent
+	// to timestamp comparison.
+	aVer := a[aSep:aEnd]
+	bVer := b[bSep:bEnd]
+	if len(aVer) == 0 {
+		if len(bVer) == 0 {
+			return 0
+		}
+		return -1
+	} else if len(bVer) == 0 {
+		return 1
+	}
+	aVer = normalizeEngineKeyVersionForCompare(aVer)
+	bVer = normalizeEngineKeyVersionForCompare(bVer)
+	return bytes.Compare(bVer, aVer)
+}
+
+// Equal implements base.Equal for Cockroach keys.
+func Equal(a, b []byte) bool {
+	aEnd := len(a) - 1
+	bEnd := len(b) - 1
+	if aEnd < 0 || bEnd < 0 {
+		panic("empty keys")
+	}
+
+	// Last byte is the version length + 1 when there is a version,
+	// else it is 0.
+	aVerLen := int(a[aEnd])
+	bVerLen := int(b[bEnd])
+
+	// Fast-path. If the key version is empty or contains only a walltime
+	// component then normalizeEngineKeyVersionForCompare is a no-op, so we don't
+	// need to split the "user key" from the version suffix before comparing to
+	// compute equality. Instead, we can check for byte equality immediately.
+	if (aVerLen <= withWall && bVerLen <= withWall) || (aVerLen == withLockTableLen && bVerLen == withLockTableLen) {
+		return bytes.Equal(a, b)
+	}
+
+	// Compute the index of the separator between the key and the version. If the
+	// separator is found to be at -1 for both keys, then we are comparing bare
+	// suffixes without a user key part. Pebble requires bare suffixes to be
+	// comparable with the same ordering as if they had a common user key.
+	aSep := aEnd - aVerLen
+	bSep := bEnd - bVerLen
+	if aSep == -1 && bSep == -1 {
+		aSep, bSep = 0, 0 // comparing bare suffixes
+	}
+	if aSep < 0 || bSep < 0 {
+		// This should never happen unless there is some sort of corruption of
+		// the keys.
+		return bytes.Equal(a, b)
+	}
+
+	// Compare the "user key" part of the key.
+	if !bytes.Equal(a[:aSep], b[:bSep]) {
+		return false
+	}
+
+	// Compare the version part of the key.
+	aVer := a[aSep:aEnd]
+	bVer := b[bSep:bEnd]
+	aVer = normalizeEngineKeyVersionForCompare(aVer)
+	bVer = normalizeEngineKeyVersionForCompare(bVer)
+	return bytes.Equal(aVer, bVer)
+}
+
+var zeroLogical [4]byte
+
+func normalizeEngineKeyVersionForCompare(a []byte) []byte {
+	// In general, the version could also be a non-timestamp version, but we know
+	// that engineKeyVersionLockTableLen+mvccEncodedTimeSentinelLen is a different
+	// constant than the above, so there is no danger here of stripping parts from
+	// a non-timestamp version.
+	if len(a) == withSynthetic {
+		// Strip the synthetic bit component from the timestamp version. The
+		// presence of the synthetic bit does not affect key ordering or equality.
+		a = a[:withLogical]
+	}
+	if len(a) == withLogical {
+		// If the timestamp version contains a logical timestamp component that is
+		// zero, strip the component. encodeMVCCTimestampToBuf will typically omit
+		// the entire logical component in these cases as an optimization, but it
+		// does not guarantee to never include a zero logical component.
+		// Additionally, we can fall into this case after stripping off other
+		// components of the key version earlier on in this function.
+		if bytes.Equal(a[withWall:], zeroLogical[:]) {
+			a = a[:withWall]
+		}
+	}
+	return a
+}
+
+func getKeyPartFromEngineKey(engineKey []byte) (key []byte, ok bool) {
+	if len(engineKey) == 0 {
+		return nil, false
+	}
+	// Last byte is the version length + 1 when there is a version,
+	// else it is 0.
+	versionLen := int(engineKey[len(engineKey)-1])
+	// keyPartEnd points to the sentinel byte.
+	keyPartEnd := len(engineKey) - 1 - versionLen
+	if keyPartEnd < 0 || engineKey[keyPartEnd] != 0x00 {
+		return nil, false
+	}
+	// Key excludes the sentinel byte.
+	return engineKey[:keyPartEnd], true
+}
diff --git a/internal/keyspan/keyspanimpl/level_iter_test.go b/internal/keyspan/keyspanimpl/level_iter_test.go
index e852f188..60a4f0ce 100644
--- a/internal/keyspan/keyspanimpl/level_iter_test.go
+++ b/internal/keyspan/keyspanimpl/level_iter_test.go
@@ -428,7 +428,7 @@ func TestLevelIter(t *testing.T) {
 		case "num-files":
 			return fmt.Sprintf("%d", len(level))
 		case "close-iter":
-			_ = iter.Close()
+			iter.Close()
 			iter = nil
 			return "ok"
 		case "iter":
diff --git a/internal/keyspan/logging_iter_test.go b/internal/keyspan/logging_iter_test.go
index 630de9c3..162a68b4 100644
--- a/internal/keyspan/logging_iter_test.go
+++ b/internal/keyspan/logging_iter_test.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/cockroachdb/datadriven"
 	"github.com/cockroachdb/pebble/internal/base"
-	"github.com/stretchr/testify/require"
 )
 
 func TestLoggingIter(t *testing.T) {
@@ -34,7 +33,7 @@ func TestLoggingIter(t *testing.T) {
 			iter = Assert(iter, base.DefaultComparer.Compare)
 			iter = InjectLogging(iter, l)
 			RunFragmentIteratorCmd(iter, d.Input, nil)
-			require.NoError(t, iter.Close())
+			iter.Close()
 			out := l.String()
 			// Hide pointer values.
 			r := regexp.MustCompile(`\(0x[0-9a-f]+\)`)
diff --git a/internal/keyspan/test_utils.go b/internal/keyspan/test_utils.go
index 7a1a599c..e9c1ef93 100644
--- a/internal/keyspan/test_utils.go
+++ b/internal/keyspan/test_utils.go
@@ -364,13 +364,12 @@ func (p *probeIterator) Prev() (*Span, error) {
 	return p.handleOp(op)
 }
 
-func (p *probeIterator) Close() error {
+func (p *probeIterator) Close() {
 	op := op{Kind: OpClose}
 	if p.iter != nil {
-		op.Err = p.iter.Close()
+		p.iter.Close()
 	}
-	_, err := p.handleOp(op)
-	return err
+	_, _ = p.handleOp(op)
 }
 
 func (p *probeIterator) WrapChildren(wrap WrapFn) {
@@ -551,7 +550,7 @@ func (i *invalidatingIter) First() (*Span, error)            { return i.invalida
 func (i *invalidatingIter) Last() (*Span, error)             { return i.invalidate(i.iter.Last()) }
 func (i *invalidatingIter) Next() (*Span, error)             { return i.invalidate(i.iter.Next()) }
 func (i *invalidatingIter) Prev() (*Span, error)             { return i.invalidate(i.iter.Prev()) }
-func (i *invalidatingIter) Close() error                     { return i.iter.Close() }
+func (i *invalidatingIter) Close()                           { i.iter.Close() }
 func (i *invalidatingIter) WrapChildren(wrap WrapFn) {
 	i.iter = wrap(i.iter)
 }
diff --git a/internal/manifest/l0_sublevels_test.go b/internal/manifest/l0_sublevels_test.go
index 503980f9..279ee5ef 100644
--- a/internal/manifest/l0_sublevels_test.go
+++ b/internal/manifest/l0_sublevels_test.go
@@ -289,7 +289,7 @@ func TestL0Sublevels(t *testing.T) {
 				SortBySmallest(fileMetas[i], base.DefaultComparer.Compare)
 			}
 
-			levelMetadata := makeLevelMetadata(base.DefaultComparer.Compare, 0, fileMetas[0])
+			levelMetadata := MakeLevelMetadata(base.DefaultComparer.Compare, 0, fileMetas[0])
 			if initialize {
 				if addL0FilesOpt {
 					SortBySeqNum(addedL0Files)
@@ -550,7 +550,7 @@ func TestAddL0FilesEquivalence(t *testing.T) {
 			continue
 		}
 
-		levelMetadata := makeLevelMetadata(testkeys.Comparer.Compare, 0, fileMetas)
+		levelMetadata := MakeLevelMetadata(testkeys.Comparer.Compare, 0, fileMetas)
 		var err error
 
 		if s2 == nil {
diff --git a/internal/manifest/version_test.go b/internal/manifest/version_test.go
index 697e3209..7feaabf3 100644
--- a/internal/manifest/version_test.go
+++ b/internal/manifest/version_test.go
@@ -22,7 +22,7 @@ import (
 )
 
 func levelMetadata(level int, files ...*FileMetadata) LevelMetadata {
-	return makeLevelMetadata(base.DefaultComparer.Compare, level, files)
+	return MakeLevelMetadata(base.DefaultComparer.Compare, level, files)
 }
 
 func ikey(s string) InternalKey {
@@ -82,7 +82,7 @@ func TestIkeyRange(t *testing.T) {
 				f = append(f, m)
 			}
 		}
-		levelMetadata := makeLevelMetadata(base.DefaultComparer.Compare, 0, f)
+		levelMetadata := MakeLevelMetadata(base.DefaultComparer.Compare, 0, f)
 
 		sm, la := KeyRange(base.DefaultComparer.Compare, levelMetadata.Iter())
 		got := string(sm.UserKey) + "-" + string(la.UserKey)
diff --git a/internal/overlap/checker_test.go b/internal/overlap/checker_test.go
new file mode 100644
index 00000000..21a2fc53
--- /dev/null
+++ b/internal/overlap/checker_test.go
@@ -0,0 +1,244 @@
+// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
+// of this source code is governed by a BSD-style license that can be found in
+// the LICENSE file.
+
+package overlap
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"math/rand"
+	"strings"
+	"testing"
+
+	"github.com/cockroachdb/datadriven"
+	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/keyspan"
+	"github.com/cockroachdb/pebble/internal/manifest"
+	"github.com/stretchr/testify/require"
+)
+
+func TestChecker(t *testing.T) {
+	tables := newTestTables()
+	byName := make(map[string]*manifest.FileMetadata)
+
+	datadriven.RunTest(t, "testdata/checker", func(t *testing.T, d *datadriven.TestData) string {
+		switch d.Cmd {
+		case "define":
+			tt := testTable{
+				name: d.CmdArgs[0].String(),
+				meta: &manifest.FileMetadata{
+					FileNum: base.FileNum(1 + len(tables.tables)),
+				},
+			}
+			tt.meta.InitPhysicalBacking()
+			byName[tt.name] = tt.meta
+			var overrideBounds base.UserKeyBounds
+			for _, section := range splitLinesInSections(d.Input) {
+				fields := strings.Fields(section[0])
+				switch fields[0] {
+				case "points:":
+					for _, key := range section[1:] {
+						tt.points = append(tt.points, base.InternalKV{
+							K: base.ParseInternalKey(key),
+						})
+					}
+
+				case "range-dels:":
+					for _, span := range section[1:] {
+						tt.rangeDels = append(tt.rangeDels, keyspan.ParseSpan(span))
+					}
+
+				case "range-keys:":
+					for _, span := range section[1:] {
+						tt.rangeKeys = append(tt.rangeKeys, keyspan.ParseSpan(span))
+					}
+
+				case "override-bounds:":
+					overrideBounds = base.ParseUserKeyBounds(strings.Join(fields[1:], " "))
+
+				default:
+					d.Fatalf(t, "unknown section %q", fields[0])
+				}
+			}
+
+			if overrideBounds.Start != nil {
+				// Note: this code matches the logic used for external ingestions.
+				if len(tt.points) > 0 || len(tt.rangeDels) > 0 {
+					endKey := base.MakeInternalKey(overrideBounds.End.Key, 0, 0)
+					if overrideBounds.End.Kind == base.Exclusive {
+						endKey = base.MakeRangeDeleteSentinelKey(overrideBounds.End.Key)
+					}
+					tt.meta.ExtendPointKeyBounds(
+						bytes.Compare,
+						base.MakeInternalKey(overrideBounds.Start, 0, base.InternalKeyKindMax),
+						endKey,
+					)
+				}
+				if len(tt.rangeKeys) > 0 {
+					tt.meta.ExtendRangeKeyBounds(
+						bytes.Compare,
+						base.MakeInternalKey(overrideBounds.Start, 0, base.InternalKeyKindRangeKeyMax),
+						base.MakeExclusiveSentinelKey(base.InternalKeyKindRangeKeyMin, overrideBounds.End.Key),
+					)
+				}
+			} else {
+				if len(tt.points) > 0 {
+					tt.meta.ExtendPointKeyBounds(bytes.Compare, tt.points[0].K, tt.points[len(tt.points)-1].K)
+				}
+				if len(tt.rangeDels) > 0 {
+					smallest, largest := boundsFromSpans(tt.rangeDels)
+					tt.meta.ExtendPointKeyBounds(bytes.Compare, smallest, largest)
+				}
+				if len(tt.rangeKeys) > 0 {
+					smallest, largest := boundsFromSpans(tt.rangeKeys)
+					tt.meta.ExtendRangeKeyBounds(bytes.Compare, smallest, largest)
+				}
+			}
+			tables.tables[tt.meta] = tt
+
+		case "overlap":
+			var metas []*manifest.FileMetadata
+			lines := strings.Split(d.Input, "\n")
+			for _, arg := range d.CmdArgs {
+				name := arg.String()
+				m := byName[name]
+				if m == nil {
+					d.Fatalf(t, "unknown table %q", name)
+				}
+				metas = append(metas, m)
+			}
+			levelMeta := manifest.MakeLevelMetadata(bytes.Compare, 1 /* level */, metas)
+			c := MakeChecker(bytes.Compare, tables)
+			var buf strings.Builder
+			for _, l := range lines {
+				bounds := base.ParseUserKeyBounds(l)
+				withLevel, err := c.LevelOverlap(context.Background(), bounds, levelMeta.Slice())
+				require.NoError(t, err)
+				switch withLevel.Result {
+				case None:
+					fmt.Fprintf(&buf, "%s: no overlap", bounds)
+				case OnlyBoundary:
+					splitFile := ""
+					if withLevel.SplitFile != nil {
+						splitFile = fmt.Sprintf(" (split file: %s)", tables.tables[withLevel.SplitFile].name)
+					}
+					fmt.Fprintf(&buf, "%s: boundary overlap, no data overlap%s", bounds, splitFile)
+				case Data:
+					fmt.Fprintf(&buf, "%s: possible data overlap", bounds)
+				default:
+					d.Fatalf(t, "invalid result %v", withLevel)
+				}
+				fmt.Fprintf(&buf, "   iterators opened: %s\n", tables.OpenedIterators())
+			}
+			return buf.String()
+
+		default:
+			d.Fatalf(t, "unknown command %q", d.Cmd)
+		}
+		return ""
+	})
+}
+
+type testTable struct {
+	name      string
+	meta      *manifest.FileMetadata
+	points    []base.InternalKV
+	rangeDels []keyspan.Span
+	rangeKeys []keyspan.Span
+}
+
+var _ IteratorFactory = (*testTables)(nil)
+
+// testTables implements the IteratorFactory interface for testing purposes.
+type testTables struct {
+	tables map[*manifest.FileMetadata]testTable
+	// openedIterators keeps track of the iterators we opened. It is reset before
+	// every overlap check.
+	openedIterators []string
+}
+
+func newTestTables() *testTables {
+	return &testTables{
+		tables: make(map[*manifest.FileMetadata]testTable),
+	}
+}
+
+func (tt *testTables) OpenedIterators() string {
+	if len(tt.openedIterators) == 0 {
+		return "none"
+	}
+	res := strings.Join(tt.openedIterators, ", ")
+	tt.openedIterators = nil
+	return res
+}
+
+func (tt *testTables) Points(
+	ctx context.Context, m *manifest.FileMetadata,
+) (base.InternalIterator, error) {
+	t := tt.get(m)
+	tt.openedIterators = append(tt.openedIterators, fmt.Sprintf("%s/points", t.name))
+	if len(t.points) == 0 && rand.Intn(2) == 0 {
+		return nil, nil
+	}
+	return base.NewFakeIter(t.points), nil
+}
+
+func (tt *testTables) RangeDels(
+	ctx context.Context, m *manifest.FileMetadata,
+) (keyspan.FragmentIterator, error) {
+	t := tt.get(m)
+	tt.openedIterators = append(tt.openedIterators, fmt.Sprintf("%s/range-del", t.name))
+	if len(t.rangeDels) == 0 && rand.Intn(2) == 0 {
+		return nil, nil
+	}
+	return keyspan.NewIter(bytes.Compare, t.rangeDels), nil
+}
+
+func (tt *testTables) RangeKeys(
+	ctx context.Context, m *manifest.FileMetadata,
+) (keyspan.FragmentIterator, error) {
+	t := tt.get(m)
+	tt.openedIterators = append(tt.openedIterators, fmt.Sprintf("%s/range-key", t.name))
+	if len(t.rangeKeys) == 0 && rand.Intn(2) == 0 {
+		return nil, nil
+	}
+	return keyspan.NewIter(bytes.Compare, t.rangeKeys), nil
+}
+
+func (tt *testTables) get(m *manifest.FileMetadata) testTable {
+	t, ok := tt.tables[m]
+	if !ok {
+		panic("table not found")
+	}
+	return t
+}
+
+// splitLinesInSections parses a multi-line string where each line that is not
+// indented starts a section which also includes all indented lines that follow
+// it.
+func splitLinesInSections(input string) [][]string {
+	var res [][]string
+	for _, l := range strings.Split(input, "\n") {
+		if l[0] == ' ' || l[0] == '\t' {
+			if len(res) == 0 {
+				panic(fmt.Sprintf("invalid first line %q", l))
+			}
+			res[len(res)-1] = append(res[len(res)-1], strings.TrimSpace(l))
+		} else {
+			res = append(res, []string{l})
+		}
+	}
+	return res
+}
+
+func boundsFromSpans(spans []keyspan.Span) (smallest, largest base.InternalKey) {
+	smallest = base.InternalKey{
+		UserKey: spans[0].Start,
+		Trailer: spans[0].Keys[0].Trailer,
+	}
+	last := spans[len(spans)-1]
+	largest = base.MakeExclusiveSentinelKey(last.Keys[len(last.Keys)-1].Kind(), last.End)
+	return smallest, largest
+}
diff --git a/internal/overlap/testdata/checker b/internal/overlap/testdata/checker
new file mode 100644
index 00000000..ea7e31f6
--- /dev/null
+++ b/internal/overlap/testdata/checker
@@ -0,0 +1,119 @@
+define t1
+points:
+  b.SET.1
+  d.SET.1
+  u.SET.1
+----
+
+overlap t1
+[a, b)
+[a, b]
+[b1, b2]
+[b, c]
+[d, e]
+[e, u)
+[e, u]
+[u, w]
+[v, w]
+----
+[a, b): no overlap   iterators opened: none
+[a, b]: possible data overlap   iterators opened: none
+[b1, b2]: boundary overlap, no data overlap (split file: t1)   iterators opened: t1/points, t1/range-del
+[b, c]: possible data overlap   iterators opened: none
+[d, e]: possible data overlap   iterators opened: t1/points
+[e, u): boundary overlap, no data overlap (split file: t1)   iterators opened: t1/points, t1/range-del
+[e, u]: possible data overlap   iterators opened: none
+[u, w]: possible data overlap   iterators opened: none
+[v, w]: no overlap   iterators opened: none
+
+define t2
+range-dels:
+  b-e:{#1,RANGEDEL}
+  h-k:{#1,RANGEDEL}
+----
+
+overlap t2
+[a, b)
+[a, b]
+[d, f]
+[e, f]
+[e, h)
+[e, h]
+[j, l)
+[k, l]
+----
+[a, b): no overlap   iterators opened: none
+[a, b]: possible data overlap   iterators opened: none
+[d, f]: possible data overlap   iterators opened: t2/points, t2/range-del
+[e, f]: boundary overlap, no data overlap (split file: t2)   iterators opened: t2/points, t2/range-del
+[e, h): boundary overlap, no data overlap (split file: t2)   iterators opened: t2/points, t2/range-del
+[e, h]: possible data overlap   iterators opened: t2/points, t2/range-del
+[j, l): possible data overlap   iterators opened: none
+[k, l]: no overlap   iterators opened: none
+
+define t3
+range-keys:
+  b-e:{#1,RANGEKEYDEL}
+  h-k:{#1,RANGEKEYDEL}
+----
+
+overlap t3
+[a, b)
+[a, b]
+[d, f]
+[e, f]
+[e, h)
+[e, h]
+[j, l)
+[k, l]
+----
+[a, b): no overlap   iterators opened: none
+[a, b]: possible data overlap   iterators opened: none
+[d, f]: possible data overlap   iterators opened: t3/range-key
+[e, f]: boundary overlap, no data overlap (split file: t3)   iterators opened: t3/range-key
+[e, h): boundary overlap, no data overlap (split file: t3)   iterators opened: t3/range-key
+[e, h]: possible data overlap   iterators opened: t3/range-key
+[j, l): possible data overlap   iterators opened: none
+[k, l]: no overlap   iterators opened: none
+
+
+define tBE
+points:
+  b.SET.1
+  d.SET.1
+range-keys:
+  c-e:{#1,RANGEKEYDEL}
+----
+
+define tFM
+override-bounds: [f, m)
+points:
+  g.SET.1
+  j.SET.1
+----
+
+define tMP
+override-bounds: [m, p)
+points:
+  n.SET.1
+  o.SET.1
+----
+
+overlap tBE tFM tMP
+[b1, b2]
+[c1, c2]
+[e, f)
+[g1, g2]
+[j1, m1]
+[n1, n2]
+[n, o]
+[a, z]
+----
+[b1, b2]: boundary overlap, no data overlap (split file: tBE)   iterators opened: tBE/points, tBE/range-del
+[c1, c2]: possible data overlap   iterators opened: tBE/points, tBE/range-del, tBE/range-key
+[e, f): no overlap   iterators opened: none
+[g1, g2]: boundary overlap, no data overlap (split file: tFM)   iterators opened: tFM/points, tFM/range-del
+[j1, m1]: possible data overlap   iterators opened: none
+[n1, n2]: boundary overlap, no data overlap (split file: tMP)   iterators opened: tMP/points, tMP/range-del
+[n, o]: possible data overlap   iterators opened: tMP/points
+[a, z]: possible data overlap   iterators opened: none
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/keyspan_probe_test.go b/keyspan_probe_test.go
index 7ce39009..dbe9fb5b 100644
--- a/keyspan_probe_test.go
+++ b/keyspan_probe_test.go
@@ -368,11 +368,10 @@ func (p *probeKeyspanIterator) WrapChildren(wrap keyspan.WrapFn) {
 	p.iter = wrap(p.iter)
 }
 
-func (p *probeKeyspanIterator) Close() error {
+func (p *probeKeyspanIterator) Close() {
 	op := keyspanOp{Kind: opSpanClose}
 	if p.iter != nil {
-		op.Err = p.iter.Close()
+		p.iter.Close()
 	}
-	_, err := p.handleOp(op)
-	return err
+	_, _ = p.handleOp(op)
 }
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/scan_internal_test.go b/scan_internal_test.go
index 949cf769..305bb7a2 100644
--- a/scan_internal_test.go
+++ b/scan_internal_test.go
@@ -416,7 +416,7 @@ func TestScanInternal(t *testing.T) {
 						require.NoError(t, w.DeleteRange(span.Start, span.End))
 					}
 					require.NoError(t, err)
-					require.NoError(t, rangeDels.Close())
+					rangeDels.Close()
 				}
 				{
 					span, err := rangeKeys.First()
@@ -432,7 +432,7 @@ func TestScanInternal(t *testing.T) {
 					}
 					require.NoError(t, err)
 				}
-				require.NoError(t, rangeKeys.Close())
+				rangeKeys.Close()
 				for kv := points.First(); kv != nil; kv = points.Next() {
 					t.Logf("writing %s", kv.K)
 					var value []byte
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 1a5e45ff..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
@@ -186,12 +186,6 @@ close: ext/0
 link: ext/0 -> db/000015.sst
 [JOB 10] ingesting: sstable created 000015
 sync: db
-open: db/000013.sst
-read-at(537, 53): db/000013.sst
-read-at(500, 37): db/000013.sst
-read-at(53, 447): db/000013.sst
-read-at(26, 27): db/000013.sst
-read-at(0, 26): db/000013.sst
 create: db/MANIFEST-000016
 close: db/MANIFEST-000014
 sync: db/MANIFEST-000016
@@ -229,8 +223,8 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 1.7KB
 Compression types: snappy: 3
-Block cache: 6 entries (970B)  hit rate: 0.0%
-Table cache: 1 entries (760B)  hit rate: 40.0%
+Block cache: 3 entries (485B)  hit rate: 0.0%
+Table cache: 0 entries (0B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
@@ -330,8 +324,8 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 3.5KB
 Compression types: snappy: 6
-Block cache: 12 entries (1.9KB)  hit rate: 7.7%
-Table cache: 1 entries (760B)  hit rate: 50.0%
+Block cache: 9 entries (1.4KB)  hit rate: 0.0%
+Table cache: 0 entries (0B)  hit rate: 50.0%
 Secondary cache: 0 entries (0B)  hit rate: 0.0%
 Snapshots: 0  earliest seq num: 0
 Table iters: 0
@@ -369,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
@@ -380,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
@@ -395,7 +389,6 @@ pebble: file deletion disablement invariant violated
 close
 ----
 close: db
-close: db/000013.sst
 sync-data: wal/000021.log
 close: wal/000021.log
 close: wal
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/ingest_external b/testdata/ingest_external
index dc6edf5a..7cca6fb2 100644
--- a/testdata/ingest_external
+++ b/testdata/ingest_external
@@ -816,9 +816,10 @@ ingest local1
 
 lsm
 ----
-L6:
+L0.0:
   000005:[a#11,SET-a#11,SET]
-  000006(000004):[aa@10#10,SET-c#inf,RANGEDEL]
+L6:
+  000004(000004):[a#10,DELSIZED-c#inf,RANGEDEL]
 
 iter
 first
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 b59e6ab7..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
@@ -487,8 +487,8 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 4.3KB
 Compression types: snappy: 7
-Block cache: 12 entries (1.9KB)  hit rate: 16.7%
-Table cache: 1 entries (760B)  hit rate: 60.0%
+Block cache: 12 entries (1.9KB)  hit rate: 9.1%
+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
@@ -498,7 +498,7 @@ Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
-       pebble-ingest,     latency: {BlockBytes:192 BlockBytesInCache:128 BlockReadDuration:10ms}
+       pebble-ingest,     latency: {BlockBytes:64 BlockBytesInCache:0 BlockReadDuration:10ms}
 
 batch
 set g g
@@ -550,8 +550,8 @@ Backing tables: 0 (0B)
 Virtual tables: 0 (0B)
 Local tables size: 6.1KB
 Compression types: snappy: 10
-Block cache: 12 entries (1.9KB)  hit rate: 16.7%
-Table cache: 1 entries (760B)  hit rate: 60.0%
+Block cache: 12 entries (1.9KB)  hit rate: 9.1%
+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
@@ -561,7 +561,7 @@ Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
-       pebble-ingest,     latency: {BlockBytes:192 BlockBytesInCache:128 BlockReadDuration:10ms}
+       pebble-ingest,     latency: {BlockBytes:64 BlockBytesInCache:0 BlockReadDuration:10ms}
 
 build ext1
 set z z
@@ -638,7 +638,7 @@ Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:301 BlockBytesInCache:44 BlockReadDuration:60ms}
-       pebble-ingest,     latency: {BlockBytes:328 BlockBytesInCache:128 BlockReadDuration:30ms}
+       pebble-ingest,     latency: {BlockBytes:200 BlockBytesInCache:0 BlockReadDuration:30ms}
 
 # Virtualize a virtual sstable.
 build ext1
@@ -740,7 +740,7 @@ Iter category stats:
                    b,     latency: {BlockBytes:44 BlockBytesInCache:0 BlockReadDuration:10ms}
                    c, non-latency: {BlockBytes:44 BlockBytesInCache:44 BlockReadDuration:0s}
    pebble-compaction, non-latency: {BlockBytes:677 BlockBytesInCache:376 BlockReadDuration:70ms}
-       pebble-ingest,     latency: {BlockBytes:400 BlockBytesInCache:200 BlockReadDuration:30ms}
+       pebble-ingest,     latency: {BlockBytes:272 BlockBytesInCache:72 BlockReadDuration:30ms}
 
 # Create a DB where lower levels are written as shared tables. All ingests also
 # become shared tables.
@@ -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