github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/libs/iavl/benchmarks/bench_test.go (about) 1 package benchmarks 2 3 import ( 4 "fmt" 5 "math/rand" 6 "os" 7 "runtime" 8 "testing" 9 10 "github.com/stretchr/testify/require" 11 12 "github.com/fibonacci-chain/fbc/libs/iavl" 13 db "github.com/fibonacci-chain/fbc/libs/tm-db" 14 ) 15 16 const historySize = 20 17 18 func randBytes(length int) []byte { 19 key := make([]byte, length) 20 // math.rand.Read always returns err=nil 21 // we do not need cryptographic randomness for this test: 22 //nolint:gosec 23 rand.Read(key) 24 return key 25 } 26 27 func prepareTree(b *testing.B, db db.DB, size, keyLen, dataLen int) (*iavl.MutableTree, [][]byte) { 28 t, err := iavl.NewMutableTreeWithOpts(db, size, nil) 29 require.NoError(b, err) 30 keys := make([][]byte, size) 31 32 for i := 0; i < size; i++ { 33 key := randBytes(keyLen) 34 t.Set(key, randBytes(dataLen)) 35 keys[i] = key 36 } 37 commitTree(b, t) 38 runtime.GC() 39 return t, keys 40 } 41 42 // commit tree saves a new version and deletes old ones according to historySize 43 func commitTree(b *testing.B, t *iavl.MutableTree) { 44 t.Hash() 45 46 _, version, _, err := t.SaveVersion(false) 47 48 if err != nil { 49 b.Errorf("Can't save: %v", err) 50 } 51 52 if version > historySize { 53 if !iavl.EnableAsyncCommit { 54 err = t.DeleteVersion(version - historySize) 55 if err != nil { 56 b.Errorf("Can't delete: %v", err) 57 } 58 } 59 } 60 } 61 62 func runQueriesFast(b *testing.B, t *iavl.MutableTree, keyLen int) { 63 if !iavl.EnableAsyncCommit { 64 require.True(b, t.IsFastCacheEnabled()) 65 } 66 for i := 0; i < b.N; i++ { 67 q := randBytes(keyLen) 68 t.Get(q) 69 } 70 } 71 72 func runKnownQueriesFast(b *testing.B, t *iavl.MutableTree, keys [][]byte) { 73 if !iavl.EnableAsyncCommit { 74 require.True(b, t.IsFastCacheEnabled()) 75 } 76 l := int32(len(keys)) 77 for i := 0; i < b.N; i++ { 78 q := keys[rand.Int31n(l)] 79 t.Get(q) 80 } 81 } 82 83 func runQueriesSlow(b *testing.B, t *iavl.MutableTree, keyLen int) { 84 b.StopTimer() 85 // Save version to get an old immutable tree to query against, 86 // Fast storage is not enabled on old tree versions, allowing us to bench the desired behavior. 87 _, version, _, err := t.SaveVersion(false) 88 require.NoError(b, err) 89 90 itree, err := t.GetImmutable(version - 1) 91 require.NoError(b, err) 92 require.False(b, itree.IsFastCacheEnabled()) // to ensure fast storage is not enabled 93 94 b.StartTimer() 95 for i := 0; i < b.N; i++ { 96 q := randBytes(keyLen) 97 itree.GetWithIndex(q) 98 } 99 } 100 101 func runKnownQueriesSlow(b *testing.B, t *iavl.MutableTree, keys [][]byte) { 102 b.StopTimer() 103 // Save version to get an old immutable tree to query against, 104 // Fast storage is not enabled on old tree versions, allowing us to bench the desired behavior. 105 _, version, _, err := t.SaveVersion(false) 106 require.NoError(b, err) 107 108 itree, err := t.GetImmutable(version - 1) 109 require.NoError(b, err) 110 require.False(b, itree.IsFastCacheEnabled()) // to ensure fast storage is not enabled 111 b.StartTimer() 112 l := int32(len(keys)) 113 for i := 0; i < b.N; i++ { 114 q := keys[rand.Int31n(l)] 115 index, value := itree.GetWithIndex(q) 116 require.True(b, index >= 0, "the index must not be negative") 117 require.NotNil(b, value, "the value should exist") 118 } 119 } 120 121 func runIterationFast(b *testing.B, t *iavl.MutableTree, expectedSize int) { 122 if !iavl.EnableAsyncCommit { 123 require.True(b, t.IsFastCacheEnabled()) // to ensure fast storage is enabled 124 } 125 for i := 0; i < b.N; i++ { 126 itr := t.ImmutableTree.Iterator(nil, nil, false) 127 iterate(b, itr, expectedSize) 128 itr.Close() 129 } 130 } 131 132 func runIterationSlow(b *testing.B, t *iavl.MutableTree, expectedSize int) { 133 for i := 0; i < b.N; i++ { 134 itr := iavl.NewIterator(nil, nil, false, t.ImmutableTree) // create slow iterator directly 135 iterate(b, itr, expectedSize) 136 itr.Close() 137 } 138 } 139 140 func iterate(b *testing.B, itr db.Iterator, expectedSize int) { 141 b.StartTimer() 142 keyValuePairs := make([][][]byte, 0, expectedSize) 143 for i := 0; i < expectedSize && itr.Valid(); i++ { 144 itr.Next() 145 keyValuePairs = append(keyValuePairs, [][]byte{itr.Key(), itr.Value()}) 146 } 147 b.StopTimer() 148 if g, w := len(keyValuePairs), expectedSize; g != w { 149 b.Errorf("iteration count mismatch: got=%d, want=%d", g, w) 150 } else { 151 b.Logf("completed %d iterations", len(keyValuePairs)) 152 } 153 } 154 155 // func runInsert(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int) *iavl.MutableTree { 156 // for i := 1; i <= b.N; i++ { 157 // t.Set(randBytes(keyLen), randBytes(dataLen)) 158 // if i%blockSize == 0 { 159 // t.Hash() 160 // t.SaveVersion() 161 // } 162 // } 163 // return t 164 // } 165 166 func runUpdate(b *testing.B, t *iavl.MutableTree, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree { 167 l := int32(len(keys)) 168 for i := 1; i <= b.N; i++ { 169 key := keys[rand.Int31n(l)] 170 t.Set(key, randBytes(dataLen)) 171 if i%blockSize == 0 { 172 commitTree(b, t) 173 } 174 } 175 return t 176 } 177 178 // func runDelete(b *testing.B, t *iavl.MutableTree, blockSize int, keys [][]byte) *iavl.MutableTree { 179 // var key []byte 180 // l := int32(len(keys)) 181 // for i := 1; i <= b.N; i++ { 182 // key = keys[rand.Int31n(l)] 183 // // key = randBytes(16) 184 // // TODO: test if removed, use more keys (from insert) 185 // t.Remove(key) 186 // if i%blockSize == 0 { 187 // commitTree(b, t) 188 // } 189 // } 190 // return t 191 // } 192 193 // runBlock measures time for an entire block, not just one tx 194 func runBlock(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree { 195 l := int32(len(keys)) 196 197 // XXX: This was adapted to work with VersionedTree but needs to be re-thought. 198 199 lastCommit := t 200 real := t 201 // check := t 202 203 for i := 0; i < b.N; i++ { 204 for j := 0; j < blockSize; j++ { 205 // 50% insert, 50% update 206 var key []byte 207 if i%2 == 0 { 208 key = keys[rand.Int31n(l)] 209 } else { 210 key = randBytes(keyLen) 211 } 212 data := randBytes(dataLen) 213 214 // perform query and write on check and then real 215 // check.Get(key) 216 // check.Set(key, data) 217 real.GetWithIndex(key) 218 real.Set(key, data) 219 } 220 221 // at the end of a block, move it all along.... 222 commitTree(b, real) 223 lastCommit = real 224 } 225 226 return lastCommit 227 } 228 229 func BenchmarkRandomBytes(b *testing.B) { 230 fmt.Printf("%s\n", iavl.GetVersionInfo()) 231 benchmarks := []struct { 232 length int 233 }{ 234 {4}, {16}, {32}, {100}, {1000}, 235 } 236 for _, bench := range benchmarks { 237 bench := bench 238 name := fmt.Sprintf("random-%d", bench.length) 239 b.Run(name, func(b *testing.B) { 240 for i := 0; i < b.N; i++ { 241 randBytes(bench.length) 242 } 243 runtime.GC() 244 }) 245 } 246 } 247 248 type benchmark struct { 249 dbType db.BackendType 250 initSize, blockSize int 251 keyLen, dataLen int 252 } 253 254 func BenchmarkMedium(b *testing.B) { 255 benchmarks := []benchmark{ 256 {"memdb", 100000, 100, 16, 40}, 257 {"goleveldb", 100000, 100, 16, 40}, 258 // FIXME: this crashes on init! Either remove support, or make it work. 259 // {"cleveldb", 100000, 100, 16, 40}, 260 } 261 runBenchmarks(b, benchmarks) 262 } 263 264 func BenchmarkSmall(b *testing.B) { 265 benchmarks := []benchmark{ 266 {"memdb", 1000, 100, 4, 10}, 267 {"goleveldb", 1000, 100, 4, 10}, 268 // FIXME: this crashes on init! Either remove support, or make it work. 269 // {"cleveldb", 100000, 100, 16, 40}, 270 } 271 runBenchmarks(b, benchmarks) 272 } 273 274 func BenchmarkLarge(b *testing.B) { 275 benchmarks := []benchmark{ 276 {"memdb", 1000000, 100, 16, 40}, 277 {"goleveldb", 1000000, 100, 16, 40}, 278 // FIXME: this crashes on init! Either remove support, or make it work. 279 // {"cleveldb", 100000, 100, 16, 40}, 280 } 281 runBenchmarks(b, benchmarks) 282 } 283 284 func BenchmarkLevelDBBatchSizes(b *testing.B) { 285 benchmarks := []benchmark{ 286 {"goleveldb", 100000, 5, 16, 40}, 287 {"goleveldb", 100000, 25, 16, 40}, 288 {"goleveldb", 100000, 100, 16, 40}, 289 {"goleveldb", 100000, 400, 16, 40}, 290 {"goleveldb", 100000, 2000, 16, 40}, 291 } 292 runBenchmarks(b, benchmarks) 293 } 294 295 // BenchmarkLevelDBLargeData is intended to push disk limits 296 // in the goleveldb, to make sure not everything is cached 297 func BenchmarkLevelDBLargeData(b *testing.B) { 298 benchmarks := []benchmark{ 299 {"goleveldb", 50000, 100, 32, 100}, 300 {"goleveldb", 50000, 100, 32, 1000}, 301 {"goleveldb", 50000, 100, 32, 10000}, 302 {"goleveldb", 50000, 100, 32, 100000}, 303 } 304 runBenchmarks(b, benchmarks) 305 } 306 307 func runBenchmarks(b *testing.B, benchmarks []benchmark) { 308 fmt.Printf("%s\n", iavl.GetVersionInfo()) 309 for _, bb := range benchmarks { 310 bb := bb 311 prefix := fmt.Sprintf("%s-%d-%d-%d-%d", bb.dbType, 312 bb.initSize, bb.blockSize, bb.keyLen, bb.dataLen) 313 314 // prepare a dir for the db and cleanup afterwards 315 dirName := fmt.Sprintf("./%s-db", prefix) 316 defer func() { 317 err := os.RemoveAll(dirName) 318 if err != nil { 319 b.Errorf("%+v\n", err) 320 } 321 }() 322 323 // note that "" leads to nil backing db! 324 var d db.DB 325 if bb.dbType != "nodb" { 326 d = db.NewDB("test", bb.dbType, dirName) 327 defer d.Close() 328 } 329 b.Run(prefix, func(sub *testing.B) { 330 runSuite(sub, d, bb.initSize, bb.blockSize, bb.keyLen, bb.dataLen) 331 }) 332 } 333 } 334 335 // returns number of MB in use 336 func memUseMB() float64 { 337 var mem runtime.MemStats 338 runtime.ReadMemStats(&mem) 339 asize := mem.Alloc 340 mb := float64(asize) / 1000000 341 return mb 342 } 343 344 func runSuite(b *testing.B, d db.DB, initSize, blockSize, keyLen, dataLen int) { 345 // measure mem usage 346 runtime.GC() 347 init := memUseMB() 348 349 t, keys := prepareTree(b, d, initSize, keyLen, dataLen) 350 used := memUseMB() - init 351 fmt.Printf("Init Tree took %0.2f MB\n", used) 352 353 b.ResetTimer() 354 b.Run("query-no-in-tree-guarantee-fast", func(sub *testing.B) { 355 sub.ReportAllocs() 356 runQueriesFast(sub, t, keyLen) 357 }) 358 b.Run("query-no-in-tree-guarantee-slow", func(sub *testing.B) { 359 sub.ReportAllocs() 360 runQueriesSlow(sub, t, keyLen) 361 }) 362 // 363 b.Run("query-hits-fast", func(sub *testing.B) { 364 sub.ReportAllocs() 365 runKnownQueriesFast(sub, t, keys) 366 }) 367 b.Run("query-hits-slow", func(sub *testing.B) { 368 sub.ReportAllocs() 369 runKnownQueriesSlow(sub, t, keys) 370 }) 371 // 372 // Iterations for BenchmarkLevelDBLargeData timeout bencher in CI so 373 // we must skip them. 374 if b.Name() != "BenchmarkLevelDBLargeData" { 375 b.Run("iteration-fast", func(sub *testing.B) { 376 sub.ReportAllocs() 377 runIterationFast(sub, t, initSize) 378 }) 379 b.Run("iteration-slow", func(sub *testing.B) { 380 sub.ReportAllocs() 381 runIterationSlow(sub, t, initSize) 382 }) 383 } 384 // 385 386 b.Run("update", func(sub *testing.B) { 387 sub.ReportAllocs() 388 t = runUpdate(sub, t, dataLen, blockSize, keys) 389 }) 390 b.Run("block", func(sub *testing.B) { 391 sub.ReportAllocs() 392 t = runBlock(sub, t, keyLen, dataLen, blockSize, keys) 393 }) 394 395 // both of these edit size of the tree too much 396 // need to run with their own tree 397 // t = nil // for gc 398 // b.Run("insert", func(sub *testing.B) { 399 // it, _ := prepareTree(d, initSize, keyLen, dataLen) 400 // sub.ResetTimer() 401 // runInsert(sub, it, keyLen, dataLen, blockSize) 402 // }) 403 // b.Run("delete", func(sub *testing.B) { 404 // dt, dkeys := prepareTree(d, initSize+sub.N, keyLen, dataLen) 405 // sub.ResetTimer() 406 // runDelete(sub, dt, blockSize, dkeys) 407 // }) 408 }