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  }