From 56fbfaf2b57c89575033cd94438e125427480775 Mon Sep 17 00:00:00 2001 From: Jianjun Chen Date: Tue, 14 Jul 2026 15:42:07 +0800 Subject: [PATCH] feat(pebblekv): expose MemTableSize/MemTableStopWritesThreshold on OpenOptions The memtable knobs were hardcoded (4 MiB / 2), which fragments a cold bulk index build into hundreds of tiny L0 files and drives a compaction storm that dominates cold-build wall-clock. Expose both on OpenOptions so a caller that knows its workload can enlarge the memtable; zero keeps the conservative built-in defaults and a sub-2 threshold is clamped to 2 (Pebble requires >= 2), so existing callers are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- core/kv/pebblekv/db.go | 28 ++++++++++++++++++++++++---- core/kv/pebblekv/pebble_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/core/kv/pebblekv/db.go b/core/kv/pebblekv/db.go index b21b121..37dd1bc 100644 --- a/core/kv/pebblekv/db.go +++ b/core/kv/pebblekv/db.go @@ -73,6 +73,17 @@ type OpenOptions struct { // an OS-level crash / power loss can lose the un-synced tail. Set Sync for a // store of record. Ignored when DisableWAL is set (there is no WAL to sync). Sync bool + // MemTableSize is the steady-state MemTable size in bytes. Zero selects the + // conservative built-in default (4 MiB). A larger memtable defers flushes, so + // more redundant writes coalesce/elide before hitting disk — cutting L0 + // churn and compaction during a bulk build, at the cost of higher transient + // build-time memory (peak ≈ MemTableStopWritesThreshold × MemTableSize). + MemTableSize uint64 + // MemTableStopWritesThreshold is the hard limit on queued memtables: writes + // stop once the queued memtable sizes exceed this × MemTableSize. Zero selects + // the built-in default (2). Values below 2 are raised to 2 (Pebble stalls + // writes whenever a memtable flushes at threshold 1). + MemTableStopWritesThreshold int } // Open opens a Pebble database at the default WAL mode — WAL on, commits not @@ -88,6 +99,17 @@ func OpenWithOptions(path string, o OpenOptions) (kv.Store, error) { return nil, fmt.Errorf("failed to get absolute path: %v", err) } + // Memtable sizing: zero selects the conservative built-in defaults; a caller + // that knows its workload (e.g. a bulk index build) can enlarge it. + memTableSize := o.MemTableSize + if memTableSize == 0 { + memTableSize = 4 * 1024 * 1024 + } + memTableStop := o.MemTableStopWritesThreshold + if memTableStop < 2 { + memTableStop = 2 + } + // Configure Pebble options opts := &pebble.Options{ Cache: pebble.NewCache(o.CacheSize), @@ -102,10 +124,8 @@ func OpenWithOptions(path string, o OpenOptions) (kv.Store, error) { // Allow more files to be open MaxOpenFiles: 8192, - // Set write buffer size to 8MB - MemTableSize: 4 * 1024 * 1024, - // Set max memtable count to 2 - MemTableStopWritesThreshold: 2, + MemTableSize: memTableSize, + MemTableStopWritesThreshold: memTableStop, // The count of L0 files necessary to trigger an L0 compaction. L0CompactionFileThreshold: 1024, diff --git a/core/kv/pebblekv/pebble_test.go b/core/kv/pebblekv/pebble_test.go index 7193dbd..4402df0 100644 --- a/core/kv/pebblekv/pebble_test.go +++ b/core/kv/pebblekv/pebble_test.go @@ -89,6 +89,33 @@ func TestOpenWithOptions_WALModes(t *testing.T) { } } +// TestOpenWithOptions_MemTable covers the memtable sizing knobs: an explicit +// larger memtable + threshold opens and round-trips, the zero value falls back +// to the built-in default, and a sub-2 threshold is clamped to 2 (Pebble +// requires >= 2). All go through the kv.Store surface. +func TestOpenWithOptions_MemTable(t *testing.T) { + cases := []struct { + name string + opts OpenOptions + }{ + {"explicit_16MiB_stop4", OpenOptions{CacheSize: 1 << 20, MemTableSize: 16 << 20, MemTableStopWritesThreshold: 4}}, + {"zero_defaults", OpenOptions{CacheSize: 1 << 20}}, + {"stop_below_2_clamped", OpenOptions{CacheSize: 1 << 20, MemTableSize: 8 << 20, MemTableStopWritesThreshold: 1}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + db, err := OpenWithOptions(t.TempDir()+"/db", c.opts) + assert.NoError(t, err) + defer db.Close() + + assert.NoError(t, db.Put([]byte("k"), []byte("v"))) + got, err := db.Get([]byte("k")) + assert.NoError(t, err) + assert.Equal(t, []byte("v"), got) + }) + } +} + func TestOpen_And_BasicOps(t *testing.T) { tmpDir := t.TempDir() db, err := Open(tmpDir+"/testdb", 4*1024*1024)