Skip to content

Commit f4af03a

Browse files
committed
test: introduce two level iterator benchmark test
Related to #3248
1 parent 527eebf commit f4af03a

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2+
// of this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
5+
package sstable
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"testing"
11+
12+
"github.com/cockroachdb/pebble/bloom"
13+
"github.com/cockroachdb/pebble/internal/base"
14+
"github.com/cockroachdb/pebble/internal/testkeys"
15+
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
16+
"github.com/cockroachdb/pebble/vfs"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
// setupTwoLevelBenchmarkData creates test data designed for two-level indexes
21+
func setupTwoLevelBenchmarkData(b *testing.B, blockSize int) (*Reader, [][]byte) {
22+
// Generate a large dataset to ensure two-level index creation
23+
// Using smaller block size to force more blocks and two-level index
24+
numKeys := 50000
25+
keys := make([][]byte, numKeys)
26+
27+
// Generate keys that will create a two-level index
28+
ks := testkeys.Alpha(5) // Use larger alphabet to support more keys
29+
for i := 0; i < numKeys; i++ {
30+
key := testkeys.Key(ks, int64(i))
31+
keys[i] = []byte(key)
32+
}
33+
34+
// Create SSTable with small block size to force two-level index
35+
mem := vfs.NewMem()
36+
f, err := mem.Create("bench", vfs.WriteCategoryUnspecified)
37+
require.NoError(b, err)
38+
39+
w := NewWriter(objstorageprovider.NewFileWritable(f), WriterOptions{
40+
BlockSize: blockSize, // Small block size
41+
IndexBlockSize: 1024, // Small index block size to force two-level
42+
Comparer: testkeys.Comparer,
43+
MergerName: "nullptr",
44+
TableFormat: TableFormatPebblev3,
45+
})
46+
47+
// Write keys to create two-level index
48+
for i, key := range keys {
49+
value := fmt.Sprintf("value-%d", i)
50+
require.NoError(b, w.Set(key, []byte(value)))
51+
}
52+
53+
require.NoError(b, w.Close())
54+
55+
// Re-open the file for reading
56+
f, err = mem.Open("bench")
57+
require.NoError(b, err)
58+
59+
// Create reader
60+
r, err := newReader(f, ReaderOptions{
61+
Comparer: testkeys.Comparer,
62+
})
63+
require.NoError(b, err)
64+
65+
// Verify we actually created a two-level index
66+
if !r.Attributes.Has(AttributeTwoLevelIndex) {
67+
b.Fatalf("Test data did not create two-level index (keys: %d, blockSize: %d)", numKeys, blockSize)
68+
}
69+
70+
return r, keys
71+
}
72+
73+
// BenchmarkTwoLevelIteratorConstruction measures just the construction time
74+
// How to: go test -bench=BenchmarkTwoLevelIteratorConstruction -run=^$ -count=10 ./sstable
75+
func BenchmarkTwoLevelIteratorConstruction(b *testing.B) {
76+
b.Run("rowblk", func(b *testing.B) {
77+
reader, _ := setupTwoLevelBenchmarkData(b, 1024)
78+
defer reader.Close()
79+
80+
opts := IterOptions{}
81+
b.ResetTimer()
82+
83+
for i := 0; i < b.N; i++ {
84+
var iter Iterator
85+
var err error
86+
87+
iter, err = newRowBlockTwoLevelIterator(context.Background(), reader, opts)
88+
89+
require.NoError(b, err)
90+
91+
// Close immediately to measure just construction
92+
iter.Close()
93+
}
94+
})
95+
}
96+
97+
// BenchmarkTwoLevelIteratorFirst measures construction + first access
98+
// How to: go test -bench=BenchmarkTwoLevelIteratorFirst -run=^$ -count=10 ./sstable
99+
func BenchmarkTwoLevelIteratorFirst(b *testing.B) {
100+
b.Run("rowblk", func(b *testing.B) {
101+
reader, _ := setupTwoLevelBenchmarkData(b, 1024)
102+
defer reader.Close()
103+
104+
opts := IterOptions{}
105+
b.ResetTimer()
106+
107+
for i := 0; i < b.N; i++ {
108+
var iter Iterator
109+
var err error
110+
111+
iter, err = newRowBlockTwoLevelIterator(context.Background(), reader, opts)
112+
113+
require.NoError(b, err)
114+
115+
// First access
116+
kv := iter.First()
117+
if kv == nil {
118+
b.Fatal("Expected non-nil key-value from First()")
119+
}
120+
121+
iter.Close()
122+
}
123+
})
124+
}
125+
126+
// BenchmarkTwoLevelIteratorSeekGE measures construction + SeekGE performance
127+
// How to: go test -bench=BenchmarkTwoLevelIteratorSeekGE -run=^$ -count=10 ./sstable
128+
func BenchmarkTwoLevelIteratorSeekGE(b *testing.B) {
129+
b.Run("rowblk", func(b *testing.B) {
130+
reader, keys := setupTwoLevelBenchmarkData(b, 1024)
131+
defer reader.Close()
132+
133+
opts := IterOptions{}
134+
seekKey := keys[len(keys)/2]
135+
136+
b.ResetTimer()
137+
138+
for i := 0; i < b.N; i++ {
139+
var iter Iterator
140+
var err error
141+
iter, err = newRowBlockTwoLevelIterator(context.Background(), reader, opts)
142+
require.NoError(b, err)
143+
144+
// First SeekGE call
145+
kv := iter.SeekGE(seekKey, base.SeekGEFlagsNone)
146+
if kv == nil {
147+
b.Fatal("Expected non-nil key-value from SeekGE")
148+
}
149+
150+
iter.Close()
151+
}
152+
})
153+
}
154+
155+
// setupBloomFilterData creates test data with bloom filter
156+
func setupBloomFilterData(b *testing.B) (*Reader, []string) {
157+
mem := vfs.NewMem()
158+
f, err := mem.Create("bench", vfs.WriteCategoryUnspecified)
159+
require.NoError(b, err)
160+
161+
w := NewWriter(objstorageprovider.NewFileWritable(f), WriterOptions{
162+
BlockSize: 512, // Smaller block size to force two-level index
163+
IndexBlockSize: 512, // Smaller index block size to force two-level index
164+
Comparer: testkeys.Comparer,
165+
MergerName: "nullptr",
166+
FilterPolicy: bloom.FilterPolicy(10), // Enable bloom filter
167+
TableFormat: TableFormatPebblev3,
168+
})
169+
170+
// Generate enough keys to force two-level index creation
171+
ks := testkeys.Alpha(5) // Use 5-character alphabet for more keys
172+
var keys []string
173+
174+
// Generate many more keys to ensure two-level index
175+
for i := range int64(10000) {
176+
// Use decreasing timestamps since testkeys.Comparer sorts them in reverse
177+
timestamp := int64(10000 - i)
178+
key := testkeys.KeyAt(ks, i%ks.Count(), timestamp)
179+
keys = append(keys, string(key))
180+
require.NoError(b, w.Set(key, []byte("value")))
181+
}
182+
183+
require.NoError(b, w.Close())
184+
185+
// Read the file back
186+
f, err = mem.Open("bench")
187+
require.NoError(b, err)
188+
189+
readable, err := NewSimpleReadable(f)
190+
require.NoError(b, err)
191+
192+
reader, err := NewReader(context.Background(), readable, ReaderOptions{
193+
Comparer: testkeys.Comparer,
194+
})
195+
require.NoError(b, err)
196+
197+
// Verify we actually created a two-level index
198+
if !reader.Attributes.Has(AttributeTwoLevelIndex) {
199+
b.Fatalf("Bloom filter test data did not create two-level index")
200+
}
201+
202+
return reader, keys
203+
}
204+
205+
// BenchmarkTwoLevelIteratorSeekPrefixGE_NoHit measures construction + SeekPrefixGE with bloom filter miss
206+
// How to: go test -bench=BenchmarkTwoLevelIteratorSeekPrefixGE_NoHit -run=^$ -count=10 ./sstable
207+
func BenchmarkTwoLevelIteratorSeekPrefixGE_NoHit(b *testing.B) {
208+
b.Run("rowblk", func(b *testing.B) {
209+
reader, _ := setupBloomFilterData(b)
210+
defer reader.Close()
211+
212+
opts := IterOptions{}
213+
// Use a prefix that doesn't exist to trigger bloom filter miss
214+
prefix := []byte("nonexistent")
215+
seekKey := []byte("nonexistent@1")
216+
217+
b.ResetTimer()
218+
219+
for i := 0; i < b.N; i++ {
220+
var iter Iterator
221+
var err error
222+
iter, err = newRowBlockTwoLevelIterator(context.Background(), reader, opts)
223+
require.NoError(b, err)
224+
225+
// First SeekPrefixGE call - should be rejected by bloom filter
226+
kv := iter.SeekPrefixGE(prefix, seekKey, base.SeekGEFlagsNone)
227+
// Expect nil due to bloom filter rejection
228+
if kv != nil {
229+
b.Fatal("Expected nil key-value from SeekPrefixGE with non-existent prefix")
230+
}
231+
232+
iter.Close()
233+
}
234+
})
235+
}
236+
237+
// BenchmarkTwoLevelIteratorSeekPrefixGE_Hit measures construction + SeekPrefixGE with bloom filter hit
238+
// How to: go test -bench=BenchmarkTwoLevelIteratorSeekPrefixGE_Hit -run=^$ -count=10 ./sstable
239+
func BenchmarkTwoLevelIteratorSeekPrefixGE_Hit(b *testing.B) {
240+
b.Run("rowblk", func(b *testing.B) {
241+
reader, keys := setupBloomFilterData(b)
242+
defer reader.Close()
243+
244+
opts := IterOptions{}
245+
// Use the first key from our generated keys to ensure it exists
246+
fullKey := []byte(keys[0])
247+
// Extract prefix using the comparer's Split method
248+
splitIndex := testkeys.Comparer.Split(fullKey)
249+
prefix := fullKey[:splitIndex]
250+
251+
b.ResetTimer()
252+
253+
for i := 0; i < b.N; i++ {
254+
var iter Iterator
255+
var err error
256+
iter, err = newRowBlockTwoLevelIterator(context.Background(), reader, opts)
257+
require.NoError(b, err)
258+
259+
// First SeekPrefixGE call - should pass bloom filter and find data
260+
kv := iter.SeekPrefixGE(prefix, fullKey, base.SeekGEFlagsNone)
261+
if kv == nil {
262+
b.Fatalf("Expected non-nil key-value from SeekPrefixGE, prefix=%s, key=%s", string(prefix), string(fullKey))
263+
}
264+
265+
iter.Close()
266+
}
267+
})
268+
}

0 commit comments

Comments
 (0)