Repository: cockroachdb/pebble (go)

Issue:
32-bit platform support
I'm getting this building on a 32bit system:

```
# github.com/cockroachdb/pebble/internal/batchskl
../../go/pkg/mod/github.com/cockroachdb/pebble@v0.0.0-20230826001808-0b401ee526b8/internal/batchskl/skl.go:310:18: maxNodesSize (untyped int constant 4294967295) overflows int
../../go/pkg/mod/github.com/cockroachdb/pebble@v0.0.0-20230826001808-0b401ee526b8/internal/batchskl/skl.go:320:16: cannot use maxNodesSize (untyped int constant 4294967295) as int value in assignment (overflows)
# github.com/cockroachdb/pebble/internal/testkeys
../../go/pkg/mod/github.com/cockroachdb/pebble@v0.0.0-20230826001808-0b401ee526b8/internal/testkeys/testkeys.go:30:46: cannot use math.MaxInt64 (untyped int constant 9223372036854775807) as int value in argument to fmt.Sprintf (overflows)
# github.com/cockroachdb/pebble/internal/arenaskl
../../go/pkg/mod/github.com/cockroachdb/pebble@v0.0.0-20230826001808-0b401ee526b8/internal/arenaskl/arena.go:46:16: math.MaxUint32 (untyped int constant 4294967295) overflows int
../../go/pkg/mod/github.com/cockroachdb/pebble@v0.0.0-20230826001808-0b401ee526b8/internal/arenaskl/arena.go:50:14: math.MaxUint32 (untyped int constant 4294967295) overflows int
```

I didn't look into it, but this is very common when an `int` is used rather than `int64` and is most likely an oversight.

Pull request description:
*: support 32-bit
This change makes the necessary code and test changes to allow building on 32-bit, and to make all tests pass.

We also add a CI target that uses `GOARCH=386`.

Fixes #2853

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index ad424925..46a13969 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -25,6 +25,19 @@ jobs:
 
     - run: make test generate
 
+  linux-32bit:
+    name: go-linux-32bit
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Set up Go
+        uses: actions/setup-go@v4
+        with:
+          go-version: "1.20"
+
+      - run: GOARCH=386 make test
+
   linux-crossversion:
     name: go-linux-crossversion
     runs-on: ubuntu-latest
diff --git a/cmd/pebble/fsbench.go b/cmd/pebble/fsbench.go
index 087a4786..94d437db 100644
--- a/cmd/pebble/fsbench.go
+++ b/cmd/pebble/fsbench.go
@@ -129,7 +129,7 @@ func deleteFile(filepath string) {
 }
 
 // Write size bytes to the file in batches.
-func writeToFile(fh vfs.File, size int) {
+func writeToFile(fh vfs.File, size int64) {
 	for size > 0 {
 		var toWrite []byte
 		if size >= writeBatchSize {
@@ -144,7 +144,7 @@ func writeToFile(fh vfs.File, size int) {
 		if written != len(toWrite) {
 			log.Fatalf("Couldn't write %d bytes to file\n", size)
 		}
-		size -= len(toWrite)
+		size -= int64(len(toWrite))
 	}
 }
 
@@ -247,7 +247,7 @@ func createBench(benchName string, benchDescription string) fsBenchmark {
 // This benchmark prepopulates a directory with some files of a given size. Then, it creates and deletes
 // a file of some size, while measuring only the performance of the delete.
 func deleteBench(
-	benchName string, benchDescription string, preNumFiles int, preFileSize int, fileSize int,
+	benchName string, benchDescription string, preNumFiles int, preFileSize int64, fileSize int64,
 ) fsBenchmark {
 
 	createBench := func(dirpath string) *fsBench {
@@ -315,7 +315,7 @@ func deleteBench(
 // of the vfs.Remove function.
 // fileSize is in bytes.
 func deleteUniformBench(
-	benchName string, benchDescription string, numFiles int, fileSize int,
+	benchName string, benchDescription string, numFiles int, fileSize int64,
 ) fsBenchmark {
 	createBench := func(dirpath string) *fsBench {
 		bench := &fsBench{}
@@ -382,7 +382,7 @@ func deleteUniformBench(
 // Only measures the sync performance.
 // The writes will be synced after every writeSize bytes have been written.
 func writeSyncBench(
-	benchName string, benchDescription string, maxFileSize int, writeSize int,
+	benchName string, benchDescription string, maxFileSize int64, writeSize int64,
 ) fsBenchmark {
 
 	if writeSize > maxFileSize {
@@ -406,7 +406,7 @@ func writeSyncBench(
 			done         atomic.Bool
 			fh           vfs.File
 			fileNum      int
-			bytesWritten int
+			bytesWritten int64
 		}
 		benchData.fh = createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, benchData.fileNum)))
 
@@ -455,7 +455,7 @@ func writeSyncBench(
 // Tests the peformance of calling the vfs.GetDiskUsage call on a directory,
 // as the number of files/total size of files in the directory grows.
 func diskUsageBench(
-	benchName string, benchDescription string, maxFileSize int, writeSize int,
+	benchName string, benchDescription string, maxFileSize int64, writeSize int64,
 ) fsBenchmark {
 
 	if writeSize > maxFileSize {
@@ -479,7 +479,7 @@ func diskUsageBench(
 			done         atomic.Bool
 			fh           vfs.File
 			fileNum      int
-			bytesWritten int
+			bytesWritten int64
 		}
 		benchData.fh = createFile(path.Join(dirpath, fmt.Sprintf("%s%d", pref, benchData.fileNum)))
 
diff --git a/compaction.go b/compaction.go
index f7e09e2c..048437bd 100644
--- a/compaction.go
+++ b/compaction.go
@@ -1769,7 +1769,7 @@ func (d *DB) passedFlushThreshold() bool {
 		if d.mu.mem.queue[n].flushForced {
 			// A flush was forced. Pretend the memtable size is the configured
 			// size. See minFlushSize below.
-			size += uint64(d.opts.MemTableSize)
+			size += d.opts.MemTableSize
 		} else {
 			size += d.mu.mem.queue[n].totalBytes()
 		}
@@ -1783,7 +1783,7 @@ func (d *DB) passedFlushThreshold() bool {
 	// configured memtable size. This prevents flushing of memtables at startup
 	// while we're undergoing the ramp period on the memtable size. See
 	// DB.newMemTable().
-	minFlushSize := uint64(d.opts.MemTableSize) / 2
+	minFlushSize := d.opts.MemTableSize / 2
 	return size >= minFlushSize
 }
 
diff --git a/compaction_picker.go b/compaction_picker.go
index d885cda2..2ad225ea 100644
--- a/compaction_picker.go
+++ b/compaction_picker.go
@@ -1200,9 +1200,9 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact
 	// bytes have been compacted further down the LSM.
 	if n := len(env.inProgressCompactions); n > 0 {
 		l0ReadAmp := p.vers.L0Sublevels.MaxDepthAfterOngoingCompactions()
-		compactionDebt := int(p.estimatedCompactionDebt(0))
+		compactionDebt := p.estimatedCompactionDebt(0)
 		ccSignal1 := n * p.opts.Experimental.L0CompactionConcurrency
-		ccSignal2 := n * p.opts.Experimental.CompactionDebtConcurrency
+		ccSignal2 := uint64(n) * p.opts.Experimental.CompactionDebtConcurrency
 		if l0ReadAmp < ccSignal1 && compactionDebt < ccSignal2 {
 			return nil
 		}
diff --git a/db.go b/db.go
index 370a9b21..79a465bf 100644
--- a/db.go
+++ b/db.go
@@ -280,7 +280,7 @@ type DB struct {
 	abbreviatedKey AbbreviatedKey
 	// The threshold for determining when a batch is "large" and will skip being
 	// inserted into a memtable.
-	largeBatchThreshold int
+	largeBatchThreshold uint64
 	// The current OPTIONS file number.
 	optionsFileNum base.DiskFileNum
 	// The on-disk size of the current OPTIONS file.
@@ -407,7 +407,7 @@ type DB struct {
 			// is allocated up to Options.MemTableSize. This reduces the memory
 			// footprint of memtables when lots of DB instances are used concurrently
 			// in test environments.
-			nextSize int
+			nextSize uint64
 		}
 
 		compact struct {
@@ -842,7 +842,7 @@ func (d *DB) applyInternal(batch *Batch, opts *WriteOptions, noSyncWait bool) er
 	if batch.db == nil {
 		batch.refreshMemTableSize()
 	}
-	if int(batch.memTableSize) >= d.largeBatchThreshold {
+	if batch.memTableSize >= d.largeBatchThreshold {
 		batch.flushable = newFlushableBatch(batch, d.opts.Comparer)
 	}
 	if err := d.commit.Commit(batch, sync, noSyncWait); err != nil {
@@ -2296,7 +2296,7 @@ func (d *DB) walPreallocateSize() int {
 	// RocksDB. Could a smaller preallocation block size be used?
 	size := d.opts.MemTableSize
 	size = (size / 10) + size
-	return size
+	return int(size)
 }
 
 func (d *DB) newMemTable(logNum FileNum, logSeqNum uint64) (*memTable, *flushableEntry) {
@@ -2323,7 +2323,7 @@ func (d *DB) newMemTable(logNum FileNum, logSeqNum uint64) (*memTable, *flushabl
 	// existing memory.
 	var mem *memTable
 	mem = d.memTableRecycle.Swap(nil)
-	if mem != nil && len(mem.arenaBuf) != size {
+	if mem != nil && uint64(len(mem.arenaBuf)) != size {
 		d.freeMemTable(mem)
 		mem = nil
 	}
@@ -2334,7 +2334,7 @@ func (d *DB) newMemTable(logNum FileNum, logSeqNum uint64) (*memTable, *flushabl
 	} else {
 		mem = new(memTable)
 		memtblOpts.arenaBuf = manual.New(int(size))
-		memtblOpts.releaseAccountingReservation = d.opts.Cache.Reserve(size)
+		memtblOpts.releaseAccountingReservation = d.opts.Cache.Reserve(int(size))
 		d.memTableCount.Add(1)
 		d.memTableReserved.Add(int64(size))
 
@@ -2420,7 +2420,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
 			for i := range d.mu.mem.queue {
 				size += d.mu.mem.queue[i].totalBytes()
 			}
-			if size >= uint64(d.opts.MemTableStopWritesThreshold)*uint64(d.opts.MemTableSize) {
+			if size >= uint64(d.opts.MemTableStopWritesThreshold)*d.opts.MemTableSize {
 				// We have filled up the current memtable, but already queued memtables
 				// are still flushing, so we wait.
 				if !stalled {
@@ -2474,7 +2474,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
 		// reduces memtable memory pressure when an application is frequently
 		// manually flushing.
 		if (b == nil) && uint64(immMem.availBytes()) > immMem.totalBytes()/2 {
-			d.mu.mem.nextSize = int(immMem.totalBytes())
+			d.mu.mem.nextSize = immMem.totalBytes()
 		}
 
 		if b != nil && b.flushable != nil {
diff --git a/internal/arenaskl/arena.go b/internal/arenaskl/arena.go
index 8ee81413..011c3b02 100644
--- a/internal/arenaskl/arena.go
+++ b/internal/arenaskl/arena.go
@@ -18,11 +18,11 @@
 package arenaskl
 
 import (
-	"math"
 	"sync/atomic"
 	"unsafe"
 
 	"github.com/cockroachdb/errors"
+	"github.com/cockroachdb/pebble/internal/constants"
 	"github.com/cockroachdb/pebble/internal/invariants"
 )
 
@@ -43,11 +43,11 @@ var (
 // NewArena allocates a new arena using the specified buffer as the backing
 // store.
 func NewArena(buf []byte) *Arena {
-	if len(buf) > math.MaxUint32 {
+	if len(buf) > constants.MaxUint32OrInt {
 		if invariants.Enabled {
 			panic(errors.AssertionFailedf("attempting to create arena of size %d", len(buf)))
 		}
-		buf = buf[:math.MaxUint32]
+		buf = buf[:constants.MaxUint32OrInt]
 	}
 	a := &Arena{
 		buf: buf,
@@ -61,10 +61,10 @@ func NewArena(buf []byte) *Arena {
 // Size returns the number of bytes allocated by the arena.
 func (a *Arena) Size() uint32 {
 	s := a.n.Load()
-	if s > math.MaxUint32 {
+	if s > constants.MaxUint32OrInt {
 		// The last failed allocation can push the size higher than len(a.buf).
-		// Saturate at MaxUint32.
-		return math.MaxUint32
+		// Saturate at the maximum representable offset.
+		return constants.MaxUint32OrInt
 	}
 	return uint32(s)
 }
@@ -91,16 +91,16 @@ func (a *Arena) alloc(size, alignment, overflow uint32) (uint32, uint32, error)
 	}
 
 	// Pad the allocation with enough bytes to ensure the requested alignment.
-	padded := size + alignment - 1
+	padded := uint64(size) + uint64(alignment) - 1
 
-	newSize := a.n.Add(uint64(padded))
-	if int(newSize)+int(overflow) > len(a.buf) {
+	newSize := a.n.Add(padded)
+	if newSize+uint64(overflow) > uint64(len(a.buf)) {
 		return 0, 0, ErrArenaFull
 	}
 
 	// Return the aligned offset.
 	offset := (uint32(newSize) - size) & ^(alignment - 1)
-	return offset, padded, nil
+	return offset, uint32(padded), nil
 }
 
 func (a *Arena) getBytes(offset uint32, size uint32) []byte {
diff --git a/internal/batchskl/skl.go b/internal/batchskl/skl.go
index 73bfa0e1..f56d95c2 100644
--- a/internal/batchskl/skl.go
+++ b/internal/batchskl/skl.go
@@ -63,14 +63,15 @@ import (
 
 	"github.com/cockroachdb/errors"
 	"github.com/cockroachdb/pebble/internal/base"
+	"github.com/cockroachdb/pebble/internal/constants"
 	"golang.org/x/exp/rand"
 )
 
 const (
 	maxHeight    = 20
-	maxNodeSize  = int(unsafe.Sizeof(node{}))
-	linksSize    = int(unsafe.Sizeof(links{}))
-	maxNodesSize = math.MaxUint32
+	maxNodeSize  = uint64(unsafe.Sizeof(node{}))
+	linksSize    = uint64(unsafe.Sizeof(links{}))
+	maxNodesSize = constants.MaxUint32OrInt
 )
 
 var (
@@ -281,7 +282,7 @@ func (s *Skiplist) newNode(
 		panic("height cannot be less than one or greater than the max height")
 	}
 
-	unusedSize := (maxHeight - int(height)) * linksSize
+	unusedSize := uint64(maxHeight-int(height)) * linksSize
 	nodeOffset, err := s.alloc(uint32(maxNodeSize - unusedSize))
 	if err != nil {
 		return 0, err
@@ -296,13 +297,13 @@ func (s *Skiplist) newNode(
 }
 
 func (s *Skiplist) alloc(size uint32) (uint32, error) {
-	offset := len(s.nodes)
+	offset := uint64(len(s.nodes))
 
 	// We only have a need for memory up to offset + size, but we never want
 	// to allocate a node whose tail points into unallocated memory.
 	minAllocSize := offset + maxNodeSize
-	if cap(s.nodes) < minAllocSize {
-		allocSize := cap(s.nodes) * 2
+	if uint64(cap(s.nodes)) < minAllocSize {
+		allocSize := uint64(cap(s.nodes)) * 2
 		if allocSize < minAllocSize {
 			allocSize = minAllocSize
 		}
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
new file mode 100644
index 00000000..8d9198ce
--- /dev/null
+++ b/internal/constants/constants.go
@@ -0,0 +1,17 @@
+// Copyright 2023 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 constants
+
+const (
+	// oneIf64Bit is 1 on 64-bit platforms and 0 on 32-bit platforms.
+	oneIf64Bit = ^uint(0) >> 63
+
+	// MaxUint32OrInt returns min(MaxUint32, MaxInt), i.e
+	// - MaxUint32 on 64-bit platforms;
+	// - MaxInt on 32-bit platforms.
+	// It is used when slices are limited to Uint32 on 64-bit platforms (the
+	// length limit for slices is naturally MaxInt on 32-bit platforms).
+	MaxUint32OrInt = (1<<31)<<oneIf64Bit - 1
+)
diff --git a/mem_table.go b/mem_table.go
index 841d6d58..22d902ea 100644
--- a/mem_table.go
+++ b/mem_table.go
@@ -127,7 +127,7 @@ func newMemTable(opts memTableOptions) *memTable {
 
 func (m *memTable) init(opts memTableOptions) {
 	if opts.size == 0 {
-		opts.size = opts.MemTableSize
+		opts.size = int(opts.MemTableSize)
 	}
 	*m = memTable{
 		cmp:                          opts.Comparer.Compare,
diff --git a/metrics.go b/metrics.go
index 24e418b2..7d66d0d4 100644
--- a/metrics.go
+++ b/metrics.go
@@ -521,3 +521,16 @@ func percent(numerator, denominator int64) float64 {
 	}
 	return 100 * float64(numerator) / float64(denominator)
 }
+
+// StringForTests is identical to m.String() on 64-bit platforms. It is used to
+// provide a platform-independent result for tests.
+func (m *Metrics) StringForTests() string {
+	mCopy := *m
+	if math.MaxInt == math.MaxInt32 {
+		// This is the difference in Sizeof(sstable.Reader{})) between 64 and 32 bit
+		// platforms.
+		const tableCacheSizeAdjustment = 212
+		mCopy.TableCache.Size += mCopy.TableCache.Count * tableCacheSizeAdjustment
+	}
+	return redact.StringWithoutMarkers(&mCopy)
+}
diff --git a/open.go b/open.go
index 950477bc..97d3a0c2 100644
--- a/open.go
+++ b/open.go
@@ -21,6 +21,7 @@ import (
 	"github.com/cockroachdb/pebble/internal/arenaskl"
 	"github.com/cockroachdb/pebble/internal/base"
 	"github.com/cockroachdb/pebble/internal/cache"
+	"github.com/cockroachdb/pebble/internal/constants"
 	"github.com/cockroachdb/pebble/internal/invariants"
 	"github.com/cockroachdb/pebble/internal/manifest"
 	"github.com/cockroachdb/pebble/internal/manual"
@@ -37,15 +38,23 @@ const (
 
 	// The max batch size is limited by the uint32 offsets stored in
 	// internal/batchskl.node, DeferredBatchOp, and flushableBatchEntry.
-	// We limit the size to MaxUint32 so that the exclusive end of an allocation
-	// fits in uint32.
-	maxBatchSize = math.MaxUint32 // just short of 4 GB
+	//
+	// We limit the size to MaxUint32 (just short of 4GB) so that the exclusive
+	// end of an allocation fits in uint32.
+	//
+	// On 32-bit systems, slices are naturally limited to MaxInt (just short of
+	// 2GB).
+	maxBatchSize = constants.MaxUint32OrInt
 
 	// The max memtable size is limited by the uint32 offsets stored in
 	// internal/arenaskl.node, DeferredBatchOp, and flushableBatchEntry.
-	// We limit the size to MaxUint32 so that the exclusive end of an allocation
-	// fits in uint32.
-	maxMemTableSize = math.MaxUint32 // just short of 4 GB
+	//
+	// We limit the size to MaxUint32 (just short of 4GB) so that the exclusive
+	// end of an allocation fits in uint32.
+	//
+	// On 32-bit systems, slices are naturally limited to MaxInt (just short of
+	// 2GB).
+	maxMemTableSize = constants.MaxUint32OrInt
 )
 
 // TableCacheSize can be used to determine the table
@@ -164,7 +173,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
 		merge:               opts.Merger.Merge,
 		split:               opts.Comparer.Split,
 		abbreviatedKey:      opts.Comparer.AbbreviatedKey,
-		largeBatchThreshold: (opts.MemTableSize - int(memTableEmptySize)) / 2,
+		largeBatchThreshold: (opts.MemTableSize - uint64(memTableEmptySize)) / 2,
 		fileLock:            fileLock,
 		dataDir:             dataDir,
 		walDir:              walDir,
diff --git a/options.go b/options.go
index 29d5ca49..51325b76 100644
--- a/options.go
+++ b/options.go
@@ -538,7 +538,7 @@ type Options struct {
 		// concurrent compaction is added. This works "on top" of
 		// L0CompactionConcurrency, so the higher of the count of compaction
 		// concurrency slots as determined by the two options is chosen.
-		CompactionDebtConcurrency int
+		CompactionDebtConcurrency uint64
 
 		// ReadCompactionRate controls the frequency of read triggered
 		// compactions by adjusting `AllowedSeeks` in manifest.FileMetadata:
@@ -797,7 +797,7 @@ type Options struct {
 	// writing the contents of the old one in the
 	// background. MemTableStopWritesThreshold places a hard limit on the size of
 	// the queued MemTables.
-	MemTableSize int
+	MemTableSize uint64
 
 	// Hard limit on the size of queued of MemTables. Writes are stopped when the
 	// sum of the queued memtable sizes exceeds
@@ -1381,7 +1381,7 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
 					}
 				}
 			case "compaction_debt_concurrency":
-				o.Experimental.CompactionDebtConcurrency, err = strconv.Atoi(value)
+				o.Experimental.CompactionDebtConcurrency, err = strconv.ParseUint(value, 10, 64)
 			case "delete_range_flush_delay":
 				// NB: This is a deprecated serialization of the
 				// `flush_delay_delete_range`.
@@ -1446,7 +1446,7 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error {
 			case "max_open_files":
 				o.MaxOpenFiles, err = strconv.Atoi(value)
 			case "mem_table_size":
-				o.MemTableSize, err = strconv.Atoi(value)
+				o.MemTableSize, err = strconv.ParseUint(value, 10, 64)
 			case "mem_table_stop_writes_threshold":
 				o.MemTableStopWritesThreshold, err = strconv.Atoi(value)
 			case "min_compaction_rate":
diff --git a/replay/sampled_metric.go b/replay/sampled_metric.go
index 16c12cd2..5edcbe1a 100644
--- a/replay/sampled_metric.go
+++ b/replay/sampled_metric.go
@@ -118,7 +118,7 @@ func (m *SampledMetric) values(buckets int) (bucketDur time.Duration, values []f
 
 	for i, b := 0, 0; i < len(m.samples); i++ {
 		// Fill any buckets that precede this value with the previous value.
-		bi := int(m.samples[i].since) / int(bucketDur)
+		bi := int(m.samples[i].since / bucketDur)
 		if bi == buckets {
 			bi = buckets - 1
 		}
diff --git a/tool/lsm.go b/tool/lsm.go
index 7f095144..1cf9e0dc 100644
--- a/tool/lsm.go
+++ b/tool/lsm.go
@@ -53,7 +53,7 @@ type lsmState struct {
 	Edits     []lsmVersionEdit                 `json:",omitempty"`
 	Files     map[base.FileNum]lsmFileMetadata `json:",omitempty"`
 	Keys      []lsmKey                         `json:",omitempty"`
-	StartEdit int
+	StartEdit int64
 }
 
 type lsmT struct {
@@ -66,9 +66,9 @@ type lsmT struct {
 	fmtKey    keyFormatter
 	embed     bool
 	pretty    bool
-	startEdit int
-	endEdit   int
-	editCount int
+	startEdit int64
+	endEdit   int64
+	editCount int64
 
 	cmp    *base.Comparer
 	state  lsmState
@@ -105,9 +105,9 @@ indicate size).
 	l.Root.Flags().Var(&l.fmtKey, "key", "key formatter")
 	l.Root.Flags().BoolVar(&l.embed, "embed", true, "embed javascript in HTML (disable for development)")
 	l.Root.Flags().BoolVar(&l.pretty, "pretty", false, "pretty JSON output")
-	l.Root.Flags().IntVar(&l.startEdit, "start-edit", 0, "starting edit # to include in visualization")
-	l.Root.Flags().IntVar(&l.endEdit, "end-edit", math.MaxInt64, "ending edit # to include in visualization")
-	l.Root.Flags().IntVar(&l.editCount, "edit-count", math.MaxInt64, "count of edits to include in visualization")
+	l.Root.Flags().Int64Var(&l.startEdit, "start-edit", 0, "starting edit # to include in visualization")
+	l.Root.Flags().Int64Var(&l.endEdit, "end-edit", math.MaxInt64, "ending edit # to include in visualization")
+	l.Root.Flags().Int64Var(&l.editCount, "edit-count", math.MaxInt64, "count of edits to include in visualization")
 	return l
 }
 
@@ -148,10 +148,10 @@ func (l *lsmT) runLSM(cmd *cobra.Command, args []string) error {
 			return err
 		}
 	}
-	if l.endEdit < len(edits) {
+	if l.endEdit < int64(len(edits)) {
 		edits = edits[:l.endEdit-l.startEdit+1]
 	}
-	if l.editCount < len(edits) {
+	if l.editCount < int64(len(edits)) {
 		edits = edits[:l.editCount]
 	}
 
@@ -334,7 +334,7 @@ func (l *lsmT) buildEdits(edits []*manifest.VersionEdit) error {
 }
 
 func (l *lsmT) coalesceEdits(edits []*manifest.VersionEdit) ([]*manifest.VersionEdit, error) {
-	if l.startEdit >= len(edits) {
+	if l.startEdit >= int64(len(edits)) {
 		return nil, errors.Errorf("start-edit is more than the number of edits, %d", len(edits))
 	}
 


Test patch:
diff --git a/batch_test.go b/batch_test.go
index 18b25a2f..1f05c07e 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -771,7 +771,7 @@ func TestBatchOpDoesIncrement(t *testing.T) {
 func TestBatchGet(t *testing.T) {
 	testCases := []struct {
 		method       string
-		memTableSize int
+		memTableSize uint64
 	}{
 		{"build", 64 << 20},
 		{"build", 2 << 10},
diff --git a/cmd/pebble/replay_test.go b/cmd/pebble/replay_test.go
index 58302cec..b3f62255 100644
--- a/cmd/pebble/replay_test.go
+++ b/cmd/pebble/replay_test.go
@@ -35,7 +35,7 @@ func TestParseOptionsStr(t *testing.T) {
 		{
 			c: replayConfig{
 				maxCacheSize:  16 << 20, /* 16 MB */
-				optionsString: fmt.Sprintf(`[Options] cache_size=%d`, 10<<30 /* 10 GB */),
+				optionsString: fmt.Sprintf(`[Options] cache_size=%d`, int64(10<<30 /* 10 GB */)),
 			},
 			options: &pebble.Options{Cache: cache.New(16 << 20 /* 16 MB */)},
 		},
diff --git a/compaction_test.go b/compaction_test.go
index 7d66ec67..7f03f87c 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -3251,7 +3251,7 @@ func TestCompactFlushQueuedLargeBatch(t *testing.T) {
 
 	// Set a record with a large value. This will be transformed into a large
 	// batch and placed in the flushable queue.
-	require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("v"), d.largeBatchThreshold), nil))
+	require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("v"), int(d.largeBatchThreshold)), nil))
 	d.mu.Lock()
 	require.Greater(t, len(d.mu.mem.queue), 1)
 	d.mu.Unlock()
diff --git a/event_listener_test.go b/event_listener_test.go
index 64800e89..ffb0cee0 100644
--- a/event_listener_test.go
+++ b/event_listener_test.go
@@ -206,7 +206,7 @@ func TestEventListener(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "sstables":
 			var buf bytes.Buffer
diff --git a/ingest_test.go b/ingest_test.go
index c7574abe..fa263d7b 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -690,7 +690,7 @@ func TestExcise(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "wait-pending-table-stats":
 			return runTableStatsCmd(td, d)
@@ -986,7 +986,7 @@ func TestIngestShared(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "wait-pending-table-stats":
 			return runTableStatsCmd(td, d)
@@ -1458,7 +1458,7 @@ func TestConcurrentExcise(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "wait-pending-table-stats":
 			return runTableStatsCmd(td, d)
@@ -1684,7 +1684,7 @@ func TestIngestExternal(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "wait-pending-table-stats":
 			return runTableStatsCmd(td, d)
@@ -2063,7 +2063,7 @@ func TestIngest(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "wait-pending-table-stats":
 			return runTableStatsCmd(td, d)
@@ -2490,7 +2490,7 @@ func TestIngestFlushQueuedLargeBatch(t *testing.T) {
 
 	// Set a record with a large value. This will be transformed into a large
 	// batch and placed in the flushable queue.
-	require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("v"), d.largeBatchThreshold), nil))
+	require.NoError(t, d.Set([]byte("a"), bytes.Repeat([]byte("v"), int(d.largeBatchThreshold)), nil))
 
 	ingest := func(keys ...string) {
 		t.Helper()
diff --git a/internal/arenaskl/arena_test.go b/internal/arenaskl/arena_test.go
index f63b5b10..f264b8de 100644
--- a/internal/arenaskl/arena_test.go
+++ b/internal/arenaskl/arena_test.go
@@ -21,6 +21,7 @@ import (
 	"math"
 	"testing"
 
+	"github.com/cockroachdb/pebble/internal/constants"
 	"github.com/stretchr/testify/require"
 )
 
@@ -31,7 +32,7 @@ func newArena(n uint32) *Arena {
 // TestArenaSizeOverflow tests that large allocations do not cause Arena's
 // internal size accounting to overflow and produce incorrect results.
 func TestArenaSizeOverflow(t *testing.T) {
-	a := newArena(math.MaxUint32)
+	a := newArena(constants.MaxUint32OrInt)
 
 	// Allocating under the limit throws no error.
 	offset, _, err := a.alloc(math.MaxUint16, 1, 0)
@@ -43,10 +44,10 @@ func TestArenaSizeOverflow(t *testing.T) {
 	// overflow if 32-bit arithmetic was used. It shouldn't.
 	_, _, err = a.alloc(math.MaxUint32, 1, 0)
 	require.Equal(t, ErrArenaFull, err)
-	require.Equal(t, uint32(math.MaxUint32), a.Size())
+	require.Equal(t, uint32(constants.MaxUint32OrInt), a.Size())
 
 	// Continuing to allocate continues to throw an error.
 	_, _, err = a.alloc(math.MaxUint16, 1, 0)
 	require.Equal(t, ErrArenaFull, err)
-	require.Equal(t, uint32(math.MaxUint32), a.Size())
+	require.Equal(t, uint32(constants.MaxUint32OrInt), a.Size())
 }
diff --git a/internal/base/lazy_value_test.go b/internal/base/lazy_value_test.go
index f877b878..82ad51c7 100644
--- a/internal/base/lazy_value_test.go
+++ b/internal/base/lazy_value_test.go
@@ -25,7 +25,7 @@ func TestLazyValue(t *testing.T) {
 	// Both 40 and 48 bytes makes iteration benchmarks like
 	// BenchmarkIteratorScan/keys=1000,r-amp=1,key-types=points-only 75%
 	// slower.
-	require.Equal(t, 32, int(unsafe.Sizeof(LazyValue{})))
+	require.True(t, unsafe.Sizeof(LazyValue{}) <= 32)
 
 	fooBytes1 := []byte("foo")
 	fooLV1 := MakeInPlaceValue(fooBytes1)
diff --git a/internal/constants/constants_test.go b/internal/constants/constants_test.go
new file mode 100644
index 00000000..fa5a0c14
--- /dev/null
+++ b/internal/constants/constants_test.go
@@ -0,0 +1,20 @@
+// Copyright 2023 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 constants
+
+import (
+	"math"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestConstants(t *testing.T) {
+	if math.MaxInt == math.MaxInt64 {
+		require.Equal(t, uint64(math.MaxUint32), uint64(MaxUint32OrInt))
+	} else {
+		require.Equal(t, uint64(math.MaxInt), uint64(MaxUint32OrInt))
+	}
+}
diff --git a/internal/testkeys/testkeys.go b/internal/testkeys/testkeys.go
index dccfd0df..d77d8648 100644
--- a/internal/testkeys/testkeys.go
+++ b/internal/testkeys/testkeys.go
@@ -37,7 +37,7 @@ func init() {
 }
 
 // MaxSuffixLen is the maximum length of a suffix generated by this package.
-var MaxSuffixLen = 1 + len(fmt.Sprintf("%d", math.MaxInt64))
+var MaxSuffixLen = 1 + len(fmt.Sprintf("%d", int64(math.MaxInt64)))
 
 // Comparer is the comparer for test keys generated by this package.
 var Comparer = &base.Comparer{
diff --git a/metrics_test.go b/metrics_test.go
index ade96aa2..8ac4d2c3 100644
--- a/metrics_test.go
+++ b/metrics_test.go
@@ -224,7 +224,7 @@ func TestMetrics(t *testing.T) {
 			d.waitTableStats()
 			d.mu.Unlock()
 
-			return d.Metrics().String()
+			return d.Metrics().StringForTests()
 
 		case "disk-usage":
 			return humanize.Bytes.Uint64(d.Metrics().DiskSpaceUsage()).String()
diff --git a/open_test.go b/open_test.go
index c2e20394..acf3f982 100644
--- a/open_test.go
+++ b/open_test.go
@@ -627,7 +627,7 @@ func TestWALReplaySequenceNumBug(t *testing.T) {
 	require.NoError(t, d.Set([]byte("2"), nil, nil))
 
 	// Write a large batch. This should go to a separate memtable.
-	largeValue := []byte(strings.Repeat("a", d.largeBatchThreshold))
+	largeValue := []byte(strings.Repeat("a", int(d.largeBatchThreshold)))
 	require.NoError(t, d.Set([]byte("1"), largeValue, nil))
 
 	// This write should go the mutable memtable after the large batch in the
@@ -694,7 +694,7 @@ func TestOpenWALReplay2(t *testing.T) {
 						require.NoError(t, d.Set([]byte("2"), largeValue, nil))
 						require.NoError(t, d.Set([]byte("3"), largeValue, nil))
 					case "large-batch":
-						largeValue := []byte(strings.Repeat("a", d.largeBatchThreshold))
+						largeValue := []byte(strings.Repeat("a", int(d.largeBatchThreshold)))
 						require.NoError(t, d.Set([]byte("1"), nil, nil))
 						require.NoError(t, d.Set([]byte("2"), largeValue, nil))
 						require.NoError(t, d.Set([]byte("3"), nil, nil))
diff --git a/options_test.go b/options_test.go
index 38931902..b239004c 100644
--- a/options_test.go
+++ b/options_test.go
@@ -279,7 +279,7 @@ func TestOptionsValidate(t *testing.T) {
 [Options]
   mem_table_size=4294967296
 `,
-			`MemTableSize \(4\.0GB\) must be < 4\.0GB`,
+			`MemTableSize \(4\.0GB\) must be < [2|4]\.0GB`,
 		},
 		{`
 [Options]
diff --git a/pacer_test.go b/pacer_test.go
index 5ec11a10..565bde9b 100644
--- a/pacer_test.go
+++ b/pacer_test.go
@@ -25,7 +25,7 @@ func TestDeletionPacer(t *testing.T) {
 		// history of deletion reporting; first value in the pair is the time,
 		// second value is the deleted bytes. The time of pacing is the same as the
 		// last time in the history.
-		history [][2]int
+		history [][2]int64
 		// expected pacing rate in MB/s.
 		expected float64
 	}{
@@ -70,7 +70,7 @@ func TestDeletionPacer(t *testing.T) {
 			freeBytes:     160 * GB,
 			obsoleteBytes: 1 * MB,
 			liveBytes:     160 * MB,
-			history:       [][2]int{{0, 5 * 60 * 200 * MB}},
+			history:       [][2]int64{{0, 5 * 60 * 200 * MB}},
 			expected:      200.0,
 		},
 		// History shows 200MB/sec deletions on average over last 5 minutes and
@@ -79,7 +79,7 @@ func TestDeletionPacer(t *testing.T) {
 			freeBytes:     6 * GB,
 			obsoleteBytes: 1 * MB,
 			liveBytes:     160 * MB,
-			history:       [][2]int{{0, 5 * 60 * 200 * MB}},
+			history:       [][2]int64{{0, 5 * 60 * 200 * MB}},
 			expected:      1224.0,
 		},
 		// History shows 200MB/sec deletions on average over last 5 minutes and
@@ -88,7 +88,7 @@ func TestDeletionPacer(t *testing.T) {
 			freeBytes:     500 * GB,
 			obsoleteBytes: 50 * GB,
 			liveBytes:     100 * GB,
-			history:       [][2]int{{0, 5 * 60 * 200 * MB}},
+			history:       [][2]int64{{0, 5 * 60 * 200 * MB}},
 			expected:      302.4,
 		},
 		// History shows 1000MB/sec deletions on average over last 5 minutes.
@@ -96,7 +96,7 @@ func TestDeletionPacer(t *testing.T) {
 			freeBytes:     160 * GB,
 			obsoleteBytes: 1 * MB,
 			liveBytes:     160 * MB,
-			history:       [][2]int{{0, 60 * 1000 * MB}, {3 * 60, 60 * 4 * 1000 * MB}, {4 * 60, 0}},
+			history:       [][2]int64{{0, 60 * 1000 * MB}, {3 * 60, 60 * 4 * 1000 * MB}, {4 * 60, 0}},
 			expected:      1000.0,
 		},
 		// First entry in history is too old, it should be discarded.
@@ -104,7 +104,7 @@ func TestDeletionPacer(t *testing.T) {
 			freeBytes:     160 * GB,
 			obsoleteBytes: 1 * MB,
 			liveBytes:     160 * MB,
-			history:       [][2]int{{0, 10 * 60 * 10000 * MB}, {3 * 60, 4 * 60 * 200 * MB}, {7 * 60, 1 * 60 * 200 * MB}},
+			history:       [][2]int64{{0, 10 * 60 * 10000 * MB}, {3 * 60, 4 * 60 * 200 * MB}, {7 * 60, 1 * 60 * 200 * MB}},
 			expected:      200.0,
 		},
 	}