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 }