Repository: petermattis/pebble
Pull request #246 (2019-08-22 19:54:00)

Add fluctuation option to benchmarks
Add option to fluctuate ops per second in the benchmarks.

This would be useful for testing how Pebble/RocksDB behaves when the read/write rates fluctuate.

petermattis: 

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


Patch:
diff --git a/batch.go b/batch.go
index 4d4334c..addd97e 100644
--- a/batch.go
+++ b/batch.go
@@ -8,7 +8,6 @@ import (
 	"encoding/binary"
 	"errors"
 	"fmt"
-	"math"
 	"sort"
 	"sync"
 	"sync/atomic"
@@ -85,14 +84,14 @@ func (s *batchStorage) Compare(a []byte, b uint32) int {
 // copy or encode keys/values directly into the batch representation instead of
 // copying into an intermediary buffer then having pebble.Batch copy off of it.
 type DeferredBatchOp struct {
-	index *batchskl.Skiplist
+	batch *Batch
 
 	// Key and Value point to parts of the binary batch representation where
 	// keys and values should be encoded/copied into. len(Key) and len(Value)
 	// bytes must be copied into these slices respectively before calling
 	// Finish(). Changing where these slices point to is not allowed.
 	Key, Value []byte
-	offset     uint32
+	offset uint32
 }
 
 // Finish completes the addition of this batch operation, and adds it to the
@@ -101,8 +100,8 @@ type DeferredBatchOp struct {
 // copying/encoding keys will result in an incomplete index, and calling Finish
 // twice may result in a panic.
 func (d DeferredBatchOp) Finish() {
-	if d.index != nil {
-		if err := d.index.Add(d.offset); err != nil {
+	if d.batch.index != nil {
+		if err := d.batch.index.Add(d.offset); err != nil {
 			// We never add duplicate entries, so an error should never occur.
 			panic(err)
 		}
@@ -212,14 +211,6 @@ type Batch struct {
 	// The db to which the batch will be committed.
 	db *DB
 
-	// The count of records in the batch. This count will be stored in the batch
-	// data whenever Repr() is called.
-	count uint32
-
-	// A deferredOp struct, stored in the Batch so that a pointer can be returned
-	// from the *Deferred() methods rather than a value.
-	deferredOp DeferredBatchOp
-
 	// An optional skiplist keyed by offset into data of the entry.
 	index         *batchskl.Skiplist
 	rangeDelIndex *batchskl.Skiplist
@@ -331,7 +322,8 @@ func (b *Batch) Apply(batch *Batch, _ *WriteOptions) error {
 	}
 	b.storage.data = append(b.storage.data, batch.storage.data[batchHeaderLen:]...)
 
-	b.setCount(b.Count() + batch.Count())
+	count := binary.LittleEndian.Uint32(batch.storage.data[8:12])
+	b.setCount(b.Count() + count)
 
 	for iter := BatchReader(b.storage.data[offset:]); len(iter) > 0; {
 		offset := uintptr(unsafe.Pointer(&iter[0])) - uintptr(unsafe.Pointer(&b.storage.data[0]))
@@ -371,64 +363,88 @@ func (b *Batch) Get(key []byte) (value []byte, err error) {
 	return b.db.getInternal(key, b, nil /* snapshot */)
 }
 
-func (b *Batch) prepareRecord(keyLen, valueLen int, kind InternalKeyKind) {
+func (b *Batch) encodeKeyValue(key, value []byte, kind InternalKeyKind) uint32 {
+	pos := len(b.storage.data)
+	offset := uint32(pos)
+	b.grow(1 + 2*maxVarintLen32 + len(key) + len(value))
+	b.storage.data[pos] = byte(kind)
+	pos, varlen1 := b.copyStr(pos+1, key)
+	_, varlen2 := b.copyStr(pos, value)
+	b.storage.data = b.storage.data[:len(b.storage.data)-(2*maxVarintLen32-varlen1-varlen2)]
+	return offset
+}
+
+func (b *Batch) encodeKeyValuePlaceholders(
+	keyLen, valueLen int, kind InternalKeyKind,
+) (keyDest, valueDest []byte, offset uint32) {
 	pos := len(b.storage.data)
-	b.deferredOp.offset = uint32(pos)
+	offset = uint32(pos)
 	b.grow(1 + 2*maxVarintLen32 + keyLen + valueLen)
 	b.storage.data[pos] = byte(kind)
 	pos++
 
 	varlen1 := putUvarint32(b.storage.data[pos:], uint32(keyLen))
 	pos += varlen1
-	b.deferredOp.Key = b.storage.data[pos : pos+keyLen]
+	keyDest = b.storage.data[pos:pos+keyLen]
 	pos += keyLen
 
 	varlen2 := putUvarint32(b.storage.data[pos:], uint32(valueLen))
 	pos += varlen2
-	b.deferredOp.Value = b.storage.data[pos : pos+valueLen]
+	valueDest = b.storage.data[pos:pos+valueLen]
 	pos += valueLen
 	b.storage.data = b.storage.data[:len(b.storage.data)-(2*maxVarintLen32-varlen1-varlen2)]
+
+	return
 }
 
 // Set adds an action to the batch that sets the key to map to the value.
 //
 // It is safe to modify the contents of the arguments after Set returns.
 func (b *Batch) Set(key, value []byte, _ *WriteOptions) error {
-	deferredOp, err := b.SetDeferred(len(key), len(value), nil)
-	if err != nil {
-		return err
-	}
-	copy(deferredOp.Key, key)
-	copy(deferredOp.Value, value)
-	// TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining
-	// in go1.13 will remove the need for this.
+	if len(b.storage.data) == 0 {
+		b.init(len(key) + len(value) + 2*binary.MaxVarintLen64 + batchHeaderLen)
+	}
+	if !b.increment() {
+		return ErrInvalidBatch
+	}
+
+	offset := b.encodeKeyValue(key, value, InternalKeyKindSet)
+
 	if b.index != nil {
-		if err := b.index.Add(deferredOp.offset); err != nil {
+		if err := b.index.Add(offset); err != nil {
 			// We never add duplicate entries, so an error should never occur.
 			panic(err)
 		}
 	}
+	b.memTableSize += memTableEntrySize(len(key), len(value))
 	return nil
 }
 
 // SetDeferred is similar to Set in that it adds a set operation to the batch,
-// except it only takes in key/value lengths instead of complete slices,
-// letting the caller encode into those objects and then call Finish() on the
-// returned object.
-func (b *Batch) SetDeferred(keyLen, valueLen int, _ *WriteOptions) (*DeferredBatchOp, error) {
+// except it only takes in key/value lengths insted of complete slices, letting
+// the caller encode into those objects and then call Finish() on the returned
+// object.
+func (b *Batch) SetDeferred(keyLen, valueLen int, _ *WriteOptions) (DeferredBatchOp, error) {
 	// Code duplication between Set and SetDeferred lets us preserve the fast
 	// path where the entire byte slices are available (in the Set case).
 	if len(b.storage.data) == 0 {
 		b.init(keyLen + valueLen + 2*binary.MaxVarintLen64 + batchHeaderLen)
 	}
 	if !b.increment() {
-		return nil, ErrInvalidBatch
+		return DeferredBatchOp{}, ErrInvalidBatch
+	}
+
+	keyDest, valueDest, offset := b.encodeKeyValuePlaceholders(keyLen, valueLen, InternalKeyKindSet)
+
+	deferredOp := DeferredBatchOp{
+		batch:  b,
+		Key:    keyDest,
+		Value:  valueDest,
+		offset: offset,
 	}
 
 	b.memTableSize += memTableEntrySize(keyLen, valueLen)
-	b.prepareRecord(keyLen, valueLen, InternalKeyKindSet)
-	b.deferredOp.index = b.index
-	return &b.deferredOp, nil
+	return deferredOp, nil
 }
 
 // Merge adds an action to the batch that merges the value at key with the new
@@ -437,93 +453,111 @@ func (b *Batch) SetDeferred(keyLen, valueLen int, _ *WriteOptions) (*DeferredBat
 //
 // It is safe to modify the contents of the arguments after Merge returns.
 func (b *Batch) Merge(key, value []byte, _ *WriteOptions) error {
-	deferredOp, err := b.MergeDeferred(len(key), len(value), nil)
-	if err != nil {
-		return err
-	}
-	copy(deferredOp.Key, key)
-	copy(deferredOp.Value, value)
-	// TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining
-	// in go1.13 will remove the need for this.
+	if len(b.storage.data) == 0 {
+		b.init(len(key) + len(value) + 2*binary.MaxVarintLen64 + batchHeaderLen)
+	}
+	if !b.increment() {
+		return ErrInvalidBatch
+	}
+
+	offset := b.encodeKeyValue(key, value, InternalKeyKindMerge)
+
 	if b.index != nil {
-		if err := b.index.Add(deferredOp.offset); err != nil {
+		if err := b.index.Add(offset); err != nil {
 			// We never add duplicate entries, so an error should never occur.
 			panic(err)
 		}
 	}
+	b.memTableSize += memTableEntrySize(len(key), len(value))
 	return nil
 }
 
 // MergeDeferred is similar to Merge in that it adds a merge operation to the
-// batch, except it only takes in key/value lengths instead of complete slices,
+// batch, except it only takes in key/value lengths insted of complete slices,
 // letting the caller encode into those objects and then call Finish() on the
 // returned object.
-func (b *Batch) MergeDeferred(keyLen, valueLen int, _ *WriteOptions) (*DeferredBatchOp, error) {
+func (b *Batch) MergeDeferred(keyLen, valueLen int, _ *WriteOptions) (DeferredBatchOp, error) {
 	// Code duplication with Merge is so that the Merge case (where byte slices
 	// are provided) can preserve the fast path.
 	if len(b.storage.data) == 0 {
 		b.init(keyLen + valueLen + 2*binary.MaxVarintLen64 + batchHeaderLen)
 	}
 	if !b.increment() {
-		return nil, ErrInvalidBatch
+		return DeferredBatchOp{}, ErrInvalidBatch
+	}
+
+	keyDest, valueDest, offset := b.encodeKeyValuePlaceholders(keyLen, valueLen, InternalKeyKindMerge)
+
+	deferredOp := DeferredBatchOp{
+		batch:  b,
+		Key:    keyDest,
+		Value:  valueDest,
+		offset: offset,
 	}
 
 	b.memTableSize += memTableEntrySize(keyLen, valueLen)
-	b.prepareRecord(keyLen, valueLen, InternalKeyKindMerge)
-	b.deferredOp.index = b.index
-	return &b.deferredOp, nil
+	return deferredOp, nil
 }
 
 // Delete adds an action to the batch that deletes the entry for key.
 //
 // It is safe to modify the contents of the arguments after Delete returns.
 func (b *Batch) Delete(key []byte, _ *WriteOptions) error {
-	deferredOp, err := b.DeleteDeferred(len(key), nil)
-	if err != nil {
-		return err
+	if len(b.storage.data) == 0 {
+		b.init(len(key) + binary.MaxVarintLen64 + batchHeaderLen)
 	}
-	copy(deferredOp.Key, key)
-	// TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining
-	// in go1.13 will remove the need for this.
+	if !b.increment() {
+		return ErrInvalidBatch
+	}
+
+	pos := len(b.storage.data)
+	offset := uint32(pos)
+	b.grow(1 + maxVarintLen32 + len(key))
+	b.storage.data[pos] = byte(InternalKeyKindDelete)
+	pos, varlen1 := b.copyStr(pos+1, key)
+	b.storage.data = b.storage.data[:len(b.storage.data)-(maxVarintLen32-varlen1)]
+
 	if b.index != nil {
-		if err := b.index.Add(deferredOp.offset); err != nil {
+		if err := b.index.Add(offset); err != nil {
 			// We never add duplicate entries, so an error should never occur.
 			panic(err)
 		}
 	}
+	b.memTableSize += memTableEntrySize(len(key), 0)
 	return nil
 }
 
-// DeleteDeferred is similar to Delete in that it adds a delete operation to
-// the batch, except it only takes in key/value lengths instead of complete
-// slices, letting the caller encode into those objects and then call Finish()
-// on the returned object.
-func (b *Batch) DeleteDeferred(keyLen int, _ *WriteOptions) (*DeferredBatchOp, error) {
+// DeleteDeferred is similar to Delete in that it adds a delete operation to the
+// batch, except it only takes in key/value lengths insted of complete slices,
+// letting the caller encode into those objects and then call Finish() on the
+// returned object.
+func (b *Batch) DeleteDeferred(keyLen int, _ *WriteOptions) (DeferredBatchOp, error) {
 	// Code duplication with Delete is so that the Delete case (where byte
 	// slices are provided) can preserve the fast path.
 	if len(b.storage.data) == 0 {
 		b.init(keyLen + binary.MaxVarintLen64 + batchHeaderLen)
 	}
 	if !b.increment() {
-		return nil, ErrInvalidBatch
+		return DeferredBatchOp{}, ErrInvalidBatch
 	}
 
-	b.memTableSize += memTableEntrySize(keyLen, 0)
+	deferredOp := DeferredBatchOp{
+		batch:     b,
+	}
 
 	pos := len(b.storage.data)
-	b.deferredOp.offset = uint32(pos)
-	b.grow(1 + maxVarintLen32 + keyLen)
+	deferredOp.offset = uint32(pos)
+	b.grow(1 + maxVarintLen32 +  keyLen)
 	b.storage.data[pos] = byte(InternalKeyKindDelete)
 	pos++
 	varlen1 := putUvarint32(b.storage.data[pos:], uint32(keyLen))
 	pos += varlen1
-	b.deferredOp.Key = b.storage.data[pos : pos+keyLen]
-	b.deferredOp.Value = nil
+	deferredOp.Key = b.storage.data[pos:pos+keyLen]
 
 	b.storage.data = b.storage.data[:len(b.storage.data)-(maxVarintLen32-varlen1)]
 
-	b.deferredOp.index = b.index
-	return &b.deferredOp, nil
+	b.memTableSize += memTableEntrySize(keyLen,0)
+	return deferredOp, nil
 }
 
 // DeleteRange deletes all of the keys (and values) in the range [start,end)
@@ -532,49 +566,28 @@ func (b *Batch) DeleteDeferred(keyLen int, _ *WriteOptions) (*DeferredBatchOp, e
 // It is safe to modify the contents of the arguments after DeleteRange
 // returns.
 func (b *Batch) DeleteRange(start, end []byte, _ *WriteOptions) error {
-	deferredOp, err := b.DeleteRangeDeferred(len(start), len(end), nil)
-	if err != nil {
-		return err
-	}
-	copy(deferredOp.Key, start)
-	copy(deferredOp.Value, end)
-	// TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining
-	// in go1.13 will remove the need for this.
-	if deferredOp.index != nil {
-		if err := deferredOp.index.Add(deferredOp.offset); err != nil {
-			// We never add duplicate entries, so an error should never occur.
-			panic(err)
-		}
-	}
-	return nil
-}
-
-// DeleteRangeDeferred is similar to DeleteRange in that it adds a delete range
-// operation to the batch, except it only takes in key lengths instead of
-// complete slices, letting the caller encode into those objects and then call
-// Finish() on the returned object. Note that DeferredBatchOp.Key should be
-// populated with the start key, and DeferredBatchOp.Value should be populated
-// with the end key.
-func (b *Batch) DeleteRangeDeferred(startLen, endLen int, _ *WriteOptions) (*DeferredBatchOp, error) {
 	if len(b.storage.data) == 0 {
-		b.init(startLen + endLen + 2*binary.MaxVarintLen64 + batchHeaderLen)
+		b.init(len(start) + len(end) + 2*binary.MaxVarintLen64 + batchHeaderLen)
 	}
 	if !b.increment() {
-		return nil, ErrInvalidBatch
+		return ErrInvalidBatch
 	}
 
-	b.memTableSize += memTableEntrySize(startLen, endLen)
-	b.prepareRecord(startLen, endLen, InternalKeyKindRangeDelete)
+	offset := b.encodeKeyValue(start, end, InternalKeyKindRangeDelete)
 
 	if b.index != nil {
-		b.tombstones = nil
 		// Range deletions are rare, so we lazily allocate the index for them.
 		if b.rangeDelIndex == nil {
 			b.rangeDelIndex = batchskl.NewSkiplist(&b.storage, 0)
 		}
-		b.deferredOp.index = b.rangeDelIndex
+		if err := b.rangeDelIndex.Add(offset); err != nil {
+			// We never add duplicate entries, so an error should never occur.
+			panic(err)
+		}
+		b.tombstones = nil
 	}
-	return &b.deferredOp, nil
+	b.memTableSize += memTableEntrySize(len(start), len(end))
+	return nil
 }
 
 // LogData adds the specified to the batch. The data will be written to the
@@ -598,19 +611,12 @@ func (b *Batch) LogData(data []byte, _ *WriteOptions) error {
 	return nil
 }
 
-// Empty returns true if the batch is empty, and false otherwise.
-func (b *Batch) Empty() bool {
-	return len(b.storage.data) <= batchHeaderLen
-}
-
 // Repr returns the underlying batch representation. It is not safe to modify
-// the contents. Reset() will not change the contents of the returned value,
-// though any other mutation operation may do so.
+// the contents.
 func (b *Batch) Repr() []byte {
 	if len(b.storage.data) == 0 {
 		b.init(batchHeaderLen)
 	}
-	binary.LittleEndian.PutUint32(b.countData(), b.count)
 	return b.storage.data
 }
 
@@ -622,8 +628,6 @@ func (b *Batch) SetRepr(data []byte) error {
 		return fmt.Errorf("invalid batch")
 	}
 	b.storage.data = data
-	b.count = binary.LittleEndian.Uint32(b.countData())
-	b.refreshMemTableSize()
 	return nil
 }
 
@@ -730,8 +734,9 @@ func (b *Batch) Reset() {
 		} else {
 			// Otherwise, reset the buffer for re-use.
 			b.storage.data = b.storage.data[:batchHeaderLen]
+			b.setCount(0)
+			b.setSeqNum(0)
 		}
-		b.count = 0
 	}
 }
 
@@ -748,11 +753,19 @@ func (b *Batch) countData() []byte {
 }
 
 func (b *Batch) increment() (ok bool) {
-	if b.count == math.MaxUint32 {
-		return false
+	p := b.countData()
+	for i := range p {
+		p[i]++
+		if p[i] != 0x00 {
+			return true
+		}
 	}
-	b.count++
-	return true
+	// The countData was "\xff\xff\xff\xff". Leave it as it was.
+	p[0] = 0xff
+	p[1] = 0xff
+	p[2] = 0xff
+	p[3] = 0xff
+	return false
 }
 
 func (b *Batch) grow(n int) {
@@ -797,13 +810,16 @@ func (b *Batch) SeqNum() uint64 {
 }
 
 func (b *Batch) setCount(v uint32) {
-	b.count = v
+	binary.LittleEndian.PutUint32(b.countData(), v)
 }
 
 // Count returns the count of memtable-modifying operations in this batch. All
 // operations with the except of LogData increment this count.
 func (b *Batch) Count() uint32 {
-	return b.count
+	if len(b.storage.data) == 0 {
+		return 0
+	}
+	return binary.LittleEndian.Uint32(b.countData())
 }
 
 // Reader returns a BatchReader for the current batch contents. If the batch is
diff --git a/cmd/pebble/main.go b/cmd/pebble/main.go
index 773171f..3e12e90 100644
--- a/cmd/pebble/main.go
+++ b/cmd/pebble/main.go
@@ -6,7 +6,6 @@ package main
 
 import (
 	"log"
-	"math"
 	"os"
 	"time"
 
@@ -19,7 +18,7 @@ var (
 	concurrency     int
 	disableWAL      bool
 	duration        time.Duration
-	maxOpsPerSec    int
+	maxOpsPerSec    string
 	rocksdb         bool
 	verbose         bool
 	waitCompactions bool
@@ -57,8 +56,8 @@ func main() {
 			&disableWAL, "disable-wal", false, "disable the WAL (voiding persistence guarantees)")
 		cmd.Flags().DurationVarP(
 			&duration, "duration", "d", 10*time.Second, "the duration to run (0, run forever)")
-		cmd.Flags().IntVarP(
-			&maxOpsPerSec, "max-ops-per-sec", "m", math.MaxInt32, "max ops per second")
+		cmd.Flags().StringVarP(
+			&maxOpsPerSec, "rate", "m", "1000000", "max ops per second [{zipf,uniform}:]min[-max][/period (sec)]")
 		cmd.Flags().BoolVar(
 			&rocksdb, "rocksdb", false,
 			"use rocksdb storage engine instead of pebble")
diff --git a/cmd/pebble/random.go b/cmd/pebble/random.go
index 5afbed7..64f4487 100644
--- a/cmd/pebble/random.go
+++ b/cmd/pebble/random.go
@@ -16,11 +16,32 @@ import (
 	"time"
 
 	"github.com/petermattis/pebble/internal/randvar"
+	"github.com/petermattis/pebble/internal/rate"
 	"golang.org/x/exp/rand"
 )
 
 var randVarRE = regexp.MustCompile(`^(?:(uniform|zipf):)?(\d+)(?:-(\d+))?$`)
 
+func newFluctuatingRateLimiter(maxOpsPerSec string) (*rate.Limiter, error) {
+	rateDist, fluctuateDuration, err := parseRateSpec(maxOpsPerSec)
+	if err != nil {
+		return nil, err
+	}
+	limiter := rate.NewLimiter(rate.Limit(rateDist.Uint64()), 1)
+	if fluctuateDuration != 0 {
+		go func(limiter *rate.Limiter) {
+			ticker := time.NewTicker(fluctuateDuration)
+			for i := 0; ; i++ {
+				select {
+				case <-ticker.C:
+					limiter.SetLimit(rate.Limit(rateDist.Uint64()))
+				}
+			}
+		}(limiter)
+	}
+	return limiter, nil
+}
+
 func parseRandVarSpec(d string) (randvar.Static, error) {
 	m := randVarRE.FindStringSubmatch(d)
 	if m == nil {
@@ -49,6 +70,27 @@ func parseRandVarSpec(d string) (randvar.Static, error) {
 	}
 }
 
+func parseRateSpec(v string) (randvar.Static, time.Duration, error) {
+	parts := strings.Split(v, "/")
+	if len(parts) == 0 || len(parts) > 2 {
+		return nil, 0, fmt.Errorf("invalid max-ops-per-sec spec: %s", v)
+	}
+	r, err := parseRandVarSpec(parts[0])
+	if err != nil {
+		return nil, 0, err
+	}
+	// Don't fluctuate by default.
+	fluctuateDuration := time.Duration(0)
+	if len(parts) == 2 {
+		fluctuateDurationFloat, err := strconv.ParseFloat(parts[1], 64)
+		if err != nil {
+			return nil, 0, err
+		}
+		fluctuateDuration = time.Duration(fluctuateDurationFloat) * time.Second
+	}
+	return r, fluctuateDuration, nil
+}
+
 func parseValuesSpec(v string) (randvar.Static, float64, error) {
 	parts := strings.Split(v, "/")
 	if len(parts) == 0 || len(parts) > 2 {
diff --git a/cmd/pebble/scan.go b/cmd/pebble/scan.go
index 8d777a0..eff6d5f 100644
--- a/cmd/pebble/scan.go
+++ b/cmd/pebble/scan.go
@@ -14,7 +14,6 @@ import (
 	"time"
 
 	"github.com/petermattis/pebble"
-	"github.com/petermattis/pebble/internal/rate"
 	"github.com/spf13/cobra"
 	"golang.org/x/exp/rand"
 )
@@ -96,7 +95,11 @@ func runScan(cmd *cobra.Command, args []string) {
 				log.Fatal(err)
 			}
 
-			limiter := rate.NewLimiter(rate.Limit(maxOpsPerSec), 1)
+			limiter, err := newFluctuatingRateLimiter(maxOpsPerSec)
+			if err != nil {
+				fmt.Println(err)
+				return
+			}
 			wg.Add(concurrency)
 			for i := 0; i < concurrency; i++ {
 				go func(i int) {
diff --git a/cmd/pebble/sync.go b/cmd/pebble/sync.go
index bb07da0..aac511a 100644
--- a/cmd/pebble/sync.go
+++ b/cmd/pebble/sync.go
@@ -13,7 +13,6 @@ import (
 	"time"
 
 	"github.com/petermattis/pebble"
-	"github.com/petermattis/pebble/internal/rate"
 	"github.com/spf13/cobra"
 	"golang.org/x/exp/rand"
 )
@@ -66,7 +65,11 @@ func runSync(cmd *cobra.Command, args []string) {
 
 	runTest(args[0], test{
 		init: func(d DB, wg *sync.WaitGroup) {
-			limiter := rate.NewLimiter(rate.Limit(maxOpsPerSec), 1)
+			limiter, err := newFluctuatingRateLimiter(maxOpsPerSec)
+			if err != nil {
+				fmt.Println(err)
+				return
+			}
 			wg.Add(concurrency)
 			for i := 0; i < concurrency; i++ {
 				latency := reg.Register("ops")
diff --git a/cmd/pebble/ycsb.go b/cmd/pebble/ycsb.go
index 11dc785..1781cbc 100644
--- a/cmd/pebble/ycsb.go
+++ b/cmd/pebble/ycsb.go
@@ -324,8 +324,13 @@ func (y *ycsb) init(db DB, wg *sync.WaitGroup) {
 			1+ycsbConfig.prepopulatedKeys+ycsbConfig.initialKeys)
 	}
 	y.keyNum = ackseq.New(uint64(ycsbConfig.initialKeys + ycsbConfig.prepopulatedKeys))
-	y.limiter = rate.NewLimiter(rate.Limit(maxOpsPerSec), 1)
 
+	var err error
+	y.limiter, err = newFluctuatingRateLimiter(maxOpsPerSec)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
 	wg.Add(concurrency)
 	for i := 0; i < concurrency; i++ {
 		go y.run(db, wg)
diff --git a/commit.go b/commit.go
index 601a57b..bd953da 100644
--- a/commit.go
+++ b/commit.go
@@ -236,7 +236,7 @@ func (p *commitPipeline) Close() {
 // WAL, and applying the batch to the memtable. Upon successful return the
 // batch's mutations will be visible for reading.
 func (p *commitPipeline) Commit(b *Batch, syncWAL bool) error {
-	if b.Empty() {
+	if len(b.storage.data) == 0 {
 		return nil
 	}
 
diff --git a/compaction.go b/compaction.go
index b2c28c9..0cc9d12 100644
--- a/compaction.go
+++ b/compaction.go
@@ -15,7 +15,6 @@ import (
 	"unsafe"
 
 	"github.com/petermattis/pebble/internal/base"
-	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/internal/rangedel"
 	"github.com/petermattis/pebble/sstable"
 	"github.com/petermattis/pebble/vfs"
@@ -36,14 +35,6 @@ func maxGrandparentOverlapBytes(opts *Options, level int) uint64 {
 	return uint64(10 * opts.Level(level).TargetFileSize)
 }
 
-// totalSize returns the total size of all the files in f.
-func totalSize(f []fileMetadata) (size uint64) {
-	for _, x := range f {
-		size += x.Size
-	}
-	return size
-}
-
 // compaction is a table compaction from one level to the next, starting from a
 // given version.
 type compaction struct {
@@ -91,8 +82,6 @@ type compaction struct {
 	grandparents    []fileMetadata
 	overlappedBytes uint64 // bytes of overlap with grandparent tables
 	seenKey         bool   // some output key has been seen
-
-	metrics map[int]*LevelMetrics
 }
 
 func newCompaction(
@@ -198,7 +187,7 @@ func newFlush(
 			}
 		}
 
-		c.grandparents = c.version.Overlaps(baseLevel, c.cmp, smallest.UserKey, largest.UserKey)
+		c.grandparents = c.version.overlaps(baseLevel, c.cmp, smallest.UserKey, largest.UserKey)
 	}
 	return c
 }
@@ -207,18 +196,18 @@ func newFlush(
 // whether the compaction was automatically scheduled or user initiated.
 func (c *compaction) setupOtherInputs() {
 	c.inputs[0] = c.expandInputs(c.inputs[0])
-	smallest0, largest0 := manifest.KeyRange(c.cmp, c.inputs[0], nil)
-	c.inputs[1] = c.version.Overlaps(c.outputLevel, c.cmp, smallest0.UserKey, largest0.UserKey)
-	smallest01, largest01 := manifest.KeyRange(c.cmp, c.inputs[0], c.inputs[1])
+	smallest0, largest0 := ikeyRange(c.cmp, c.inputs[0], nil)
+	c.inputs[1] = c.version.overlaps(c.outputLevel, c.cmp, smallest0.UserKey, largest0.UserKey)
+	smallest01, largest01 := ikeyRange(c.cmp, c.inputs[0], c.inputs[1])
 
 	// Grow the inputs if it doesn't affect the number of level+1 files.
 	if c.grow(smallest01, largest01) {
-		smallest01, largest01 = manifest.KeyRange(c.cmp, c.inputs[0], c.inputs[1])
+		smallest01, largest01 = ikeyRange(c.cmp, c.inputs[0], c.inputs[1])
 	}
 
 	// Compute the set of outputLevel+1 files that overlap this compaction.
 	if c.outputLevel+1 < numLevels {
-		c.grandparents = c.version.Overlaps(c.outputLevel+1, c.cmp, smallest01.UserKey, largest01.UserKey)
+		c.grandparents = c.version.overlaps(c.outputLevel+1, c.cmp, smallest01.UserKey, largest01.UserKey)
 	}
 }
 
@@ -233,7 +222,7 @@ func (c *compaction) expandInputs(inputs []fileMetadata) []fileMetadata {
 		// get a "clean cut".
 		return inputs
 	}
-	files := c.version.Files[c.startLevel]
+	files := c.version.files[c.startLevel]
 	// Pointer arithmetic to figure out the index if inputs[0] with
 	// files[0]. This requires that the inputs slice is a sub-slice of
 	// files. This is true for non-L0 files returned from version.overlaps.
@@ -249,10 +238,10 @@ func (c *compaction) expandInputs(inputs []fileMetadata) []fileMetadata {
 	for ; end < len(files); end++ {
 		cur := &files[end-1]
 		next := &files[end]
-		if c.cmp(cur.Largest.UserKey, next.Smallest.UserKey) < 0 {
+		if c.cmp(cur.largest.UserKey, next.smallest.UserKey) < 0 {
 			break
 		}
-		if cur.Largest.Trailer == InternalKeyRangeDeleteSentinel {
+		if cur.largest.Trailer == InternalKeyRangeDeleteSentinel {
 			// The range deletion sentinel key is set for the largest key in a table
 			// when a range deletion tombstone straddles a table. It isn't necessary
 			// to include the next table in the compaction as cur.largest.UserKey
@@ -272,7 +261,7 @@ func (c *compaction) grow(sm, la InternalKey) bool {
 	if len(c.inputs[1]) == 0 {
 		return false
 	}
-	grow0 := c.version.Overlaps(c.startLevel, c.cmp, sm.UserKey, la.UserKey)
+	grow0 := c.version.overlaps(c.startLevel, c.cmp, sm.UserKey, la.UserKey)
 	grow0 = c.expandInputs(grow0)
 	if len(grow0) <= len(c.inputs[0]) {
 		return false
@@ -280,8 +269,8 @@ func (c *compaction) grow(sm, la InternalKey) bool {
 	if totalSize(grow0)+totalSize(c.inputs[1]) >= c.maxExpandedBytes {
 		return false
 	}
-	sm1, la1 := manifest.KeyRange(c.cmp, grow0, nil)
-	grow1 := c.version.Overlaps(c.outputLevel, c.cmp, sm1.UserKey, la1.UserKey)
+	sm1, la1 := ikeyRange(c.cmp, grow0, nil)
+	grow1 := c.version.overlaps(c.outputLevel, c.cmp, sm1.UserKey, la1.UserKey)
 	if len(grow1) != len(c.inputs[1]) {
 		return false
 	}
@@ -323,11 +312,11 @@ func (c *compaction) trivialMove() bool {
 func (c *compaction) shouldStopBefore(key InternalKey) bool {
 	for len(c.grandparents) > 0 {
 		g := &c.grandparents[0]
-		if base.InternalCompare(c.cmp, key, g.Largest) <= 0 {
+		if base.InternalCompare(c.cmp, key, g.largest) <= 0 {
 			break
 		}
 		if c.seenKey {
-			c.overlappedBytes += g.Size
+			c.overlappedBytes += g.size
 		}
 		c.grandparents = c.grandparents[1:]
 	}
@@ -345,7 +334,7 @@ func (c *compaction) shouldStopBefore(key InternalKey) bool {
 // lower level in the LSM.
 func (c *compaction) allowZeroSeqNum(iter internalIterator) bool {
 	if len(c.flushing) != 0 {
-		if len(c.version.Files[0]) > 0 {
+		if len(c.version.files[0]) > 0 {
 			// We can only allow zeroing of seqnum for L0 tables if no other L0 tables
 			// exist. Otherwise we may violate the invariant that L0 tables are ordered
 			// by increasing seqnum. This could be relaxed with a bit more intelligence
@@ -365,11 +354,11 @@ func (c *compaction) allowZeroSeqNum(iter internalIterator) bool {
 		files := c.inputs[i]
 		for j := range files {
 			f := &files[j]
-			if lower == nil || c.cmp(lower, f.Smallest.UserKey) > 0 {
-				lower = f.Smallest.UserKey
+			if lower == nil || c.cmp(lower, f.smallest.UserKey) > 0 {
+				lower = f.smallest.UserKey
 			}
-			if upper == nil || c.cmp(upper, f.Largest.UserKey) < 0 {
-				upper = f.Largest.UserKey
+			if upper == nil || c.cmp(upper, f.largest.UserKey) < 0 {
+				upper = f.largest.UserKey
 			}
 		}
 	}
@@ -397,9 +386,9 @@ func (c *compaction) elideTombstone(key []byte) bool {
 	// successive elideTombstones calls and we can keep some state in between
 	// calls.
 	for ; level < numLevels; level++ {
-		for _, f := range c.version.Files[level] {
-			if c.cmp(key, f.Largest.UserKey) <= 0 {
-				if c.cmp(key, f.Smallest.UserKey) >= 0 {
+		for _, f := range c.version.files[level] {
+			if c.cmp(key, f.largest.UserKey) <= 0 {
+				if c.cmp(key, f.smallest.UserKey) >= 0 {
 					return false
 				}
 				// For levels below level 0, the files within a level are in
@@ -428,7 +417,7 @@ func (c *compaction) elideRangeTombstone(start, end []byte) bool {
 	}
 
 	for ; level < numLevels; level++ {
-		overlaps := c.version.Overlaps(level, c.cmp, start, end)
+		overlaps := c.version.overlaps(level, c.cmp, start, end)
 		if len(overlaps) > 0 {
 			return false
 		}
@@ -443,14 +432,14 @@ func (c *compaction) atomicUnitBounds(f *fileMetadata) (lower, upper []byte) {
 		files := c.inputs[i]
 		for j := range files {
 			if f == &files[j] {
-				lowerBound := f.Smallest.UserKey
+				lowerBound := f.smallest.UserKey
 				for k := j; k > 0; k-- {
 					cur := &files[k]
 					prev := &files[k-1]
-					if c.cmp(prev.Largest.UserKey, cur.Smallest.UserKey) < 0 {
+					if c.cmp(prev.largest.UserKey, cur.smallest.UserKey) < 0 {
 						break
 					}
-					if prev.Largest.Trailer == InternalKeyRangeDeleteSentinel {
+					if prev.largest.Trailer == InternalKeyRangeDeleteSentinel {
 						// The range deletion sentinel key is set for the largest key in a
 						// table when a range deletion tombstone straddles a table. It
 						// isn't necessary to include the next table in the atomic
@@ -458,17 +447,17 @@ func (c *compaction) atomicUnitBounds(f *fileMetadata) (lower, upper []byte) {
 						// in the table.
 						break
 					}
-					lowerBound = prev.Smallest.UserKey
+					lowerBound = prev.smallest.UserKey
 				}
 
-				upperBound := f.Largest.UserKey
+				upperBound := f.largest.UserKey
 				for k := j + 1; k < len(files); k++ {
 					cur := &files[k-1]
 					next := &files[k]
-					if c.cmp(cur.Largest.UserKey, next.Smallest.UserKey) < 0 {
+					if c.cmp(cur.largest.UserKey, next.smallest.UserKey) < 0 {
 						break
 					}
-					if cur.Largest.Trailer == InternalKeyRangeDeleteSentinel {
+					if cur.largest.Trailer == InternalKeyRangeDeleteSentinel {
 						// The range deletion sentinel key is set for the largest key in a
 						// table when a range deletion tombstone straddles a table. It
 						// isn't necessary to include the next table in the atomic
@@ -478,7 +467,7 @@ func (c *compaction) atomicUnitBounds(f *fileMetadata) (lower, upper []byte) {
 					}
 					// cur.largest.UserKey == next.largest.UserKey, so next is part of
 					// the atomic compaction unit.
-					upperBound = next.Largest.UserKey
+					upperBound = next.largest.UserKey
 				}
 				return lowerBound, upperBound
 			}
@@ -564,7 +553,7 @@ func (c *compaction) newInputIter(
 			f := &c.inputs[0][i]
 			iter, rangeDelIter, err := newIters(f, nil /* iter options */, &c.bytesIterated)
 			if err != nil {
-				return nil, fmt.Errorf("pebble: could not open table %d: %v", f.FileNum, err)
+				return nil, fmt.Errorf("pebble: could not open table %d: %v", f.fileNum, err)
 			}
 			iters = append(iters, iter)
 			if rangeDelIter != nil {
@@ -591,7 +580,7 @@ func (c *compaction) String() string {
 		}
 		fmt.Fprintf(&buf, "%d:", level)
 		for _, f := range c.inputs[i] {
-			fmt.Fprintf(&buf, " %d:%s-%s", f.FileNum, f.Smallest, f.Largest)
+			fmt.Fprintf(&buf, " %d:%s-%s", f.fileNum, f.smallest, f.largest)
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
@@ -704,11 +693,11 @@ func (d *DB) flush1() error {
 			Err:   err,
 		}
 		if err == nil {
-			for i := range ve.NewFiles {
-				e := &ve.NewFiles[i]
-				info.Output = append(info.Output, e.Meta.TableInfo(d.dirname))
+			for i := range ve.newFiles {
+				e := &ve.newFiles[i]
+				info.Output = append(info.Output, e.meta.tableInfo(d.dirname))
 			}
-			if len(ve.NewFiles) == 0 {
+			if len(ve.newFiles) == 0 {
 				info.Err = errEmptyTable
 			}
 		}
@@ -721,14 +710,14 @@ func (d *DB) flush1() error {
 
 	// The flush succeeded or it produced an empty sstable. In either case we
 	// want to bump the log number.
-	ve.LogNum, _ = d.mu.mem.queue[n].logInfo()
-	metrics := c.metrics[0]
+	ve.logNumber, _ = d.mu.mem.queue[n].logInfo()
+	metrics := ve.metrics[0]
 	for i := 0; i < n; i++ {
 		_, size := d.mu.mem.queue[i].logInfo()
 		metrics.BytesIn += size
 	}
 
-	err = d.mu.versions.logAndApply(jobID, ve, c.metrics, d.dataDir)
+	err = d.mu.versions.logAndApply(jobID, ve, d.dataDir)
 	for _, fileNum := range pendingOutputs {
 		if _, ok := d.mu.compact.pendingOutputs[fileNum]; !ok {
 			panic("pebble: expected pending output not present")
@@ -830,7 +819,7 @@ func (d *DB) compact1() (err error) {
 		for i := range c.inputs {
 			for j := range c.inputs[i] {
 				m := &c.inputs[i][j]
-				info.Input.Tables[i] = append(info.Input.Tables[i], m.TableInfo(d.dirname))
+				info.Input.Tables[i] = append(info.Input.Tables[i], m.tableInfo(d.dirname))
 			}
 		}
 	}
@@ -848,9 +837,9 @@ func (d *DB) compact1() (err error) {
 	if d.opts.EventListener.CompactionEnd != nil {
 		info.Err = err
 		if err == nil {
-			for i := range ve.NewFiles {
-				e := &ve.NewFiles[i]
-				info.Output.Tables = append(info.Output.Tables, e.Meta.TableInfo(d.dirname))
+			for i := range ve.newFiles {
+				e := &ve.newFiles[i]
+				info.Output.Tables = append(info.Output.Tables, e.meta.tableInfo(d.dirname))
 			}
 		}
 		d.opts.EventListener.CompactionEnd(info)
@@ -859,7 +848,7 @@ func (d *DB) compact1() (err error) {
 	if err != nil {
 		return err
 	}
-	err = d.mu.versions.logAndApply(jobID, ve, c.metrics, d.dataDir)
+	err = d.mu.versions.logAndApply(jobID, ve, d.dataDir)
 	for _, fileNum := range pendingOutputs {
 		if _, ok := d.mu.compact.pendingOutputs[fileNum]; !ok {
 			panic("pebble: expected pending output not present")
@@ -889,20 +878,19 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 	// merge later on.
 	if c.trivialMove() {
 		meta := &c.inputs[0][0]
-		c.metrics = map[int]*LevelMetrics{
-			c.outputLevel: &LevelMetrics{
-				BytesMoved: meta.Size,
+		return &versionEdit{
+			deletedFiles: map[deletedFileEntry]bool{
+				deletedFileEntry{level: c.startLevel, fileNum: meta.fileNum}: true,
 			},
-		}
-		ve := &versionEdit{
-			DeletedFiles: map[deletedFileEntry]bool{
-				deletedFileEntry{Level: c.startLevel, FileNum: meta.FileNum}: true,
+			newFiles: []newFileEntry{
+				{level: c.outputLevel, meta: *meta},
 			},
-			NewFiles: []newFileEntry{
-				{Level: c.outputLevel, Meta: *meta},
+			metrics: map[int]*LevelMetrics{
+				c.outputLevel: &LevelMetrics{
+					BytesMoved: meta.size,
+				},
 			},
-		}
-		return ve, nil, nil
+		}, nil, nil
 	}
 
 	defer func() {
@@ -946,22 +934,22 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 		}
 	}()
 
-	ve = &versionEdit{
-		DeletedFiles: map[deletedFileEntry]bool{},
-	}
-
 	metrics := &LevelMetrics{
 		BytesIn:   totalSize(c.inputs[0]),
 		BytesRead: totalSize(c.inputs[1]),
 	}
 	metrics.BytesRead += metrics.BytesIn
-	c.metrics = map[int]*LevelMetrics{
-		c.outputLevel: metrics,
+
+	ve = &versionEdit{
+		deletedFiles: map[deletedFileEntry]bool{},
+		metrics: map[int]*LevelMetrics{
+			c.outputLevel: metrics,
+		},
 	}
 
 	newOutput := func() error {
 		d.mu.Lock()
-		fileNum := d.mu.versions.getNextFileNum()
+		fileNum := d.mu.versions.nextFileNum()
 		d.mu.compact.pendingOutputs[fileNum] = struct{}{}
 		pendingOutputs = append(pendingOutputs, fileNum)
 		d.mu.Unlock()
@@ -989,10 +977,10 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 		filenames = append(filenames, filename)
 		tw = sstable.NewWriter(file, d.opts, d.opts.Level(c.outputLevel))
 
-		ve.NewFiles = append(ve.NewFiles, newFileEntry{
-			Level: c.outputLevel,
-			Meta: fileMetadata{
-				FileNum: fileNum,
+		ve.newFiles = append(ve.newFiles, newFileEntry{
+			level: c.outputLevel,
+			meta: fileMetadata{
+				fileNum: fileNum,
 			},
 		})
 		return nil
@@ -1027,20 +1015,20 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 			return err
 		}
 		tw = nil
-		meta := &ve.NewFiles[len(ve.NewFiles)-1].Meta
-		meta.Size = writerMeta.Size
-		meta.SmallestSeqNum = writerMeta.SmallestSeqNum
-		meta.LargestSeqNum = writerMeta.LargestSeqNum
+		meta := &ve.newFiles[len(ve.newFiles)-1].meta
+		meta.size = writerMeta.Size
+		meta.smallestSeqNum = writerMeta.SmallestSeqNum
+		meta.largestSeqNum = writerMeta.LargestSeqNum
 
-		metrics.BytesWritten += meta.Size
+		metrics.BytesWritten += meta.size
 
 		// The handling of range boundaries is a bit complicated.
-		if n := len(ve.NewFiles); n > 1 {
+		if n := len(ve.newFiles); n > 1 {
 			// This is not the first output. Bound the smallest range key by the
 			// previous tables largest key.
-			prevMeta := &ve.NewFiles[n-2].Meta
+			prevMeta := &ve.newFiles[n-2].meta
 			if writerMeta.SmallestRange.UserKey != nil &&
-				d.cmp(writerMeta.SmallestRange.UserKey, prevMeta.Largest.UserKey) <= 0 {
+				d.cmp(writerMeta.SmallestRange.UserKey, prevMeta.largest.UserKey) <= 0 {
 				// The range boundary user key is less than or equal to the previous
 				// table's largest key. We need the tables to be key-space partitioned,
 				// so force the boundary to a key that we know is larger than the
@@ -1054,7 +1042,7 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 				// (the previous file could not end with a key at seqnum zero if this
 				// file had a tombstone extending into it).
 				writerMeta.SmallestRange = base.MakeInternalKey(
-					prevMeta.Largest.UserKey, 0, InternalKeyKindRangeDelete)
+					prevMeta.largest.UserKey, 0, InternalKeyKindRangeDelete)
 			}
 		}
 
@@ -1065,8 +1053,8 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 			}
 		}
 
-		meta.Smallest = writerMeta.Smallest(d.cmp)
-		meta.Largest = writerMeta.Largest(d.cmp)
+		meta.smallest = writerMeta.Smallest(d.cmp)
+		meta.largest = writerMeta.Largest(d.cmp)
 		return nil
 	}
 
@@ -1106,9 +1094,9 @@ func (d *DB) runCompaction(jobID int, c *compaction, pacer pacer) (
 			level = c.outputLevel
 		}
 		for _, f := range c.inputs[i] {
-			ve.DeletedFiles[deletedFileEntry{
-				Level:   level,
-				FileNum: f.FileNum,
+			ve.deletedFiles[deletedFileEntry{
+				level:   level,
+				fileNum: f.fileNum,
 			}] = true
 		}
 	}
@@ -1140,8 +1128,8 @@ func (d *DB) scanObsoleteFiles(list []string) {
 		liveFileNums[fileNum] = struct{}{}
 	}
 	d.mu.versions.addLiveFileNums(liveFileNums)
-	logNumber := d.mu.versions.logNum
-	manifestFileNumber := d.mu.versions.manifestFileNum
+	logNumber := d.mu.versions.logNumber
+	manifestFileNumber := d.mu.versions.manifestFileNumber
 
 	var obsoleteLogs []uint64
 	var obsoleteTables []uint64
@@ -1207,7 +1195,7 @@ func (d *DB) deleteObsoleteFiles(jobID int) {
 	for i := range d.mu.log.queue {
 		// NB: d.mu.versions.logNumber is the file number of the latest log that
 		// has had its contents persisted to the LSM.
-		if d.mu.log.queue[i] >= d.mu.versions.logNum {
+		if d.mu.log.queue[i] >= d.mu.versions.logNumber {
 			obsoleteLogs = d.mu.log.queue[:i]
 			d.mu.log.queue = d.mu.log.queue[i:]
 			d.mu.versions.metrics.WAL.Files -= int64(len(obsoleteLogs))
diff --git a/compaction_picker.go b/compaction_picker.go
index 5c7b64c..af5694b 100644
--- a/compaction_picker.go
+++ b/compaction_picker.go
@@ -6,8 +6,6 @@ package pebble
 
 import (
 	"math"
-
-	"github.com/petermattis/pebble/internal/manifest"
 )
 
 // compactionPicker holds the state and logic for picking a compaction. A
@@ -64,10 +62,10 @@ func (p *compactionPicker) estimatedCompactionDebt(l0ExtraSize uint64) uint64 {
 		return 0
 	}
 
-	compactionDebt := totalSize(p.vers.Files[0]) + l0ExtraSize
+	compactionDebt := totalSize(p.vers.files[0]) + l0ExtraSize
 	bytesAddedToNextLevel := compactionDebt
 
-	levelSize := totalSize(p.vers.Files[p.baseLevel])
+	levelSize := totalSize(p.vers.files[p.baseLevel])
 	// estimatedL0CompactionSize is the estimated size of the L0 component in the
 	// current or next L0->LBase compaction. This is needed to estimate the number
 	// of L0->LBase compactions which will need to occur for the LSM tree to
@@ -83,7 +81,7 @@ func (p *compactionPicker) estimatedCompactionDebt(l0ExtraSize uint64) uint64 {
 	for level := p.baseLevel; level < numLevels-1; level++ {
 		levelSize += bytesAddedToNextLevel
 		bytesAddedToNextLevel = 0
-		nextLevelSize = totalSize(p.vers.Files[level+1])
+		nextLevelSize = totalSize(p.vers.files[level+1])
 		if levelSize > uint64(p.levelMaxBytes[level]) {
 			bytesAddedToNextLevel = levelSize - uint64(p.levelMaxBytes[level])
 			levelRatio := float64(nextLevelSize) / float64(levelSize)
@@ -100,7 +98,7 @@ func (p *compactionPicker) initLevelMaxBytes(v *version, opts *Options) {
 	firstNonEmptyLevel := -1
 	var bottomLevelSize int64
 	for level := 1; level < numLevels; level++ {
-		levelSize := int64(totalSize(v.Files[level]))
+		levelSize := int64(totalSize(v.files[level]))
 		if levelSize > 0 {
 			if firstNonEmptyLevel == -1 {
 				firstNonEmptyLevel = level
@@ -187,11 +185,11 @@ func (p *compactionPicker) initTarget(v *version, opts *Options) {
 	// wish to avoid too many files when the individual file size is small
 	// (perhaps because of a small write-buffer setting, or very high
 	// compression ratios, or lots of overwrites/deletions).
-	p.score = float64(len(v.Files[0])) / float64(opts.L0CompactionThreshold)
+	p.score = float64(len(v.files[0])) / float64(opts.L0CompactionThreshold)
 	p.level = 0
 
 	for level := 1; level < numLevels-1; level++ {
-		score := float64(totalSize(v.Files[level])) / float64(p.levelMaxBytes[level])
+		score := float64(totalSize(v.files[level])) / float64(p.levelMaxBytes[level])
 		if p.score < score {
 			p.score = score
 			p.level = level
@@ -222,11 +220,11 @@ func (p *compactionPicker) initTarget(v *version, opts *Options) {
 		// The current heuristic matches the RocksDB kOldestSmallestSeqFirst
 		// heuristic.
 		smallestSeqNum := uint64(math.MaxUint64)
-		files := v.Files[p.level]
+		files := v.files[p.level]
 		for i := range files {
 			f := &files[i]
-			if smallestSeqNum > f.SmallestSeqNum {
-				smallestSeqNum = f.SmallestSeqNum
+			if smallestSeqNum > f.smallestSeqNum {
+				smallestSeqNum = f.smallestSeqNum
 				p.file = i
 			}
 		}
@@ -235,10 +233,10 @@ func (p *compactionPicker) initTarget(v *version, opts *Options) {
 
 	// No levels exceeded their size threshold. Check for forced compactions.
 	for level := 0; level < numLevels-1; level++ {
-		files := v.Files[p.level]
+		files := v.files[p.level]
 		for i := range files {
 			f := &files[i]
-			if f.MarkedForCompaction {
+			if f.markedForCompaction {
 				p.score = 1.0
 				p.level = level
 				p.file = i
@@ -263,13 +261,13 @@ func (p *compactionPicker) pickAuto(
 
 	vers := p.vers
 	c = newCompaction(opts, vers, p.level, p.baseLevel, bytesCompacted)
-	c.inputs[0] = vers.Files[c.startLevel][p.file : p.file+1]
+	c.inputs[0] = vers.files[c.startLevel][p.file : p.file+1]
 
 	// Files in level 0 may overlap each other, so pick up all overlapping ones.
 	if c.startLevel == 0 {
 		cmp := opts.Comparer.Compare
-		smallest, largest := manifest.KeyRange(cmp, c.inputs[0], nil)
-		c.inputs[0] = vers.Overlaps(0, cmp, smallest.UserKey, largest.UserKey)
+		smallest, largest := ikeyRange(cmp, c.inputs[0], nil)
+		c.inputs[0] = vers.overlaps(0, cmp, smallest.UserKey, largest.UserKey)
 		if len(c.inputs[0]) == 0 {
 			panic("pebble: empty compaction")
 		}
@@ -293,7 +291,7 @@ func (p *compactionPicker) pickManual(
 	c = newCompaction(opts, cur, manual.level, p.baseLevel, bytesCompacted)
 	manual.outputLevel = c.outputLevel
 	cmp := opts.Comparer.Compare
-	c.inputs[0] = cur.Overlaps(manual.level, cmp, manual.start.UserKey, manual.end.UserKey)
+	c.inputs[0] = cur.overlaps(manual.level, cmp, manual.start.UserKey, manual.end.UserKey)
 	if len(c.inputs[0]) == 0 {
 		return nil
 	}
diff --git a/db.go b/db.go
index bce86fd..91475c1 100644
--- a/db.go
+++ b/db.go
@@ -284,7 +284,7 @@ func (d *DB) getInternal(key []byte, b *Batch, s *Snapshot) ([]byte, error) {
 	get.key = key
 	get.batch = b
 	get.mem = readState.memtables
-	get.l0 = readState.current.Files[0]
+	get.l0 = readState.current.files[0]
 	get.version = readState.current
 
 	i := &buf.dbi
@@ -417,8 +417,6 @@ func (d *DB) commitApply(b *Batch, mem *memTable) error {
 }
 
 func (d *DB) commitWrite(b *Batch, wg *sync.WaitGroup) (*memTable, error) {
-	repr := b.Repr()
-
 	d.mu.Lock()
 
 	if b.flushable != nil {
@@ -429,7 +427,7 @@ func (d *DB) commitWrite(b *Batch, wg *sync.WaitGroup) (*memTable, error) {
 	err := d.makeRoomForWrite(b)
 
 	if err == nil {
-		d.mu.log.bytesIn += uint64(len(repr))
+		d.mu.log.bytesIn += uint64(len(b.storage.data))
 	}
 
 	// Grab a reference to the memtable while holding DB.mu. Note that
@@ -446,7 +444,7 @@ func (d *DB) commitWrite(b *Batch, wg *sync.WaitGroup) (*memTable, error) {
 		return mem, nil
 	}
 
-	size, err := d.mu.log.SyncRecord(repr, wg)
+	size, err := d.mu.log.SyncRecord(b.storage.data, wg)
 	if err != nil {
 		panic(err)
 	}
@@ -532,8 +530,8 @@ func (d *DB) newIterInternal(
 
 	// The level 0 files need to be added from newest to oldest.
 	current := readState.current
-	for i := len(current.Files[0]) - 1; i >= 0; i-- {
-		f := &current.Files[0][i]
+	for i := len(current.files[0]) - 1; i >= 0; i-- {
+		f := &current.files[0][i]
 		iter, rangeDelIter, err := d.newIters(f, &dbi.opts, nil)
 		if err != nil {
 			dbi.err = err
@@ -545,8 +543,8 @@ func (d *DB) newIterInternal(
 	}
 
 	start := len(rangeDelIters)
-	for level := 1; level < len(current.Files); level++ {
-		if len(current.Files[level]) == 0 {
+	for level := 1; level < len(current.files); level++ {
+		if len(current.files[level]) == 0 {
 			continue
 		}
 		rangeDelIters = append(rangeDelIters, nil)
@@ -559,8 +557,8 @@ func (d *DB) newIterInternal(
 
 	// Add level iterators for the remaining files.
 	levels := buf.levels[:]
-	for level := 1; level < len(current.Files); level++ {
-		if len(current.Files[level]) == 0 {
+	for level := 1; level < len(current.files); level++ {
+		if len(current.files[level]) == 0 {
 			continue
 		}
 
@@ -572,7 +570,7 @@ func (d *DB) newIterInternal(
 			li = &levelIter{}
 		}
 
-		li.init(&dbi.opts, d.cmp, d.newIters, current.Files[level], nil)
+		li.init(&dbi.opts, d.cmp, d.newIters, current.files[level], nil)
 		li.initRangeDel(&rangeDelIters[0])
 		li.initLargestUserKey(&largestUserKeys[0])
 		iters = append(iters, li)
@@ -667,8 +665,8 @@ func (d *DB) Close() error {
 		d.readState.val.unrefLocked()
 
 		current := d.mu.versions.currentVersion()
-		for v := d.mu.versions.versions.Front(); true; v = v.Next() {
-			refs := v.Refs()
+		for v := d.mu.versions.versions.front(); true; v = v.next {
+			refs := atomic.LoadInt32(&v.refs)
 			if v == current {
 				if refs != 1 {
 					return fmt.Errorf("leaked iterators: current\n%s", v)
@@ -694,13 +692,13 @@ func (d *DB) Compact(start, end []byte /* CompactionOptions */) error {
 
 	iStart := base.MakeInternalKey(start, InternalKeySeqNumMax, InternalKeyKindMax)
 	iEnd := base.MakeInternalKey(end, 0, 0)
-	meta := []*fileMetadata{&fileMetadata{Smallest: iStart, Largest: iEnd}}
+	meta := []*fileMetadata{&fileMetadata{smallest: iStart, largest: iEnd}}
 
 	d.mu.Lock()
 	maxLevelWithFiles := 1
 	cur := d.mu.versions.currentVersion()
 	for level := 0; level < numLevels; level++ {
-		if len(cur.Overlaps(level, d.cmp, start, end)) > 0 {
+		if len(cur.overlaps(level, d.cmp, start, end)) > 0 {
 			maxLevelWithFiles = level + 1
 		}
 	}
@@ -896,7 +894,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
 			d.mu.compact.cond.Wait()
 			continue
 		}
-		if len(d.mu.versions.currentVersion().Files[0]) > d.opts.L0StopWritesThreshold {
+		if len(d.mu.versions.currentVersion().files[0]) > d.opts.L0StopWritesThreshold {
 			// There are too many level-0 files, so we wait.
 			if !stalled {
 				stalled = true
@@ -918,7 +916,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
 		if !d.opts.DisableWAL {
 			jobID := d.mu.nextJobID
 			d.mu.nextJobID++
-			newLogNumber = d.mu.versions.getNextFileNum()
+			newLogNumber = d.mu.versions.nextFileNum()
 			d.mu.mem.switching = true
 			d.mu.Unlock()
 
diff --git a/get_iter.go b/get_iter.go
index 1af54cf..6a65eb5 100644
--- a/get_iter.go
+++ b/get_iter.go
@@ -134,7 +134,7 @@ func (g *getIter) Next() (*InternalKey, []byte) {
 				l := &g.l0[n-1]
 				g.iter, g.rangeDelIter, g.err = g.newIters(
 					l,
-					nil, /* iter options */
+					nil /* iter options */,
 					nil /* bytes iterated */)
 				if g.err != nil {
 					return nil, nil
@@ -149,12 +149,12 @@ func (g *getIter) Next() (*InternalKey, []byte) {
 		if g.level >= numLevels {
 			return nil, nil
 		}
-		if len(g.version.Files[g.level]) == 0 {
+		if len(g.version.files[g.level]) == 0 {
 			g.level++
 			continue
 		}
 
-		g.levelIter.init(nil, g.cmp, g.newIters, g.version.Files[g.level], nil)
+		g.levelIter.init(nil, g.cmp, g.newIters, g.version.files[g.level], nil)
 		g.levelIter.initRangeDel(&g.rangeDelIter)
 		g.level++
 		g.iter = &g.levelIter
diff --git a/ingest.go b/ingest.go
index 4b40826..149f693 100644
--- a/ingest.go
+++ b/ingest.go
@@ -46,21 +46,21 @@ func ingestLoad1(opts *Options, path string, dbNum, fileNum uint64) (*fileMetada
 	}
 
 	meta := &fileMetadata{}
-	meta.FileNum = fileNum
-	meta.Size = uint64(stat.Size())
-	meta.Smallest = InternalKey{}
-	meta.Largest = InternalKey{}
+	meta.fileNum = fileNum
+	meta.size = uint64(stat.Size())
+	meta.smallest = InternalKey{}
+	meta.largest = InternalKey{}
 	smallestSet, largestSet := false, false
 
 	{
 		iter := r.NewIter(nil /* lower */, nil /* upper */)
 		defer iter.Close()
 		if key, _ := iter.First(); key != nil {
-			meta.Smallest = key.Clone()
+			meta.smallest = key.Clone()
 			smallestSet = true
 		}
 		if key, _ := iter.Last(); key != nil {
-			meta.Largest = key.Clone()
+			meta.largest = key.Clone()
 			largestSet = true
 		}
 		if err := iter.Error(); err != nil {
@@ -72,15 +72,15 @@ func ingestLoad1(opts *Options, path string, dbNum, fileNum uint64) (*fileMetada
 		defer iter.Close()
 		if key, _ := iter.First(); key != nil {
 			if !smallestSet ||
-				base.InternalCompare(opts.Comparer.Compare, meta.Smallest, *key) > 0 {
-				meta.Smallest = key.Clone()
+				base.InternalCompare(opts.Comparer.Compare, meta.smallest, *key) > 0 {
+				meta.smallest = key.Clone()
 			}
 		}
 		if key, val := iter.Last(); key != nil {
 			end := base.MakeRangeDeleteSentinelKey(val)
 			if !largestSet ||
-				base.InternalCompare(opts.Comparer.Compare, meta.Largest, end) < 0 {
-				meta.Largest = end.Clone()
+				base.InternalCompare(opts.Comparer.Compare, meta.largest, end) < 0 {
+				meta.largest = end.Clone()
 			}
 		}
 	}
@@ -108,11 +108,11 @@ func ingestSortAndVerify(cmp Compare, meta []*fileMetadata) error {
 	}
 
 	sort.Slice(meta, func(i, j int) bool {
-		return cmp(meta[i].Smallest.UserKey, meta[j].Smallest.UserKey) < 0
+		return cmp(meta[i].smallest.UserKey, meta[j].smallest.UserKey) < 0
 	})
 
 	for i := 1; i < len(meta); i++ {
-		if sstableKeyCompare(cmp, meta[i-1].Largest, meta[i].Smallest) >= 0 {
+		if sstableKeyCompare(cmp, meta[i-1].largest, meta[i].smallest) >= 0 {
 			return fmt.Errorf("files have overlapping ranges")
 		}
 	}
@@ -122,7 +122,7 @@ func ingestSortAndVerify(cmp Compare, meta []*fileMetadata) error {
 func ingestCleanup(fs vfs.FS, dirname string, meta []*fileMetadata) error {
 	var firstErr error
 	for i := range meta {
-		target := base.MakeFilename(dirname, fileTypeTable, meta[i].FileNum)
+		target := base.MakeFilename(dirname, fileTypeTable, meta[i].fileNum)
 		if err := fs.Remove(target); err != nil {
 			if firstErr != nil {
 				firstErr = err
@@ -134,7 +134,7 @@ func ingestCleanup(fs vfs.FS, dirname string, meta []*fileMetadata) error {
 
 func ingestLink(opts *Options, dirname string, paths []string, meta []*fileMetadata) error {
 	for i := range paths {
-		target := base.MakeFilename(dirname, fileTypeTable, meta[i].FileNum)
+		target := base.MakeFilename(dirname, fileTypeTable, meta[i].fileNum)
 		err := opts.FS.Link(paths[i], target)
 		if err != nil {
 			if err2 := ingestCleanup(opts.FS, dirname, meta[:i]); err2 != nil {
@@ -154,11 +154,11 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo
 		defer iter.Close()
 
 		for _, m := range meta {
-			key, _ := iter.SeekGE(m.Smallest.UserKey)
+			key, _ := iter.SeekGE(m.smallest.UserKey)
 			if key == nil {
 				continue
 			}
-			if cmp(key.UserKey, m.Largest.UserKey) <= 0 {
+			if cmp(key.UserKey, m.largest.UserKey) <= 0 {
 				return true
 			}
 		}
@@ -168,17 +168,17 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo
 	if iter := mem.newRangeDelIter(nil); iter != nil {
 		defer iter.Close()
 		for _, m := range meta {
-			key, val := iter.SeekLT(m.Smallest.UserKey)
+			key, val := iter.SeekLT(m.smallest.UserKey)
 			if key == nil {
 				key, val = iter.Next()
 			}
 			for ; key != nil; key, val = iter.Next() {
-				if cmp(key.UserKey, m.Largest.UserKey) > 0 {
+				if cmp(key.UserKey, m.largest.UserKey) > 0 {
 					// The start of the tombstone is after the largest key in the
 					// ingested table.
 					break
 				}
-				if cmp(val, m.Smallest.UserKey) > 0 {
+				if cmp(val, m.smallest.UserKey) > 0 {
 					// The end of the tombstone is greater than the smallest in the
 					// table. Note that the tombstone end key is exclusive, thus ">0"
 					// instead of ">=0".
@@ -193,12 +193,12 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo
 
 func ingestUpdateSeqNum(opts *Options, dirname string, seqNum uint64, meta []*fileMetadata) error {
 	for _, m := range meta {
-		m.Smallest = base.MakeInternalKey(m.Smallest.UserKey, seqNum, m.Smallest.Kind())
-		m.Largest = base.MakeInternalKey(m.Largest.UserKey, seqNum, m.Largest.Kind())
+		m.smallest = base.MakeInternalKey(m.smallest.UserKey, seqNum, m.smallest.Kind())
+		m.largest = base.MakeInternalKey(m.largest.UserKey, seqNum, m.largest.Kind())
 		// Setting smallestSeqNum == largestSeqNum triggers the setting of
 		// Properties.GlobalSeqNum when an sstable is loaded.
-		m.SmallestSeqNum = seqNum
-		m.LargestSeqNum = seqNum
+		m.smallestSeqNum = seqNum
+		m.largestSeqNum = seqNum
 		seqNum++
 
 		// TODO(peter): Update the global sequence number property. This is only
@@ -209,13 +209,13 @@ func ingestUpdateSeqNum(opts *Options, dirname string, seqNum uint64, meta []*fi
 
 func ingestTargetLevel(cmp Compare, v *version, meta *fileMetadata) int {
 	// Find the lowest level which does not have any files which overlap meta.
-	if len(v.Overlaps(0, cmp, meta.Smallest.UserKey, meta.Largest.UserKey)) != 0 {
+	if len(v.overlaps(0, cmp, meta.smallest.UserKey, meta.largest.UserKey)) != 0 {
 		return 0
 	}
 
 	level := 1
 	for ; level < numLevels; level++ {
-		if len(v.Overlaps(level, cmp, meta.Smallest.UserKey, meta.Largest.UserKey)) != 0 {
+		if len(v.overlaps(level, cmp, meta.smallest.UserKey, meta.largest.UserKey)) != 0 {
 			break
 		}
 	}
@@ -265,7 +265,7 @@ func (d *DB) Ingest(paths []string) error {
 	d.mu.Lock()
 	pendingOutputs := make([]uint64, len(paths))
 	for i := range paths {
-		pendingOutputs[i] = d.mu.versions.getNextFileNum()
+		pendingOutputs[i] = d.mu.versions.nextFileNum()
 	}
 	for _, fileNum := range pendingOutputs {
 		d.mu.compact.pendingOutputs[fileNum] = struct{}{}
@@ -369,18 +369,18 @@ func (d *DB) Ingest(paths []string) error {
 	if d.opts.EventListener.TableIngested != nil {
 		info := TableIngestInfo{
 			JobID:        jobID,
-			GlobalSeqNum: meta[0].SmallestSeqNum,
+			GlobalSeqNum: meta[0].smallestSeqNum,
 			Err:          err,
 		}
 		if ve != nil {
 			info.Tables = make([]struct {
 				TableInfo
 				Level int
-			}, len(ve.NewFiles))
-			for i := range ve.NewFiles {
-				e := &ve.NewFiles[i]
-				info.Tables[i].Level = e.Level
-				info.Tables[i].TableInfo = e.Meta.TableInfo(d.dirname)
+			}, len(ve.newFiles))
+			for i := range ve.newFiles {
+				e := &ve.newFiles[i]
+				info.Tables[i].Level = e.level
+				info.Tables[i].TableInfo = e.meta.tableInfo(d.dirname)
 			}
 		}
 		d.opts.EventListener.TableIngested(info)
@@ -394,25 +394,25 @@ func (d *DB) ingestApply(jobID int, meta []*fileMetadata) (*versionEdit, error)
 	defer d.mu.Unlock()
 
 	ve := &versionEdit{
-		NewFiles: make([]newFileEntry, len(meta)),
+		newFiles: make([]newFileEntry, len(meta)),
+		metrics:  make(map[int]*LevelMetrics),
 	}
-	metrics := make(map[int]*LevelMetrics)
 	current := d.mu.versions.currentVersion()
 	for i := range meta {
 		// Determine the lowest level in the LSM for which the sstable doesn't
 		// overlap any existing files in the level.
 		m := meta[i]
-		f := &ve.NewFiles[i]
-		f.Level = ingestTargetLevel(d.cmp, current, m)
-		f.Meta = *m
-		levelMetrics := metrics[f.Level]
-		if levelMetrics == nil {
-			levelMetrics = &LevelMetrics{}
-			metrics[f.Level] = levelMetrics
+		f := &ve.newFiles[i]
+		f.level = ingestTargetLevel(d.cmp, current, m)
+		f.meta = *m
+		metrics := ve.metrics[f.level]
+		if metrics == nil {
+			metrics = &LevelMetrics{}
+			ve.metrics[f.level] = metrics
 		}
-		levelMetrics.BytesIngested += m.Size
+		metrics.BytesIngested += m.size
 	}
-	if err := d.mu.versions.logAndApply(jobID, ve, metrics, d.dataDir); err != nil {
+	if err := d.mu.versions.logAndApply(jobID, ve, d.dataDir); err != nil {
 		return nil, err
 	}
 	d.updateReadStateLocked()
diff --git a/level_iter.go b/level_iter.go
index a1fdad9..0b66e64 100644
--- a/level_iter.go
+++ b/level_iter.go
@@ -108,7 +108,7 @@ func (l *levelIter) findFileGE(key []byte) int {
 	//
 	// TODO(peter): inline the binary search.
 	return sort.Search(len(l.files), func(i int) bool {
-		largest := &l.files[i].Largest
+		largest := &l.files[i].largest
 		c := l.cmp(largest.UserKey, key)
 		if c > 0 {
 			return true
@@ -120,7 +120,7 @@ func (l *levelIter) findFileGE(key []byte) int {
 func (l *levelIter) findFileLT(key []byte) int {
 	// Find the last file whose smallest key is < ikey.
 	index := sort.Search(len(l.files), func(i int) bool {
-		return l.cmp(l.files[i].Smallest.UserKey, key) >= 0
+		return l.cmp(l.files[i].smallest.UserKey, key) >= 0
 	})
 	return index - 1
 }
@@ -150,14 +150,14 @@ func (l *levelIter) loadFile(index, dir int) bool {
 		f := &l.files[l.index]
 		l.tableOpts.LowerBound = l.opts.GetLowerBound()
 		if l.tableOpts.LowerBound != nil {
-			if l.cmp(f.Largest.UserKey, l.tableOpts.LowerBound) < 0 {
+			if l.cmp(f.largest.UserKey, l.tableOpts.LowerBound) < 0 {
 				// The largest key in the sstable is smaller than the lower bound.
 				if dir < 0 {
 					return false
 				}
 				continue
 			}
-			if l.cmp(l.tableOpts.LowerBound, f.Smallest.UserKey) < 0 {
+			if l.cmp(l.tableOpts.LowerBound, f.smallest.UserKey) < 0 {
 				// The lower bound is smaller than the smallest key in the
 				// table. Iteration within the table does not need to check the lower
 				// bound.
@@ -166,7 +166,7 @@ func (l *levelIter) loadFile(index, dir int) bool {
 		}
 		l.tableOpts.UpperBound = l.opts.GetUpperBound()
 		if l.tableOpts.UpperBound != nil {
-			if l.cmp(f.Smallest.UserKey, l.tableOpts.UpperBound) >= 0 {
+			if l.cmp(f.smallest.UserKey, l.tableOpts.UpperBound) >= 0 {
 				// The smallest key in the sstable is greater than or equal to the
 				// lower bound.
 				if dir > 0 {
@@ -174,7 +174,7 @@ func (l *levelIter) loadFile(index, dir int) bool {
 				}
 				continue
 			}
-			if l.cmp(l.tableOpts.UpperBound, f.Largest.UserKey) > 0 {
+			if l.cmp(l.tableOpts.UpperBound, f.largest.UserKey) > 0 {
 				// The upper bound is greater than the largest key in the
 				// table. Iteration within the table does not need to check the upper
 				// bound.
@@ -191,7 +191,7 @@ func (l *levelIter) loadFile(index, dir int) bool {
 			*l.rangeDelIter = rangeDelIter
 		}
 		if l.largestUserKey != nil {
-			*l.largestUserKey = f.Largest.UserKey
+			*l.largestUserKey = f.largest.UserKey
 		}
 		return true
 	}
@@ -334,8 +334,8 @@ func (l *levelIter) skipEmptyFileForward() (*InternalKey, []byte) {
 			// We're being used as part of an Iterator and we've reached the end of
 			// the sstable. If the boundary is a range deletion tombstone, return
 			// that key.
-			if f := &l.files[l.index]; f.Largest.Kind() == InternalKeyKindRangeDelete {
-				l.boundary = &f.Largest
+			if f := &l.files[l.index]; f.largest.Kind() == InternalKeyKindRangeDelete {
+				l.boundary = &f.largest
 				return l.boundary, nil
 			}
 			*l.rangeDelIter = nil
@@ -362,8 +362,8 @@ func (l *levelIter) skipEmptyFileBackward() (*InternalKey, []byte) {
 			// We're being used as part of an Iterator and we've reached the end of
 			// the sstable. If the boundary is a range deletion tombstone, return
 			// that key.
-			if f := &l.files[l.index]; f.Smallest.Kind() == InternalKeyKindRangeDelete {
-				l.boundary = &f.Smallest
+			if f := &l.files[l.index]; f.smallest.Kind() == InternalKeyKindRangeDelete {
+				l.boundary = &f.smallest
 				return l.boundary, nil
 			}
 			*l.rangeDelIter = nil
diff --git a/mem_table.go b/mem_table.go
index 5cdf439..aac52df 100644
--- a/mem_table.go
+++ b/mem_table.go
@@ -5,7 +5,6 @@
 package pebble
 
 import (
-	"fmt"
 	"sync"
 	"sync/atomic"
 	"unsafe"
@@ -165,8 +164,7 @@ func (m *memTable) apply(batch *Batch, seqNum uint64) error {
 		}
 	}
 	if seqNum != startSeqNum+uint64(batch.Count()) {
-		panic(fmt.Sprintf("pebble: inconsistent batch count: %d vs %d",
-			seqNum, startSeqNum+uint64(batch.Count())))
+		panic("pebble: inconsistent batch count")
 	}
 	if tombstoneCount != 0 {
 		m.tombstones.invalidate(tombstoneCount)
diff --git a/metrics.go b/metrics.go
index c4b971d..2b27992 100644
--- a/metrics.go
+++ b/metrics.go
@@ -53,8 +53,6 @@ func (m *LevelMetrics) WriteAmp() float64 {
 	return float64(m.BytesWritten) / float64(m.BytesIn)
 }
 
-// format generates a string of the receiver's metrics, formatting it into the
-// supplied buffer.
 func (m *LevelMetrics) format(buf *bytes.Buffer) {
 	fmt.Fprintf(buf, "%6d %7s %7.2f %7s %7s %7s %7s %7s %7.1f\n",
 		m.NumFiles,
diff --git a/open.go b/open.go
index 7f1db8b..5590e7c 100644
--- a/open.go
+++ b/open.go
@@ -37,8 +37,8 @@ func allocDBNum() uint64 {
 func createDB(dirname string, opts *Options) (retErr error) {
 	const manifestFileNum = 1
 	ve := versionEdit{
-		ComparerName: opts.Comparer.Name,
-		NextFileNum:  manifestFileNum + 1,
+		comparatorName: opts.Comparer.Name,
+		nextFileNumber: manifestFileNum + 1,
 	}
 	manifestFilename := base.MakeFilename(dirname, fileTypeManifest, manifestFileNum)
 	f, err := opts.FS.Create(manifestFilename)
@@ -57,7 +57,7 @@ func createDB(dirname string, opts *Options) (retErr error) {
 	if err != nil {
 		return err
 	}
-	err = ve.Encode(w)
+	err = ve.encode(w)
 	if err != nil {
 		return err
 	}
@@ -197,7 +197,7 @@ func Open(dirname string, opts *Options) (*DB, error) {
 		}
 		switch ft {
 		case fileTypeLog:
-			if fn >= d.mu.versions.logNum || fn == d.mu.versions.prevLogNum {
+			if fn >= d.mu.versions.logNumber || fn == d.mu.versions.prevLogNumber {
 				logFiles = append(logFiles, fileNumAndName{fn, filename})
 			}
 		case fileTypeOptions:
@@ -228,9 +228,9 @@ func Open(dirname string, opts *Options) (*DB, error) {
 
 	if !d.opts.ReadOnly {
 		// Create an empty .log file.
-		ve.LogNum = d.mu.versions.getNextFileNum()
-		d.mu.log.queue = append(d.mu.log.queue, ve.LogNum)
-		logFile, err := opts.FS.Create(base.MakeFilename(d.walDirname, fileTypeLog, ve.LogNum))
+		ve.logNumber = d.mu.versions.nextFileNum()
+		d.mu.log.queue = append(d.mu.log.queue, ve.logNumber)
+		logFile, err := opts.FS.Create(base.MakeFilename(d.walDirname, fileTypeLog, ve.logNumber))
 		if err != nil {
 			return nil, err
 		}
@@ -241,11 +241,11 @@ func Open(dirname string, opts *Options) (*DB, error) {
 			BytesPerSync:    d.opts.BytesPerSync,
 			PreallocateSize: d.walPreallocateSize(),
 		})
-		d.mu.log.LogWriter = record.NewLogWriter(logFile, ve.LogNum)
+		d.mu.log.LogWriter = record.NewLogWriter(logFile, ve.logNumber)
 		d.mu.versions.metrics.WAL.Files++
 
 		// Write a new manifest to disk.
-		if err := d.mu.versions.logAndApply(0, &ve, nil, d.dataDir); err != nil {
+		if err := d.mu.versions.logAndApply(0, &ve, d.dataDir); err != nil {
 			return nil, err
 		}
 	}
@@ -253,7 +253,7 @@ func Open(dirname string, opts *Options) (*DB, error) {
 
 	if !d.opts.ReadOnly {
 		// Write the current options to disk.
-		d.optionsFileNum = d.mu.versions.getNextFileNum()
+		d.optionsFileNum = d.mu.versions.nextFileNum()
 		optionsFile, err := opts.FS.Create(base.MakeFilename(dirname, fileTypeOptions, d.optionsFileNum))
 		if err != nil {
 			return nil, err
@@ -267,10 +267,8 @@ func Open(dirname string, opts *Options) (*DB, error) {
 		}
 	}
 
-	if !d.opts.ReadOnly {
-		d.scanObsoleteFiles(ls)
-		d.deleteObsoleteFiles(jobID)
-	}
+	d.scanObsoleteFiles(ls)
+	d.deleteObsoleteFiles(jobID)
 	d.maybeScheduleFlush()
 	d.maybeScheduleCompaction()
 
@@ -331,7 +329,8 @@ func (d *DB) replayWAL(
 		// TODO(peter): If the batch is too large to fit in the memtable, flush the
 		// existing memtable and write the batch as a separate L0 table.
 		b = Batch{}
-		b.SetRepr(buf.Bytes())
+		b.storage.data = buf.Bytes()
+		b.refreshMemTableSize()
 		seqNum := b.SeqNum()
 		maxSeqNum = seqNum + uint64(b.Count())
 
@@ -359,21 +358,14 @@ func (d *DB) replayWAL(
 		buf.Reset()
 	}
 
-	if d.opts.ReadOnly {
-		// In read-only mode, each WAL file is replayed into its own memtable. This
-		// is done so that the WAL metrics can be accurately provided.
-		mem.logSize = uint64(rr.Offset())
-		d.mu.mem.mutable = newMemTable(d.opts)
-		d.mu.mem.queue = append(d.mu.mem.queue, d.mu.mem.mutable)
-		d.mu.versions.metrics.WAL.Files++
-	} else if mem != nil && !mem.empty() {
+	if mem != nil && !mem.empty() && !d.opts.ReadOnly {
 		c := newFlush(d.opts, d.mu.versions.currentVersion(),
 			1 /* base level */, []flushable{mem}, &d.bytesFlushed)
 		newVE, pendingOutputs, err := d.runCompaction(jobID, c, nilPacer)
 		if err != nil {
 			return 0, err
 		}
-		ve.NewFiles = append(ve.NewFiles, newVE.NewFiles...)
+		ve.newFiles = append(ve.newFiles, newVE.newFiles...)
 		// Strictly speaking, it's too early to delete from d.pendingOutputs, but
 		// we are replaying the log file, which happens before Open returns, so
 		// there is no possibility of deleteObsoleteFiles being called concurrently
diff --git a/read_state.go b/read_state.go
index 0d80fc5..e507a3b 100644
--- a/read_state.go
+++ b/read_state.go
@@ -37,7 +37,7 @@ func (s *readState) ref() {
 // is held by the caller.
 func (s *readState) unref() {
 	if atomic.AddInt32(&s.refcnt, -1) == 0 {
-		s.current.Unref()
+		s.current.unref()
 	}
 }
 
@@ -47,7 +47,7 @@ func (s *readState) unref() {
 // unref() if DB.mu is NOT held by the caller.
 func (s *readState) unrefLocked() {
 	if atomic.AddInt32(&s.refcnt, -1) == 0 {
-		s.current.UnrefLocked()
+		s.current.unrefLocked()
 	}
 }
 
@@ -69,7 +69,7 @@ func (d *DB) updateReadStateLocked() {
 		current:   d.mu.versions.currentVersion(),
 		memtables: d.mu.mem.queue,
 	}
-	s.current.Ref()
+	s.current.ref()
 
 	d.readState.Lock()
 	old := d.readState.val
diff --git a/table_cache.go b/table_cache.go
index 5a163ea..9bd4f1b 100644
--- a/table_cache.go
+++ b/table_cache.go
@@ -42,7 +42,7 @@ func (c *tableCache) getShard(fileNum uint64) *tableCacheShard {
 func (c *tableCache) newIters(
 	meta *fileMetadata, opts *IterOptions, bytesIterated *uint64,
 ) (internalIterator, internalIterator, error) {
-	return c.getShard(meta.FileNum).newIters(meta, opts, bytesIterated)
+	return c.getShard(meta.fileNum).newIters(meta, opts, bytesIterated)
 }
 
 func (c *tableCache) evict(fileNum uint64) {
@@ -152,7 +152,7 @@ func (c *tableCacheShard) newIters(
 //
 // c.mu must be held when calling this.
 func (c *tableCacheShard) releaseNode(n *tableCacheNode) {
-	delete(c.mu.nodes, n.meta.FileNum)
+	delete(c.mu.nodes, n.meta.fileNum)
 	n.next.prev = n.prev
 	n.prev.next = n.next
 	n.prev = nil
@@ -180,7 +180,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata) *tableCacheNode {
 	// Fast-path for a hit in the cache. We grab the lock in shared mode, and use
 	// a batching mechanism to perform updates to the LRU list.
 	c.mu.RLock()
-	if n := c.mu.nodes[meta.FileNum]; n != nil {
+	if n := c.mu.nodes[meta.fileNum]; n != nil {
 		// The caller is responsible for decrementing the refCount.
 		atomic.AddInt32(&n.refCount, 1)
 		c.mu.RUnlock()
@@ -209,7 +209,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata) *tableCacheNode {
 		c.hitsPool.Put(hits)
 	}
 
-	n := c.mu.nodes[meta.FileNum]
+	n := c.mu.nodes[meta.fileNum]
 	if n == nil {
 		n = &tableCacheNode{
 			// Cache the closure invoked when an iterator is closed. This avoids an
@@ -228,7 +228,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata) *tableCacheNode {
 			refCount: 1,
 			loaded:   make(chan struct{}),
 		}
-		c.mu.nodes[meta.FileNum] = n
+		c.mu.nodes[meta.fileNum] = n
 		if len(c.mu.nodes) > c.size {
 			// Release the tail node.
 			c.releaseNode(c.mu.lru.prev)
@@ -328,16 +328,16 @@ type tableCacheNode struct {
 
 func (n *tableCacheNode) load(c *tableCacheShard) {
 	// Try opening the fileTypeTable first.
-	f, err := c.fs.Open(base.MakeFilename(c.dirname, fileTypeTable, n.meta.FileNum),
+	f, err := c.fs.Open(base.MakeFilename(c.dirname, fileTypeTable, n.meta.fileNum),
 		vfs.RandomReadsOption)
 	if err != nil {
 		n.err = err
 		close(n.loaded)
 		return
 	}
-	n.reader, n.err = sstable.NewReader(f, c.dbNum, n.meta.FileNum, c.opts)
-	if n.meta.SmallestSeqNum == n.meta.LargestSeqNum {
-		n.reader.Properties.GlobalSeqNum = n.meta.LargestSeqNum
+	n.reader, n.err = sstable.NewReader(f, c.dbNum, n.meta.fileNum, c.opts)
+	if n.meta.smallestSeqNum == n.meta.largestSeqNum {
+		n.reader.Properties.GlobalSeqNum = n.meta.largestSeqNum
 	}
 	close(n.loaded)
 }
diff --git a/tool/db.go b/tool/db.go
index 6304dd3..b9963b3 100644
--- a/tool/db.go
+++ b/tool/db.go
@@ -7,9 +7,7 @@ package tool
 import (
 	"fmt"
 
-	"github.com/petermattis/pebble"
 	"github.com/petermattis/pebble/internal/base"
-	"github.com/petermattis/pebble/sstable"
 	"github.com/spf13/cobra"
 )
 
@@ -20,31 +18,10 @@ type dbT struct {
 	Check *cobra.Command
 	LSM   *cobra.Command
 	Scan  *cobra.Command
-
-	// Configuration.
-	opts      *sstable.Options
-	comparers sstable.Comparers
-	mergers   sstable.Mergers
-
-	// Flags.
-	comparerName string
-	mergerName   string
-	fmtKey       formatter
-	fmtValue     formatter
-	start        key
-	end          key
 }
 
-func newDB(
-	opts *base.Options, comparers sstable.Comparers, mergers sstable.Mergers,
-) *dbT {
-	d := &dbT{
-		opts:      opts,
-		comparers: comparers,
-		mergers:   mergers,
-	}
-	d.fmtKey.mustSet("quoted")
-	d.fmtValue.mustSet("[%x]")
+func newDB(opts *base.Options) *dbT {
+	d := &dbT{}
 
 	d.Root = &cobra.Command{
 		Use:   "db",
@@ -82,112 +59,20 @@ by another process.
 	}
 
 	d.Root.AddCommand(d.Check, d.LSM, d.Scan)
-
-	for _, cmd := range []*cobra.Command{d.Check, d.LSM, d.Scan} {
-		cmd.Flags().StringVar(
-			&d.comparerName, "comparer", "", "comparer name (use default if empty)")
-		cmd.Flags().StringVar(
-			&d.mergerName, "merger", "", "merger name (use default if empty)")
-	}
-
-	d.Scan.Flags().Var(
-		&d.fmtKey, "key", "key formatter")
-	d.Scan.Flags().Var(
-		&d.fmtValue, "value", "value formatter")
-	d.Scan.Flags().Var(
-		&d.start, "start", "start key for the scan")
-	d.Scan.Flags().Var(
-		&d.end, "end", "end key for the scan")
 	return d
 }
 
-func (d *dbT) openDB(dir string) (*pebble.DB, error) {
-	if d.comparerName != "" {
-		d.opts.Comparer = d.comparers[d.comparerName]
-		if d.opts.Comparer == nil {
-			return nil, fmt.Errorf("unknown comparer %q", d.comparerName)
-		}
-	}
-	if d.mergerName != "" {
-		d.opts.Merger = d.mergers[d.mergerName]
-		if d.opts.Merger == nil {
-			return nil, fmt.Errorf("unknown merger %q", d.mergerName)
-		}
-	}
-	return pebble.Open(dir, d.opts)
-}
-
 func (d *dbT) runCheck(cmd *cobra.Command, args []string) {
-	// The check command is equivalent to scanning over all of the records, but
-	// not outputting anything.
-	d.fmtKey.Set("null")
-	d.fmtValue.Set("null")
-	d.start, d.end = nil, nil
-	d.runScan(cmd, args)
+	fmt.Fprintf(stderr, "TODO(peter): \"db check\" unimplemented\n")
+	osExit(1)
 }
 
 func (d *dbT) runLSM(cmd *cobra.Command, args []string) {
-	db, err := d.openDB(args[0])
-	if err != nil {
-		fmt.Fprintf(stdout, "%s\n", err)
-		return
-	}
-
-	fmt.Fprintf(stdout, "%s", db.Metrics())
-
-	if err := db.Close(); err != nil {
-		fmt.Fprintf(stdout, "%s\n", err)
-	}
+	fmt.Fprintf(stderr, "TODO(peter): \"db lsm\" unimplemented\n")
+	osExit(1)
 }
 
 func (d *dbT) runScan(cmd *cobra.Command, args []string) {
-	db, err := d.openDB(args[0])
-	if err != nil {
-		fmt.Fprintf(stdout, "%s\n", err)
-		return
-	}
-
-	start := timeNow()
-	fmtKeys := d.fmtKey.spec != "null"
-	fmtValues := d.fmtValue.spec != "null"
-	var count int64
-
-	iter := db.NewIter(&pebble.IterOptions{
-		UpperBound: d.end,
-	})
-	for valid := iter.SeekGE(d.start); valid; valid = iter.Next() {
-		if fmtKeys || fmtValues {
-			needDelimiter := false
-			if fmtKeys {
-				d.fmtKey.fn(stdout, iter.Key())
-				needDelimiter = true
-			}
-			if fmtValues {
-				if needDelimiter {
-					stdout.Write([]byte{' '})
-				}
-				d.fmtValue.fn(stdout, iter.Value())
-			}
-			stdout.Write([]byte{'\n'})
-		}
-
-		count++
-	}
-
-	if err := iter.Close(); err != nil {
-		fmt.Fprintf(stdout, "%s\n", err)
-	}
-
-	elapsed := timeNow().Sub(start)
-
-	plural := ""
-	if count != 1 {
-		plural = "s"
-	}
-	fmt.Fprintf(stdout, "scanned %d record%s in %0.1fs\n",
-		count, plural, elapsed.Seconds())
-
-	if err := db.Close(); err != nil {
-		fmt.Fprintf(stdout, "%s\n", err)
-	}
+	fmt.Fprintf(stderr, "TODO(peter): \"db scan\" unimplemented\n")
+	osExit(1)
 }
diff --git a/tool/manifest.go b/tool/manifest.go
index 17df667..7a1ea71 100644
--- a/tool/manifest.go
+++ b/tool/manifest.go
@@ -8,10 +8,6 @@ import (
 	"fmt"
 
 	"github.com/petermattis/pebble/internal/base"
-	"github.com/petermattis/pebble/internal/manifest"
-	"github.com/petermattis/pebble/internal/record"
-	"github.com/petermattis/pebble/sstable"
-	"github.com/petermattis/pebble/vfs"
 	"github.com/spf13/cobra"
 )
 
@@ -20,18 +16,10 @@ import (
 type manifestT struct {
 	Root *cobra.Command
 	Dump *cobra.Command
-
-	opts      *sstable.Options
-	comparers sstable.Comparers
-	fmtKey    formatter
 }
 
-func newManifest(opts *base.Options, comparers sstable.Comparers) *manifestT {
-	m := &manifestT{
-		opts:      opts,
-		comparers: comparers,
-	}
-	m.fmtKey.mustSet("quoted")
+func newManifest(opts *base.Options) *manifestT {
+	m := &manifestT{}
 
 	m.Root = &cobra.Command{
 		Use:   "manifest",
@@ -48,101 +36,10 @@ Print the contents of the MANIFEST files.
 	}
 
 	m.Root.AddCommand(m.Dump)
-
-	m.Dump.Flags().Var(
-		&m.fmtKey, "key", "key formatter")
 	return m
 }
 
 func (m *manifestT) runDump(cmd *cobra.Command, args []string) {
-	for _, arg := range args {
-		func() {
-			f, err := vfs.Default.Open(arg)
-			if err != nil {
-				fmt.Fprintf(stderr, "%s\n", err)
-				return
-			}
-			defer f.Close()
-
-			fmt.Fprintf(stdout, "%s\n", arg)
-
-			var bve manifest.BulkVersionEdit
-			var cmp *base.Comparer
-			rr := record.NewReader(f, 0 /* logNum */)
-			for {
-				offset := rr.Offset()
-				r, err := rr.Next()
-				if err != nil {
-					fmt.Fprintf(stdout, "%s\n", err)
-					break
-				}
-
-				var ve manifest.VersionEdit
-				err = ve.Decode(r)
-				if err != nil {
-					fmt.Fprintf(stdout, "%s\n", err)
-					break
-				}
-				bve.Accumulate(&ve)
-
-				empty := true
-				fmt.Fprintf(stdout, "%d\n", offset)
-				if ve.ComparerName != "" {
-					empty = false
-					fmt.Fprintf(stdout, "  comparer:     %s", ve.ComparerName)
-					cmp = m.comparers[ve.ComparerName]
-					if cmp == nil {
-						fmt.Fprintf(stdout, " (unknown)")
-					}
-					fmt.Fprintf(stdout, "\n")
-				}
-				if ve.LogNum != 0 {
-					empty = false
-					fmt.Fprintf(stdout, "  log-num:      %d\n", ve.LogNum)
-				}
-				if ve.PrevLogNum != 0 {
-					empty = false
-					fmt.Fprintf(stdout, "  prev-log-num: %d\n", ve.PrevLogNum)
-				}
-				if ve.LastSeqNum != 0 {
-					empty = false
-					fmt.Fprintf(stdout, "  last-seq-num: %d\n", ve.LastSeqNum)
-				}
-				for df := range ve.DeletedFiles {
-					empty = false
-					fmt.Fprintf(stdout, "  deleted:      L%d %d\n", df.Level, df.FileNum)
-				}
-				for _, nf := range ve.NewFiles {
-					empty = false
-					fmt.Fprintf(stdout, "  added:        L%d %d:%d",
-						nf.Level, nf.Meta.FileNum, nf.Meta.Size)
-					formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest)
-					fmt.Fprintf(stdout, "\n")
-				}
-				if empty {
-					// NB: An empty version edit can happen if we log a version edit with
-					// a zero field. RocksDB does this with a version edit that contains
-					// `LogNum == 0`.
-					fmt.Fprintf(stdout, "  <empty>\n")
-				}
-			}
-
-			if cmp != nil {
-				v, err := bve.Apply(m.opts, nil, cmp.Compare)
-				if err != nil {
-					fmt.Fprintf(stdout, "%s\n", err)
-					return
-				}
-				for level := range v.Files {
-					fmt.Fprintf(stdout, "--- L%d ---\n", level)
-					for j := range v.Files[level] {
-						f := &v.Files[level][j]
-						fmt.Fprintf(stdout, "  %d:%d", f.FileNum, f.Size)
-						formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest)
-						fmt.Fprintf(stdout, "\n")
-					}
-				}
-			}
-		}()
-	}
+	fmt.Fprintf(stderr, "TODO(peter): \"manifest dump\" unimplemented\n")
+	osExit(1)
 }
diff --git a/tool/tool.go b/tool/tool.go
index cb7c03d..b9ee812 100644
--- a/tool/tool.go
+++ b/tool/tool.go
@@ -34,21 +34,16 @@ type T struct {
 
 // New creates a new introspection tool.
 func New() *T {
-	t := &T{
-		opts: base.Options{
-			Filters:  make(map[string]FilterPolicy),
-			ReadOnly: true,
-		},
-		comparers: make(sstable.Comparers),
-		mergers:   make(sstable.Mergers),
-	}
-
+	t := &T{}
+	t.opts.Filters = make(map[string]FilterPolicy)
+	t.comparers = make(sstable.Comparers)
+	t.mergers = make(sstable.Mergers)
 	t.RegisterComparer(base.DefaultComparer)
 	t.RegisterFilter(bloom.FilterPolicy(10))
 	t.RegisterMerger(base.DefaultMerger)
 
-	t.db = newDB(&t.opts, t.comparers, t.mergers)
-	t.manifest = newManifest(&t.opts, t.comparers)
+	t.db = newDB(&t.opts)
+	t.manifest = newManifest(&t.opts)
 	t.sstable = newSSTable(&t.opts, t.comparers, t.mergers)
 	t.wal = newWAL(&t.opts)
 	t.Commands = []*cobra.Command{
diff --git a/tool/util.go b/tool/util.go
index a48c1b7..b104731 100644
--- a/tool/util.go
+++ b/tool/util.go
@@ -11,7 +11,6 @@ import (
 	"os"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/petermattis/pebble/internal/base"
 )
@@ -19,7 +18,6 @@ import (
 var stdout = io.Writer(os.Stdout)
 var stderr = io.Writer(os.Stderr)
 var osExit = os.Exit
-var timeNow = time.Now
 
 type key []byte
 
@@ -102,30 +100,15 @@ func formatSize(w io.Writer, v []byte) {
 	fmt.Fprintf(w, "<%d>", len(v))
 }
 
-func formatKey(w io.Writer, fmtKey formatter, key *base.InternalKey) bool {
-	if fmtKey.spec == "null" {
-		return false
-	}
-	fmtKey.fn(w, key.UserKey)
-	fmt.Fprintf(w, "#%d,%d", key.SeqNum(), key.Kind())
-	return true
-}
-
-func formatKeyRange(w io.Writer, fmtKey formatter, start, end *base.InternalKey) {
-	if fmtKey.spec == "null" {
-		return
-	}
-	w.Write([]byte{'['})
-	formatKey(w, fmtKey, start)
-	w.Write([]byte{'-'})
-	formatKey(w, fmtKey, end)
-	w.Write([]byte{']'})
-}
-
 func formatKeyValue(
 	w io.Writer, fmtKey formatter, fmtValue formatter, key *base.InternalKey, value []byte,
 ) {
-	needDelimiter := formatKey(w, fmtKey, key)
+	needDelimiter := false
+	if fmtKey.spec != "null" {
+		fmtKey.fn(w, key.UserKey)
+		fmt.Fprintf(w, "#%d,%d", key.SeqNum(), key.Kind())
+		needDelimiter = true
+	}
 	if fmtValue.spec != "null" {
 		if needDelimiter {
 			w.Write([]byte{' '})
diff --git a/internal/manifest/version.go b/version.go
similarity index 50%
rename from internal/manifest/version.go
rename to version.go
index 56db33f..8561292 100644
--- a/internal/manifest/version.go
+++ b/version.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package manifest
+package pebble
 
 import (
 	"bytes"
@@ -14,119 +14,108 @@ import (
 	"github.com/petermattis/pebble/internal/base"
 )
 
-type Compare = base.Compare
-type InternalKey = base.InternalKey
-type Options = base.Options
-type TableInfo = base.TableInfo
-
-// FileMetadata holds the metadata for an on-disk table.
-type FileMetadata struct {
+// fileMetadata holds the metadata for an on-disk table.
+type fileMetadata struct {
 	// reference count for the file: incremented when a file is added to a
 	// version and decremented when the version is unreferenced. The file is
 	// obsolete when the reference count falls to zero. This is a pointer because
 	// fileMetadata is copied by value from version to version, but we want the
 	// reference count to be shared.
 	refs *int32
-	// FileNum is the file number.
-	FileNum uint64
-	// Size is the Size of the file, in bytes.
-	Size uint64
-	// Smallest and Largest are the inclusive bounds for the internal keys
+	// fileNum is the file number.
+	fileNum uint64
+	// size is the size of the file, in bytes.
+	size uint64
+	// smallest and largest are the inclusive bounds for the internal keys
 	// stored in the table.
-	Smallest InternalKey
-	Largest  InternalKey
-	// Smallest and largest sequence numbers in the table.
-	SmallestSeqNum uint64
-	LargestSeqNum  uint64
+	smallest InternalKey
+	largest  InternalKey
+	// smallest and largest sequence numbers in the table.
+	smallestSeqNum uint64
+	largestSeqNum  uint64
 	// true if client asked us nicely to compact this file.
-	MarkedForCompaction bool
+	markedForCompaction bool
 }
 
-func (m *FileMetadata) String() string {
-	return fmt.Sprintf("%d:%s-%s", m.FileNum, m.Smallest, m.Largest)
+func (m *fileMetadata) String() string {
+	return fmt.Sprintf("%d:%s-%s", m.fileNum, m.smallest, m.largest)
 }
 
-// TableInfo returns a subset of the FileMetadata state formatted as a
-// TableInfo.
-func (m *FileMetadata) TableInfo(dirname string) TableInfo {
+func (m *fileMetadata) tableInfo(dirname string) TableInfo {
 	return TableInfo{
-		Path:           base.MakeFilename(dirname, base.FileTypeTable, m.FileNum),
-		FileNum:        m.FileNum,
-		Size:           m.Size,
-		Smallest:       m.Smallest,
-		Largest:        m.Largest,
-		SmallestSeqNum: m.SmallestSeqNum,
-		LargestSeqNum:  m.LargestSeqNum,
+		Path:           base.MakeFilename(dirname, fileTypeTable, m.fileNum),
+		FileNum:        m.fileNum,
+		Size:           m.size,
+		Smallest:       m.smallest,
+		Largest:        m.largest,
+		SmallestSeqNum: m.smallestSeqNum,
+		LargestSeqNum:  m.largestSeqNum,
+	}
+}
+
+// totalSize returns the total size of all the files in f.
+func totalSize(f []fileMetadata) (size uint64) {
+	for _, x := range f {
+		size += x.size
 	}
+	return size
 }
 
-// KeyRange returns the minimum smallest and maximum largest internalKey for
+// ikeyRange returns the minimum smallest and maximum largest internalKey for
 // all the fileMetadata in f0 and f1.
-func KeyRange(ucmp Compare, f0, f1 []FileMetadata) (smallest, largest InternalKey) {
+func ikeyRange(ucmp Compare, f0, f1 []fileMetadata) (smallest, largest InternalKey) {
 	first := true
-	for _, f := range [2][]FileMetadata{f0, f1} {
+	for _, f := range [2][]fileMetadata{f0, f1} {
 		for _, meta := range f {
 			if first {
 				first = false
-				smallest, largest = meta.Smallest, meta.Largest
+				smallest, largest = meta.smallest, meta.largest
 				continue
 			}
-			if base.InternalCompare(ucmp, meta.Smallest, smallest) < 0 {
-				smallest = meta.Smallest
+			if base.InternalCompare(ucmp, meta.smallest, smallest) < 0 {
+				smallest = meta.smallest
 			}
-			if base.InternalCompare(ucmp, meta.Largest, largest) > 0 {
-				largest = meta.Largest
+			if base.InternalCompare(ucmp, meta.largest, largest) > 0 {
+				largest = meta.largest
 			}
 		}
 	}
 	return smallest, largest
 }
 
-type bySeqNum []FileMetadata
+type bySeqNum []fileMetadata
 
 func (b bySeqNum) Len() int { return len(b) }
 func (b bySeqNum) Less(i, j int) bool {
 	// NB: This is the same ordering that RocksDB uses for L0 files.
 
 	// Sort first by largest sequence number.
-	if b[i].LargestSeqNum != b[j].LargestSeqNum {
-		return b[i].LargestSeqNum < b[j].LargestSeqNum
+	if b[i].largestSeqNum != b[j].largestSeqNum {
+		return b[i].largestSeqNum < b[j].largestSeqNum
 	}
 	// Then by smallest sequence number.
-	if b[i].SmallestSeqNum != b[j].SmallestSeqNum {
-		return b[i].SmallestSeqNum < b[j].SmallestSeqNum
+	if b[i].smallestSeqNum != b[j].smallestSeqNum {
+		return b[i].smallestSeqNum < b[j].smallestSeqNum
 	}
 	// Break ties by file number.
-	return b[i].FileNum < b[j].FileNum
+	return b[i].fileNum < b[j].fileNum
 }
 func (b bySeqNum) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
 
-// SortBySeqNum sorts the specified files by decreasing sequence number.
-func SortBySeqNum(files []FileMetadata) {
-	sort.Sort(bySeqNum(files))
-}
-
 type bySmallest struct {
-	dat []FileMetadata
+	dat []fileMetadata
 	cmp Compare
 }
 
 func (b bySmallest) Len() int { return len(b.dat) }
 func (b bySmallest) Less(i, j int) bool {
-	return base.InternalCompare(b.cmp, b.dat[i].Smallest, b.dat[j].Smallest) < 0
+	return base.InternalCompare(b.cmp, b.dat[i].smallest, b.dat[j].smallest) < 0
 }
 func (b bySmallest) Swap(i, j int) { b.dat[i], b.dat[j] = b.dat[j], b.dat[i] }
 
-// SortBySmallest sorts the specified files by smallest key using the supplied
-// comparison function to order user keys.
-func SortBySmallest(files []FileMetadata, cmp Compare) {
-	sort.Sort(bySmallest{files, cmp})
-}
-
-// NumLevels is the number of levels a Version contains.
-const NumLevels = 7
+const numLevels = 7
 
-// Version is a collection of file metadata for on-disk tables at various
+// version is a collection of file metadata for on-disk tables at various
 // levels. In-memory DBs are written to level-0 tables, and compactions
 // migrate data from level N to level N+1. The tables map internal keys (which
 // are a user key, a delete or set bit, and a sequence number) to user values.
@@ -146,128 +135,106 @@ const NumLevels = 7
 // Finally, for every internal key in a table at level X, there is no internal
 // key in a higher level table that has both the same user key and a higher
 // sequence number.
-type Version struct {
+type version struct {
 	refs int32
 
-	Files [NumLevels][]FileMetadata
+	files [numLevels][]fileMetadata
 
-	// The callback to invoke when the last reference to a version is
-	// removed. Will be called with list.mu held.
-	Deleted func(obsolete []uint64)
+	// The version set this version is associated with.
+	vs *versionSet
 
 	// The list the version is linked into.
-	list *VersionList
+	list *versionList
 
 	// The next/prev link for the versionList doubly-linked list of versions.
-	prev, next *Version
+	prev, next *version
 }
 
-func (v *Version) String() string {
+func (v *version) String() string {
 	var buf bytes.Buffer
-	for level := 0; level < NumLevels; level++ {
-		if len(v.Files[level]) == 0 {
+	for level := 0; level < numLevels; level++ {
+		if len(v.files[level]) == 0 {
 			continue
 		}
 		fmt.Fprintf(&buf, "%d:", level)
-		for j := range v.Files[level] {
-			f := &v.Files[level][j]
-			fmt.Fprintf(&buf, " %s-%s", f.Smallest.UserKey, f.Largest.UserKey)
+		for j := range v.files[level] {
+			f := &v.files[level][j]
+			fmt.Fprintf(&buf, " %s-%s", f.smallest.UserKey, f.largest.UserKey)
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
 	return buf.String()
 }
 
-// DebugString returns an alternative format to String() which includes
-// sequence number and kind information for the sstable boundaries.
-func (v *Version) DebugString() string {
+func (v *version) DebugString() string {
 	var buf bytes.Buffer
-	for level := 0; level < NumLevels; level++ {
-		if len(v.Files[level]) == 0 {
+	for level := 0; level < numLevels; level++ {
+		if len(v.files[level]) == 0 {
 			continue
 		}
 		fmt.Fprintf(&buf, "%d:", level)
-		for j := range v.Files[level] {
-			f := &v.Files[level][j]
-			fmt.Fprintf(&buf, " %s-%s", f.Smallest, f.Largest)
+		for j := range v.files[level] {
+			f := &v.files[level][j]
+			fmt.Fprintf(&buf, " %s-%s", f.smallest, f.largest)
 		}
 		fmt.Fprintf(&buf, "\n")
 	}
 	return buf.String()
 }
 
-// Refs returns the number of references to the version.
-func (v *Version) Refs() int32 {
-	return atomic.LoadInt32(&v.refs)
-}
-
-// Ref increments the version refcount.
-func (v *Version) Ref() {
+func (v *version) ref() {
 	atomic.AddInt32(&v.refs, 1)
 }
 
-// Unref decrements the version refcount. If the last reference to the version
-// was removed, the version is removed from the list of versions and the
-// Deleted callback is invoked. Requires that the VersionList mutex is NOT
-// locked.
-func (v *Version) Unref() {
+func (v *version) unref() {
 	if atomic.AddInt32(&v.refs, -1) == 0 {
 		obsolete := v.unrefFiles()
 		l := v.list
 		l.mu.Lock()
-		l.Remove(v)
-		v.Deleted(obsolete)
+		l.remove(v)
+		v.vs.addObsoleteLocked(obsolete)
 		l.mu.Unlock()
 	}
 }
 
-// UnrefLocked decrements the version refcount. If the last reference to the
-// version was removed, the version is removed from the list of versions and
-// the Deleted callback is invoked. Requires that the VersionList mutex is
-// already locked.
-func (v *Version) UnrefLocked() {
+func (v *version) unrefLocked() {
 	if atomic.AddInt32(&v.refs, -1) == 0 {
-		v.list.Remove(v)
-		v.Deleted(v.unrefFiles())
+		v.list.remove(v)
+		v.vs.addObsoleteLocked(v.unrefFiles())
 	}
 }
 
-func (v *Version) unrefFiles() []uint64 {
+func (v *version) unrefFiles() []uint64 {
 	var obsolete []uint64
-	for _, files := range v.Files {
+	for _, files := range v.files {
 		for i := range files {
 			f := &files[i]
 			if atomic.AddInt32(f.refs, -1) == 0 {
-				obsolete = append(obsolete, f.FileNum)
+				obsolete = append(obsolete, f.fileNum)
 			}
 		}
 	}
 	return obsolete
 }
 
-// Next returns the next version in the list of versions.
-func (v *Version) Next() *Version {
-	return v.next
-}
-
-// Overlaps returns all elements of v.files[level] whose user key range
+// overlaps returns all elements of v.files[level] whose user key range
 // intersects the inclusive range [start, end]. If level is non-zero then the
 // user key ranges of v.files[level] are assumed to not overlap (although they
 // may touch). If level is zero then that assumption cannot be made, and the
 // [start, end] range is expanded to the union of those matching ranges so far
 // and the computation is repeated until [start, end] stabilizes.
-func (v *Version) Overlaps(
+func (v *version) overlaps(
 	level int, cmp Compare, start, end []byte,
-) (ret []FileMetadata) {
+) (ret []fileMetadata) {
 	if level == 0 {
 		// The sstables in level 0 can overlap with each other. As soon as we find
 		// one sstable that overlaps with our target range, we need to expand the
 		// range and find all sstables that overlap with the expanded range.
 	loop:
 		for {
-			for _, meta := range v.Files[level] {
-				smallest := meta.Smallest.UserKey
-				largest := meta.Largest.UserKey
+			for _, meta := range v.files[level] {
+				smallest := meta.smallest.UserKey
+				largest := meta.largest.UserKey
 				if cmp(largest, start) < 0 {
 					// meta is completely before the specified range; skip it.
 					continue
@@ -300,12 +267,12 @@ func (v *Version) Overlaps(
 
 	// Binary search to find the range of files which overlaps with our target
 	// range.
-	files := v.Files[level]
+	files := v.files[level]
 	lower := sort.Search(len(files), func(i int) bool {
-		return cmp(files[i].Largest.UserKey, start) >= 0
+		return cmp(files[i].largest.UserKey, start) >= 0
 	})
 	upper := sort.Search(len(files), func(i int) bool {
-		return cmp(files[i].Smallest.UserKey, end) > 0
+		return cmp(files[i].smallest.UserKey, end) > 0
 	})
 	if lower >= upper {
 		return nil
@@ -313,35 +280,35 @@ func (v *Version) Overlaps(
 	return files[lower:upper]
 }
 
-// CheckOrdering checks that the files are consistent with respect to
+// checkOrdering checks that the files are consistent with respect to
 // increasing file numbers (for level 0 files) and increasing and non-
 // overlapping internal key ranges (for level non-0 files).
-func (v *Version) CheckOrdering(cmp Compare) error {
-	for level, ff := range v.Files {
+func (v *version) checkOrdering(cmp Compare) error {
+	for level, ff := range v.files {
 		if level == 0 {
 			for i := 1; i < len(ff); i++ {
 				prev := &ff[i-1]
 				f := &ff[i]
-				if prev.LargestSeqNum >= f.LargestSeqNum {
+				if prev.largestSeqNum >= f.largestSeqNum {
 					return fmt.Errorf("level 0 files are not in increasing largest seqNum order: %d, %d",
-						prev.LargestSeqNum, f.LargestSeqNum)
+						prev.largestSeqNum, f.largestSeqNum)
 				}
-				if prev.SmallestSeqNum >= f.SmallestSeqNum {
+				if prev.smallestSeqNum >= f.smallestSeqNum {
 					return fmt.Errorf("level 0 files are not in increasing smallest seqNum order: %d, %d",
-						prev.SmallestSeqNum, f.SmallestSeqNum)
+						prev.smallestSeqNum, f.smallestSeqNum)
 				}
 			}
 		} else {
 			for i := 1; i < len(ff); i++ {
 				prev := &ff[i-1]
 				f := &ff[i]
-				if base.InternalCompare(cmp, prev.Largest, f.Smallest) >= 0 {
+				if base.InternalCompare(cmp, prev.largest, f.smallest) >= 0 {
 					return fmt.Errorf("level non-0 files are not in increasing ikey order: %s, %s\n%s",
-						prev.Largest, f.Smallest, v.DebugString())
+						prev.largest, f.smallest, v.DebugString())
 				}
-				if base.InternalCompare(cmp, f.Smallest, f.Largest) > 0 {
+				if base.InternalCompare(cmp, f.smallest, f.largest) > 0 {
 					return fmt.Errorf("level non-0 file has inconsistent bounds: %s, %s",
-						f.Smallest, f.Largest)
+						f.smallest, f.largest)
 				}
 			}
 		}
@@ -349,40 +316,29 @@ func (v *Version) CheckOrdering(cmp Compare) error {
 	return nil
 }
 
-// VersionList holds a list of versions. The versions are ordered from oldest
-// to newest.
-type VersionList struct {
+type versionList struct {
 	mu   *sync.Mutex
-	root Version
+	root version
 }
 
-// Init initializes the version list.
-func (l *VersionList) Init(mu *sync.Mutex) {
-	l.mu = mu
+func (l *versionList) init() {
 	l.root.next = &l.root
 	l.root.prev = &l.root
 }
 
-// Empty returns true if the list is empty, and false otherwise.
-func (l *VersionList) Empty() bool {
+func (l *versionList) empty() bool {
 	return l.root.next == &l.root
 }
 
-// Front returns the oldest version in the list. Note that this version is only
-// valid if Empty() returns true.
-func (l *VersionList) Front() *Version {
+func (l *versionList) front() *version {
 	return l.root.next
 }
 
-// Back returns the newest version in the list. Note that this version is only
-// valid if Empty() returns true.
-func (l *VersionList) Back() *Version {
+func (l *versionList) back() *version {
 	return l.root.prev
 }
 
-// PushBack adds a new version to the back of the list. This new version
-// becomes the "newest" version in the list.
-func (l *VersionList) PushBack(v *Version) {
+func (l *versionList) pushBack(v *version) {
 	if v.list != nil || v.prev != nil || v.next != nil {
 		panic("pebble: version list is inconsistent")
 	}
@@ -393,8 +349,7 @@ func (l *VersionList) PushBack(v *Version) {
 	v.list = l
 }
 
-// Remove removes the specified version from the list.
-func (l *VersionList) Remove(v *Version) {
+func (l *versionList) remove(v *version) {
 	if v == &l.root {
 		panic("pebble: cannot remove version list root node")
 	}
diff --git a/internal/manifest/version_edit.go b/version_edit.go
similarity index 65%
rename from internal/manifest/version_edit.go
rename to version_edit.go
index 9017805..377c8bc 100644
--- a/internal/manifest/version_edit.go
+++ b/version_edit.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package manifest
+package pebble
 
 import (
 	"bufio"
@@ -11,6 +11,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"sort"
 	"sync/atomic"
 
 	"github.com/petermattis/pebble/internal/base"
@@ -55,34 +56,28 @@ const (
 	customTagNonSafeIgnoreMask = 1 << 6
 )
 
-// DeletedFileEntry holds the state for a file deletion from a level. The file
-// itself might still be referenced by another level.
-type DeletedFileEntry struct {
-	Level   int
-	FileNum uint64
+type deletedFileEntry struct {
+	level   int
+	fileNum uint64
 }
 
-// NewFileEntry holds the state for a new file or one moved from a different
-// level.
-type NewFileEntry struct {
-	Level int
-	Meta  FileMetadata
+type newFileEntry struct {
+	level int
+	meta  fileMetadata
 }
 
-// VersionEdit holds the state for an edit to a Version along with other
-// on-disk state (log numbers, next file number, and the last sequence number).
-type VersionEdit struct {
-	ComparerName string
-	LogNum       uint64
-	PrevLogNum   uint64
-	NextFileNum  uint64
-	LastSeqNum   uint64
-	DeletedFiles map[DeletedFileEntry]bool // set of DeletedFileEntry values
-	NewFiles     []NewFileEntry
+type versionEdit struct {
+	comparatorName string
+	logNumber      uint64
+	prevLogNumber  uint64
+	nextFileNumber uint64
+	lastSequence   uint64
+	deletedFiles   map[deletedFileEntry]bool // A set of deletedFileEntry values.
+	newFiles       []newFileEntry
+	metrics        map[int]*LevelMetrics // level -> metrics update
 }
 
-// Decode decodes an edit from the specified reader.
-func (v *VersionEdit) Decode(r io.Reader) error {
+func (v *versionEdit) decode(r io.Reader) error {
 	br, ok := r.(byteReader)
 	if !ok {
 		br = bufio.NewReader(r)
@@ -102,28 +97,28 @@ func (v *VersionEdit) Decode(r io.Reader) error {
 			if err != nil {
 				return err
 			}
-			v.ComparerName = string(s)
+			v.comparatorName = string(s)
 
 		case tagLogNumber:
 			n, err := d.readUvarint()
 			if err != nil {
 				return err
 			}
-			v.LogNum = n
+			v.logNumber = n
 
 		case tagNextFileNumber:
 			n, err := d.readUvarint()
 			if err != nil {
 				return err
 			}
-			v.NextFileNum = n
+			v.nextFileNumber = n
 
 		case tagLastSequence:
 			n, err := d.readUvarint()
 			if err != nil {
 				return err
 			}
-			v.LastSeqNum = n
+			v.lastSequence = n
 
 		case tagCompactPointer:
 			if _, err := d.readLevel(); err != nil {
@@ -143,10 +138,10 @@ func (v *VersionEdit) Decode(r io.Reader) error {
 			if err != nil {
 				return err
 			}
-			if v.DeletedFiles == nil {
-				v.DeletedFiles = make(map[DeletedFileEntry]bool)
+			if v.deletedFiles == nil {
+				v.deletedFiles = make(map[deletedFileEntry]bool)
 			}
-			v.DeletedFiles[DeletedFileEntry{level, fileNum}] = true
+			v.deletedFiles[deletedFileEntry{level, fileNum}] = true
 
 		case tagNewFile, tagNewFile2, tagNewFile3, tagNewFile4:
 			level, err := d.readLevel()
@@ -219,16 +214,16 @@ func (v *VersionEdit) Decode(r io.Reader) error {
 					}
 				}
 			}
-			v.NewFiles = append(v.NewFiles, NewFileEntry{
-				Level: level,
-				Meta: FileMetadata{
-					FileNum:             fileNum,
-					Size:                size,
-					Smallest:            base.DecodeInternalKey(smallest),
-					Largest:             base.DecodeInternalKey(largest),
-					SmallestSeqNum:      smallestSeqNum,
-					LargestSeqNum:       largestSeqNum,
-					MarkedForCompaction: markedForCompaction,
+			v.newFiles = append(v.newFiles, newFileEntry{
+				level: level,
+				meta: fileMetadata{
+					fileNum:             fileNum,
+					size:                size,
+					smallest:            base.DecodeInternalKey(smallest),
+					largest:             base.DecodeInternalKey(largest),
+					smallestSeqNum:      smallestSeqNum,
+					largestSeqNum:       largestSeqNum,
+					markedForCompaction: markedForCompaction,
 				},
 			})
 
@@ -237,7 +232,7 @@ func (v *VersionEdit) Decode(r io.Reader) error {
 			if err != nil {
 				return err
 			}
-			v.PrevLogNum = n
+			v.prevLogNumber = n
 
 		case tagColumnFamily, tagColumnFamilyAdd, tagColumnFamilyDrop, tagMaxColumnFamily:
 			return fmt.Errorf("column families are not supported")
@@ -249,51 +244,50 @@ func (v *VersionEdit) Decode(r io.Reader) error {
 	return nil
 }
 
-// Encode encodes an edit to the specified writer.
-func (v *VersionEdit) Encode(w io.Writer) error {
+func (v *versionEdit) encode(w io.Writer) error {
 	e := versionEditEncoder{new(bytes.Buffer)}
-	if v.ComparerName != "" {
+	if v.comparatorName != "" {
 		e.writeUvarint(tagComparator)
-		e.writeString(v.ComparerName)
+		e.writeString(v.comparatorName)
 	}
-	if v.LogNum != 0 {
+	if v.logNumber != 0 {
 		e.writeUvarint(tagLogNumber)
-		e.writeUvarint(v.LogNum)
+		e.writeUvarint(v.logNumber)
 	}
-	if v.PrevLogNum != 0 {
+	if v.prevLogNumber != 0 {
 		e.writeUvarint(tagPrevLogNumber)
-		e.writeUvarint(v.PrevLogNum)
+		e.writeUvarint(v.prevLogNumber)
 	}
-	if v.NextFileNum != 0 {
+	if v.nextFileNumber != 0 {
 		e.writeUvarint(tagNextFileNumber)
-		e.writeUvarint(v.NextFileNum)
+		e.writeUvarint(v.nextFileNumber)
 	}
-	if v.LastSeqNum != 0 {
+	if v.lastSequence != 0 {
 		e.writeUvarint(tagLastSequence)
-		e.writeUvarint(v.LastSeqNum)
+		e.writeUvarint(v.lastSequence)
 	}
-	for x := range v.DeletedFiles {
+	for x := range v.deletedFiles {
 		e.writeUvarint(tagDeletedFile)
-		e.writeUvarint(uint64(x.Level))
-		e.writeUvarint(x.FileNum)
+		e.writeUvarint(uint64(x.level))
+		e.writeUvarint(x.fileNum)
 	}
-	for _, x := range v.NewFiles {
+	for _, x := range v.newFiles {
 		var customFields bool
-		if x.Meta.MarkedForCompaction {
+		if x.meta.markedForCompaction {
 			customFields = true
 			e.writeUvarint(tagNewFile4)
 		} else {
 			e.writeUvarint(tagNewFile2)
 		}
-		e.writeUvarint(uint64(x.Level))
-		e.writeUvarint(x.Meta.FileNum)
-		e.writeUvarint(x.Meta.Size)
-		e.writeKey(x.Meta.Smallest)
-		e.writeKey(x.Meta.Largest)
-		e.writeUvarint(x.Meta.SmallestSeqNum)
-		e.writeUvarint(x.Meta.LargestSeqNum)
+		e.writeUvarint(uint64(x.level))
+		e.writeUvarint(x.meta.fileNum)
+		e.writeUvarint(x.meta.size)
+		e.writeKey(x.meta.smallest)
+		e.writeKey(x.meta.largest)
+		e.writeUvarint(x.meta.smallestSeqNum)
+		e.writeUvarint(x.meta.largestSeqNum)
 		if customFields {
-			if x.Meta.MarkedForCompaction {
+			if x.meta.markedForCompaction {
 				e.writeUvarint(customTagNeedsCompaction)
 				e.writeBytes([]byte{1})
 			}
@@ -329,7 +323,7 @@ func (d versionEditDecoder) readLevel() (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	if u >= NumLevels {
+	if u >= numLevels {
 		return 0, errCorruptManifest
 	}
 	return int(u), nil
@@ -373,49 +367,49 @@ func (e versionEditEncoder) writeUvarint(u uint64) {
 	e.Write(buf[:n])
 }
 
-// BulkVersionEdit summarizes the files added and deleted from a set of version
+// bulkVersionEdit summarizes the files added and deleted from a set of version
 // edits.
-type BulkVersionEdit struct {
-	Added   [NumLevels][]FileMetadata
-	Deleted [NumLevels]map[uint64]bool // map[uint64]bool is a set of fileNums
+//
+// The C++ LevelDB code calls this concept a VersionSet::Builder.
+type bulkVersionEdit struct {
+	added   [numLevels][]fileMetadata
+	deleted [numLevels]map[uint64]bool // map[uint64]bool is a set of fileNums.
 }
 
-// Accumulate adds the file addition and deletions in the specified version
-// edit to the bulk edit's internal state.
-func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) {
-	for df := range ve.DeletedFiles {
-		dmap := b.Deleted[df.Level]
+func (b *bulkVersionEdit) accumulate(ve *versionEdit) {
+	for df := range ve.deletedFiles {
+		dmap := b.deleted[df.level]
 		if dmap == nil {
 			dmap = make(map[uint64]bool)
-			b.Deleted[df.Level] = dmap
+			b.deleted[df.level] = dmap
 		}
-		dmap[df.FileNum] = true
+		dmap[df.fileNum] = true
 	}
 
-	for _, nf := range ve.NewFiles {
-		if dmap := b.Deleted[nf.Level]; dmap != nil {
-			delete(dmap, nf.Meta.FileNum)
+	for _, nf := range ve.newFiles {
+		if dmap := b.deleted[nf.level]; dmap != nil {
+			delete(dmap, nf.meta.fileNum)
 		}
-		b.Added[nf.Level] = append(b.Added[nf.Level], nf.Meta)
+		b.added[nf.level] = append(b.added[nf.level], nf.meta)
 	}
 }
 
-// Apply applies the delta b to a base version to produce a new version. The
+// apply applies the delta b to a base version to produce a new version. The
 // new version is consistent with respect to the internal key comparer icmp.
 //
 // base may be nil, which is equivalent to a pointer to a zero version.
-func (b *BulkVersionEdit) Apply(
-	opts *Options, base *Version, cmp Compare,
-) (*Version, error) {
-	v := new(Version)
-	for level := range v.Files {
-		if len(b.Added[level]) == 0 && len(b.Deleted[level]) == 0 {
+func (b *bulkVersionEdit) apply(
+	opts *Options, base *version, cmp Compare,
+) (*version, error) {
+	v := new(version)
+	for level := range v.files {
+		if len(b.added[level]) == 0 && len(b.deleted[level]) == 0 {
 			// There are no edits on this level.
 			if base == nil {
 				continue
 			}
-			files := base.Files[level]
-			v.Files[level] = files
+			files := base.files[level]
+			v.files[level] = files
 			// We still have to bump the ref count for all files.
 			for i := range files {
 				atomic.AddInt32(files[i].refs, 1)
@@ -423,30 +417,30 @@ func (b *BulkVersionEdit) Apply(
 			continue
 		}
 
-		combined := [2][]FileMetadata{
+		combined := [2][]fileMetadata{
 			nil,
-			b.Added[level],
+			b.added[level],
 		}
 		if base != nil {
-			combined[0] = base.Files[level]
+			combined[0] = base.files[level]
 		}
 		n := len(combined[0]) + len(combined[1])
 		if n == 0 {
 			continue
 		}
-		v.Files[level] = make([]FileMetadata, 0, n)
-		dmap := b.Deleted[level]
+		v.files[level] = make([]fileMetadata, 0, n)
+		dmap := b.deleted[level]
 
 		for _, ff := range combined {
 			for _, f := range ff {
-				if dmap != nil && dmap[f.FileNum] {
+				if dmap != nil && dmap[f.fileNum] {
 					continue
 				}
 				if f.refs == nil {
 					f.refs = new(int32)
 				}
 				atomic.AddInt32(f.refs, 1)
-				v.Files[level] = append(v.Files[level], f)
+				v.files[level] = append(v.files[level], f)
 			}
 		}
 
@@ -455,12 +449,12 @@ func (b *BulkVersionEdit) Apply(
 		// efficient to sort b.addFiles[level] and then merge the two sorted
 		// slices.
 		if level == 0 {
-			SortBySeqNum(v.Files[level])
+			sort.Sort(bySeqNum(v.files[level]))
 		} else {
-			SortBySmallest(v.Files[level], cmp)
+			sort.Sort(bySmallest{v.files[level], cmp})
 		}
 	}
-	if err := v.CheckOrdering(cmp); err != nil {
+	if err := v.checkOrdering(cmp); err != nil {
 		return nil, fmt.Errorf("pebble: internal error: %v", err)
 	}
 	return v, nil
diff --git a/version_set.go b/version_set.go
index b90f02f..c8c3b1e 100644
--- a/version_set.go
+++ b/version_set.go
@@ -12,22 +12,10 @@ import (
 	"sync/atomic"
 
 	"github.com/petermattis/pebble/internal/base"
-	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/internal/record"
 	"github.com/petermattis/pebble/vfs"
 )
 
-const numLevels = manifest.NumLevels
-
-// Provide type aliases for the various manifest structs.
-type bulkVersionEdit = manifest.BulkVersionEdit
-type deletedFileEntry = manifest.DeletedFileEntry
-type fileMetadata = manifest.FileMetadata
-type newFileEntry = manifest.NewFileEntry
-type version = manifest.Version
-type versionEdit = manifest.VersionEdit
-type versionList = manifest.VersionList
-
 // versionSet manages a collection of immutable versions, and manages the
 // creation of a new version from the most recent version. A new versions is
 // created from an existing version by applying a version edit which is just
@@ -51,19 +39,16 @@ type versionSet struct {
 
 	metrics VersionMetrics
 
-	// A pointer to versionSet.addObsoleteLocked. Avoids allocating a new closure
-	// on the creation of every version.
-	obsoleteFn        func(obsolete []uint64)
 	obsoleteTables    []uint64
 	obsoleteManifests []uint64
 	obsoleteOptions   []uint64
 
-	logNum          uint64
-	prevLogNum      uint64
-	nextFileNum     uint64
-	logSeqNum       uint64 // next seqNum to use for WAL writes
-	visibleSeqNum   uint64 // visible seqNum (<= logSeqNum)
-	manifestFileNum uint64
+	logNumber          uint64
+	prevLogNumber      uint64
+	nextFileNumber     uint64
+	logSeqNum          uint64 // next seqNum to use for WAL writes
+	visibleSeqNum      uint64 // visible seqNum (<= logSeqNum)
+	manifestFileNumber uint64
 
 	manifestFile vfs.File
 	manifest     *record.Writer
@@ -76,16 +61,16 @@ type versionSet struct {
 func (vs *versionSet) load(dirname string, opts *Options, mu *sync.Mutex) error {
 	vs.dirname = dirname
 	vs.mu = mu
+	vs.versions.mu = mu
 	vs.writerCond.L = mu
 	vs.opts = opts
 	vs.fs = opts.FS
 	vs.cmp = opts.Comparer.Compare
 	vs.cmpName = opts.Comparer.Name
 	vs.dynamicBaseLevel = true
-	vs.versions.Init(mu)
-	vs.obsoleteFn = vs.addObsoleteLocked
+	vs.versions.init()
 	// For historical reasons, the next file number is initialized to 2.
-	vs.nextFileNum = 2
+	vs.nextFileNumber = 2
 
 	// Read the CURRENT file to find the current manifest file.
 	current, err := vs.fs.Open(base.MakeFilename(dirname, fileTypeCurrent, 0))
@@ -131,52 +116,46 @@ func (vs *versionSet) load(dirname string, opts *Options, mu *sync.Mutex) error
 			return err
 		}
 		var ve versionEdit
-		err = ve.Decode(r)
+		err = ve.decode(r)
 		if err != nil {
 			return err
 		}
-		if ve.ComparerName != "" {
-			if ve.ComparerName != vs.cmpName {
+		if ve.comparatorName != "" {
+			if ve.comparatorName != vs.cmpName {
 				return fmt.Errorf("pebble: manifest file %q for DB %q: "+
 					"comparer name from file %q != comparer name from Options %q",
-					b, dirname, ve.ComparerName, vs.cmpName)
+					b, dirname, ve.comparatorName, vs.cmpName)
 			}
 		}
-		bve.Accumulate(&ve)
-		if ve.LogNum != 0 {
-			vs.logNum = ve.LogNum
+		bve.accumulate(&ve)
+		if ve.logNumber != 0 {
+			vs.logNumber = ve.logNumber
 		}
-		if ve.PrevLogNum != 0 {
-			vs.prevLogNum = ve.PrevLogNum
+		if ve.prevLogNumber != 0 {
+			vs.prevLogNumber = ve.prevLogNumber
 		}
-		if ve.NextFileNum != 0 {
-			vs.nextFileNum = ve.NextFileNum
+		if ve.nextFileNumber != 0 {
+			vs.nextFileNumber = ve.nextFileNumber
 		}
-		if ve.LastSeqNum != 0 {
-			vs.logSeqNum = ve.LastSeqNum
+		if ve.lastSequence != 0 {
+			vs.logSeqNum = ve.lastSequence
 		}
 	}
-	if vs.logNum == 0 || vs.nextFileNum == 0 {
-		if vs.nextFileNum == 2 {
+	if vs.logNumber == 0 || vs.nextFileNumber == 0 {
+		if vs.nextFileNumber == 2 {
 			// We have a freshly created DB.
 		} else {
 			return fmt.Errorf("pebble: incomplete manifest file %q for DB %q", b, dirname)
 		}
 	}
-	vs.markFileNumUsed(vs.logNum)
-	vs.markFileNumUsed(vs.prevLogNum)
+	vs.markFileNumUsed(vs.logNumber)
+	vs.markFileNumUsed(vs.prevLogNumber)
 
-	newVersion, err := bve.Apply(opts, nil, vs.cmp)
+	newVersion, err := bve.apply(opts, nil, vs.cmp)
 	if err != nil {
 		return err
 	}
 	vs.append(newVersion)
-
-	for i := range vs.metrics.Levels {
-		l := &vs.metrics.Levels[i]
-		l.NumFiles = int64(len(newVersion.Files[i]))
-		l.Size = uint64(totalSize(newVersion.Files[i]))
-	}
 	return nil
 }
 
@@ -184,12 +163,7 @@ func (vs *versionSet) load(dirname string, opts *Options, mu *sync.Mutex) error
 // to the current version, and installs the new version. DB.mu must be held
 // when calling this method and will be released temporarily while performing
 // file I/O.
-func (vs *versionSet) logAndApply(
-	jobID int,
-	ve *versionEdit,
-	metrics map[int]*LevelMetrics,
-	dir vfs.File,
-) error {
+func (vs *versionSet) logAndApply(jobID int, ve *versionEdit, dir vfs.File) error {
 	// Wait for any existing writing to the manifest to complete, then mark the
 	// manifest as busy.
 	for vs.writing {
@@ -201,21 +175,21 @@ func (vs *versionSet) logAndApply(
 		vs.writerCond.Signal()
 	}()
 
-	if ve.LogNum != 0 {
-		if ve.LogNum < vs.logNum || vs.nextFileNum <= ve.LogNum {
-			panic(fmt.Sprintf("pebble: inconsistent versionEdit logNumber %d", ve.LogNum))
+	if ve.logNumber != 0 {
+		if ve.logNumber < vs.logNumber || vs.nextFileNumber <= ve.logNumber {
+			panic(fmt.Sprintf("pebble: inconsistent versionEdit logNumber %d", ve.logNumber))
 		}
 	}
-	ve.NextFileNum = vs.nextFileNum
-	ve.LastSeqNum = atomic.LoadUint64(&vs.logSeqNum)
+	ve.nextFileNumber = vs.nextFileNumber
+	ve.lastSequence = atomic.LoadUint64(&vs.logSeqNum)
 	currentVersion := vs.currentVersion()
 	var newVersion *version
 
 	// Generate a new manifest if we don't currently have one, or the current one
 	// is too large.
-	var newManifestFileNum uint64
+	var newManifestFileNumber uint64
 	if vs.manifest == nil || vs.manifest.Size() >= vs.opts.MaxManifestFileSize {
-		newManifestFileNum = vs.getNextFileNum()
+		newManifestFileNumber = vs.nextFileNum()
 	}
 
 	var picker *compactionPicker
@@ -224,21 +198,21 @@ func (vs *versionSet) logAndApply(
 		defer vs.mu.Lock()
 
 		var bve bulkVersionEdit
-		bve.Accumulate(ve)
+		bve.accumulate(ve)
 
 		var err error
-		newVersion, err = bve.Apply(vs.opts, currentVersion, vs.cmp)
+		newVersion, err = bve.apply(vs.opts, currentVersion, vs.cmp)
 		if err != nil {
 			return err
 		}
 
-		if newManifestFileNum != 0 {
-			if err := vs.createManifest(vs.dirname, newManifestFileNum); err != nil {
+		if newManifestFileNumber != 0 {
+			if err := vs.createManifest(vs.dirname, newManifestFileNumber); err != nil {
 				if vs.opts.EventListener.ManifestCreated != nil {
 					vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{
 						JobID:   jobID,
-						Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNum),
-						FileNum: newManifestFileNum,
+						Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNumber),
+						FileNum: newManifestFileNumber,
 						Err:     err,
 					})
 				}
@@ -255,7 +229,7 @@ func (vs *versionSet) logAndApply(
 		// fraught. Instead we rely on the standard recovery mechanism run when a
 		// database is open. In particular, that mechanism generates a new MANIFEST
 		// and ensures it is synced.
-		if err := ve.Encode(w); err != nil {
+		if err := ve.encode(w); err != nil {
 			vs.opts.Logger.Fatalf("MANIFEST write failed: %v", err)
 			return err
 		}
@@ -267,8 +241,8 @@ func (vs *versionSet) logAndApply(
 			vs.opts.Logger.Fatalf("MANIFEST sync failed: %v", err)
 			return err
 		}
-		if newManifestFileNum != 0 {
-			if err := setCurrentFile(vs.dirname, vs.fs, newManifestFileNum); err != nil {
+		if newManifestFileNumber != 0 {
+			if err := setCurrentFile(vs.dirname, vs.fs, newManifestFileNumber); err != nil {
 				vs.opts.Logger.Fatalf("MANIFEST set current failed: %v", err)
 				return err
 			}
@@ -279,8 +253,8 @@ func (vs *versionSet) logAndApply(
 			if vs.opts.EventListener.ManifestCreated != nil {
 				vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{
 					JobID:   jobID,
-					Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNum),
-					FileNum: newManifestFileNum,
+					Path:    base.MakeFilename(vs.dirname, fileTypeManifest, newManifestFileNumber),
+					FileNum: newManifestFileNumber,
 				})
 			}
 		}
@@ -295,29 +269,29 @@ func (vs *versionSet) logAndApply(
 
 	// Install the new version.
 	vs.append(newVersion)
-	if ve.LogNum != 0 {
-		vs.logNum = ve.LogNum
+	if ve.logNumber != 0 {
+		vs.logNumber = ve.logNumber
 	}
-	if ve.PrevLogNum != 0 {
-		vs.prevLogNum = ve.PrevLogNum
+	if ve.prevLogNumber != 0 {
+		vs.prevLogNumber = ve.prevLogNumber
 	}
-	if newManifestFileNum != 0 {
-		if vs.manifestFileNum != 0 {
-			vs.obsoleteManifests = append(vs.obsoleteManifests, vs.manifestFileNum)
+	if newManifestFileNumber != 0 {
+		if vs.manifestFileNumber != 0 {
+			vs.obsoleteManifests = append(vs.obsoleteManifests, vs.manifestFileNumber)
 		}
-		vs.manifestFileNum = newManifestFileNum
+		vs.manifestFileNumber = newManifestFileNumber
 	}
 	vs.picker = picker
 
-	if metrics != nil {
-		for level, update := range metrics {
+	if ve.metrics != nil {
+		for level, update := range ve.metrics {
 			vs.metrics.Levels[level].Add(update)
 		}
 	}
 	for i := range vs.metrics.Levels {
 		l := &vs.metrics.Levels[i]
-		l.NumFiles = int64(len(newVersion.Files[i]))
-		l.Size = uint64(totalSize(newVersion.Files[i]))
+		l.NumFiles = int64(len(newVersion.files[i]))
+		l.Size = uint64(totalSize(newVersion.files[i]))
 	}
 	return nil
 }
@@ -347,13 +321,13 @@ func (vs *versionSet) createManifest(dirname string, fileNum uint64) (err error)
 	manifest = record.NewWriter(manifestFile)
 
 	snapshot := versionEdit{
-		ComparerName: vs.cmpName,
+		comparatorName: vs.cmpName,
 	}
-	for level, fileMetadata := range vs.currentVersion().Files {
+	for level, fileMetadata := range vs.currentVersion().files {
 		for _, meta := range fileMetadata {
-			snapshot.NewFiles = append(snapshot.NewFiles, newFileEntry{
-				Level: level,
-				Meta:  meta,
+			snapshot.newFiles = append(snapshot.newFiles, newFileEntry{
+				level: level,
+				meta:  meta,
 			})
 		}
 	}
@@ -362,7 +336,7 @@ func (vs *versionSet) createManifest(dirname string, fileNum uint64) (err error)
 	if err1 != nil {
 		return err1
 	}
-	if err := snapshot.Encode(w); err != nil {
+	if err := snapshot.encode(w); err != nil {
 		return err
 	}
 
@@ -372,44 +346,40 @@ func (vs *versionSet) createManifest(dirname string, fileNum uint64) (err error)
 }
 
 func (vs *versionSet) markFileNumUsed(fileNum uint64) {
-	if vs.nextFileNum <= fileNum {
-		vs.nextFileNum = fileNum + 1
+	if vs.nextFileNumber <= fileNum {
+		vs.nextFileNumber = fileNum + 1
 	}
 }
 
-func (vs *versionSet) getNextFileNum() uint64 {
-	x := vs.nextFileNum
-	vs.nextFileNum++
+func (vs *versionSet) nextFileNum() uint64 {
+	x := vs.nextFileNumber
+	vs.nextFileNumber++
 	return x
 }
 
 func (vs *versionSet) append(v *version) {
-	if v.Refs() != 0 {
+	if v.refs != 0 {
 		panic("pebble: version should be unreferenced")
 	}
-	if !vs.versions.Empty() {
-		vs.versions.Back().UnrefLocked()
+	if !vs.versions.empty() {
+		vs.versions.back().unrefLocked()
 	}
-	v.Deleted = vs.obsoleteFn
-	v.Ref()
-	vs.versions.PushBack(v)
+	v.vs = vs
+	v.ref()
+	vs.versions.pushBack(v)
 }
 
 func (vs *versionSet) currentVersion() *version {
-	return vs.versions.Back()
+	return vs.versions.back()
 }
 
 func (vs *versionSet) addLiveFileNums(m map[uint64]struct{}) {
-	current := vs.currentVersion()
-	for v := vs.versions.Front(); true; v = v.Next() {
-		for _, ff := range v.Files {
+	for v := vs.versions.root.next; v != &vs.versions.root; v = v.next {
+		for _, ff := range v.files {
 			for _, f := range ff {
-				m[f.FileNum] = struct{}{}
+				m[f.fileNum] = struct{}{}
 			}
 		}
-		if v == current {
-			break
-		}
 	}
 }
 


Test patch:
diff --git a/batch_test.go b/batch_test.go
index 83cc543..36c062d 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -15,7 +15,6 @@ import (
 	"github.com/petermattis/pebble/internal/base"
 	"github.com/petermattis/pebble/internal/datadriven"
 	"github.com/petermattis/pebble/vfs"
-	"github.com/stretchr/testify/require"
 )
 
 func TestBatch(t *testing.T) {
@@ -24,7 +23,7 @@ func TestBatch(t *testing.T) {
 		key, value string
 	}
 
-	verifyTestCases := func(b *Batch, testCases []testCase) {
+	verifyTestCases := func(b Batch, testCases []testCase) {
 		r := b.Reader()
 
 		for _, tc := range testCases {
@@ -79,7 +78,7 @@ func TestBatch(t *testing.T) {
 			_ = b.LogData([]byte(tc.key), nil)
 		}
 	}
-	verifyTestCases(&b, testCases)
+	verifyTestCases(b, testCases)
 
 	b.Reset()
 	// Run the same operations, this time using the Deferred variants of each
@@ -104,45 +103,12 @@ func TestBatch(t *testing.T) {
 			copy(d.Value, value)
 			d.Finish()
 		case InternalKeyKindRangeDelete:
-			d, _ := b.DeleteRangeDeferred(len(key), len(value), nil)
-			copy(d.Key, key)
-			copy(d.Value, value)
-			d.Finish()
+			_ = b.DeleteRange([]byte(tc.key), []byte(tc.value), nil)
 		case InternalKeyKindLogData:
 			_ = b.LogData([]byte(tc.key), nil)
 		}
 	}
-	verifyTestCases(&b, testCases)
-}
-
-func TestBatchEmpty(t *testing.T) {
-	var b Batch
-	require.True(t, b.Empty())
-
-	b.Set(nil, nil, nil)
-	require.False(t, b.Empty())
-	b.Reset()
-	require.True(t, b.Empty())
-
-	b.Merge(nil, nil, nil)
-	require.False(t, b.Empty())
-	b.Reset()
-	require.True(t, b.Empty())
-
-	b.Delete(nil, nil)
-	require.False(t, b.Empty())
-	b.Reset()
-	require.True(t, b.Empty())
-
-	b.DeleteRange(nil, nil, nil)
-	require.False(t, b.Empty())
-	b.Reset()
-	require.True(t, b.Empty())
-
-	b.LogData(nil, nil)
-	require.False(t, b.Empty())
-	b.Reset()
-	require.True(t, b.Empty())
+	verifyTestCases(b, testCases)
 }
 
 func TestBatchIncrement(t *testing.T) {
@@ -176,12 +142,12 @@ func TestBatchIncrement(t *testing.T) {
 		0xffffffff,
 	}
 	for _, tc := range testCases {
-		var buf [batchHeaderLen]byte
+		var buf [12]byte
 		binary.LittleEndian.PutUint32(buf[8:12], tc)
 		var b Batch
-		b.SetRepr(buf[:])
+		b.storage.data = buf[:]
 		b.increment()
-		got := binary.LittleEndian.Uint32(b.Repr()[8:12])
+		got := binary.LittleEndian.Uint32(buf[8:12])
 		want := tc + 1
 		if tc == 0xffffffff {
 			want = tc
@@ -528,8 +494,8 @@ func TestFlushableBatchDeleteRange(t *testing.T) {
 func TestFlushableBatchBytesIterated(t *testing.T) {
 	batch := newBatch(nil)
 	for j := 0; j < 1000; j++ {
-		key := make([]byte, 8+j%3)
-		value := make([]byte, 7+j%5)
+		key := make([]byte, 8 + j%3)
+		value := make([]byte, 7 + j%5)
 		batch.Set(key, value, nil)
 
 		fb := newFlushableBatch(batch, DefaultComparer)
diff --git a/compaction_picker_test.go b/compaction_picker_test.go
index 33b1eaf..75c495c 100644
--- a/compaction_picker_test.go
+++ b/compaction_picker_test.go
@@ -38,7 +38,7 @@ func loadVersion(d *datadriven.TestData) (*version, *Options, string) {
 			if err != nil {
 				return nil, nil, err.Error()
 			}
-			if vers.Files[level] != nil {
+			if vers.files[level] != nil {
 				return nil, nil, fmt.Sprintf("level %d already filled", level)
 			}
 			size, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64)
@@ -47,13 +47,13 @@ func loadVersion(d *datadriven.TestData) (*version, *Options, string) {
 			}
 			if level == 0 {
 				for i := uint64(0); i < size; i++ {
-					vers.Files[level] = append(vers.Files[level], fileMetadata{
-						Size: 1,
+					vers.files[level] = append(vers.files[level], fileMetadata{
+						size: 1,
 					})
 				}
 			} else {
-				vers.Files[level] = append(vers.Files[level], fileMetadata{
-					Size: size,
+				vers.files[level] = append(vers.files[level], fileMetadata{
+					size: size,
 				})
 			}
 		}
diff --git a/compaction_test.go b/compaction_test.go
index 6b637fe..3416180 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -17,7 +17,6 @@ import (
 
 	"github.com/petermattis/pebble/internal/base"
 	"github.com/petermattis/pebble/internal/datadriven"
-	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/sstable"
 	"github.com/petermattis/pebble/vfs"
 )
@@ -26,7 +25,7 @@ func TestPickCompaction(t *testing.T) {
 	fileNums := func(f []fileMetadata) string {
 		ss := make([]string, 0, len(f))
 		for _, meta := range f {
-			ss = append(ss, strconv.Itoa(int(meta.FileNum)))
+			ss = append(ss, strconv.Itoa(int(meta.fileNum)))
 		}
 		sort.Strings(ss)
 		return strings.Join(ss, ",")
@@ -42,13 +41,13 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "no compaction",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("j.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("j.SET.102"),
 						},
 					},
 				},
@@ -59,13 +58,13 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "1 L0 file",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("j.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("j.SET.102"),
 						},
 					},
 				},
@@ -81,19 +80,19 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "2 L0 files (0 overlaps)",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("j.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("j.SET.102"),
 						},
 						{
-							FileNum:  110,
-							Size:     1,
-							Smallest: base.ParseInternalKey("k.SET.111"),
-							Largest:  base.ParseInternalKey("l.SET.112"),
+							fileNum:  110,
+							size:     1,
+							smallest: base.ParseInternalKey("k.SET.111"),
+							largest:  base.ParseInternalKey("l.SET.112"),
 						},
 					},
 				},
@@ -109,19 +108,19 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "2 L0 files, with ikey overlap",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("p.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("p.SET.102"),
 						},
 						{
-							FileNum:  110,
-							Size:     1,
-							Smallest: base.ParseInternalKey("j.SET.111"),
-							Largest:  base.ParseInternalKey("q.SET.112"),
+							fileNum:  110,
+							size:     1,
+							smallest: base.ParseInternalKey("j.SET.111"),
+							largest:  base.ParseInternalKey("q.SET.112"),
 						},
 					},
 				},
@@ -137,19 +136,19 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "2 L0 files, with ukey overlap",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("i.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("i.SET.102"),
 						},
 						{
-							FileNum:  110,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.111"),
-							Largest:  base.ParseInternalKey("i.SET.112"),
+							fileNum:  110,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.111"),
+							largest:  base.ParseInternalKey("i.SET.112"),
 						},
 					},
 				},
@@ -165,27 +164,27 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "1 L0 file, 2 L1 files (0 overlaps)",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("i.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("i.SET.102"),
 						},
 					},
 					1: []fileMetadata{
 						{
-							FileNum:  200,
-							Size:     1,
-							Smallest: base.ParseInternalKey("a.SET.201"),
-							Largest:  base.ParseInternalKey("b.SET.202"),
+							fileNum:  200,
+							size:     1,
+							smallest: base.ParseInternalKey("a.SET.201"),
+							largest:  base.ParseInternalKey("b.SET.202"),
 						},
 						{
-							FileNum:  210,
-							Size:     1,
-							Smallest: base.ParseInternalKey("y.SET.211"),
-							Largest:  base.ParseInternalKey("z.SET.212"),
+							fileNum:  210,
+							size:     1,
+							smallest: base.ParseInternalKey("y.SET.211"),
+							largest:  base.ParseInternalKey("z.SET.212"),
 						},
 					},
 				},
@@ -201,53 +200,53 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "1 L0 file, 2 L1 files (1 overlap), 4 L2 files (3 overlaps)",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					0: []fileMetadata{
 						{
-							FileNum:  100,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i.SET.101"),
-							Largest:  base.ParseInternalKey("t.SET.102"),
+							fileNum:  100,
+							size:     1,
+							smallest: base.ParseInternalKey("i.SET.101"),
+							largest:  base.ParseInternalKey("t.SET.102"),
 						},
 					},
 					1: []fileMetadata{
 						{
-							FileNum:  200,
-							Size:     1,
-							Smallest: base.ParseInternalKey("a.SET.201"),
-							Largest:  base.ParseInternalKey("e.SET.202"),
+							fileNum:  200,
+							size:     1,
+							smallest: base.ParseInternalKey("a.SET.201"),
+							largest:  base.ParseInternalKey("e.SET.202"),
 						},
 						{
-							FileNum:  210,
-							Size:     1,
-							Smallest: base.ParseInternalKey("f.SET.211"),
-							Largest:  base.ParseInternalKey("j.SET.212"),
+							fileNum:  210,
+							size:     1,
+							smallest: base.ParseInternalKey("f.SET.211"),
+							largest:  base.ParseInternalKey("j.SET.212"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							FileNum:  300,
-							Size:     1,
-							Smallest: base.ParseInternalKey("a.SET.301"),
-							Largest:  base.ParseInternalKey("b.SET.302"),
+							fileNum:  300,
+							size:     1,
+							smallest: base.ParseInternalKey("a.SET.301"),
+							largest:  base.ParseInternalKey("b.SET.302"),
 						},
 						{
-							FileNum:  310,
-							Size:     1,
-							Smallest: base.ParseInternalKey("c.SET.311"),
-							Largest:  base.ParseInternalKey("g.SET.312"),
+							fileNum:  310,
+							size:     1,
+							smallest: base.ParseInternalKey("c.SET.311"),
+							largest:  base.ParseInternalKey("g.SET.312"),
 						},
 						{
-							FileNum:  320,
-							Size:     1,
-							Smallest: base.ParseInternalKey("h.SET.321"),
-							Largest:  base.ParseInternalKey("m.SET.322"),
+							fileNum:  320,
+							size:     1,
+							smallest: base.ParseInternalKey("h.SET.321"),
+							largest:  base.ParseInternalKey("m.SET.322"),
 						},
 						{
-							FileNum:  330,
-							Size:     1,
-							Smallest: base.ParseInternalKey("n.SET.331"),
-							Largest:  base.ParseInternalKey("z.SET.332"),
+							fileNum:  330,
+							size:     1,
+							smallest: base.ParseInternalKey("n.SET.331"),
+							largest:  base.ParseInternalKey("z.SET.332"),
 						},
 					},
 				},
@@ -263,45 +262,45 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "4 L1 files, 2 L2 files, can grow",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							FileNum:  200,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i1.SET.201"),
-							Largest:  base.ParseInternalKey("i2.SET.202"),
+							fileNum:  200,
+							size:     1,
+							smallest: base.ParseInternalKey("i1.SET.201"),
+							largest:  base.ParseInternalKey("i2.SET.202"),
 						},
 						{
-							FileNum:  210,
-							Size:     1,
-							Smallest: base.ParseInternalKey("j1.SET.211"),
-							Largest:  base.ParseInternalKey("j2.SET.212"),
+							fileNum:  210,
+							size:     1,
+							smallest: base.ParseInternalKey("j1.SET.211"),
+							largest:  base.ParseInternalKey("j2.SET.212"),
 						},
 						{
-							FileNum:  220,
-							Size:     1,
-							Smallest: base.ParseInternalKey("k1.SET.221"),
-							Largest:  base.ParseInternalKey("k2.SET.222"),
+							fileNum:  220,
+							size:     1,
+							smallest: base.ParseInternalKey("k1.SET.221"),
+							largest:  base.ParseInternalKey("k2.SET.222"),
 						},
 						{
-							FileNum:  230,
-							Size:     1,
-							Smallest: base.ParseInternalKey("l1.SET.231"),
-							Largest:  base.ParseInternalKey("l2.SET.232"),
+							fileNum:  230,
+							size:     1,
+							smallest: base.ParseInternalKey("l1.SET.231"),
+							largest:  base.ParseInternalKey("l2.SET.232"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							FileNum:  300,
-							Size:     1,
-							Smallest: base.ParseInternalKey("a0.SET.301"),
-							Largest:  base.ParseInternalKey("l0.SET.302"),
+							fileNum:  300,
+							size:     1,
+							smallest: base.ParseInternalKey("a0.SET.301"),
+							largest:  base.ParseInternalKey("l0.SET.302"),
 						},
 						{
-							FileNum:  310,
-							Size:     1,
-							Smallest: base.ParseInternalKey("l2.SET.311"),
-							Largest:  base.ParseInternalKey("z2.SET.312"),
+							fileNum:  310,
+							size:     1,
+							smallest: base.ParseInternalKey("l2.SET.311"),
+							largest:  base.ParseInternalKey("z2.SET.312"),
 						},
 					},
 				},
@@ -317,45 +316,45 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "4 L1 files, 2 L2 files, can't grow (range)",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							FileNum:  200,
-							Size:     1,
-							Smallest: base.ParseInternalKey("i1.SET.201"),
-							Largest:  base.ParseInternalKey("i2.SET.202"),
+							fileNum:  200,
+							size:     1,
+							smallest: base.ParseInternalKey("i1.SET.201"),
+							largest:  base.ParseInternalKey("i2.SET.202"),
 						},
 						{
-							FileNum:  210,
-							Size:     1,
-							Smallest: base.ParseInternalKey("j1.SET.211"),
-							Largest:  base.ParseInternalKey("j2.SET.212"),
+							fileNum:  210,
+							size:     1,
+							smallest: base.ParseInternalKey("j1.SET.211"),
+							largest:  base.ParseInternalKey("j2.SET.212"),
 						},
 						{
-							FileNum:  220,
-							Size:     1,
-							Smallest: base.ParseInternalKey("k1.SET.221"),
-							Largest:  base.ParseInternalKey("k2.SET.222"),
+							fileNum:  220,
+							size:     1,
+							smallest: base.ParseInternalKey("k1.SET.221"),
+							largest:  base.ParseInternalKey("k2.SET.222"),
 						},
 						{
-							FileNum:  230,
-							Size:     1,
-							Smallest: base.ParseInternalKey("l1.SET.231"),
-							Largest:  base.ParseInternalKey("l2.SET.232"),
+							fileNum:  230,
+							size:     1,
+							smallest: base.ParseInternalKey("l1.SET.231"),
+							largest:  base.ParseInternalKey("l2.SET.232"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							FileNum:  300,
-							Size:     1,
-							Smallest: base.ParseInternalKey("a0.SET.301"),
-							Largest:  base.ParseInternalKey("j0.SET.302"),
+							fileNum:  300,
+							size:     1,
+							smallest: base.ParseInternalKey("a0.SET.301"),
+							largest:  base.ParseInternalKey("j0.SET.302"),
 						},
 						{
-							FileNum:  310,
-							Size:     1,
-							Smallest: base.ParseInternalKey("j2.SET.311"),
-							Largest:  base.ParseInternalKey("z2.SET.312"),
+							fileNum:  310,
+							size:     1,
+							smallest: base.ParseInternalKey("j2.SET.311"),
+							largest:  base.ParseInternalKey("z2.SET.312"),
 						},
 					},
 				},
@@ -371,45 +370,45 @@ func TestPickCompaction(t *testing.T) {
 		{
 			desc: "4 L1 files, 2 L2 files, can't grow (size)",
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							FileNum:  200,
-							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							Smallest: base.ParseInternalKey("i1.SET.201"),
-							Largest:  base.ParseInternalKey("i2.SET.202"),
+							fileNum:  200,
+							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							smallest: base.ParseInternalKey("i1.SET.201"),
+							largest:  base.ParseInternalKey("i2.SET.202"),
 						},
 						{
-							FileNum:  210,
-							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							Smallest: base.ParseInternalKey("j1.SET.211"),
-							Largest:  base.ParseInternalKey("j2.SET.212"),
+							fileNum:  210,
+							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							smallest: base.ParseInternalKey("j1.SET.211"),
+							largest:  base.ParseInternalKey("j2.SET.212"),
 						},
 						{
-							FileNum:  220,
-							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							Smallest: base.ParseInternalKey("k1.SET.221"),
-							Largest:  base.ParseInternalKey("k2.SET.222"),
+							fileNum:  220,
+							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							smallest: base.ParseInternalKey("k1.SET.221"),
+							largest:  base.ParseInternalKey("k2.SET.222"),
 						},
 						{
-							FileNum:  230,
-							Size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
-							Smallest: base.ParseInternalKey("l1.SET.231"),
-							Largest:  base.ParseInternalKey("l2.SET.232"),
+							fileNum:  230,
+							size:     expandedCompactionByteSizeLimit(opts, 1) - 1,
+							smallest: base.ParseInternalKey("l1.SET.231"),
+							largest:  base.ParseInternalKey("l2.SET.232"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							FileNum:  300,
-							Size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
-							Smallest: base.ParseInternalKey("a0.SET.301"),
-							Largest:  base.ParseInternalKey("l0.SET.302"),
+							fileNum:  300,
+							size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
+							smallest: base.ParseInternalKey("a0.SET.301"),
+							largest:  base.ParseInternalKey("l0.SET.302"),
 						},
 						{
-							FileNum:  310,
-							Size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
-							Smallest: base.ParseInternalKey("l2.SET.311"),
-							Largest:  base.ParseInternalKey("z2.SET.312"),
+							fileNum:  310,
+							size:     expandedCompactionByteSizeLimit(opts, 2) - 1,
+							smallest: base.ParseInternalKey("l2.SET.311"),
+							largest:  base.ParseInternalKey("z2.SET.312"),
 						},
 					},
 				},
@@ -429,7 +428,7 @@ func TestPickCompaction(t *testing.T) {
 			cmp:     DefaultComparer.Compare,
 			cmpName: DefaultComparer.Name,
 		}
-		vs.versions.Init(nil)
+		vs.versions.init()
 		vs.append(&tc.version)
 		vs.picker = &tc.picker
 		vs.picker.vers = &tc.version
@@ -466,45 +465,45 @@ func TestElideTombstone(t *testing.T) {
 			desc:  "non-empty",
 			level: 1,
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					1: []fileMetadata{
 						{
-							Smallest: base.ParseInternalKey("c.SET.801"),
-							Largest:  base.ParseInternalKey("g.SET.800"),
+							smallest: base.ParseInternalKey("c.SET.801"),
+							largest:  base.ParseInternalKey("g.SET.800"),
 						},
 						{
-							Smallest: base.ParseInternalKey("x.SET.701"),
-							Largest:  base.ParseInternalKey("y.SET.700"),
+							smallest: base.ParseInternalKey("x.SET.701"),
+							largest:  base.ParseInternalKey("y.SET.700"),
 						},
 					},
 					2: []fileMetadata{
 						{
-							Smallest: base.ParseInternalKey("d.SET.601"),
-							Largest:  base.ParseInternalKey("h.SET.600"),
+							smallest: base.ParseInternalKey("d.SET.601"),
+							largest:  base.ParseInternalKey("h.SET.600"),
 						},
 						{
-							Smallest: base.ParseInternalKey("r.SET.501"),
-							Largest:  base.ParseInternalKey("t.SET.500"),
+							smallest: base.ParseInternalKey("r.SET.501"),
+							largest:  base.ParseInternalKey("t.SET.500"),
 						},
 					},
 					3: []fileMetadata{
 						{
-							Smallest: base.ParseInternalKey("f.SET.401"),
-							Largest:  base.ParseInternalKey("g.SET.400"),
+							smallest: base.ParseInternalKey("f.SET.401"),
+							largest:  base.ParseInternalKey("g.SET.400"),
 						},
 						{
-							Smallest: base.ParseInternalKey("w.SET.301"),
-							Largest:  base.ParseInternalKey("x.SET.300"),
+							smallest: base.ParseInternalKey("w.SET.301"),
+							largest:  base.ParseInternalKey("x.SET.300"),
 						},
 					},
 					4: []fileMetadata{
 						{
-							Smallest: base.ParseInternalKey("f.SET.201"),
-							Largest:  base.ParseInternalKey("m.SET.200"),
+							smallest: base.ParseInternalKey("f.SET.201"),
+							largest:  base.ParseInternalKey("m.SET.200"),
 						},
 						{
-							Smallest: base.ParseInternalKey("t.SET.101"),
-							Largest:  base.ParseInternalKey("t.SET.100"),
+							smallest: base.ParseInternalKey("t.SET.101"),
+							largest:  base.ParseInternalKey("t.SET.100"),
 						},
 					},
 				},
@@ -536,23 +535,23 @@ func TestElideTombstone(t *testing.T) {
 			desc:  "repeated ukey",
 			level: 1,
 			version: version{
-				Files: [numLevels][]fileMetadata{
+				files: [numLevels][]fileMetadata{
 					6: []fileMetadata{
 						{
-							Smallest: base.ParseInternalKey("i.SET.401"),
-							Largest:  base.ParseInternalKey("i.SET.400"),
+							smallest: base.ParseInternalKey("i.SET.401"),
+							largest:  base.ParseInternalKey("i.SET.400"),
 						},
 						{
-							Smallest: base.ParseInternalKey("i.SET.301"),
-							Largest:  base.ParseInternalKey("k.SET.300"),
+							smallest: base.ParseInternalKey("i.SET.301"),
+							largest:  base.ParseInternalKey("k.SET.300"),
 						},
 						{
-							Smallest: base.ParseInternalKey("k.SET.201"),
-							Largest:  base.ParseInternalKey("m.SET.200"),
+							smallest: base.ParseInternalKey("k.SET.201"),
+							largest:  base.ParseInternalKey("m.SET.200"),
 						},
 						{
-							Smallest: base.ParseInternalKey("m.SET.101"),
-							Largest:  base.ParseInternalKey("m.SET.100"),
+							smallest: base.ParseInternalKey("m.SET.101"),
+							largest:  base.ParseInternalKey("m.SET.100"),
 						},
 					},
 				},
@@ -621,14 +620,14 @@ func TestCompaction(t *testing.T) {
 		}
 		ss := []string(nil)
 		v := d.mu.versions.currentVersion()
-		for _, files := range v.Files {
+		for _, files := range v.files {
 			for _, meta := range files {
-				f, err := mem.Open(base.MakeFilename("", fileTypeTable, meta.FileNum))
+				f, err := mem.Open(base.MakeFilename("", fileTypeTable, meta.fileNum))
 				if err != nil {
 					return "", "", fmt.Errorf("Open: %v", err)
 				}
 				defer f.Close()
-				r, err := sstable.NewReader(f, 0, meta.FileNum, nil)
+				r, err := sstable.NewReader(f, 0, meta.fileNum, nil)
 				if err != nil {
 					return "", "", fmt.Errorf("NewReader: %v", err)
 				}
@@ -808,8 +807,8 @@ func TestCompactionShouldStopBefore(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			Smallest: InternalKey{UserKey: []byte(parts[0])},
-			Largest:  InternalKey{UserKey: []byte(parts[1])},
+			smallest: InternalKey{UserKey: []byte(parts[0])},
+			largest:  InternalKey{UserKey: []byte(parts[1])},
 		}
 	}
 
@@ -829,13 +828,13 @@ func TestCompactionShouldStopBefore(t *testing.T) {
 
 					meta := parseMeta(parts[0])
 					var err error
-					meta.Size, err = strconv.ParseUint(parts[1], 10, 64)
+					meta.size, err = strconv.ParseUint(parts[1], 10, 64)
 					if err != nil {
 						return err.Error()
 					}
 					grandparents = append(grandparents, meta)
 				}
-				manifest.SortBySmallest(grandparents, cmp)
+				sort.Sort(bySmallest{grandparents, cmp})
 				return ""
 
 			case "compact":
@@ -913,8 +912,8 @@ func TestCompactionExpandInputs(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			Smallest: base.ParseInternalKey(parts[0]),
-			Largest:  base.ParseInternalKey(parts[1]),
+			smallest: base.ParseInternalKey(parts[0]),
+			largest:  base.ParseInternalKey(parts[1]),
 		}
 	}
 
@@ -928,10 +927,10 @@ func TestCompactionExpandInputs(t *testing.T) {
 				}
 				for _, data := range strings.Split(d.Input, "\n") {
 					meta := parseMeta(data)
-					meta.FileNum = uint64(len(files))
+					meta.fileNum = uint64(len(files))
 					files = append(files, meta)
 				}
-				manifest.SortBySmallest(files, cmp)
+				sort.Sort(bySmallest{files, cmp})
 				return ""
 
 			case "expand-inputs":
@@ -940,7 +939,7 @@ func TestCompactionExpandInputs(t *testing.T) {
 					version:    &version{},
 					startLevel: 1,
 				}
-				c.version.Files[c.startLevel] = files
+				c.version.files[c.startLevel] = files
 				if len(d.CmdArgs) != 1 {
 					return fmt.Sprintf("%s expects 1 argument", d.Cmd)
 				}
@@ -954,7 +953,7 @@ func TestCompactionExpandInputs(t *testing.T) {
 				var buf bytes.Buffer
 				for i := range inputs {
 					f := &inputs[i]
-					fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest)
+					fmt.Fprintf(&buf, "%d: %s-%s\n", f.fileNum, f.smallest, f.largest)
 				}
 				return buf.String()
 
@@ -974,8 +973,8 @@ func TestCompactionAtomicUnitBounds(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			Smallest: base.ParseInternalKey(parts[0]),
-			Largest:  base.ParseInternalKey(parts[1]),
+			smallest: base.ParseInternalKey(parts[0]),
+			largest:  base.ParseInternalKey(parts[1]),
 		}
 	}
 
@@ -989,10 +988,10 @@ func TestCompactionAtomicUnitBounds(t *testing.T) {
 				}
 				for _, data := range strings.Split(d.Input, "\n") {
 					meta := parseMeta(data)
-					meta.FileNum = uint64(len(files))
+					meta.fileNum = uint64(len(files))
 					files = append(files, meta)
 				}
-				manifest.SortBySmallest(files, cmp)
+				sort.Sort(bySmallest{files, cmp})
 				return ""
 
 			case "atomic-unit-bounds":
@@ -1031,8 +1030,8 @@ func TestCompactionAllowZeroSeqNum(t *testing.T) {
 			t.Fatalf("malformed table spec: %s: %s", s, err)
 		}
 		meta = fileMetadata{
-			Smallest: InternalKey{UserKey: []byte(match[2])},
-			Largest:  InternalKey{UserKey: []byte(match[3])},
+			smallest: InternalKey{UserKey: []byte(match[2])},
+			largest:  InternalKey{UserKey: []byte(match[3])},
 		}
 		return level, meta
 	}
diff --git a/data_test.go b/data_test.go
index f15b61b..62356fe 100644
--- a/data_test.go
+++ b/data_test.go
@@ -308,10 +308,10 @@ func runDBDefineCmd(td *datadriven.TestData, opts *Options) (*DB, error) {
 		if err != nil {
 			return nil
 		}
-		for _, f := range newVE.NewFiles {
-			ve.NewFiles = append(ve.NewFiles, newFileEntry{
-				Level: level,
-				Meta:  f.Meta,
+		for _, f := range newVE.newFiles {
+			ve.newFiles = append(ve.newFiles, newFileEntry{
+				level: level,
+				meta:  f.meta,
 			})
 		}
 		level = -1
@@ -361,16 +361,16 @@ func runDBDefineCmd(td *datadriven.TestData, opts *Options) (*DB, error) {
 		return nil, err
 	}
 
-	if len(ve.NewFiles) > 0 {
+	if len(ve.newFiles) > 0 {
 		jobID := d.mu.nextJobID
 		d.mu.nextJobID++
-		if err := d.mu.versions.logAndApply(jobID, ve, nil, d.dataDir); err != nil {
+		if err := d.mu.versions.logAndApply(jobID, ve, d.dataDir); err != nil {
 			return nil, err
 		}
 		d.updateReadStateLocked()
-		for i := range ve.NewFiles {
-			meta := &ve.NewFiles[i].Meta
-			delete(d.mu.compact.pendingOutputs, meta.FileNum)
+		for i := range ve.newFiles {
+			meta := &ve.newFiles[i].meta
+			delete(d.mu.compact.pendingOutputs, meta.fileNum)
 		}
 	}
 
diff --git a/db_test.go b/db_test.go
index 52b0ae9..6bb6fb7 100644
--- a/db_test.go
+++ b/db_test.go
@@ -629,7 +629,7 @@ func TestRollManifest(t *testing.T) {
 	manifestFileNumber := func() uint64 {
 		d.mu.Lock()
 		defer d.mu.Unlock()
-		return d.mu.versions.manifestFileNum
+		return d.mu.versions.manifestFileNumber
 	}
 
 	current := func() string {
diff --git a/get_iter_test.go b/get_iter_test.go
index 1d0227f..cd4ac56 100644
--- a/get_iter_test.go
+++ b/get_iter_test.go
@@ -471,7 +471,7 @@ func TestGetIter(t *testing.T) {
 		newIter := func(
 			meta *fileMetadata, _ *IterOptions, _ *uint64,
 		) (internalIterator, internalIterator, error) {
-			d, ok := m[meta.FileNum]
+			d, ok := m[meta.fileNum]
 			if !ok {
 				return nil, nil, errors.New("no such file")
 			}
@@ -485,7 +485,7 @@ func TestGetIter(t *testing.T) {
 			m[tt.fileNum] = d
 
 			meta := fileMetadata{
-				FileNum: tt.fileNum,
+				fileNum: tt.fileNum,
 			}
 			for i, datum := range tt.data {
 				s := strings.Split(datum, " ")
@@ -496,30 +496,30 @@ func TestGetIter(t *testing.T) {
 				}
 
 				if i == 0 {
-					meta.Smallest = ikey
-					meta.SmallestSeqNum = ikey.SeqNum()
-					meta.Largest = ikey
-					meta.LargestSeqNum = ikey.SeqNum()
+					meta.smallest = ikey
+					meta.smallestSeqNum = ikey.SeqNum()
+					meta.largest = ikey
+					meta.largestSeqNum = ikey.SeqNum()
 				} else {
-					if base.InternalCompare(cmp, ikey, meta.Smallest) < 0 {
-						meta.Smallest = ikey
+					if base.InternalCompare(cmp, ikey, meta.smallest) < 0 {
+						meta.smallest = ikey
 					}
-					if base.InternalCompare(cmp, ikey, meta.Largest) > 0 {
-						meta.Largest = ikey
+					if base.InternalCompare(cmp, ikey, meta.largest) > 0 {
+						meta.largest = ikey
 					}
-					if meta.SmallestSeqNum > ikey.SeqNum() {
-						meta.SmallestSeqNum = ikey.SeqNum()
+					if meta.smallestSeqNum > ikey.SeqNum() {
+						meta.smallestSeqNum = ikey.SeqNum()
 					}
-					if meta.LargestSeqNum < ikey.SeqNum() {
-						meta.LargestSeqNum = ikey.SeqNum()
+					if meta.largestSeqNum < ikey.SeqNum() {
+						meta.largestSeqNum = ikey.SeqNum()
 					}
 				}
 			}
 
-			v.Files[tt.level] = append(v.Files[tt.level], meta)
+			v.files[tt.level] = append(v.files[tt.level], meta)
 		}
 
-		err := v.CheckOrdering(cmp)
+		err := v.checkOrdering(cmp)
 		if tc.badOrdering && err == nil {
 			t.Errorf("desc=%q: want bad ordering, got nil error", desc)
 			continue
@@ -539,7 +539,7 @@ func TestGetIter(t *testing.T) {
 			get.equal = equal
 			get.newIters = newIter
 			get.key = ikey.UserKey
-			get.l0 = v.Files[0]
+			get.l0 = v.files[0]
 			get.version = v
 			get.snapshot = ikey.SeqNum() + 1
 
diff --git a/ingest_test.go b/ingest_test.go
index b9554a0..f937070 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -17,7 +17,6 @@ import (
 	"github.com/kr/pretty"
 	"github.com/petermattis/pebble/internal/base"
 	"github.com/petermattis/pebble/internal/datadriven"
-	"github.com/petermattis/pebble/internal/manifest"
 	"github.com/petermattis/pebble/sstable"
 	"github.com/petermattis/pebble/vfs"
 	"golang.org/x/exp/rand"
@@ -57,7 +56,7 @@ func TestIngestLoad(t *testing.T) {
 			}
 			var buf bytes.Buffer
 			for _, m := range meta {
-				fmt.Fprintf(&buf, "%d: %s-%s\n", m.FileNum, m.Smallest, m.Largest)
+				fmt.Fprintf(&buf, "%d: %s-%s\n", m.fileNum, m.smallest, m.largest)
 			}
 			return buf.String()
 
@@ -87,7 +86,7 @@ func TestIngestLoadRand(t *testing.T) {
 		paths[i] = fmt.Sprint(i)
 		pending[i] = uint64(rng.Int63())
 		expected[i] = &fileMetadata{
-			FileNum: pending[i],
+			fileNum: pending[i],
 		}
 
 		func() {
@@ -108,8 +107,8 @@ func TestIngestLoadRand(t *testing.T) {
 				return base.InternalCompare(cmp, keys[i], keys[j]) < 0
 			})
 
-			expected[i].Smallest = keys[0]
-			expected[i].Largest = keys[len(keys)-1]
+			expected[i].smallest = keys[0]
+			expected[i].largest = keys[len(keys)-1]
 
 			w := sstable.NewWriter(f, nil, LevelOptions{})
 			for i := range keys {
@@ -123,7 +122,7 @@ func TestIngestLoadRand(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
-			expected[i].Size = meta.Size
+			expected[i].size = meta.Size
 		}()
 	}
 
@@ -198,8 +197,8 @@ func TestIngestSortAndVerify(t *testing.T) {
 						return fmt.Sprintf("range %v-%v is not valid", smallest, largest)
 					}
 					meta = append(meta, &fileMetadata{
-						Smallest: smallest,
-						Largest:  largest,
+						smallest: smallest,
+						largest:  largest,
 					})
 				}
 				err := ingestSortAndVerify(cmp, meta)
@@ -207,7 +206,7 @@ func TestIngestSortAndVerify(t *testing.T) {
 					return fmt.Sprintf("%v\n", err)
 				}
 				for i := range meta {
-					fmt.Fprintf(&buf, "%v-%v\n", meta[i].Smallest, meta[i].Largest)
+					fmt.Fprintf(&buf, "%v-%v\n", meta[i].smallest, meta[i].largest)
 				}
 				return buf.String()
 
@@ -239,7 +238,7 @@ func TestIngestLink(t *testing.T) {
 			for j := range paths {
 				paths[j] = fmt.Sprintf("external%d", j)
 				meta[j] = &fileMetadata{}
-				meta[j].FileNum = uint64(j)
+				meta[j].fileNum = uint64(j)
 				f, err := mem.Create(paths[j])
 				if err != nil {
 					t.Fatal(err)
@@ -334,8 +333,8 @@ func TestIngestMemtableOverlaps(t *testing.T) {
 					parts[0], parts[1] = parts[1], parts[0]
 				}
 				return &fileMetadata{
-					Smallest: InternalKey{UserKey: []byte(parts[0])},
-					Largest:  InternalKey{UserKey: []byte(parts[1])},
+					smallest: InternalKey{UserKey: []byte(parts[0])},
+					largest:  InternalKey{UserKey: []byte(parts[1])},
 				}
 			}
 
@@ -396,8 +395,8 @@ func TestIngestTargetLevel(t *testing.T) {
 			t.Fatalf("malformed table spec: %s", s)
 		}
 		return fileMetadata{
-			Smallest: InternalKey{UserKey: []byte(parts[0])},
-			Largest:  InternalKey{UserKey: []byte(parts[1])},
+			smallest: InternalKey{UserKey: []byte(parts[0])},
+			largest:  InternalKey{UserKey: []byte(parts[1])},
 		}
 	}
 
@@ -417,17 +416,17 @@ func TestIngestTargetLevel(t *testing.T) {
 				if err != nil {
 					return err.Error()
 				}
-				if vers.Files[level] != nil {
+				if vers.files[level] != nil {
 					return fmt.Sprintf("level %d already filled", level)
 				}
 				for _, table := range strings.Fields(parts[1]) {
-					vers.Files[level] = append(vers.Files[level], parseMeta(table))
+					vers.files[level] = append(vers.files[level], parseMeta(table))
 				}
 
 				if level == 0 {
-					manifest.SortBySeqNum(vers.Files[level])
+					sort.Sort(bySeqNum(vers.files[level]))
 				} else {
-					manifest.SortBySmallest(vers.Files[level], cmp)
+					sort.Sort(bySmallest{vers.files[level], cmp})
 				}
 			}
 			return ""
diff --git a/level_iter_test.go b/level_iter_test.go
index 4125488..cbadb73 100644
--- a/level_iter_test.go
+++ b/level_iter_test.go
@@ -27,7 +27,7 @@ func TestLevelIter(t *testing.T) {
 	newIters := func(
 		meta *fileMetadata, opts *IterOptions, bytesIterated *uint64,
 	) (internalIterator, internalIterator, error) {
-		f := *iters[meta.FileNum]
+		f := *iters[meta.fileNum]
 		return &f, nil, nil
 	}
 
@@ -47,10 +47,10 @@ func TestLevelIter(t *testing.T) {
 				iters = append(iters, f)
 
 				meta := fileMetadata{
-					FileNum: uint64(len(files)),
+					fileNum: uint64(len(files)),
 				}
-				meta.Smallest = f.keys[0]
-				meta.Largest = f.keys[len(f.keys)-1]
+				meta.smallest = f.keys[0]
+				meta.largest = f.keys[len(f.keys)-1]
 				files = append(files, meta)
 			}
 
@@ -129,7 +129,7 @@ func TestLevelIterBoundaries(t *testing.T) {
 	newIters := func(
 		meta *fileMetadata, _ *IterOptions, _ *uint64,
 	) (internalIterator, internalIterator, error) {
-		return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+		return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 	}
 
 	datadriven.RunTest(t, "testdata/level_iter_boundaries", func(d *datadriven.TestData) string {
@@ -193,14 +193,14 @@ func TestLevelIterBoundaries(t *testing.T) {
 			}
 			readers = append(readers, r)
 			files = append(files, fileMetadata{
-				FileNum:  fileNum,
-				Smallest: meta.Smallest(cmp),
-				Largest:  meta.Largest(cmp),
+				fileNum:  fileNum,
+				smallest: meta.Smallest(cmp),
+				largest:  meta.Largest(cmp),
 			})
 
 			var buf bytes.Buffer
 			for _, f := range files {
-				fmt.Fprintf(&buf, "%d: %s-%s\n", f.FileNum, f.Smallest, f.Largest)
+				fmt.Fprintf(&buf, "%d: %s-%s\n", f.fileNum, f.smallest, f.largest)
 			}
 			return buf.String()
 
@@ -274,10 +274,10 @@ func buildLevelIterTables(
 	for i := range readers {
 		iter := readers[i].NewIter(nil /* lower */, nil /* upper */)
 		key, _ := iter.First()
-		meta[i].FileNum = uint64(i)
-		meta[i].Smallest = *key
+		meta[i].fileNum = uint64(i)
+		meta[i].smallest = *key
 		key, _ = iter.Last()
-		meta[i].Largest = *key
+		meta[i].largest = *key
 	}
 	return readers, meta, keys
 }
@@ -295,7 +295,7 @@ func BenchmarkLevelIterSeekGE(b *testing.B) {
 							newIters := func(
 								meta *fileMetadata, _ *IterOptions, _ *uint64,
 							) (internalIterator, internalIterator, error) {
-								return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+								return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 							}
 							l := newLevelIter(nil, DefaultComparer.Compare, newIters, files, nil)
 							rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano())))
@@ -323,7 +323,7 @@ func BenchmarkLevelIterNext(b *testing.B) {
 							newIters := func(
 								meta *fileMetadata, _ *IterOptions, _ *uint64,
 							) (internalIterator, internalIterator, error) {
-								return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+								return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 							}
 							l := newLevelIter(nil, DefaultComparer.Compare, newIters, files, nil)
 
@@ -353,7 +353,7 @@ func BenchmarkLevelIterPrev(b *testing.B) {
 							newIters := func(
 								meta *fileMetadata, _ *IterOptions, _ *uint64,
 							) (internalIterator, internalIterator, error) {
-								return readers[meta.FileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
+								return readers[meta.fileNum].NewIter(nil /* lower */, nil /* upper */), nil, nil
 							}
 							l := newLevelIter(nil, DefaultComparer.Compare, newIters, files, nil)
 
diff --git a/table_cache_test.go b/table_cache_test.go
index 65d9b51..40cb55c 100644
--- a/table_cache_test.go
+++ b/table_cache_test.go
@@ -174,7 +174,7 @@ func testTableCacheRandomAccess(t *testing.T, concurrent bool) {
 			fileNum, sleepTime := rng.Intn(tableCacheTestNumTables), rng.Intn(1000)
 			rngMu.Unlock()
 			iter, _, err := c.newIters(
-				&fileMetadata{FileNum: uint64(fileNum)},
+				&fileMetadata{fileNum: uint64(fileNum)},
 				nil, /* iter options */
 				nil /* bytes iterated */)
 			if err != nil {
@@ -236,7 +236,7 @@ func TestTableCacheFrequentlyUsed(t *testing.T) {
 	for i := 0; i < N; i++ {
 		for _, j := range [...]int{pinned0, i % tableCacheTestNumTables, pinned1} {
 			iter, _, err := c.newIters(
-				&fileMetadata{FileNum: uint64(j)},
+				&fileMetadata{fileNum: uint64(j)},
 				nil, /* iter options */
 				nil /* bytes iterated */)
 			if err != nil {
@@ -274,7 +274,7 @@ func TestTableCacheEvictions(t *testing.T) {
 	for i := 0; i < N; i++ {
 		j := rng.Intn(tableCacheTestNumTables)
 		iter, _, err := c.newIters(
-			&fileMetadata{FileNum: uint64(j)},
+			&fileMetadata{fileNum: uint64(j)},
 			nil, /* iter options */
 			nil /* bytes iterated */)
 		if err != nil {
@@ -317,7 +317,7 @@ func TestTableCacheIterLeak(t *testing.T) {
 		t.Fatal(err)
 	}
 	if _, _, err := c.newIters(
-		&fileMetadata{FileNum: 0},
+		&fileMetadata{fileNum: 0},
 		nil, /* iter options */
 		nil /* bytes iterated */); err != nil {
 		t.Fatal(err)
diff --git a/tool/data_test.go b/tool/data_test.go
index 167eda8..f3f86c3 100644
--- a/tool/data_test.go
+++ b/tool/data_test.go
@@ -10,9 +10,7 @@ import (
 	"path/filepath"
 	"strings"
 	"testing"
-	"time"
 
-	"github.com/petermattis/pebble/internal/base"
 	"github.com/petermattis/pebble/internal/datadriven"
 	"github.com/spf13/cobra"
 )
@@ -49,34 +47,14 @@ func runTests(t *testing.T, path string) {
 				stderr = &buf
 				osExit = func(int) {}
 
-				var secs int64
-				timeNow = func() time.Time { secs++; return time.Unix(secs, 0) }
-
 				defer func() {
 					stdout = os.Stdout
 					stderr = os.Stderr
 					osExit = os.Exit
-					timeNow = time.Now
 				}()
 
-				tool := New()
-				// Register a test comparer and merger so that we can check the
-				// behavior of tools when the comparer and merger do not match.
-				tool.RegisterComparer(func() *Comparer {
-					var c Comparer
-					c = *base.DefaultComparer
-					c.Name = "test-comparer"
-					return &c
-				}())
-				tool.RegisterMerger(func() *Merger {
-					var m Merger
-					m = *base.DefaultMerger
-					m.Name = "test-merger"
-					return &m
-				}())
-
 				c := &cobra.Command{}
-				c.AddCommand(tool.Commands...)
+				c.AddCommand(New().Commands...)
 				c.SetArgs(args)
 				c.SetOutput(&buf)
 				if err := c.Execute(); err != nil {
diff --git a/tool/db_test.go b/tool/db_test.go
deleted file mode 100644
index 7b1ce69..0000000
--- a/tool/db_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright 2019 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 tool
-
-import (
-	"testing"
-)
-
-func TestDB(t *testing.T) {
-	runTests(t, "testdata/db_*")
-}
diff --git a/tool/manifest_test.go b/tool/manifest_test.go
deleted file mode 100644
index aa5633c..0000000
--- a/tool/manifest_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright 2019 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 tool
-
-import (
-	"testing"
-)
-
-func TestManifest(t *testing.T) {
-	runTests(t, "testdata/manifest_*")
-}
diff --git a/tool/testdata/db_check b/tool/testdata/db_check
deleted file mode 100644
index 590f495..0000000
--- a/tool/testdata/db_check
+++ /dev/null
@@ -1,39 +0,0 @@
-db check
-----
-accepts 1 arg(s), received 0
-
-db check
-non-existent
-----
-open non-existent/LOCK: no such file or directory
-
-db check
-../testdata/db-stage-4
-----
-scanned 2 records in 1.0s
-
-db check
-../testdata/db-stage-4
---comparer=foo
-----
-unknown comparer "foo"
-
-db check
-../testdata/db-stage-4
---comparer=test-comparer
-----
-pebble: manifest file "MANIFEST-000005" for DB "../testdata/db-stage-4": comparer name from file "leveldb.BytewiseComparator" != comparer name from Options "test-comparer"
-
-db check
-../testdata/db-stage-4
---merger=foo
-----
-unknown merger "foo"
-
-# TODO(peter): this DB does not have any merge records and the merge
-# operator in the OPTIONS file is "nullptr".
-db check
-../testdata/db-stage-4
---merger=test-merger
-----
-scanned 2 records in 1.0s
diff --git a/tool/testdata/db_lsm b/tool/testdata/db_lsm
deleted file mode 100644
index 0277f41..0000000
--- a/tool/testdata/db_lsm
+++ /dev/null
@@ -1,22 +0,0 @@
-db lsm
-----
-accepts 1 arg(s), received 0
-
-db lsm
-non-existent
-----
-open non-existent/LOCK: no such file or directory
-
-db lsm
-../testdata/db-stage-4
-----
-level__files____size___score______in__ingest____move____read___write___w-amp
-  WAL      1    82 B       -     0 B       -       -       -    82 B     0.0
-    0      1   986 B    0.25     0 B     0 B     0 B     0 B     0 B     0.0
-    1      0     0 B    0.00     0 B     0 B     0 B     0 B     0 B     0.0
-    2      0     0 B    0.00     0 B     0 B     0 B     0 B     0 B     0.0
-    3      0     0 B    0.00     0 B     0 B     0 B     0 B     0 B     0.0
-    4      0     0 B    0.00     0 B     0 B     0 B     0 B     0 B     0.0
-    5      0     0 B    0.00     0 B     0 B     0 B     0 B     0 B     0.0
-    6      0     0 B    0.00     0 B     0 B     0 B     0 B     0 B     0.0
-total      1   986 B    0.00    82 B     0 B     0 B     0 B    82 B     1.0
diff --git a/tool/testdata/db_scan b/tool/testdata/db_scan
deleted file mode 100644
index 007f7b2..0000000
--- a/tool/testdata/db_scan
+++ /dev/null
@@ -1,77 +0,0 @@
-db scan
-----
-accepts 1 arg(s), received 0
-
-db scan
-non-existent
-----
-open non-existent/LOCK: no such file or directory
-
-db scan
-../testdata/db-stage-4
-----
-foo [66697665]
-quux [736978]
-scanned 2 records in 1.0s
-
-db scan
-../testdata/db-stage-4
---comparer=foo
-----
-unknown comparer "foo"
-
-db scan
-../testdata/db-stage-4
---comparer=test-comparer
-----
-pebble: manifest file "MANIFEST-000005" for DB "../testdata/db-stage-4": comparer name from file "leveldb.BytewiseComparator" != comparer name from Options "test-comparer"
-
-db scan
-../testdata/db-stage-4
---merger=foo
-----
-unknown merger "foo"
-
-# TODO(peter): this DB does not have any merge records and the merge
-# operator in the OPTIONS file is "nullptr".
-db scan
-../testdata/db-stage-4
---merger=test-merger
-----
-foo [66697665]
-quux [736978]
-scanned 2 records in 1.0s
-
-db scan
-../testdata/db-stage-4
---key=%x
---value=size
-----
-666f6f <4>
-71757578 <3>
-scanned 2 records in 1.0s
-
-db scan
-../testdata/db-stage-4
---key=%x
---value=null
---start=quux
-----
-71757578
-scanned 1 record in 1.0s
-
-db scan
-../testdata/db-stage-4
---key=null
---value=size
---end=quux
-----
-<4>
-scanned 1 record in 1.0s
-
-db scan
-../testdata/db-stage-4
---key=null
---value=null
-----
-scanned 2 records in 1.0s
diff --git a/tool/testdata/manifest_dump b/tool/testdata/manifest_dump
deleted file mode 100644
index 235bd7b..0000000
--- a/tool/testdata/manifest_dump
+++ /dev/null
@@ -1,79 +0,0 @@
-manifest dump
-----
-requires at least 1 arg(s), only received 0
-
-manifest dump
-../testdata/db-stage-2/MANIFEST-000001
-----
-../testdata/db-stage-2/MANIFEST-000001
-0
-  <empty>
-EOF
-
-manifest dump
-../testdata/db-stage-4/MANIFEST-000005
-----
-../testdata/db-stage-4/MANIFEST-000005
-0
-  comparer:     leveldb.BytewiseComparator
-35
-  <empty>
-44
-  log-num:      4
-  last-seq-num: 5
-  added:        L0 4:986[bar#5,0-foo#4,1]
-EOF
---- L0 ---
-  4:986[bar#5,0-foo#4,1]
---- L1 ---
---- L2 ---
---- L3 ---
---- L4 ---
---- L5 ---
---- L6 ---
-
-manifest dump
-../testdata/db-stage-4/MANIFEST-000005
---key=%x
-----
-../testdata/db-stage-4/MANIFEST-000005
-0
-  comparer:     leveldb.BytewiseComparator
-35
-  <empty>
-44
-  log-num:      4
-  last-seq-num: 5
-  added:        L0 4:986[626172#5,0-666f6f#4,1]
-EOF
---- L0 ---
-  4:986[626172#5,0-666f6f#4,1]
---- L1 ---
---- L2 ---
---- L3 ---
---- L4 ---
---- L5 ---
---- L6 ---
-
-manifest dump
-../testdata/db-stage-4/MANIFEST-000005
---key=null
-----
-../testdata/db-stage-4/MANIFEST-000005
-0
-  comparer:     leveldb.BytewiseComparator
-35
-  <empty>
-44
-  log-num:      4
-  last-seq-num: 5
-  added:        L0 4:986
-EOF
---- L0 ---
-  4:986
---- L1 ---
---- L2 ---
---- L3 ---
---- L4 ---
---- L5 ---
---- L6 ---
diff --git a/internal/manifest/version_edit_test.go b/version_edit_test.go
similarity index 64%
rename from internal/manifest/version_edit_test.go
rename to version_edit_test.go
index 6d1ea9b..da163d6 100644
--- a/internal/manifest/version_edit_test.go
+++ b/version_edit_test.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package manifest
+package pebble
 
 import (
 	"bytes"
@@ -19,13 +19,13 @@ import (
 	"github.com/petermattis/pebble/internal/record"
 )
 
-func checkRoundTrip(e0 VersionEdit) error {
-	var e1 VersionEdit
+func checkRoundTrip(e0 versionEdit) error {
+	var e1 versionEdit
 	buf := new(bytes.Buffer)
-	if err := e0.Encode(buf); err != nil {
+	if err := e0.encode(buf); err != nil {
 		return fmt.Errorf("encode: %v", err)
 	}
-	if err := e1.Decode(buf); err != nil {
+	if err := e1.decode(buf); err != nil {
 		return fmt.Errorf("decode: %v", err)
 	}
 	if !reflect.DeepEqual(e1, e0) {
@@ -35,46 +35,46 @@ func checkRoundTrip(e0 VersionEdit) error {
 }
 
 func TestVersionEditRoundTrip(t *testing.T) {
-	testCases := []VersionEdit{
+	testCases := []versionEdit{
 		// An empty version edit.
 		{},
 		// A complete version edit.
 		{
-			ComparerName: "11",
-			LogNum:       22,
-			PrevLogNum:   33,
-			NextFileNum:  44,
-			LastSeqNum:   55,
-			DeletedFiles: map[DeletedFileEntry]bool{
-				DeletedFileEntry{
-					Level:   3,
-					FileNum: 703,
+			comparatorName: "11",
+			logNumber:      22,
+			prevLogNumber:  33,
+			nextFileNumber: 44,
+			lastSequence:   55,
+			deletedFiles: map[deletedFileEntry]bool{
+				deletedFileEntry{
+					level:   3,
+					fileNum: 703,
 				}: true,
-				DeletedFileEntry{
-					Level:   4,
-					FileNum: 704,
+				deletedFileEntry{
+					level:   4,
+					fileNum: 704,
 				}: true,
 			},
-			NewFiles: []NewFileEntry{
+			newFiles: []newFileEntry{
 				{
-					Level: 5,
-					Meta: FileMetadata{
-						FileNum:  805,
-						Size:     8050,
-						Smallest: base.DecodeInternalKey([]byte("abc\x00\x01\x02\x03\x04\x05\x06\x07")),
-						Largest:  base.DecodeInternalKey([]byte("xyz\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
+					level: 5,
+					meta: fileMetadata{
+						fileNum:  805,
+						size:     8050,
+						smallest: base.DecodeInternalKey([]byte("abc\x00\x01\x02\x03\x04\x05\x06\x07")),
+						largest:  base.DecodeInternalKey([]byte("xyz\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
 					},
 				},
 				{
-					Level: 6,
-					Meta: FileMetadata{
-						FileNum:             806,
-						Size:                8060,
-						Smallest:            base.DecodeInternalKey([]byte("A\x00\x01\x02\x03\x04\x05\x06\x07")),
-						Largest:             base.DecodeInternalKey([]byte("Z\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
-						SmallestSeqNum:      3,
-						LargestSeqNum:       5,
-						MarkedForCompaction: true,
+					level: 6,
+					meta: fileMetadata{
+						fileNum:             806,
+						size:                8060,
+						smallest:            base.DecodeInternalKey([]byte("A\x00\x01\x02\x03\x04\x05\x06\x07")),
+						largest:             base.DecodeInternalKey([]byte("Z\x01\xff\xfe\xfd\xfc\xfb\xfa\xf9")),
+						smallestSeqNum:      3,
+						largestSeqNum:       5,
+						markedForCompaction: true,
 					},
 				},
 			},
@@ -91,7 +91,7 @@ func TestVersionEditDecode(t *testing.T) {
 	testCases := []struct {
 		filename     string
 		encodedEdits []string
-		edits        []VersionEdit
+		edits        []versionEdit
 	}{
 		// db-stage-1 and db-stage-2 have the same manifest.
 		{
@@ -99,9 +99,9 @@ func TestVersionEditDecode(t *testing.T) {
 			encodedEdits: []string{
 				"\x02\x00\x03\x02\x04\x00",
 			},
-			edits: []VersionEdit{
+			edits: []versionEdit{
 				{
-					NextFileNum: 2,
+					nextFileNumber: 2,
 				},
 			},
 		},
@@ -115,26 +115,26 @@ func TestVersionEditDecode(t *testing.T) {
 					"\x00\x05\x00\x00\x00\x00\x00\x00\vfoo\x01\x04\x00" +
 					"\x00\x00\x00\x00\x00\x03\x05",
 			},
-			edits: []VersionEdit{
+			edits: []versionEdit{
 				{
-					ComparerName: "leveldb.BytewiseComparator",
+					comparatorName: "leveldb.BytewiseComparator",
 				},
 				{},
 				{
-					LogNum:      4,
-					PrevLogNum:  0,
-					NextFileNum: 6,
-					LastSeqNum:  5,
-					NewFiles: []NewFileEntry{
+					logNumber:      4,
+					prevLogNumber:  0,
+					nextFileNumber: 6,
+					lastSequence:   5,
+					newFiles: []newFileEntry{
 						{
-							Level: 0,
-							Meta: FileMetadata{
-								FileNum:        4,
-								Size:           986,
-								Smallest:       base.MakeInternalKey([]byte("bar"), 5, base.InternalKeyKindDelete),
-								Largest:        base.MakeInternalKey([]byte("foo"), 4, base.InternalKeyKindSet),
-								SmallestSeqNum: 3,
-								LargestSeqNum:  5,
+							level: 0,
+							meta: fileMetadata{
+								fileNum:        4,
+								size:           986,
+								smallest:       base.MakeInternalKey([]byte("bar"), 5, InternalKeyKindDelete),
+								largest:        base.MakeInternalKey([]byte("foo"), 4, InternalKeyKindSet),
+								smallestSeqNum: 3,
+								largestSeqNum:  5,
 							},
 						},
 					},
@@ -145,7 +145,7 @@ func TestVersionEditDecode(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
-			f, err := os.Open("../../testdata/" + tc.filename)
+			f, err := os.Open("testdata/" + tc.filename)
 			if err != nil {
 				t.Fatalf("filename=%q: open error: %v", tc.filename, err)
 			}
@@ -171,8 +171,8 @@ func TestVersionEditDecode(t *testing.T) {
 					t.Fatalf("filename=%q i=%d: got encoded %q, want %q", tc.filename, i, s, tc.encodedEdits[i])
 				}
 
-				var edit VersionEdit
-				err = edit.Decode(bytes.NewReader(encodedEdit))
+				var edit versionEdit
+				err = edit.decode(bytes.NewReader(encodedEdit))
 				if err != nil {
 					t.Fatalf("filename=%q i=%d: decode error: %v", tc.filename, i, err)
 				}
diff --git a/internal/manifest/version_test.go b/version_test.go
similarity index 58%
rename from internal/manifest/version_test.go
rename to version_test.go
index 71ece03..ea7a8c1 100644
--- a/internal/manifest/version_test.go
+++ b/version_test.go
@@ -2,7 +2,7 @@
 // of this source code is governed by a BSD-style license that can be found in
 // the LICENSE file.
 
-package manifest
+package pebble
 
 import (
 	"fmt"
@@ -13,10 +13,6 @@ import (
 	"github.com/petermattis/pebble/internal/base"
 )
 
-func ikey(s string) InternalKey {
-	return base.MakeInternalKey([]byte(s), 0, base.InternalKeyKindSet)
-}
-
 func TestIkeyRange(t *testing.T) {
 	testCases := []struct {
 		input, want string
@@ -59,23 +55,23 @@ func TestIkeyRange(t *testing.T) {
 		},
 	}
 	for _, tc := range testCases {
-		var f []FileMetadata
+		var f []fileMetadata
 		if tc.input != "" {
 			for _, s := range strings.Split(tc.input, " ") {
-				f = append(f, FileMetadata{
-					Smallest: ikey(s[0:1]),
-					Largest:  ikey(s[2:3]),
+				f = append(f, fileMetadata{
+					smallest: ikey(s[0:1]),
+					largest:  ikey(s[2:3]),
 				})
 			}
 		}
 
-		smallest0, largest0 := KeyRange(base.DefaultComparer.Compare, f, nil)
+		smallest0, largest0 := ikeyRange(DefaultComparer.Compare, f, nil)
 		got0 := string(smallest0.UserKey) + "-" + string(largest0.UserKey)
 		if got0 != tc.want {
 			t.Errorf("first []fileMetadata is %v\ngot  %s\nwant %s", tc.input, got0, tc.want)
 		}
 
-		smallest1, largest1 := KeyRange(base.DefaultComparer.Compare, nil, f)
+		smallest1, largest1 := ikeyRange(DefaultComparer.Compare, nil, f)
 		got1 := string(smallest1.UserKey) + "-" + string(largest1.UserKey)
 		if got1 != tc.want {
 			t.Errorf("second []fileMetadata is %v\ngot  %s\nwant %s", tc.input, got1, tc.want)
@@ -84,88 +80,88 @@ func TestIkeyRange(t *testing.T) {
 }
 
 func TestOverlaps(t *testing.T) {
-	m00 := FileMetadata{
-		FileNum:  700,
-		Size:     1,
-		Smallest: base.ParseInternalKey("b.SET.7008"),
-		Largest:  base.ParseInternalKey("e.SET.7009"),
+	m00 := fileMetadata{
+		fileNum:  700,
+		size:     1,
+		smallest: base.ParseInternalKey("b.SET.7008"),
+		largest:  base.ParseInternalKey("e.SET.7009"),
 	}
-	m01 := FileMetadata{
-		FileNum:  701,
-		Size:     1,
-		Smallest: base.ParseInternalKey("c.SET.7018"),
-		Largest:  base.ParseInternalKey("f.SET.7019"),
+	m01 := fileMetadata{
+		fileNum:  701,
+		size:     1,
+		smallest: base.ParseInternalKey("c.SET.7018"),
+		largest:  base.ParseInternalKey("f.SET.7019"),
 	}
-	m02 := FileMetadata{
-		FileNum:  702,
-		Size:     1,
-		Smallest: base.ParseInternalKey("f.SET.7028"),
-		Largest:  base.ParseInternalKey("g.SET.7029"),
+	m02 := fileMetadata{
+		fileNum:  702,
+		size:     1,
+		smallest: base.ParseInternalKey("f.SET.7028"),
+		largest:  base.ParseInternalKey("g.SET.7029"),
 	}
-	m03 := FileMetadata{
-		FileNum:  703,
-		Size:     1,
-		Smallest: base.ParseInternalKey("x.SET.7038"),
-		Largest:  base.ParseInternalKey("y.SET.7039"),
+	m03 := fileMetadata{
+		fileNum:  703,
+		size:     1,
+		smallest: base.ParseInternalKey("x.SET.7038"),
+		largest:  base.ParseInternalKey("y.SET.7039"),
 	}
-	m04 := FileMetadata{
-		FileNum:  704,
-		Size:     1,
-		Smallest: base.ParseInternalKey("n.SET.7048"),
-		Largest:  base.ParseInternalKey("p.SET.7049"),
+	m04 := fileMetadata{
+		fileNum:  704,
+		size:     1,
+		smallest: base.ParseInternalKey("n.SET.7048"),
+		largest:  base.ParseInternalKey("p.SET.7049"),
 	}
-	m05 := FileMetadata{
-		FileNum:  705,
-		Size:     1,
-		Smallest: base.ParseInternalKey("p.SET.7058"),
-		Largest:  base.ParseInternalKey("p.SET.7059"),
+	m05 := fileMetadata{
+		fileNum:  705,
+		size:     1,
+		smallest: base.ParseInternalKey("p.SET.7058"),
+		largest:  base.ParseInternalKey("p.SET.7059"),
 	}
-	m06 := FileMetadata{
-		FileNum:  706,
-		Size:     1,
-		Smallest: base.ParseInternalKey("p.SET.7068"),
-		Largest:  base.ParseInternalKey("u.SET.7069"),
+	m06 := fileMetadata{
+		fileNum:  706,
+		size:     1,
+		smallest: base.ParseInternalKey("p.SET.7068"),
+		largest:  base.ParseInternalKey("u.SET.7069"),
 	}
-	m07 := FileMetadata{
-		FileNum:  707,
-		Size:     1,
-		Smallest: base.ParseInternalKey("r.SET.7078"),
-		Largest:  base.ParseInternalKey("s.SET.7079"),
+	m07 := fileMetadata{
+		fileNum:  707,
+		size:     1,
+		smallest: base.ParseInternalKey("r.SET.7078"),
+		largest:  base.ParseInternalKey("s.SET.7079"),
 	}
 
-	m10 := FileMetadata{
-		FileNum:  710,
-		Size:     1,
-		Smallest: base.ParseInternalKey("d.SET.7108"),
-		Largest:  base.ParseInternalKey("g.SET.7109"),
+	m10 := fileMetadata{
+		fileNum:  710,
+		size:     1,
+		smallest: base.ParseInternalKey("d.SET.7108"),
+		largest:  base.ParseInternalKey("g.SET.7109"),
 	}
-	m11 := FileMetadata{
-		FileNum:  711,
-		Size:     1,
-		Smallest: base.ParseInternalKey("g.SET.7118"),
-		Largest:  base.ParseInternalKey("j.SET.7119"),
+	m11 := fileMetadata{
+		fileNum:  711,
+		size:     1,
+		smallest: base.ParseInternalKey("g.SET.7118"),
+		largest:  base.ParseInternalKey("j.SET.7119"),
 	}
-	m12 := FileMetadata{
-		FileNum:  712,
-		Size:     1,
-		Smallest: base.ParseInternalKey("n.SET.7128"),
-		Largest:  base.ParseInternalKey("p.SET.7129"),
+	m12 := fileMetadata{
+		fileNum:  712,
+		size:     1,
+		smallest: base.ParseInternalKey("n.SET.7128"),
+		largest:  base.ParseInternalKey("p.SET.7129"),
 	}
-	m13 := FileMetadata{
-		FileNum:  713,
-		Size:     1,
-		Smallest: base.ParseInternalKey("p.SET.7138"),
-		Largest:  base.ParseInternalKey("p.SET.7139"),
+	m13 := fileMetadata{
+		fileNum:  713,
+		size:     1,
+		smallest: base.ParseInternalKey("p.SET.7138"),
+		largest:  base.ParseInternalKey("p.SET.7139"),
 	}
-	m14 := FileMetadata{
-		FileNum:  714,
-		Size:     1,
-		Smallest: base.ParseInternalKey("p.SET.7148"),
-		Largest:  base.ParseInternalKey("u.SET.7149"),
+	m14 := fileMetadata{
+		fileNum:  714,
+		size:     1,
+		smallest: base.ParseInternalKey("p.SET.7148"),
+		largest:  base.ParseInternalKey("u.SET.7149"),
 	}
 
-	v := Version{
-		Files: [NumLevels][]FileMetadata{
+	v := version{
+		files: [numLevels][]fileMetadata{
 			0: {m00, m01, m02, m03, m04, m05, m06, m07},
 			1: {m10, m11, m12, m13, m14},
 		},
@@ -242,12 +238,12 @@ func TestOverlaps(t *testing.T) {
 		{2, "a", "z", ""},
 	}
 
-	cmp := base.DefaultComparer.Compare
+	cmp := DefaultComparer.Compare
 	for _, tc := range testCases {
-		o := v.Overlaps(tc.level, cmp, []byte(tc.ukey0), []byte(tc.ukey1))
+		o := v.overlaps(tc.level, cmp, []byte(tc.ukey0), []byte(tc.ukey1))
 		s := make([]string, len(o))
 		for i, meta := range o {
-			s[i] = fmt.Sprintf("m%02d", meta.FileNum%100)
+			s[i] = fmt.Sprintf("m%02d", meta.fileNum%100)
 		}
 		got := strings.Join(s, " ")
 		if got != tc.want {
@@ -257,13 +253,15 @@ func TestOverlaps(t *testing.T) {
 }
 
 func TestVersionUnref(t *testing.T) {
-	list := &VersionList{}
-	list.Init(&sync.Mutex{})
-	v := &Version{Deleted: func([]uint64) {}}
-	v.Ref()
-	list.PushBack(v)
-	v.Unref()
-	if !list.Empty() {
+	list := &versionList{
+		mu: &sync.Mutex{},
+	}
+	list.init()
+	v := &version{vs: &versionSet{}}
+	v.ref()
+	list.pushBack(v)
+	v.unref()
+	if !list.empty() {
 		t.Fatalf("expected version list to be empty")
 	}
 }