Skip to content

Commit 5983a42

Browse files
committed
⚡️ perf: cache pools in FSM state machine
Adds an in-memory pool cache to StateMachine, mirroring the existing accounts cache. GetPool checks the cache before hitting Pebble; SetPool populates the cache after a successful store write (cache-after-write avoids leaking uncommitted state if the store op fails). Returned and cached pools are deep-cloned via clonePool() to prevent pointer aliasing on the Points slice — a caller mutating its returned Pool.Points cannot affect the cached entry, and SetPool callers cannot leak external mutation into the cache. The pools map is wired through New(), Copy(), and ResetCaches() so it is invalidated on per-tx rollback (state.go:338) and per-block reset, alongside the accounts cache. Targets the per-block 90s GetPool cost observed in staging localnet-1 profiling (177K calls × 0.504ms).
1 parent 22fae6e commit 5983a42

3 files changed

Lines changed: 35 additions & 1 deletion

File tree

fsm/account.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,19 @@ func (s *StateMachine) marshalAccount(account *Account) ([]byte, lib.ErrorI) {
253253
to simply prove that no-one owns the private key for that account
254254
*/
255255

256+
// clonePool returns an independent copy of the pool so callers can mutate without
257+
// affecting the cached entry (Pool.Points is a slice of pointers).
258+
func clonePool(p *Pool) *Pool {
259+
out := &Pool{Id: p.Id, Amount: p.Amount, TotalPoolPoints: p.TotalPoolPoints}
260+
if len(p.Points) > 0 {
261+
out.Points = make([]*lib.PoolPoints, len(p.Points))
262+
for i, pp := range p.Points {
263+
out.Points[i] = &lib.PoolPoints{Address: pp.Address, Points: pp.Points}
264+
}
265+
}
266+
return out
267+
}
268+
256269
// GetPool() returns a Pool structure for a specific ID
257270
func (s *StateMachine) GetPool(id uint64) (*Pool, lib.ErrorI) {
258271
startTime := time.Now()
@@ -261,6 +274,10 @@ func (s *StateMachine) GetPool(id uint64) (*Pool, lib.ErrorI) {
261274
s.Metrics.StateOperationTime.WithLabelValues("get_pool").Observe(time.Since(startTime).Seconds())
262275
}
263276
}()
277+
// check cache (return clone to prevent aliasing on the cached struct)
278+
if pool, found := s.cache.pools[id]; found {
279+
return clonePool(pool), nil
280+
}
264281
// get the pool bytes from the state using the Key a specific id
265282
bz, err := s.Get(KeyForPool(id))
266283
if err != nil {
@@ -273,6 +290,8 @@ func (s *StateMachine) GetPool(id uint64) (*Pool, lib.ErrorI) {
273290
}
274291
// set the pool id from the key
275292
pool.Id = id
293+
// populate cache with a clone so the returned pointer is not aliased
294+
s.cache.pools[id] = clonePool(pool)
276295
// return the pool
277296
return pool, nil
278297
}
@@ -334,7 +353,13 @@ func (s *StateMachine) SetPool(pool *Pool) (err lib.ErrorI) {
334353
}()
335354
// if the pool has a 0 balance
336355
if pool.Amount == 0 {
337-
return s.Delete(KeyForPool(pool.Id))
356+
if err = s.Delete(KeyForPool(pool.Id)); err != nil {
357+
return
358+
}
359+
// cache a clone after successful delete (matches accounts cache: zero-amount is
360+
// indistinguishable from a missing entry via unmarshalPool(nil))
361+
s.cache.pools[pool.Id] = clonePool(pool)
362+
return
338363
}
339364
// convert the pool to bytes
340365
bz, err := s.marshalPool(pool)
@@ -345,6 +370,9 @@ func (s *StateMachine) SetPool(pool *Pool) (err lib.ErrorI) {
345370
if err = s.Set(KeyForPool(pool.Id), bz); err != nil {
346371
return
347372
}
373+
// cache a clone after successful write so external mutation of the passed-in pool
374+
// cannot leak into the cache
375+
s.cache.pools[pool.Id] = clonePool(pool)
348376
return
349377
}
350378

fsm/state.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type rootCacheStateStore interface {
4545
// cache is the set of items to be cached used by the state machine
4646
type cache struct {
4747
accounts map[uint64]*Account // cache of accounts accessed
48+
pools map[uint64]*Pool // cache of pools accessed
4849
feeParams *FeeParams // fee params for the current block
4950
valParams *ValidatorParams // validator params for the current block
5051
rootDexBatch *lib.DexBatch // root dex batch
@@ -66,6 +67,7 @@ func New(c lib.Config, store lib.StoreI, plugin *lib.Plugin, metrics *lib.Metric
6667
events: new(lib.EventsTracker),
6768
cache: &cache{
6869
accounts: make(map[uint64]*Account),
70+
pools: make(map[uint64]*Pool),
6971
},
7072
}
7173
// initialize the state machine
@@ -604,6 +606,7 @@ func (s *StateMachine) Copy() (*StateMachine, lib.ErrorI) {
604606
log: s.log,
605607
cache: &cache{
606608
accounts: make(map[uint64]*Account),
609+
pools: make(map[uint64]*Pool),
607610
rootDexBatch: s.cache.rootDexBatch,
608611
},
609612
LastValidatorSet: s.LastValidatorSet,
@@ -714,6 +717,7 @@ func (s *StateMachine) Reset() {
714717
// ResetCaches() dumps the state machine caches
715718
func (s *StateMachine) ResetCaches() {
716719
s.cache.accounts = make(map[uint64]*Account)
720+
s.cache.pools = make(map[uint64]*Pool)
717721
// Params caches must not outlive the current store view, otherwise Reset()/rollback
718722
// can leave the FSM reading stale values that disagree with the underlying store.
719723
s.cache.valParams = nil

fsm/state_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func TestInitialize(t *testing.T) {
7373
log: log,
7474
cache: &cache{
7575
accounts: make(map[uint64]*Account),
76+
pools: make(map[uint64]*Pool),
7677
},
7778
}
7879
// set the data dir path
@@ -415,6 +416,7 @@ func newTestStateMachine(t *testing.T) StateMachine {
415416
log: log,
416417
cache: &cache{
417418
accounts: make(map[uint64]*Account),
419+
pools: make(map[uint64]*Pool),
418420
},
419421
}
420422
require.NoError(t, sm.SetParams(DefaultParams()))

0 commit comments

Comments
 (0)