diff --git a/cache/cache.go b/cache/cache.go index 4eb2b3ae5e44..3d7206e79050 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -19,6 +19,9 @@ type Cacher[K comparable, V any] interface { // Flush removes all entries from the cache Flush() + // Returns the number of elements currently in the cache + Len() int + // Returns fraction of cache currently filled (0 --> 1) PortionFilled() float64 } diff --git a/cache/empty_cache.go b/cache/empty_cache.go index f6d0a082e09b..767cf5b74266 100644 --- a/cache/empty_cache.go +++ b/cache/empty_cache.go @@ -19,6 +19,10 @@ func (*Empty[K, _]) Evict(K) {} func (*Empty[_, _]) Flush() {} +func (*Empty[_, _]) Len() int { + return 0 +} + func (*Empty[_, _]) PortionFilled() float64 { return 0 } diff --git a/cache/lru_cache.go b/cache/lru_cache.go index 9c561f7c910a..19c10054e82b 100644 --- a/cache/lru_cache.go +++ b/cache/lru_cache.go @@ -50,6 +50,13 @@ func (c *LRU[_, _]) Flush() { c.flush() } +func (c *LRU[_, _]) Len() int { + c.lock.Lock() + defer c.lock.Unlock() + + return c.len() +} + func (c *LRU[_, _]) PortionFilled() float64 { c.lock.Lock() defer c.lock.Unlock() @@ -88,8 +95,15 @@ func (c *LRU[K, V]) flush() { c.elements = linkedhashmap.New[K, V]() } +func (c *LRU[_, _]) len() int { + if c.elements == nil { + return 0 + } + return c.elements.Len() +} + func (c *LRU[_, _]) portionFilled() float64 { - return float64(c.elements.Len()) / float64(c.Size) + return float64(c.len()) / float64(c.Size) } // Initializes [c.elements] if it's nil. diff --git a/cache/lru_sized_cache.go b/cache/lru_sized_cache.go index 9035e7383a51..6d093e033195 100644 --- a/cache/lru_sized_cache.go +++ b/cache/lru_sized_cache.go @@ -59,6 +59,13 @@ func (c *sizedLRU[K, V]) Flush() { c.flush() } +func (c *sizedLRU[_, _]) Len() int { + c.lock.Lock() + defer c.lock.Unlock() + + return c.len() +} + func (c *sizedLRU[_, _]) PortionFilled() float64 { c.lock.Lock() defer c.lock.Unlock() @@ -110,6 +117,10 @@ func (c *sizedLRU[K, V]) flush() { c.currentSize = 0 } +func (c *sizedLRU[_, _]) len() int { + return c.elements.Len() +} + func (c *sizedLRU[_, _]) portionFilled() float64 { return float64(c.currentSize) / float64(c.maxSize) } diff --git a/cache/metercacher/cache.go b/cache/metercacher/cache.go index 451dcc9d7087..6b6fcd909c81 100644 --- a/cache/metercacher/cache.go +++ b/cache/metercacher/cache.go @@ -33,6 +33,7 @@ func (c *Cache[K, V]) Put(key K, value V) { c.Cacher.Put(key, value) end := c.clock.Time() c.put.Observe(float64(end.Sub(start))) + c.len.Set(float64(c.Cacher.Len())) c.portionFilled.Set(c.Cacher.PortionFilled()) } @@ -52,10 +53,12 @@ func (c *Cache[K, V]) Get(key K) (V, bool) { func (c *Cache[K, _]) Evict(key K) { c.Cacher.Evict(key) + c.len.Set(float64(c.Cacher.Len())) c.portionFilled.Set(c.Cacher.PortionFilled()) } func (c *Cache[_, _]) Flush() { c.Cacher.Flush() + c.len.Set(float64(c.Cacher.Len())) c.portionFilled.Set(c.Cacher.PortionFilled()) } diff --git a/cache/metercacher/metrics.go b/cache/metercacher/metrics.go index 147d309d8375..4d11af040c34 100644 --- a/cache/metercacher/metrics.go +++ b/cache/metercacher/metrics.go @@ -33,11 +33,12 @@ func newCounterMetric(namespace, name string, reg prometheus.Registerer, errs *w } type metrics struct { - get, - put metric.Averager + get metric.Averager + put metric.Averager + len prometheus.Gauge portionFilled prometheus.Gauge - hit, - miss prometheus.Counter + hit prometheus.Counter + miss prometheus.Counter } func (m *metrics) Initialize( @@ -47,6 +48,13 @@ func (m *metrics) Initialize( errs := wrappers.Errs{} m.get = newAveragerMetric(namespace, "get", reg, &errs) m.put = newAveragerMetric(namespace, "put", reg, &errs) + m.len = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: namespace, + Name: "len", + Help: "number of entries", + }, + ) m.portionFilled = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: namespace, diff --git a/cache/test_cacher.go b/cache/test_cacher.go index e512ca067a15..1b029bcb4b21 100644 --- a/cache/test_cacher.go +++ b/cache/test_cacher.go @@ -72,9 +72,16 @@ func TestEviction(t *testing.T, cache Cacher[ids.ID, int64]) { expectedValue2 := int64(2) expectedValue3 := int64(3) + require.Zero(cache.Len()) + cache.Put(id1, expectedValue1) + + require.Equal(1, cache.Len()) + cache.Put(id2, expectedValue2) + require.Equal(2, cache.Len()) + val, found := cache.Get(id1) require.True(found) require.Equal(expectedValue1, val) @@ -87,6 +94,7 @@ func TestEviction(t *testing.T, cache Cacher[ids.ID, int64]) { require.False(found) cache.Put(id3, expectedValue3) + require.Equal(2, cache.Len()) _, found = cache.Get(id1) require.False(found) diff --git a/snow/engine/snowman/transitive.go b/snow/engine/snowman/transitive.go index e6f7a45670df..516dd3d12b72 100644 --- a/snow/engine/snowman/transitive.go +++ b/snow/engine/snowman/transitive.go @@ -22,12 +22,14 @@ import ( "github.com/ava-labs/avalanchego/snow/events" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/bag" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/utils/wrappers" ) -const nonVerifiedCacheSize = 128 +const nonVerifiedCacheSize = 128 * units.MiB var _ Engine = (*Transitive)(nil) @@ -35,6 +37,10 @@ func New(config Config) (Engine, error) { return newTransitive(config) } +func cachedBlockSize(_ ids.ID, blk snowman.Block) int { + return ids.IDLen + len(blk.Bytes()) + constants.PointerOverhead +} + // Transitive implements the Engine interface by attempting to fetch all // Transitive dependencies. type Transitive struct { @@ -92,7 +98,10 @@ func newTransitive(config Config) (*Transitive, error) { nonVerifiedCache, err := metercacher.New[ids.ID, snowman.Block]( "non_verified_cache", config.Ctx.Registerer, - &cache.LRU[ids.ID, snowman.Block]{Size: nonVerifiedCacheSize}, + cache.NewSizedLRU[ids.ID, snowman.Block]( + nonVerifiedCacheSize, + cachedBlockSize, + ), ) if err != nil { return nil, err diff --git a/utils/constants/memory.go b/utils/constants/memory.go new file mode 100644 index 000000000000..c8740ceba6f7 --- /dev/null +++ b/utils/constants/memory.go @@ -0,0 +1,8 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package constants + +// PointerOverhead is used to approximate the memory footprint from allocating a +// pointer. +const PointerOverhead = 8 diff --git a/vms/components/chain/state.go b/vms/components/chain/state.go index d18fa8f954d4..6311e550ce6c 100644 --- a/vms/components/chain/state.go +++ b/vms/components/chain/state.go @@ -17,8 +17,17 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/consensus/snowman" "github.com/ava-labs/avalanchego/snow/engine/snowman/block" + "github.com/ava-labs/avalanchego/utils/constants" ) +func cachedBlockSize(_ ids.ID, bw *BlockWrapper) int { + return ids.IDLen + len(bw.Bytes()) + 2*constants.PointerOverhead +} + +func cachedBlockBytesSize(blockBytes string, _ ids.ID) int { + return len(blockBytes) + ids.IDLen +} + // State implements an efficient caching layer used to wrap a VM // implementation. type State struct { @@ -138,11 +147,20 @@ func (s *State) initialize(config *Config) { func NewState(config *Config) *State { c := &State{ - verifiedBlocks: make(map[ids.ID]*BlockWrapper), - decidedBlocks: &cache.LRU[ids.ID, *BlockWrapper]{Size: config.DecidedCacheSize}, - missingBlocks: &cache.LRU[ids.ID, struct{}]{Size: config.MissingCacheSize}, - unverifiedBlocks: &cache.LRU[ids.ID, *BlockWrapper]{Size: config.UnverifiedCacheSize}, - bytesToIDCache: &cache.LRU[string, ids.ID]{Size: config.BytesToIDCacheSize}, + verifiedBlocks: make(map[ids.ID]*BlockWrapper), + decidedBlocks: cache.NewSizedLRU[ids.ID, *BlockWrapper]( + config.DecidedCacheSize, + cachedBlockSize, + ), + missingBlocks: &cache.LRU[ids.ID, struct{}]{Size: config.MissingCacheSize}, + unverifiedBlocks: cache.NewSizedLRU[ids.ID, *BlockWrapper]( + config.UnverifiedCacheSize, + cachedBlockSize, + ), + bytesToIDCache: cache.NewSizedLRU[string, ids.ID]( + config.BytesToIDCacheSize, + cachedBlockBytesSize, + ), } c.initialize(config) return c @@ -155,7 +173,10 @@ func NewMeteredState( decidedCache, err := metercacher.New[ids.ID, *BlockWrapper]( "decided_cache", registerer, - &cache.LRU[ids.ID, *BlockWrapper]{Size: config.DecidedCacheSize}, + cache.NewSizedLRU[ids.ID, *BlockWrapper]( + config.DecidedCacheSize, + cachedBlockSize, + ), ) if err != nil { return nil, err @@ -171,7 +192,10 @@ func NewMeteredState( unverifiedCache, err := metercacher.New[ids.ID, *BlockWrapper]( "unverified_cache", registerer, - &cache.LRU[ids.ID, *BlockWrapper]{Size: config.UnverifiedCacheSize}, + cache.NewSizedLRU[ids.ID, *BlockWrapper]( + config.UnverifiedCacheSize, + cachedBlockSize, + ), ) if err != nil { return nil, err @@ -179,7 +203,10 @@ func NewMeteredState( bytesToIDCache, err := metercacher.New[string, ids.ID]( "bytes_to_id_cache", registerer, - &cache.LRU[string, ids.ID]{Size: config.BytesToIDCacheSize}, + cache.NewSizedLRU[string, ids.ID]( + config.BytesToIDCacheSize, + cachedBlockBytesSize, + ), ) if err != nil { return nil, err diff --git a/vms/components/chain/state_test.go b/vms/components/chain/state_test.go index f572738bf874..add7723e9fcf 100644 --- a/vms/components/chain/state_test.go +++ b/vms/components/chain/state_test.go @@ -585,7 +585,7 @@ func TestStateBytesToIDCache(t *testing.T) { DecidedCacheSize: 0, MissingCacheSize: 0, UnverifiedCacheSize: 0, - BytesToIDCacheSize: 1, + BytesToIDCacheSize: 1 + ids.IDLen, // Size of one block LastAcceptedBlock: genesisBlock, GetBlock: getBlock, UnmarshalBlock: parseBlock, diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 401113709910..f1f1d4c5ee98 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -40,8 +40,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) -const pointerOverhead = wrappers.LongLen - var ( _ State = (*state)(nil) @@ -383,23 +381,23 @@ type txAndStatus struct { func txSize(_ ids.ID, tx *txs.Tx) int { if tx == nil { - return ids.IDLen + pointerOverhead + return ids.IDLen + constants.PointerOverhead } - return ids.IDLen + len(tx.Bytes()) + pointerOverhead + return ids.IDLen + len(tx.Bytes()) + constants.PointerOverhead } func txAndStatusSize(_ ids.ID, t *txAndStatus) int { if t == nil { - return ids.IDLen + pointerOverhead + return ids.IDLen + constants.PointerOverhead } - return ids.IDLen + len(t.tx.Bytes()) + wrappers.IntLen + pointerOverhead + return ids.IDLen + len(t.tx.Bytes()) + wrappers.IntLen + 2*constants.PointerOverhead } func blockSize(_ ids.ID, blk blocks.Block) int { if blk == nil { - return ids.IDLen + pointerOverhead + return ids.IDLen + constants.PointerOverhead } - return ids.IDLen + len(blk.Bytes()) + pointerOverhead + return ids.IDLen + len(blk.Bytes()) + constants.PointerOverhead } func New( diff --git a/vms/proposervm/state/block_state.go b/vms/proposervm/state/block_state.go index 6d426d2ec25b..3e566dcb7825 100644 --- a/vms/proposervm/state/block_state.go +++ b/vms/proposervm/state/block_state.go @@ -14,10 +14,13 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/choices" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/proposervm/block" ) -const blockCacheSize = 8192 +const blockCacheSize = 256 * units.MiB var ( errBlockWrongVersion = errors.New("wrong version") @@ -45,10 +48,20 @@ type blockWrapper struct { block block.Block } +func cachedBlockSize(_ ids.ID, bw *blockWrapper) int { + if bw == nil { + return ids.IDLen + constants.PointerOverhead + } + return ids.IDLen + len(bw.Block) + wrappers.IntLen + 2*constants.PointerOverhead +} + func NewBlockState(db database.Database) BlockState { return &blockState{ - blkCache: &cache.LRU[ids.ID, *blockWrapper]{Size: blockCacheSize}, - db: db, + blkCache: cache.NewSizedLRU[ids.ID, *blockWrapper]( + blockCacheSize, + cachedBlockSize, + ), + db: db, } } @@ -56,7 +69,10 @@ func NewMeteredBlockState(db database.Database, namespace string, metrics promet blkCache, err := metercacher.New[ids.ID, *blockWrapper]( fmt.Sprintf("%s_block_cache", namespace), metrics, - &cache.LRU[ids.ID, *blockWrapper]{Size: blockCacheSize}, + cache.NewSizedLRU[ids.ID, *blockWrapper]( + blockCacheSize, + cachedBlockSize, + ), ) return &blockState{ diff --git a/vms/proposervm/vm.go b/vms/proposervm/vm.go index ee0ac9f2f75e..b61ae90aecee 100644 --- a/vms/proposervm/vm.go +++ b/vms/proposervm/vm.go @@ -31,6 +31,7 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/proposervm/indexer" "github.com/ava-labs/avalanchego/vms/proposervm/proposer" "github.com/ava-labs/avalanchego/vms/proposervm/scheduler" @@ -46,7 +47,7 @@ const ( DefaultMinBlockDelay = time.Second checkIndexedFrequency = 10 * time.Second - innerBlkCacheSize = 512 + innerBlkCacheSize = 128 * units.MiB ) var ( @@ -75,6 +76,10 @@ func init() { } } +func cachedBlockSize(_ ids.ID, blk snowman.Block) int { + return ids.IDLen + len(blk.Bytes()) + constants.PointerOverhead +} + type VM struct { block.ChainVM blockBuilderVM block.BuildBlockWithContextChainVM @@ -192,7 +197,10 @@ func (vm *VM) Initialize( innerBlkCache, err := metercacher.New[ids.ID, snowman.Block]( "inner_block_cache", registerer, - &cache.LRU[ids.ID, snowman.Block]{Size: innerBlkCacheSize}, + cache.NewSizedLRU[ids.ID, snowman.Block]( + innerBlkCacheSize, + cachedBlockSize, + ), ) if err != nil { return err diff --git a/vms/proposervm/vm_test.go b/vms/proposervm/vm_test.go index a552ce4942d4..35998084d595 100644 --- a/vms/proposervm/vm_test.go +++ b/vms/proposervm/vm_test.go @@ -27,6 +27,7 @@ import ( "github.com/ava-labs/avalanchego/snow/engine/snowman/block/mocks" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/staking" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/proposervm/proposer" @@ -2229,6 +2230,8 @@ func TestVMInnerBlkCache(t *testing.T) { // We will ask the inner VM to parse. mockInnerBlkNearTip := snowman.NewMockBlock(ctrl) mockInnerBlkNearTip.EXPECT().Height().Return(uint64(1)).Times(2) + mockInnerBlkNearTip.EXPECT().Bytes().Return(blkNearTipInnerBytes).Times(1) + innerVM.EXPECT().ParseBlock(gomock.Any(), blkNearTipInnerBytes).Return(mockInnerBlkNearTip, nil).Times(2) _, err = vm.ParseBlock(context.Background(), blkNearTip.Bytes()) require.NoError(err) @@ -2444,6 +2447,7 @@ func TestVM_VerifyBlockWithContext(t *testing.T) { ).Return(nil) innerBlk.MockBlock.EXPECT().Parent().Return(ids.GenerateTestID()).AnyTimes() innerBlk.MockBlock.EXPECT().ID().Return(ids.GenerateTestID()).AnyTimes() + innerBlk.MockBlock.EXPECT().Bytes().Return(utils.RandomBytes(1024)).AnyTimes() blk := NewMockPostForkBlock(ctrl) blk.EXPECT().getInnerBlk().Return(innerBlk).AnyTimes() diff --git a/vms/rpcchainvm/vm_client.go b/vms/rpcchainvm/vm_client.go index 2d18483df9b6..1a7d6220cb0c 100644 --- a/vms/rpcchainvm/vm_client.go +++ b/vms/rpcchainvm/vm_client.go @@ -41,6 +41,7 @@ import ( "github.com/ava-labs/avalanchego/snow/validators/gvalidators" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/resource" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/chain" @@ -62,11 +63,12 @@ import ( warppb "github.com/ava-labs/avalanchego/proto/pb/warp" ) +// TODO: Enable these to be configured by the user const ( - decidedCacheSize = 2048 + decidedCacheSize = 256 * units.MiB missingCacheSize = 2048 - unverifiedCacheSize = 2048 - bytesToIDCacheSize = 2048 + unverifiedCacheSize = 64 * units.MiB + bytesToIDCacheSize = 64 * units.MiB ) var (