From 1c0105b6ca1f4a2d97c128b6ef8d5cb85f6b5b4b Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Sun, 21 Jun 2026 23:40:23 -0400 Subject: [PATCH] perf(txn): reduce per-commit allocations on managed write path Replace pendingWrites map[string]*Entry with a fingerprint-keyed store (pendingEntries: map[uint64][]*Entry with byte-compare collision buckets). This removes the per-write string(e.Key) allocation that Go performs on every map insert. Get/read-your-writes, last-write-wins, managed duplicate-version handling and iteration semantics are preserved. Carve all key+ts buffers at commit from a single per-commit arena (keyArena) instead of one y.KeyWithTs allocation per key. The arena copies user keys (never aliases them) and is kept alive by the Entry.Key slices handed to the async write channel. Add white-box and end-to-end regression tests and BenchmarkManagedCommit (-benchmem) demonstrating the allocs/op reduction. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01NtGkC4K2J2XYwcAKwjhHbM --- txn.go | 45 ++--- txn_pending.go | 154 +++++++++++++++++ txn_pending_test.go | 400 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 578 insertions(+), 21 deletions(-) create mode 100644 txn_pending.go create mode 100644 txn_pending_test.go diff --git a/txn.go b/txn.go index 9c916d18a..fe9dd9683 100644 --- a/txn.go +++ b/txn.go @@ -248,8 +248,8 @@ type Txn struct { conflictKeys map[uint64]struct{} readsLock sync.Mutex // guards the reads slice. See addReadKey. - pendingWrites map[string]*Entry // cache stores any writes done by txn. - duplicateWrites []*Entry // Used in managed mode to store duplicate entries. + pendingWrites *pendingEntries // cache stores any writes done by txn. + duplicateWrites []*Entry // Used in managed mode to store duplicate entries. numIterators atomic.Int32 discarded bool @@ -310,13 +310,13 @@ func (pi *pendingWritesIterator) Close() error { } func (txn *Txn) newPendingWritesIterator(reversed bool) *pendingWritesIterator { - if !txn.update || len(txn.pendingWrites) == 0 { + if !txn.update || txn.pendingWrites.len() == 0 { return nil } - entries := make([]*Entry, 0, len(txn.pendingWrites)) - for _, e := range txn.pendingWrites { + entries := make([]*Entry, 0, txn.pendingWrites.len()) + txn.pendingWrites.rangeEntries(func(e *Entry) { entries = append(entries, e) - } + }) // Number of pending writes per transaction shouldn't be too big in general. sort.Slice(entries, func(i, j int) bool { cmp := bytes.Compare(entries[i].Key, entries[j].Key) @@ -388,10 +388,9 @@ func (txn *Txn) modify(e *Entry) error { // If a duplicate entry was inserted in managed mode, move it to the duplicate writes slice. // Add the entry to duplicateWrites only if both the entries have different versions. For // same versions, we will overwrite the existing entry. - if oldEntry, ok := txn.pendingWrites[string(e.Key)]; ok && oldEntry.version != e.version { + if oldEntry, duplicate := txn.pendingWrites.set(e); duplicate { txn.duplicateWrites = append(txn.duplicateWrites, oldEntry) } - txn.pendingWrites[string(e.Key)] = e return nil } @@ -444,7 +443,7 @@ func (txn *Txn) Get(key []byte) (item *Item, rerr error) { item = new(Item) if txn.update { - if e, has := txn.pendingWrites[string(key)]; has && bytes.Equal(key, e.Key) { + if e, has := txn.pendingWrites.get(key); has { if isDeletedOrExpired(e.meta, e.ExpiresAt) { return nil, ErrKeyNotFound } @@ -540,21 +539,27 @@ func (txn *Txn) commitAndSend() (func() error, error) { keepTogether = false } } - for _, e := range txn.pendingWrites { + // arenaSize accumulates the total bytes needed for all key+ts buffers so we + // can carve them from a single allocation instead of one per key. + var arenaSize int + txn.pendingWrites.rangeEntries(func(e *Entry) { setVersion(e) - } + arenaSize += len(e.Key) + 8 + }) // The duplicateWrites slice will be non-empty only if there are duplicate // entries with different versions. for _, e := range txn.duplicateWrites { setVersion(e) + arenaSize += len(e.Key) + 8 } - entries := make([]*Entry, 0, len(txn.pendingWrites)+len(txn.duplicateWrites)+1) + entries := make([]*Entry, 0, txn.pendingWrites.len()+len(txn.duplicateWrites)+1) + arena := newKeyArena(arenaSize) processEntry := func(e *Entry) { // Suffix the keys with commit ts, so the key versions are sorted in // descending order of commit timestamp. - e.Key = y.KeyWithTs(e.Key, e.version) + e.Key = arena.keyWithTs(e.Key, e.version) // Add bitTxn only if these entries are part of a transaction. We // support SetEntryAt(..) in managed mode which means a single // transaction can have entries with different timestamps. If entries @@ -572,9 +577,7 @@ func (txn *Txn) commitAndSend() (func() error, error) { // var b strings.Builder // fmt.Fprintf(&b, "Read: %d. Commit: %d. reads: %v. writes: %v. Keys: ", // txn.readTs, commitTs, txn.reads, txn.conflictKeys) - for _, e := range txn.pendingWrites { - processEntry(e) - } + txn.pendingWrites.rangeEntries(processEntry) for _, e := range txn.duplicateWrites { processEntry(e) } @@ -611,11 +614,11 @@ func (txn *Txn) commitPrecheck() error { return errors.New("Trying to commit a discarded txn") } keepTogether := true - for _, e := range txn.pendingWrites { + txn.pendingWrites.rangeEntries(func(e *Entry) { if e.version != 0 { keepTogether = false } - } + }) // If keepTogether is True, it implies transaction markers will be added. // In that case, commitTs should not be never be zero. This might happen if @@ -649,7 +652,7 @@ func (txn *Txn) commitPrecheck() error { func (txn *Txn) Commit() error { // txn.conflictKeys can be zero if conflict detection is turned off. So we // should check txn.pendingWrites. - if len(txn.pendingWrites) == 0 { + if txn.pendingWrites.len() == 0 { // Discard the transaction so that the read is marked done. txn.Discard() return nil @@ -702,7 +705,7 @@ func (txn *Txn) CommitWith(cb func(error)) { panic("Nil callback provided to CommitWith") } - if len(txn.pendingWrites) == 0 { + if txn.pendingWrites.len() == 0 { // Do not run these callbacks from here, because the CommitWith and the // callback might be acquiring the same locks. Instead run the callback // from another goroutine. @@ -774,7 +777,7 @@ func (db *DB) newTransaction(update, isManaged bool) *Txn { if db.opt.DetectConflicts { txn.conflictKeys = make(map[uint64]struct{}) } - txn.pendingWrites = make(map[string]*Entry) + txn.pendingWrites = newPendingEntries() } if !isManaged { txn.readTs = db.orc.readTs() diff --git a/txn_pending.go b/txn_pending.go new file mode 100644 index 000000000..6582e6722 --- /dev/null +++ b/txn_pending.go @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: © 2017-2025 Istari Digital, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package badger + +import ( + "bytes" + "encoding/binary" + "math" + + "github.com/dgraph-io/ristretto/v2/z" +) + +// pendingEntries stores the writes buffered by a transaction before commit. +// +// It replaces the previous map[string]*Entry. The previous representation paid +// one heap allocation per write because Go copies the key into a freshly +// allocated string on every `m[string(key)] = e` insert (the assignment-key +// conversion is NOT elided by the compiler, only the lookup conversion is). +// +// Instead we key on a 64-bit fingerprint (z.MemHash) of the user key and store +// the entries directly. Because a 64-bit hash can collide, each bucket holds a +// small slice of entries that is disambiguated with a byte-wise key comparison. +// In the overwhelmingly common no-collision case a bucket holds a single entry +// and lookups/inserts are O(1) with zero allocation. +// +// Semantics preserved vs the old map: +// - read-your-own-writes: get() matches on exact user-key bytes. +// - last-write-wins for the same (key, version): set() overwrites in place. +// - managed multi-version: set() reports the displaced entry when the new +// entry has a different version, so the caller can move it to +// duplicateWrites. +// - iteration: rangeEntries() visits every live entry exactly once (in +// unspecified order, exactly like Go map iteration was). +type pendingEntries struct { + m map[uint64][]*Entry + // n is the number of distinct live keys held. + n int + // hash is the key-fingerprint function. It is a field purely so tests can + // force collisions; production always uses z.MemHash. + hash func([]byte) uint64 +} + +func newPendingEntries() *pendingEntries { + return &pendingEntries{ + m: make(map[uint64][]*Entry), + hash: z.MemHash, + } +} + +// A nil *pendingEntries is valid and behaves as an empty, read-only store. +// This mirrors the previous map[string]*Entry field which was left nil for +// read-only transactions and relied on nil-map reads being safe. + +func (p *pendingEntries) len() int { + if p == nil { + return 0 + } + return p.n +} + +// get returns the live entry for key, if present. +func (p *pendingEntries) get(key []byte) (*Entry, bool) { + if p == nil { + return nil, false + } + bucket := p.m[p.hash(key)] + for _, e := range bucket { + if bytes.Equal(e.Key, key) { + return e, true + } + } + return nil, false +} + +// set inserts or replaces the entry for e.Key. +// +// It returns the previously stored entry for the same key (if any) and a +// `duplicate` flag that is true only when an entry already existed AND it had a +// different version than e. This mirrors the original modify() logic: +// +// if old, ok := pendingWrites[string(e.Key)]; ok && old.version != e.version { +// duplicateWrites = append(duplicateWrites, old) +// } +// pendingWrites[string(e.Key)] = e +func (p *pendingEntries) set(e *Entry) (old *Entry, duplicate bool) { + fp := p.hash(e.Key) + bucket := p.m[fp] + for i, cur := range bucket { + if bytes.Equal(cur.Key, e.Key) { + old = cur + bucket[i] = e + return old, old.version != e.version + } + } + // New key for this bucket. + p.m[fp] = append(bucket, e) + p.n++ + return nil, false +} + +// rangeEntries calls fn for every live entry exactly once. +func (p *pendingEntries) rangeEntries(fn func(*Entry)) { + if p == nil { + return + } + for _, bucket := range p.m { + for _, e := range bucket { + fn(e) + } + } +} + +// keyArena is a single contiguous buffer from which key+timestamp byte slices +// are carved at commit time. It collapses N per-key y.KeyWithTs allocations +// (one make([]byte, len(key)+8) per entry) into a single allocation per commit. +// +// Safety: the arena allocates fresh storage and copies the user key into it; it +// never mutates or aliases the caller-provided Entry.Key bytes. The produced +// slices are sub-slices of the arena with cap clamped to len, so appending to +// one slice cannot scribble over the next. The arena is referenced by the +// Entry.Key slices handed to the write channel and therefore lives, via those +// references, for as long as the asynchronous write needs it. +type keyArena struct { + buf []byte + off int +} + +func newKeyArena(size int) *keyArena { + return &keyArena{buf: make([]byte, size)} +} + +// keyWithTs writes key followed by the encoded timestamp into the arena and +// returns the resulting slice. It is byte-for-byte identical to y.KeyWithTs. +// +// If the arena is exhausted (which should not happen when sized correctly) it +// falls back to a standalone allocation so correctness never depends on the +// size estimate. +func (a *keyArena) keyWithTs(key []byte, ts uint64) []byte { + need := len(key) + 8 + if a.off+need > len(a.buf) { + out := make([]byte, need) + copy(out, key) + binary.BigEndian.PutUint64(out[len(key):], math.MaxUint64-ts) + return out + } + out := a.buf[a.off : a.off+need : a.off+need] + a.off += need + copy(out, key) + binary.BigEndian.PutUint64(out[len(key):], math.MaxUint64-ts) + return out +} diff --git a/txn_pending_test.go b/txn_pending_test.go new file mode 100644 index 000000000..2ada95663 --- /dev/null +++ b/txn_pending_test.go @@ -0,0 +1,400 @@ +/* + * SPDX-FileCopyrightText: © 2017-2025 Istari Digital, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +package badger + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" + "math/rand" + "os" + "sort" + "testing" + + "github.com/stretchr/testify/require" +) + +// keyWithTsRef is a reference implementation matching y.KeyWithTs, used to +// validate the arena-backed keyWithTs against the canonical encoding. +func keyWithTsRef(key []byte, ts uint64) []byte { + out := make([]byte, len(key)+8) + copy(out, key) + binary.BigEndian.PutUint64(out[len(key):], math.MaxUint64-ts) + return out +} + +// --------------------------------------------------------------------------- +// White-box tests for the pendingEntries store. These exercise the collision +// handling, duplicate-version handling and read-your-own-writes lookup logic +// directly, independent of the surrounding DB machinery. +// --------------------------------------------------------------------------- + +// TestPendingEntriesNilSafe verifies a nil *pendingEntries behaves as an empty +// store, matching the old nil-map semantics for read-only transactions. +func TestPendingEntriesNilSafe(t *testing.T) { + var pw *pendingEntries + require.Equal(t, 0, pw.len()) + _, ok := pw.get([]byte("x")) + require.False(t, ok) + count := 0 + pw.rangeEntries(func(*Entry) { count++ }) + require.Equal(t, 0, count) +} + +func TestPendingEntriesBasic(t *testing.T) { + pw := newPendingEntries() + require.Equal(t, 0, pw.len()) + + e1 := &Entry{Key: []byte("a"), Value: []byte("1")} + old, dup := pw.set(e1) + require.Nil(t, old) + require.False(t, dup) + require.Equal(t, 1, pw.len()) + + got, ok := pw.get([]byte("a")) + require.True(t, ok) + require.Equal(t, e1, got) + + _, ok = pw.get([]byte("b")) + require.False(t, ok) +} + +func TestPendingEntriesOverwriteSameVersion(t *testing.T) { + pw := newPendingEntries() + e1 := &Entry{Key: []byte("a"), Value: []byte("1"), version: 5} + e2 := &Entry{Key: []byte("a"), Value: []byte("2"), version: 5} + + pw.set(e1) + old, dup := pw.set(e2) + // Same version => overwrite, old returned but NOT flagged as duplicate. + require.Equal(t, e1, old) + require.False(t, dup) + require.Equal(t, 1, pw.len()) + + got, ok := pw.get([]byte("a")) + require.True(t, ok) + require.Equal(t, e2, got) +} + +func TestPendingEntriesDuplicateDifferentVersion(t *testing.T) { + pw := newPendingEntries() + e1 := &Entry{Key: []byte("a"), Value: []byte("1"), version: 5} + e2 := &Entry{Key: []byte("a"), Value: []byte("2"), version: 7} + + pw.set(e1) + old, dup := pw.set(e2) + // Different version => the OLD entry must be reported as a duplicate. + require.Equal(t, e1, old) + require.True(t, dup) + // The live entry for the key is now e2. + require.Equal(t, 1, pw.len()) + got, ok := pw.get([]byte("a")) + require.True(t, ok) + require.Equal(t, e2, got) +} + +// forceCollision constructs a pendingEntries whose hashing always collides so +// the bucket slice path (multiple distinct keys in one bucket) is exercised. +func TestPendingEntriesCollisions(t *testing.T) { + pw := newPendingEntries() + pw.hash = func(b []byte) uint64 { return 42 } // everything collides + + keys := [][]byte{[]byte("alpha"), []byte("beta"), []byte("gamma"), []byte("a")} + for i, k := range keys { + e := &Entry{Key: k, Value: []byte(fmt.Sprintf("v%d", i))} + old, dup := pw.set(e) + require.Nil(t, old) + require.False(t, dup) + } + require.Equal(t, len(keys), pw.len()) + + for i, k := range keys { + got, ok := pw.get(k) + require.True(t, ok, "key %s", k) + require.Equal(t, []byte(fmt.Sprintf("v%d", i)), got.Value) + } + + // Lookup of a non-present key that hashes into the same bucket must miss. + _, ok := pw.get([]byte("delta")) + require.False(t, ok) + + // Overwrite within a colliding bucket. + e := &Entry{Key: []byte("beta"), Value: []byte("new")} + old, dup := pw.set(e) + require.NotNil(t, old) + require.False(t, dup) + require.Equal(t, len(keys), pw.len()) + got, ok := pw.get([]byte("beta")) + require.True(t, ok) + require.Equal(t, []byte("new"), got.Value) +} + +// TestPendingEntriesRangeMatchesMap cross-checks range() against an oracle map. +func TestPendingEntriesRangeMatchesMap(t *testing.T) { + pw := newPendingEntries() + oracle := map[string]*Entry{} + r := rand.New(rand.NewSource(42)) + for i := 0; i < 2000; i++ { + k := []byte(fmt.Sprintf("k-%d", r.Intn(500))) + e := &Entry{Key: k, Value: []byte(fmt.Sprintf("v-%d", i))} + pw.set(e) + oracle[string(k)] = e + } + require.Equal(t, len(oracle), pw.len()) + + collected := map[string]*Entry{} + pw.rangeEntries(func(e *Entry) { + collected[string(e.Key)] = e + }) + require.Equal(t, len(oracle), len(collected)) + for k, e := range oracle { + require.Equal(t, e, collected[k]) + got, ok := pw.get([]byte(k)) + require.True(t, ok) + require.Equal(t, e, got) + } +} + +// --------------------------------------------------------------------------- +// End-to-end semantic regression tests through the public API. +// --------------------------------------------------------------------------- + +// TestTxnReadYourWrites verifies that Get sees pending writes within the txn, +// including overwrites and deletes, before commit. +func TestTxnReadYourWrites(t *testing.T) { + runBadgerTest(t, nil, func(t *testing.T, db *DB) { + txn := db.NewTransaction(true) + defer txn.Discard() + + require.NoError(t, txn.Set([]byte("foo"), []byte("bar"))) + item, err := txn.Get([]byte("foo")) + require.NoError(t, err) + v, err := item.ValueCopy(nil) + require.NoError(t, err) + require.Equal(t, []byte("bar"), v) + + // Overwrite (last write wins). + require.NoError(t, txn.Set([]byte("foo"), []byte("baz"))) + item, err = txn.Get([]byte("foo")) + require.NoError(t, err) + v, err = item.ValueCopy(nil) + require.NoError(t, err) + require.Equal(t, []byte("baz"), v) + + // Delete then read-your-own-write -> not found. + require.NoError(t, txn.Delete([]byte("foo"))) + _, err = txn.Get([]byte("foo")) + require.Equal(t, ErrKeyNotFound, err) + }) +} + +// TestTxnManagedDuplicateVersions checks the managed multi-version write path: +// writing the same key at different versions in one batch keeps all versions. +func TestTxnManagedDuplicateVersions(t *testing.T) { + dir, err := os.MkdirTemp("", "badger-test") + require.NoError(t, err) + defer removeDir(dir) + + opt := getTestOptions(dir) + opt.managedTxns = true + db, err := Open(opt) + require.NoError(t, err) + defer func() { require.NoError(t, db.Close()) }() + + wb := db.NewManagedWriteBatch() + key := []byte("dup-key") + // Same key, three different versions in a single batch. + require.NoError(t, wb.SetEntryAt(NewEntry(key, []byte("v100")), 100)) + require.NoError(t, wb.SetEntryAt(NewEntry(key, []byte("v200")), 200)) + require.NoError(t, wb.SetEntryAt(NewEntry(key, []byte("v300")), 300)) + require.NoError(t, wb.Flush()) + + // All three versions must be readable at their respective read timestamps. + for _, tc := range []struct { + readTs uint64 + want string + }{{150, "v100"}, {250, "v200"}, {350, "v300"}} { + txn := db.NewTransactionAt(tc.readTs, false) + item, err := txn.Get(key) + require.NoError(t, err, "readTs=%d", tc.readTs) + v, err := item.ValueCopy(nil) + require.NoError(t, err) + require.Equal(t, tc.want, string(v), "readTs=%d", tc.readTs) + txn.Discard() + } +} + +// TestTxnConflictDetection ensures conflict detection still works after the +// pendingWrites refactor (default non-managed mode, DetectConflicts=true). +func TestTxnConflictDetectionStillWorks(t *testing.T) { + runBadgerTest(t, nil, func(t *testing.T, db *DB) { + require.NoError(t, db.Update(func(txn *Txn) error { + return txn.Set([]byte("k"), []byte("v0")) + })) + + txn1 := db.NewTransaction(true) + txn2 := db.NewTransaction(true) + + // Both read k. + _, err := txn1.Get([]byte("k")) + require.NoError(t, err) + _, err = txn2.Get([]byte("k")) + require.NoError(t, err) + + require.NoError(t, txn1.Set([]byte("k"), []byte("v1"))) + require.NoError(t, txn2.Set([]byte("k"), []byte("v2"))) + + require.NoError(t, txn1.Commit()) + // txn2 read k which txn1 modified -> conflict. + require.Equal(t, ErrConflict, txn2.Commit()) + }) +} + +// TestTxnIteratorOverPendingWrites verifies the pending-writes iterator returns +// keys in sorted order and merges with committed data correctly. +func TestTxnIteratorOverPendingWrites(t *testing.T) { + runBadgerTest(t, nil, func(t *testing.T, db *DB) { + // Commit some baseline keys. + require.NoError(t, db.Update(func(txn *Txn) error { + for _, k := range []string{"b", "d", "f"} { + if err := txn.Set([]byte(k), []byte("committed-"+k)); err != nil { + return err + } + } + return nil + })) + + require.NoError(t, db.Update(func(txn *Txn) error { + // Pending writes interleaved with committed keys. + for _, k := range []string{"a", "c", "e", "g"} { + if err := txn.Set([]byte(k), []byte("pending-"+k)); err != nil { + return err + } + } + // Overwrite a committed key in the txn. + if err := txn.Set([]byte("d"), []byte("pending-d")); err != nil { + return err + } + + var gotKeys []string + it := txn.NewIterator(DefaultIteratorOptions) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + gotKeys = append(gotKeys, string(item.Key())) + v, err := item.ValueCopy(nil) + require.NoError(t, err) + if string(item.Key()) == "d" { + require.Equal(t, "pending-d", string(v)) + } + } + require.True(t, sort.StringsAreSorted(gotKeys), "keys not sorted: %v", gotKeys) + require.Equal(t, []string{"a", "b", "c", "d", "e", "f", "g"}, gotKeys) + return nil + })) + }) +} + +// TestTxnManyWritesRoundTrip is a fuzz-ish test that commits many keys and +// verifies every one reads back correctly, exercising fingerprint buckets at +// scale. +func TestTxnManyWritesRoundTrip(t *testing.T) { + runBadgerTest(t, nil, func(t *testing.T, db *DB) { + const n = 5000 + want := map[string]string{} + require.NoError(t, db.Update(func(txn *Txn) error { + for i := 0; i < n; i++ { + k := fmt.Sprintf("key-%d", i) + v := fmt.Sprintf("val-%d", i) + want[k] = v + if err := txn.Set([]byte(k), []byte(v)); err != nil { + return err + } + } + return nil + })) + require.NoError(t, db.View(func(txn *Txn) error { + for k, v := range want { + item, err := txn.Get([]byte(k)) + require.NoError(t, err) + got, err := item.ValueCopy(nil) + require.NoError(t, err) + require.Equal(t, v, string(got)) + } + return nil + })) + }) +} + +// BenchmarkManagedCommit measures allocations on the managed-mode commit hot +// path that dgraph exercises (managedTxns=true, DetectConflicts=false). It +// reports allocs/op so the per-write string-key allocation and per-key +// KeyWithTs allocation reductions are visible with `-benchmem`. +// +// Run: go test -run=^$ -bench=BenchmarkManagedCommit -benchmem . +func BenchmarkManagedCommit(b *testing.B) { + for _, n := range []int{1, 16, 128} { + b.Run(fmt.Sprintf("keys=%d", n), func(b *testing.B) { + dir, err := os.MkdirTemp("", "badger-bench") + require.NoError(b, err) + defer removeDir(dir) + + opt := getTestOptions(dir).WithInMemory(true).WithDetectConflicts(false) + opt.Dir = "" + opt.ValueDir = "" + opt.managedTxns = true + db, err := Open(opt) + require.NoError(b, err) + defer func() { require.NoError(b, db.Close()) }() + + // Pre-build the key/value byte slices once so the benchmark measures + // the commit-path allocations, not test fixture allocations. + keys := make([][]byte, n) + vals := make([][]byte, n) + for i := 0; i < n; i++ { + keys[i] = []byte(fmt.Sprintf("benchmark-key-%08d", i)) + vals[i] = []byte(fmt.Sprintf("benchmark-value-%08d", i)) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + ts := uint64(i + 1) + txn := db.NewTransactionAt(ts, true) + for j := 0; j < n; j++ { + if err := txn.SetEntry(NewEntry(keys[j], vals[j])); err != nil { + b.Fatal(err) + } + } + if err := txn.CommitAt(ts, nil); err != nil { + b.Fatal(err) + } + } + }) + } +} + +// TestKeyWithTsBufMatchesKeyWithTs verifies the arena buffer helper produces +// byte-identical output to y.KeyWithTs. +func TestKeyWithTsBufMatchesKeyWithTs(t *testing.T) { + r := rand.New(rand.NewSource(1)) + for i := 0; i < 1000; i++ { + klen := r.Intn(64) + key := make([]byte, klen) + r.Read(key) + ts := r.Uint64() + + a := newKeyArena(len(key) + 8) + got := a.keyWithTs(key, ts) + want := keyWithTsRef(key, ts) + require.True(t, bytes.Equal(got, want), "i=%d key=%x ts=%d got=%x want=%x", i, key, ts, got, want) + // The arena-backed slice must NOT share storage with the input key. + if len(key) > 0 { + require.NotSame(t, &key[0], &got[0]) + } + } +}