Skip to content

Commit

Permalink
triedb/pathdb: introduce lookup structure to optimize state access
Browse files Browse the repository at this point in the history
  • Loading branch information
rjl493456442 committed Dec 25, 2024
1 parent 985a84e commit 267cf19
Show file tree
Hide file tree
Showing 9 changed files with 1,352 additions and 59 deletions.
15 changes: 8 additions & 7 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,14 @@ type StateDB struct {
witness *stateless.Witness

// Measurements gathered during execution for debugging purposes
AccountReads time.Duration
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration
AccountReads time.Duration
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration

SnapshotCommits time.Duration
TrieDBCommits time.Duration

Expand Down
4 changes: 2 additions & 2 deletions triedb/pathdb/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ func (db *Database) Enable(root common.Hash) error {

// Re-construct a new disk layer backed by persistent state
// and schedule the state snapshot generation if it's permitted.
db.tree.reset(generateSnapshot(db, root))
db.tree.init(generateSnapshot(db, root))
log.Info("Rebuilt trie database", "root", root)
return nil
}
Expand Down Expand Up @@ -491,7 +491,7 @@ func (db *Database) Recover(root common.Hash) error {
// reset layer with newly created disk layer. It must be
// done after each revert operation, otherwise the new
// disk layer won't be accessible from outside.
db.tree.reset(dl)
db.tree.init(dl)
}
rawdb.DeleteTrieJournal(db.diskdb)
_, err := truncateFromHead(db.diskdb, db.freezer, dl.stateID())
Expand Down
4 changes: 2 additions & 2 deletions triedb/pathdb/difflayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes *no
}

// persist flushes the diff layer and all its parent layers to disk layer.
func (dl *diffLayer) persist(force bool) (layer, error) {
func (dl *diffLayer) persist(force bool) (*diskLayer, error) {
if parent, ok := dl.parentLayer().(*diffLayer); ok {
// Hold the lock to prevent any read operation until the new
// parent is linked correctly.
Expand All @@ -183,7 +183,7 @@ func (dl *diffLayer) size() uint64 {

// diffToDisk merges a bottom-most diff into the persistent disk layer underneath
// it. The method will panic if called onto a non-bottom-most diff layer.
func diffToDisk(layer *diffLayer, force bool) (layer, error) {
func diffToDisk(layer *diffLayer, force bool) (*diskLayer, error) {
disk, ok := layer.parentLayer().(*diskLayer)
if !ok {
panic(fmt.Sprintf("unknown layer type: %T", layer.parentLayer()))
Expand Down
9 changes: 0 additions & 9 deletions triedb/pathdb/disklayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,6 @@ func (dl *diskLayer) setGenerator(generator *generator) {
dl.generator = generator
}

// isStale return whether this layer has become stale (was flattened across) or if
// it's still live.
func (dl *diskLayer) isStale() bool {
dl.lock.RLock()
defer dl.lock.RUnlock()

return dl.stale
}

// markStale sets the stale flag as true.
func (dl *diskLayer) markStale() {
dl.lock.Lock()
Expand Down
177 changes: 145 additions & 32 deletions triedb/pathdb/layertree.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,41 @@ import (
// thread-safe to use. However, callers need to ensure the thread-safety
// of the referenced layer by themselves.
type layerTree struct {
lock sync.RWMutex
layers map[common.Hash]layer
base *diskLayer
layers map[common.Hash]layer
descendants map[common.Hash]map[common.Hash]struct{}
lookup *lookup
lock sync.RWMutex
}

// newLayerTree constructs the layerTree with the given head layer.
func newLayerTree(head layer) *layerTree {
tree := new(layerTree)
tree.reset(head)
tree.init(head)
return tree
}

// reset initializes the layerTree by the given head layer.
// All the ancestors will be iterated out and linked in the tree.
func (tree *layerTree) reset(head layer) {
// init initializes the layerTree by the given head layer.
func (tree *layerTree) init(head layer) {
tree.lock.Lock()
defer tree.lock.Unlock()

var layers = make(map[common.Hash]layer)
for head != nil {
layers[head.rootHash()] = head
head = head.parentLayer()
current := head
tree.layers = make(map[common.Hash]layer)
tree.descendants = make(map[common.Hash]map[common.Hash]struct{})

for {
tree.layers[current.rootHash()] = current
tree.fillAncestors(current)

parent := current.parentLayer()
if parent == nil {
break
}
current = parent
}
tree.layers = layers
tree.base = current.(*diskLayer) // panic if it's not a disk layer
tree.lookup = newLookup(head, tree.isDescendant)
}

// get retrieves a layer belonging to the given state root.
Expand All @@ -65,6 +77,43 @@ func (tree *layerTree) get(root common.Hash) layer {
return tree.layers[types.TrieRootHash(root)]
}

// isDescendant returns whether the specified layer with given root is a
// descendant of a specific ancestor.
//
// This function assumes the read lock has been held.
func (tree *layerTree) isDescendant(root common.Hash, ancestor common.Hash) bool {
subset := tree.descendants[ancestor]
if subset == nil {
return false
}
_, ok := subset[root]
return ok
}

// fillAncestors identifies the ancestors of the given layer and populates the
// descendants set. The ancestors include the diff layers below the supplied
// layer and also the disk layer.
//
// This function assumes the write lock has been held.
func (tree *layerTree) fillAncestors(layer layer) {
hash := layer.rootHash()
for {
parent := layer.parentLayer()
if parent == nil {
break
}
layer = parent

phash := parent.rootHash()
subset := tree.descendants[phash]
if subset == nil {
subset = make(map[common.Hash]struct{})
tree.descendants[phash] = subset
}
subset[hash] = struct{}{}
}
}

// forEach iterates the stored layers inside and applies the
// given callback on them.
func (tree *layerTree) forEach(onLayer func(layer)) {
Expand Down Expand Up @@ -103,8 +152,11 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
l := parent.update(root, parent.stateID()+1, block, newNodeSet(nodes.Flatten()), states)

tree.lock.Lock()
defer tree.lock.Unlock()

tree.layers[l.rootHash()] = l
tree.lock.Unlock()
tree.fillAncestors(l)
tree.lookup.addLayer(l)
return nil
}

Expand All @@ -130,8 +182,14 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
if err != nil {
return err
}
// Replace the entire layer tree with the flat base
tree.layers = map[common.Hash]layer{base.rootHash(): base}
tree.base = base

// Reset the layer tree with the single new disk layer
tree.layers = map[common.Hash]layer{
base.rootHash(): base,
}
tree.descendants = make(map[common.Hash]map[common.Hash]struct{})
tree.lookup = newLookup(base, tree.isDescendant)
return nil
}
// Dive until we run out of layers or reach the persistent database
Expand All @@ -146,6 +204,11 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
}
// We're out of layers, flatten anything below, stopping if it's the disk or if
// the memory limit is not yet exceeded.
var (
err error
replaced layer
newBase *diskLayer
)
switch parent := diff.parentLayer().(type) {
case *diskLayer:
return nil
Expand All @@ -155,14 +218,33 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
// parent is linked correctly.
diff.lock.Lock()

base, err := parent.persist(false)
// Hold the reference of the original layer being replaced
replaced = parent

// Replace the original parent layer with new disk layer. The procedure
// can be illustrated as below:
//
// Before change:
// Chain:
// C1->C2->C3->C4 (HEAD)
// ->C2'->C3'->C4'
//
// After change:
// Chain:
// (a) C3->C4 (HEAD)
// (b) C1->C2
// ->C2'->C3'->C4'
// The original C3 is replaced by the new base (with root C3)
// Dangling layers in (b) will be removed later
newBase, err = parent.persist(false)
if err != nil {
diff.lock.Unlock()
return err
}
tree.layers[base.rootHash()] = base
diff.parent = base
tree.layers[newBase.rootHash()] = newBase

// Link the new parent and release the lock
diff.parent = newBase
diff.lock.Unlock()

default:
Expand All @@ -176,19 +258,28 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
children[parent] = append(children[parent], root)
}
}
clearDiff := func(layer layer) {
diff, ok := layer.(*diffLayer)
if !ok {
return
}
tree.lookup.removeLayer(diff)
}
var remove func(root common.Hash)
remove = func(root common.Hash) {
clearDiff(tree.layers[root])

// Unlink the layer from the layer tree and cascade to its children
delete(tree.descendants, root)
delete(tree.layers, root)
for _, child := range children[root] {
remove(child)
}
delete(children, root)
}
for root, layer := range tree.layers {
if dl, ok := layer.(*diskLayer); ok && dl.isStale() {
remove(root)
}
}
remove(tree.base.rootHash()) // remove the old/stale disk layer
clearDiff(replaced) // remove the lookup data of the stale parent being replaced
tree.base = newBase // update the base layer with newly constructed one
return nil
}

Expand All @@ -197,17 +288,39 @@ func (tree *layerTree) bottom() *diskLayer {
tree.lock.RLock()
defer tree.lock.RUnlock()

if len(tree.layers) == 0 {
return nil // Shouldn't happen, empty tree
return tree.base
}

// lookupAccount returns the layer that is confirmed to contain the account data
// being searched for.
func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) (layer, error) {
tree.lock.RLock()
defer tree.lock.RUnlock()

tip := tree.lookup.accountTip(accountHash, state, tree.base.root)
if tip == (common.Hash{}) {
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
}
// pick a random one as the entry point
var current layer
for _, layer := range tree.layers {
current = layer
break
l := tree.layers[tip]
if l == nil {
return nil, fmt.Errorf("triedb layer [%#x] missing", tip)
}
for current.parentLayer() != nil {
current = current.parentLayer()
return l, nil
}

// lookupStorage returns the layer that is confirmed to contain the storage slot
// data being searched for.
func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Hash, state common.Hash) (layer, error) {
tree.lock.RLock()
defer tree.lock.RUnlock()

tip := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root)
if tip == (common.Hash{}) {
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
}
l := tree.layers[tip]
if l == nil {
return nil, fmt.Errorf("triedb layer [%#x] missing", tip)
}
return current.(*diskLayer)
return l, nil
}
Loading

0 comments on commit 267cf19

Please sign in to comment.