github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/stdmap/backDataHeapBenchmark_test.go (about)

     1  package stdmap_test
     2  
     3  import (
     4  	"runtime"
     5  	"runtime/debug"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	lru "github.com/hashicorp/golang-lru"
    11  	zlog "github.com/rs/zerolog/log"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/onflow/flow-go/model/flow"
    15  	herocache "github.com/onflow/flow-go/module/mempool/herocache/backdata"
    16  	"github.com/onflow/flow-go/module/mempool/herocache/backdata/heropool"
    17  	"github.com/onflow/flow-go/module/mempool/stdmap"
    18  	"github.com/onflow/flow-go/module/metrics"
    19  	"github.com/onflow/flow-go/utils/unittest"
    20  )
    21  
    22  // BenchmarkBaselineLRU benchmarks heap allocation performance of
    23  // hashicorp LRU cache with 50K capacity against writing 100M entities,
    24  // with Garbage Collection (GC) disabled.
    25  func BenchmarkBaselineLRU(b *testing.B) {
    26  	unittest.SkipBenchmarkUnless(b, unittest.BENCHMARK_EXPERIMENT, "skips benchmarking baseline LRU, set environment variable to enable")
    27  
    28  	defer debug.SetGCPercent(debug.SetGCPercent(-1)) // disable GC
    29  
    30  	limit := uint(50)
    31  	backData := stdmap.NewBackend(
    32  		stdmap.WithBackData(newBaselineLRU(int(limit))),
    33  		stdmap.WithLimit(limit))
    34  
    35  	entities := unittest.EntityListFixture(uint(100_000))
    36  	testAddEntities(b, limit, backData, entities)
    37  
    38  	unittest.PrintHeapInfo(unittest.Logger()) // heap info after writing 100M entities
    39  	gcAndWriteHeapProfile()                   // runs garbage collection
    40  	unittest.PrintHeapInfo(unittest.Logger()) // heap info after running garbage collection
    41  }
    42  
    43  // BenchmarkArrayBackDataLRU benchmarks heap allocation performance of
    44  // ArrayBackData-based cache (aka heroCache) with 50K capacity against writing 100M entities,
    45  // with Garbage Collection (GC) disabled.
    46  func BenchmarkArrayBackDataLRU(b *testing.B) {
    47  	defer debug.SetGCPercent(debug.SetGCPercent(-1)) // disable GC
    48  	limit := uint(50_000)
    49  
    50  	backData := stdmap.NewBackend(
    51  		stdmap.WithBackData(
    52  			herocache.NewCache(
    53  				uint32(limit),
    54  				8,
    55  				heropool.LRUEjection,
    56  				unittest.Logger(),
    57  				metrics.NewNoopCollector())),
    58  		stdmap.WithLimit(limit))
    59  
    60  	entities := unittest.EntityListFixture(uint(100_000_000))
    61  	testAddEntities(b, limit, backData, entities)
    62  
    63  	unittest.PrintHeapInfo(unittest.Logger()) // heap info after writing 100M entities
    64  	gcAndWriteHeapProfile()                   // runs garbage collection
    65  	unittest.PrintHeapInfo(unittest.Logger()) // heap info after running garbage collection
    66  }
    67  
    68  func gcAndWriteHeapProfile() {
    69  	// see <https://pkg.go.dev/runtime/pprof>
    70  	t1 := time.Now()
    71  	runtime.GC() // get up-to-date statistics
    72  	elapsed := time.Since(t1).Seconds()
    73  	zlog.Info().
    74  		Float64("gc-elapsed-time", elapsed).
    75  		Msg("garbage collection done")
    76  }
    77  
    78  // testAddEntities is a test helper that checks entities are added successfully to the backdata.
    79  // and each entity is retrievable right after it is written to backdata.
    80  func testAddEntities(t testing.TB, limit uint, b *stdmap.Backend, entities []*unittest.MockEntity) {
    81  	// adding elements
    82  	t1 := time.Now()
    83  	for i, e := range entities {
    84  		require.False(t, b.Has(e.ID()))
    85  		// adding each element must be successful.
    86  		require.True(t, b.Add(*e))
    87  
    88  		if uint(i) < limit {
    89  			// when we are below limit the total of
    90  			// backdata should be incremented by each addition.
    91  			require.Equal(t, b.Size(), uint(i+1))
    92  		} else {
    93  			// when we cross the limit, the ejection kicks in, and
    94  			// size must be steady at the limit.
    95  			require.Equal(t, uint(b.Size()), limit)
    96  		}
    97  
    98  		// entity should be immediately retrievable
    99  		actual, ok := b.ByID(e.ID())
   100  		require.True(t, ok)
   101  		require.Equal(t, *e, actual)
   102  	}
   103  	elapsed := time.Since(t1)
   104  	zlog.Info().Dur("interaction_time", elapsed).Msg("adding elements done")
   105  }
   106  
   107  // baseLineLRU implements a BackData wrapper around hashicorp lru, which makes
   108  // it compliant to be used as BackData component in mempool.Backend. Note that
   109  // this is used only as an experimental baseline, and so it's not exported for
   110  // production.
   111  type baselineLRU struct {
   112  	c     *lru.Cache // used to incorporate an LRU cache
   113  	limit int
   114  
   115  	// atomicAdjustMutex is used to synchronize concurrent access to the
   116  	// underlying LRU cache. This is needed because hashicorp LRU does not
   117  	// provide thread-safety for atomic adjust-with-init or get-with-init operations.
   118  	atomicAdjustMutex sync.Mutex
   119  }
   120  
   121  func newBaselineLRU(limit int) *baselineLRU {
   122  	var err error
   123  	c, err := lru.New(limit)
   124  	if err != nil {
   125  		panic(err)
   126  	}
   127  
   128  	return &baselineLRU{
   129  		c:     c,
   130  		limit: limit,
   131  	}
   132  }
   133  
   134  // Has checks if we already contain the item with the given hash.
   135  func (b *baselineLRU) Has(entityID flow.Identifier) bool {
   136  	_, ok := b.c.Get(entityID)
   137  	return ok
   138  }
   139  
   140  // Add adds the given item to the pool.
   141  func (b *baselineLRU) Add(entityID flow.Identifier, entity flow.Entity) bool {
   142  	b.c.Add(entityID, entity)
   143  	return true
   144  }
   145  
   146  // Remove will remove the item with the given hash.
   147  func (b *baselineLRU) Remove(entityID flow.Identifier) (flow.Entity, bool) {
   148  	e, ok := b.c.Get(entityID)
   149  	if !ok {
   150  		return nil, false
   151  	}
   152  	entity, ok := e.(flow.Entity)
   153  	if !ok {
   154  		return nil, false
   155  	}
   156  
   157  	return entity, b.c.Remove(entityID)
   158  }
   159  
   160  // Adjust will adjust the value item using the given function if the given key can be found.
   161  // Returns a bool which indicates whether the value was updated as well as the updated value
   162  func (b *baselineLRU) Adjust(entityID flow.Identifier, f func(flow.Entity) flow.Entity) (flow.Entity, bool) {
   163  	entity, removed := b.Remove(entityID)
   164  	if !removed {
   165  		return nil, false
   166  	}
   167  
   168  	newEntity := f(entity)
   169  	newEntityID := newEntity.ID()
   170  
   171  	b.Add(newEntityID, newEntity)
   172  
   173  	return newEntity, true
   174  }
   175  
   176  // AdjustWithInit will adjust the value item using the given function if the given key can be found.
   177  // If the key is not found, the init function will be called to create a new value.
   178  // Returns a bool which indicates whether the value was updated as well as the updated value and
   179  // a bool indicating whether the value was initialized.
   180  // Note: this is a benchmark helper, hence, the adjust-with-init provides serializability w.r.t other concurrent adjust-with-init or get-with-init operations,
   181  // and does not provide serializability w.r.t concurrent add, adjust or get operations.
   182  func (b *baselineLRU) AdjustWithInit(entityID flow.Identifier, adjust func(flow.Entity) flow.Entity, init func() flow.Entity) (flow.Entity, bool) {
   183  	b.atomicAdjustMutex.Lock()
   184  	defer b.atomicAdjustMutex.Unlock()
   185  
   186  	if b.Has(entityID) {
   187  		return b.Adjust(entityID, adjust)
   188  	}
   189  	added := b.Add(entityID, init())
   190  	if !added {
   191  		return nil, false
   192  	}
   193  	return b.Adjust(entityID, adjust)
   194  }
   195  
   196  // GetWithInit will retrieve the value item if the given key can be found.
   197  // If the key is not found, the init function will be called to create a new value.
   198  // Returns a bool which indicates whether the entity was found (or created).
   199  func (b *baselineLRU) GetWithInit(entityID flow.Identifier, init func() flow.Entity) (flow.Entity, bool) {
   200  	newE := init()
   201  	e, ok, _ := b.c.PeekOrAdd(entityID, newE)
   202  	if !ok {
   203  		// if the entity was not found, it means that the new entity was added to the cache.
   204  		return newE, true
   205  	}
   206  	// if the entity was found, it means that the new entity was not added to the cache.
   207  	return e.(flow.Entity), true
   208  }
   209  
   210  // ByID returns the given item from the pool.
   211  func (b *baselineLRU) ByID(entityID flow.Identifier) (flow.Entity, bool) {
   212  	e, ok := b.c.Get(entityID)
   213  	if !ok {
   214  		return nil, false
   215  	}
   216  
   217  	entity, ok := e.(flow.Entity)
   218  	if !ok {
   219  		return nil, false
   220  	}
   221  	return entity, ok
   222  }
   223  
   224  // Size will return the total of the backend.
   225  func (b *baselineLRU) Size() uint {
   226  	return uint(b.c.Len())
   227  }
   228  
   229  // All returns all entities from the pool.
   230  func (b *baselineLRU) All() map[flow.Identifier]flow.Entity {
   231  	all := make(map[flow.Identifier]flow.Entity)
   232  	for _, entityID := range b.c.Keys() {
   233  		id, ok := entityID.(flow.Identifier)
   234  		if !ok {
   235  			panic("could not assert to entity id")
   236  		}
   237  
   238  		entity, ok := b.ByID(id)
   239  		if !ok {
   240  			panic("could not retrieve entity from mempool")
   241  		}
   242  		all[id] = entity
   243  	}
   244  
   245  	return all
   246  }
   247  
   248  func (b *baselineLRU) Identifiers() flow.IdentifierList {
   249  	ids := make(flow.IdentifierList, b.c.Len())
   250  	entityIds := b.c.Keys()
   251  	total := len(entityIds)
   252  	for i := 0; i < total; i++ {
   253  		id, ok := entityIds[i].(flow.Identifier)
   254  		if !ok {
   255  			panic("could not assert to entity id")
   256  		}
   257  		ids[i] = id
   258  	}
   259  	return ids
   260  }
   261  
   262  func (b *baselineLRU) Entities() []flow.Entity {
   263  	entities := make([]flow.Entity, b.c.Len())
   264  	entityIds := b.c.Keys()
   265  	total := len(entityIds)
   266  	for i := 0; i < total; i++ {
   267  		id, ok := entityIds[i].(flow.Identifier)
   268  		if !ok {
   269  			panic("could not assert to entity id")
   270  		}
   271  
   272  		entity, ok := b.ByID(id)
   273  		if !ok {
   274  			panic("could not retrieve entity from mempool")
   275  		}
   276  		entities[i] = entity
   277  	}
   278  	return entities
   279  }
   280  
   281  // Clear removes all entities from the pool.
   282  func (b *baselineLRU) Clear() {
   283  	var err error
   284  	b.c, err = lru.New(b.limit)
   285  	if err != nil {
   286  		panic(err)
   287  	}
   288  }