github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/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/gnolang/gno/tm2/pkg/db"
    13  	_ "github.com/gnolang/gno/tm2/pkg/db/_all"
    14  	"github.com/gnolang/gno/tm2/pkg/iavl"
    15  )
    16  
    17  const historySize = 20
    18  
    19  func randBytes(length int) []byte {
    20  	key := make([]byte, length)
    21  	// math.rand.Read always returns err=nil
    22  	// we do not need cryptographic randomness for this test:
    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  	b.Helper()
    29  
    30  	t := iavl.NewMutableTree(db, size)
    31  	keys := make([][]byte, size)
    32  
    33  	for i := 0; i < size; i++ {
    34  		key := randBytes(keyLen)
    35  		t.Set(key, randBytes(dataLen))
    36  		keys[i] = key
    37  	}
    38  	commitTree(b, t)
    39  	runtime.GC()
    40  	return t, keys
    41  }
    42  
    43  // commit tree saves a new version and deletes and old one...
    44  func commitTree(b *testing.B, t *iavl.MutableTree) {
    45  	b.Helper()
    46  
    47  	t.Hash()
    48  	_, version, err := t.SaveVersion()
    49  	if err != nil {
    50  		b.Errorf("Can't save: %v", err)
    51  	}
    52  	if version > historySize {
    53  		err = t.DeleteVersion(version - historySize)
    54  		if err != nil {
    55  			b.Errorf("Can't delete: %v", err)
    56  		}
    57  	}
    58  }
    59  
    60  func runQueries(b *testing.B, t *iavl.MutableTree, keyLen int) {
    61  	b.Helper()
    62  
    63  	for i := 0; i < b.N; i++ {
    64  		q := randBytes(keyLen)
    65  		t.Get(q)
    66  	}
    67  }
    68  
    69  func runKnownQueries(b *testing.B, t *iavl.MutableTree, keys [][]byte) {
    70  	b.Helper()
    71  
    72  	l := int32(len(keys))
    73  	for i := 0; i < b.N; i++ {
    74  		q := keys[rand.Int31n(l)]
    75  		t.Get(q)
    76  	}
    77  }
    78  
    79  func runUpdate(b *testing.B, t *iavl.MutableTree, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree {
    80  	b.Helper()
    81  
    82  	l := int32(len(keys))
    83  	for i := 1; i <= b.N; i++ {
    84  		key := keys[rand.Int31n(l)]
    85  		t.Set(key, randBytes(dataLen))
    86  		if i%blockSize == 0 {
    87  			commitTree(b, t)
    88  		}
    89  	}
    90  	return t
    91  }
    92  
    93  // runBlock measures time for an entire block, not just one tx
    94  func runBlock(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree {
    95  	b.Helper()
    96  
    97  	l := int32(len(keys))
    98  
    99  	// XXX: This was adapted to work with VersionedTree but needs to be re-thought.
   100  
   101  	lastCommit := t
   102  	realTree := t
   103  	// check := t
   104  
   105  	for i := 0; i < b.N; i++ {
   106  		for j := 0; j < blockSize; j++ {
   107  			// 50% insert, 50% update
   108  			var key []byte
   109  			if i%2 == 0 {
   110  				key = keys[rand.Int31n(l)]
   111  			} else {
   112  				key = randBytes(keyLen)
   113  			}
   114  			data := randBytes(dataLen)
   115  
   116  			// perform query and write on check and then real
   117  			// check.Get(key)
   118  			// check.Set(key, data)
   119  			realTree.Get(key)
   120  			realTree.Set(key, data)
   121  		}
   122  
   123  		// at the end of a block, move it all along....
   124  		commitTree(b, realTree)
   125  		lastCommit = realTree
   126  	}
   127  
   128  	return lastCommit
   129  }
   130  
   131  func BenchmarkRandomBytes(b *testing.B) {
   132  	benchmarks := []struct {
   133  		length int
   134  	}{
   135  		{4}, {16}, {32}, {100}, {1000},
   136  	}
   137  	for _, bench := range benchmarks {
   138  		name := fmt.Sprintf("random-%d", bench.length)
   139  		b.Run(name, func(b *testing.B) {
   140  			for i := 0; i < b.N; i++ {
   141  				randBytes(bench.length)
   142  			}
   143  			runtime.GC()
   144  		})
   145  	}
   146  }
   147  
   148  type benchmark struct {
   149  	dbType              db.BackendType
   150  	initSize, blockSize int
   151  	keyLen, dataLen     int
   152  }
   153  
   154  func BenchmarkSmall(b *testing.B) {
   155  	ls := db.BackendList()
   156  	bs := make([]benchmark, 0, len(ls))
   157  	for _, backend := range ls {
   158  		bs = append(bs, benchmark{backend, 1_000, 100, 16, 40})
   159  	}
   160  	runBenchmarks(b, bs)
   161  }
   162  
   163  func BenchmarkMedium(b *testing.B) {
   164  	ls := db.BackendList()
   165  	bs := make([]benchmark, 0, len(ls))
   166  	for _, backend := range ls {
   167  		bs = append(bs, benchmark{backend, 100_000, 100, 16, 40})
   168  	}
   169  	runBenchmarks(b, bs)
   170  }
   171  
   172  func BenchmarkLarge(b *testing.B) {
   173  	ls := db.BackendList()
   174  	bs := make([]benchmark, 0, len(ls))
   175  	for _, backend := range ls {
   176  		bs = append(bs, benchmark{backend, 1_000_000, 100, 16, 40})
   177  	}
   178  	runBenchmarks(b, bs)
   179  }
   180  
   181  func BenchmarkLevelDBBatchSizes(b *testing.B) {
   182  	benchmarks := []benchmark{
   183  		{db.GoLevelDBBackend, 100000, 5, 16, 40},
   184  		{db.GoLevelDBBackend, 100000, 25, 16, 40},
   185  		{db.GoLevelDBBackend, 100000, 100, 16, 40},
   186  		{db.GoLevelDBBackend, 100000, 400, 16, 40},
   187  		{db.GoLevelDBBackend, 100000, 2000, 16, 40},
   188  	}
   189  	runBenchmarks(b, benchmarks)
   190  }
   191  
   192  // BenchmarkLevelDBLargeData is intended to push disk limits
   193  // in the leveldb, to make sure not everything is cached
   194  func BenchmarkLevelDBLargeData(b *testing.B) {
   195  	benchmarks := []benchmark{
   196  		{db.GoLevelDBBackend, 50000, 100, 32, 100},
   197  		{db.GoLevelDBBackend, 50000, 100, 32, 1000},
   198  		{db.GoLevelDBBackend, 50000, 100, 32, 10000},
   199  		{db.GoLevelDBBackend, 50000, 100, 32, 100000},
   200  	}
   201  	runBenchmarks(b, benchmarks)
   202  }
   203  
   204  func runBenchmarks(b *testing.B, benchmarks []benchmark) {
   205  	b.Helper()
   206  
   207  	for _, bb := range benchmarks {
   208  		prefix := fmt.Sprintf("%s-%d-%d-%d-%d", bb.dbType, bb.initSize,
   209  			bb.blockSize, bb.keyLen, bb.dataLen)
   210  
   211  		// prepare a dir for the db and cleanup afterwards
   212  		dirName := fmt.Sprintf("./%s-db", prefix)
   213  		defer func() {
   214  			err := os.RemoveAll(dirName)
   215  			if err != nil {
   216  				b.Errorf("%+v\n", err)
   217  			}
   218  		}()
   219  
   220  		// note that "" leads to nil backing db!
   221  		var d db.DB
   222  		var err error
   223  		if bb.dbType != "nodb" {
   224  			d, err = db.NewDB("test", bb.dbType, dirName)
   225  			require.NoError(b, err)
   226  			defer d.Close()
   227  		}
   228  		b.Run(prefix, func(sub *testing.B) {
   229  			runSuite(sub, d, bb.initSize, bb.blockSize, bb.keyLen, bb.dataLen)
   230  		})
   231  	}
   232  }
   233  
   234  // returns number of MB in use
   235  func memUseMB() float64 {
   236  	var mem runtime.MemStats
   237  	runtime.ReadMemStats(&mem)
   238  	asize := mem.Alloc
   239  	mb := float64(asize) / 1000000
   240  	return mb
   241  }
   242  
   243  func runSuite(b *testing.B, d db.DB, initSize, blockSize, keyLen, dataLen int) {
   244  	b.Helper()
   245  
   246  	// measure mem usage
   247  	runtime.GC()
   248  	init := memUseMB()
   249  
   250  	t, keys := prepareTree(b, d, initSize, keyLen, dataLen)
   251  	used := memUseMB() - init
   252  	fmt.Printf("Init Tree took %0.2f MB\n", used)
   253  
   254  	b.ResetTimer()
   255  
   256  	b.Run("query-miss", func(sub *testing.B) {
   257  		runQueries(sub, t, keyLen)
   258  	})
   259  	b.Run("query-hits", func(sub *testing.B) {
   260  		runKnownQueries(sub, t, keys)
   261  	})
   262  	b.Run("update", func(sub *testing.B) {
   263  		t = runUpdate(sub, t, dataLen, blockSize, keys)
   264  	})
   265  	b.Run("block", func(sub *testing.B) {
   266  		t = runBlock(sub, t, keyLen, dataLen, blockSize, keys)
   267  	})
   268  
   269  	// both of these edit size of the tree too much
   270  	// need to run with their own tree
   271  	// t = nil // for gc
   272  	// b.Run("insert", func(sub *testing.B) {
   273  	// 	it, _ := prepareTree(d, initSize, keyLen, dataLen)
   274  	// 	sub.ResetTimer()
   275  	// 	runInsert(sub, it, keyLen, dataLen, blockSize)
   276  	// })
   277  	// b.Run("delete", func(sub *testing.B) {
   278  	// 	dt, dkeys := prepareTree(d, initSize+sub.N, keyLen, dataLen)
   279  	// 	sub.ResetTimer()
   280  	// 	runDelete(sub, dt, blockSize, dkeys)
   281  	// })
   282  }