Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trie): Implement LRU cache for in memory trie #3624

Closed
wants to merge 7 commits into from
3 changes: 2 additions & 1 deletion dot/state/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import (
"github.com/ChainSafe/gossamer/lib/runtime"
"github.com/ChainSafe/gossamer/lib/trie"
"github.com/ChainSafe/gossamer/lib/utils"
lrucache "github.com/ChainSafe/gossamer/lib/utils/lru-cache"
"github.com/stretchr/testify/require"
)

func newTriesEmpty() *Tries {
return &Tries{
rootToTrie: make(map[common.Hash]*trie.Trie),
rootToTrie: lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries),
triesGauge: triesGauge,
setCounter: setCounter,
deleteCounter: deleteCounter,
Expand Down
44 changes: 16 additions & 28 deletions dot/state/tries.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
package state

import (
"sync"

"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/trie"
lrucache "github.com/ChainSafe/gossamer/lib/utils/lru-cache"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
Expand All @@ -30,11 +29,12 @@
})
)

const MaxInMemoryTries = 100
Copy link
Member

@P1sar P1sar Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How this was defined? Should we maybe add some description for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is still WIP, I want to use the same number used in substrate but I have to do a little research to find it. We don't use to review draft PRs because the code can change. Anyways, thanks for the comment.


// Tries is a thread safe map of root hash
// to trie.
type Tries struct {
rootToTrie map[common.Hash]*trie.Trie
mapMutex sync.RWMutex
rootToTrie *lrucache.LRUCache[common.Hash, *trie.Trie]
triesGauge prometheus.Gauge
setCounter prometheus.Counter
deleteCounter prometheus.Counter
Expand All @@ -43,8 +43,10 @@
// NewTries creates a new thread safe map of root hash
// to trie.
func NewTries() (tries *Tries) {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)

return &Tries{
rootToTrie: make(map[common.Hash]*trie.Trie),
rootToTrie: cache,
triesGauge: triesGauge,
setCounter: setCounter,
deleteCounter: deleteCounter,
Expand All @@ -66,41 +68,27 @@
// softSet sets the given trie at the given root hash
// in the memory map only if it is not already set.
func (t *Tries) softSet(root common.Hash, trie *trie.Trie) {
t.mapMutex.Lock()
defer t.mapMutex.Unlock()

_, has := t.rootToTrie[root]
if has {
return
if t.rootToTrie.SoftPut(root, trie) {

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/network)

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/rpc/modules)

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/rpc/subscription)

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / zombienet-tests

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / unit-tests

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)

Check failure on line 71 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.SoftPut undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method SoftPut)
t.triesGauge.Inc()
t.setCounter.Inc()
}

t.triesGauge.Inc()
t.setCounter.Inc()
t.rootToTrie[root] = trie
}

func (t *Tries) delete(root common.Hash) {
t.mapMutex.Lock()
defer t.mapMutex.Unlock()
delete(t.rootToTrie, root)
// Note we use .Set instead of .Dec in case nothing
// was deleted since nothing existed at the hash given.
t.triesGauge.Set(float64(len(t.rootToTrie)))
t.deleteCounter.Inc()
if t.rootToTrie.Delete(root) {

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/network)

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/rpc/modules)

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/rpc/subscription)

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / zombienet-tests

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / unit-tests

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)

Check failure on line 78 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.Delete undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Delete)
t.triesGauge.Dec()
t.deleteCounter.Inc()
}
}

// get retrieves the trie corresponding to the root hash given
// from the in-memory thread safe map.
func (t *Tries) get(root common.Hash) (tr *trie.Trie) {
t.mapMutex.RLock()
defer t.mapMutex.RUnlock()
return t.rootToTrie[root]
return t.rootToTrie.Get(root)
}

// len returns the current numbers of tries
// stored in the in-memory map.
func (t *Tries) len() int {
t.mapMutex.RLock()
defer t.mapMutex.RUnlock()
return len(t.rootToTrie)
return t.rootToTrie.Len()

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/network)

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/rpc/modules)

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / integration-tests (github.com/ChainSafe/gossamer/dot/rpc/subscription)

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / zombienet-tests

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / unit-tests

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)) (typecheck)

Check failure on line 93 in dot/state/tries.go

View workflow job for this annotation

GitHub Actions / linting

t.rootToTrie.Len undefined (type *lrucache.LRUCache["github.com/ChainSafe/gossamer/lib/common".Hash, *trie.Trie] has no field or method Len)) (typecheck)
}
127 changes: 76 additions & 51 deletions dot/state/tries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ import (
"github.com/ChainSafe/gossamer/internal/trie/node"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/trie"
lrucache "github.com/ChainSafe/gossamer/lib/utils/lru-cache"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)

var emptyTrie = trie.NewEmptyTrie()

func Test_NewTries(t *testing.T) {
t.Parallel()

rootToTrie := NewTries()

expectedTries := &Tries{
rootToTrie: map[common.Hash]*trie.Trie{},
rootToTrie: lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries),
triesGauge: triesGauge,
setCounter: setCounter,
deleteCounter: deleteCounter,
Expand All @@ -35,13 +38,12 @@ func Test_Tries_SetEmptyTrie(t *testing.T) {
tries.SetEmptyTrie()

expectedTries := &Tries{
rootToTrie: map[common.Hash]*trie.Trie{
trie.EmptyHash: trie.NewEmptyTrie(),
},
rootToTrie: lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries),
triesGauge: triesGauge,
setCounter: setCounter,
deleteCounter: deleteCounter,
}
expectedTries.rootToTrie.Put(trie.EmptyHash, trie.NewEmptyTrie())

assert.Equal(t, expectedTries, tries)
}
Expand All @@ -58,45 +60,54 @@ func Test_Tries_SetTrie(t *testing.T) {
tries.SetTrie(tr)

expectedTries := &Tries{
rootToTrie: map[common.Hash]*trie.Trie{
tr.MustHash(trie.NoMaxInlineValueSize): tr,
},
rootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(tr.MustHash(trie.NoMaxInlineValueSize), tr)
return cache
}(),
triesGauge: triesGauge,
setCounter: setCounter,
deleteCounter: deleteCounter,
}

expectedTries.rootToTrie.Put(tr.MustHash(trie.NoMaxInlineValueSize), tr)

assert.Equal(t, expectedTries, tries)
}

func Test_Tries_softSet(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
rootToTrie map[common.Hash]*trie.Trie
rootToTrie *lrucache.LRUCache[common.Hash, *trie.Trie]
root common.Hash
trie *trie.Trie
triesGaugeInc bool
expectedRootToTrie map[common.Hash]*trie.Trie
expectedRootToTrie *lrucache.LRUCache[common.Hash, *trie.Trie]
}{
"set_new_in_map": {
rootToTrie: map[common.Hash]*trie.Trie{},
rootToTrie: lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries),
root: common.Hash{1, 2, 3},
trie: trie.NewEmptyTrie(),
triesGaugeInc: true,
expectedRootToTrie: map[common.Hash]*trie.Trie{
{1, 2, 3}: trie.NewEmptyTrie(),
},
expectedRootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{1, 2, 3}, trie.NewEmptyTrie())
return cache
}(),
},
"do_not_override_in_map": {
rootToTrie: map[common.Hash]*trie.Trie{
{1, 2, 3}: {},
},
rootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{1, 2, 3}, emptyTrie)
return cache
}(),
root: common.Hash{1, 2, 3},
trie: trie.NewEmptyTrie(),
expectedRootToTrie: map[common.Hash]*trie.Trie{
{1, 2, 3}: {},
},
trie: emptyTrie,
expectedRootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{1, 2, 3}, emptyTrie)
return cache
}(),
},
}

Expand Down Expand Up @@ -133,34 +144,42 @@ func Test_Tries_delete(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
rootToTrie map[common.Hash]*trie.Trie
rootToTrie *lrucache.LRUCache[common.Hash, *trie.Trie]
root common.Hash
deleteCounterInc bool
expectedRootToTrie map[common.Hash]*trie.Trie
counterUpdated bool
expectedRootToTrie *lrucache.LRUCache[common.Hash, *trie.Trie]
triesGaugeSet float64
}{
"not_found": {
rootToTrie: map[common.Hash]*trie.Trie{
{3, 4, 5}: {},
},
rootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{3, 4, 5}, emptyTrie)
return cache
}(),
root: common.Hash{1, 2, 3},
triesGaugeSet: 1,
expectedRootToTrie: map[common.Hash]*trie.Trie{
{3, 4, 5}: {},
},
deleteCounterInc: true,
expectedRootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{3, 4, 5}, emptyTrie)
return cache
}(),
counterUpdated: false,
},
"deleted": {
rootToTrie: map[common.Hash]*trie.Trie{
{1, 2, 3}: {},
{3, 4, 5}: {},
},
rootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{1, 2, 3}, emptyTrie)
cache.Put(common.Hash{3, 4, 5}, emptyTrie)
return cache
}(),
root: common.Hash{1, 2, 3},
triesGaugeSet: 1,
expectedRootToTrie: map[common.Hash]*trie.Trie{
{3, 4, 5}: {},
},
deleteCounterInc: true,
expectedRootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{3, 4, 5}, emptyTrie)
return cache
}(),
counterUpdated: true,
},
}

Expand All @@ -170,11 +189,11 @@ func Test_Tries_delete(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
triesGauge := NewMockGauge(ctrl)
triesGauge.EXPECT().Set(testCase.triesGaugeSet)

deleteCounter := NewMockCounter(ctrl)
if testCase.deleteCounterInc {

if testCase.counterUpdated {
deleteCounter.EXPECT().Inc()
triesGauge.EXPECT().Dec()
}

tries := &Tries{
Expand All @@ -189,6 +208,7 @@ func Test_Tries_delete(t *testing.T) {
})
}
}

func Test_Tries_get(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
Expand All @@ -202,12 +222,15 @@ func Test_Tries_get(t *testing.T) {
}{
"found_in_map": {
tries: &Tries{
rootToTrie: map[common.Hash]*trie.Trie{
{1, 2, 3}: trie.NewTrie(&node.Node{
rootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
tr := trie.NewTrie(&node.Node{
PartialKey: []byte{1, 2, 3},
StorageValue: []byte{1},
}, db),
},
}, db)
cache.Put(common.Hash{1, 2, 3}, tr)
return cache
}(),
},
root: common.Hash{1, 2, 3},
trie: trie.NewTrie(&node.Node{
Expand All @@ -218,7 +241,7 @@ func Test_Tries_get(t *testing.T) {
"not_found_in_map": {
// similar to not found in database
tries: &Tries{
rootToTrie: map[common.Hash]*trie.Trie{},
rootToTrie: lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries),
},
root: common.Hash{1, 2, 3},
},
Expand All @@ -245,14 +268,16 @@ func Test_Tries_len(t *testing.T) {
}{
"empty_map": {
tries: &Tries{
rootToTrie: map[common.Hash]*trie.Trie{},
rootToTrie: lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries),
},
},
"non_empty_map": {
tries: &Tries{
rootToTrie: map[common.Hash]*trie.Trie{
{1, 2, 3}: {},
},
rootToTrie: func() *lrucache.LRUCache[common.Hash, *trie.Trie] {
cache := lrucache.NewLRUCache[common.Hash, *trie.Trie](MaxInMemoryTries)
cache.Put(common.Hash{1, 2, 3}, emptyTrie)
return cache
}(),
},
length: 1,
},
Expand Down
Loading
Loading