Skip to content

Commit 8b9f2d4

Browse files
authored
triedb/pathdb: introduce lookup structure to optimize state access (#30971)
This pull request introduces a mechanism to improve state lookup efficiency in pathdb by maintaining a lookup structure that eliminates unnecessary iteration over diff layers. The core idea is to track a mutation history for each dirty state entry residing in the diff layers. This history records the state roots of all layers in which the entry was modified, sorted from oldest to newest. During state lookup, this mutation history is queried to find the most recent layer whose state root either matches the target root or is a descendant of it. This allows us to quickly identify the layer containing the relevant data, avoiding the need to iterate through all diff layers from top to bottom. Besides, the overhead for state lookup is constant, no matter how many diff layers are retained in the pathdb, which unlocks the potential to hold more diff layers. Of course, maintaining this lookup structure introduces some overhead. For each state transition, we need to: (a) update the mutation records for the modified state entries, and (b) remove stale mutation records associated with outdated layers. On our benchmark machine, it will introduce around 1ms overhead which is acceptable.
1 parent 3f7b8bc commit 8b9f2d4

File tree

8 files changed

+1382
-50
lines changed

8 files changed

+1382
-50
lines changed

triedb/pathdb/database.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ func (db *Database) Enable(root common.Hash) error {
487487

488488
// Re-construct a new disk layer backed by persistent state
489489
// and schedule the state snapshot generation if it's permitted.
490-
db.tree.reset(generateSnapshot(db, root, db.isVerkle || db.config.SnapshotNoBuild))
490+
db.tree.init(generateSnapshot(db, root, db.isVerkle || db.config.SnapshotNoBuild))
491491
log.Info("Rebuilt trie database", "root", root)
492492
return nil
493493
}
@@ -529,7 +529,7 @@ func (db *Database) Recover(root common.Hash) error {
529529
// reset layer with newly created disk layer. It must be
530530
// done after each revert operation, otherwise the new
531531
// disk layer won't be accessible from outside.
532-
db.tree.reset(dl)
532+
db.tree.init(dl)
533533
}
534534
rawdb.DeleteTrieJournal(db.diskdb)
535535

triedb/pathdb/difflayer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes *no
156156
}
157157

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

184184
// diffToDisk merges a bottom-most diff into the persistent disk layer underneath
185185
// it. The method will panic if called onto a non-bottom-most diff layer.
186-
func diffToDisk(layer *diffLayer, force bool) (layer, error) {
186+
func diffToDisk(layer *diffLayer, force bool) (*diskLayer, error) {
187187
disk, ok := layer.parentLayer().(*diskLayer)
188188
if !ok {
189189
panic(fmt.Sprintf("unknown layer type: %T", layer.parentLayer()))

triedb/pathdb/disklayer.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,6 @@ func (dl *diskLayer) setGenerator(generator *generator) {
9494
dl.generator = generator
9595
}
9696

97-
// isStale return whether this layer has become stale (was flattened across) or if
98-
// it's still live.
99-
func (dl *diskLayer) isStale() bool {
100-
dl.lock.RLock()
101-
defer dl.lock.RUnlock()
102-
103-
return dl.stale
104-
}
105-
10697
// markStale sets the stale flag as true.
10798
func (dl *diskLayer) markStale() {
10899
dl.lock.Lock()

triedb/pathdb/layertree.go

Lines changed: 162 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,50 @@ import (
3131
// thread-safe to use. However, callers need to ensure the thread-safety
3232
// of the referenced layer by themselves.
3333
type layerTree struct {
34-
lock sync.RWMutex
34+
base *diskLayer
3535
layers map[common.Hash]layer
36+
37+
// descendants is a two-dimensional map where the keys represent
38+
// an ancestor state root, and the values are the state roots of
39+
// all its descendants.
40+
//
41+
// For example: r -> [c1, c2, ..., cn], where c1 through cn are
42+
// the descendants of state r.
43+
//
44+
// This map includes all the existing diff layers and the disk layer.
45+
descendants map[common.Hash]map[common.Hash]struct{}
46+
lookup *lookup
47+
lock sync.RWMutex
3648
}
3749

3850
// newLayerTree constructs the layerTree with the given head layer.
3951
func newLayerTree(head layer) *layerTree {
4052
tree := new(layerTree)
41-
tree.reset(head)
53+
tree.init(head)
4254
return tree
4355
}
4456

45-
// reset initializes the layerTree by the given head layer.
46-
// All the ancestors will be iterated out and linked in the tree.
47-
func (tree *layerTree) reset(head layer) {
57+
// init initializes the layerTree by the given head layer.
58+
func (tree *layerTree) init(head layer) {
4859
tree.lock.Lock()
4960
defer tree.lock.Unlock()
5061

51-
var layers = make(map[common.Hash]layer)
52-
for head != nil {
53-
layers[head.rootHash()] = head
54-
head = head.parentLayer()
62+
current := head
63+
tree.layers = make(map[common.Hash]layer)
64+
tree.descendants = make(map[common.Hash]map[common.Hash]struct{})
65+
66+
for {
67+
tree.layers[current.rootHash()] = current
68+
tree.fillAncestors(current)
69+
70+
parent := current.parentLayer()
71+
if parent == nil {
72+
break
73+
}
74+
current = parent
5575
}
56-
tree.layers = layers
76+
tree.base = current.(*diskLayer) // panic if it's not a disk layer
77+
tree.lookup = newLookup(head, tree.isDescendant)
5778
}
5879

5980
// get retrieves a layer belonging to the given state root.
@@ -64,6 +85,43 @@ func (tree *layerTree) get(root common.Hash) layer {
6485
return tree.layers[root]
6586
}
6687

88+
// isDescendant returns whether the specified layer with given root is a
89+
// descendant of a specific ancestor.
90+
//
91+
// This function assumes the read lock has been held.
92+
func (tree *layerTree) isDescendant(root common.Hash, ancestor common.Hash) bool {
93+
subset := tree.descendants[ancestor]
94+
if subset == nil {
95+
return false
96+
}
97+
_, ok := subset[root]
98+
return ok
99+
}
100+
101+
// fillAncestors identifies the ancestors of the given layer and populates the
102+
// descendants set. The ancestors include the diff layers below the supplied
103+
// layer and also the disk layer.
104+
//
105+
// This function assumes the write lock has been held.
106+
func (tree *layerTree) fillAncestors(layer layer) {
107+
hash := layer.rootHash()
108+
for {
109+
parent := layer.parentLayer()
110+
if parent == nil {
111+
break
112+
}
113+
layer = parent
114+
115+
phash := parent.rootHash()
116+
subset := tree.descendants[phash]
117+
if subset == nil {
118+
subset = make(map[common.Hash]struct{})
119+
tree.descendants[phash] = subset
120+
}
121+
subset[hash] = struct{}{}
122+
}
123+
}
124+
67125
// forEach iterates the stored layers inside and applies the
68126
// given callback on them.
69127
func (tree *layerTree) forEach(onLayer func(layer)) {
@@ -101,8 +159,16 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
101159
l := parent.update(root, parent.stateID()+1, block, newNodeSet(nodes.Flatten()), states)
102160

103161
tree.lock.Lock()
162+
defer tree.lock.Unlock()
163+
164+
// Link the given layer into the layer set
104165
tree.layers[l.rootHash()] = l
105-
tree.lock.Unlock()
166+
167+
// Link the given layer into its ancestors (up to the current disk layer)
168+
tree.fillAncestors(l)
169+
170+
// Link the given layer into the state mutation history
171+
tree.lookup.addLayer(l)
106172
return nil
107173
}
108174

@@ -127,8 +193,16 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
127193
if err != nil {
128194
return err
129195
}
130-
// Replace the entire layer tree with the flat base
131-
tree.layers = map[common.Hash]layer{base.rootHash(): base}
196+
tree.base = base
197+
198+
// Reset the layer tree with the single new disk layer
199+
tree.layers = map[common.Hash]layer{
200+
base.rootHash(): base,
201+
}
202+
// Resets the descendants map, since there's only a single disk layer
203+
// with no descendants.
204+
tree.descendants = make(map[common.Hash]map[common.Hash]struct{})
205+
tree.lookup = newLookup(base, tree.isDescendant)
132206
return nil
133207
}
134208
// Dive until we run out of layers or reach the persistent database
@@ -143,6 +217,11 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
143217
}
144218
// We're out of layers, flatten anything below, stopping if it's the disk or if
145219
// the memory limit is not yet exceeded.
220+
var (
221+
err error
222+
replaced layer
223+
newBase *diskLayer
224+
)
146225
switch parent := diff.parentLayer().(type) {
147226
case *diskLayer:
148227
return nil
@@ -152,14 +231,33 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
152231
// parent is linked correctly.
153232
diff.lock.Lock()
154233

155-
base, err := parent.persist(false)
234+
// Hold the reference of the original layer being replaced
235+
replaced = parent
236+
237+
// Replace the original parent layer with new disk layer. The procedure
238+
// can be illustrated as below:
239+
//
240+
// Before change:
241+
// Chain:
242+
// C1->C2->C3->C4 (HEAD)
243+
// ->C2'->C3'->C4'
244+
//
245+
// After change:
246+
// Chain:
247+
// (a) C3->C4 (HEAD)
248+
// (b) C1->C2
249+
// ->C2'->C3'->C4'
250+
// The original C3 is replaced by the new base (with root C3)
251+
// Dangling layers in (b) will be removed later
252+
newBase, err = parent.persist(false)
156253
if err != nil {
157254
diff.lock.Unlock()
158255
return err
159256
}
160-
tree.layers[base.rootHash()] = base
161-
diff.parent = base
257+
tree.layers[newBase.rootHash()] = newBase
162258

259+
// Link the new parent and release the lock
260+
diff.parent = newBase
163261
diff.lock.Unlock()
164262

165263
default:
@@ -173,19 +271,28 @@ func (tree *layerTree) cap(root common.Hash, layers int) error {
173271
children[parent] = append(children[parent], root)
174272
}
175273
}
274+
clearDiff := func(layer layer) {
275+
diff, ok := layer.(*diffLayer)
276+
if !ok {
277+
return
278+
}
279+
tree.lookup.removeLayer(diff)
280+
}
176281
var remove func(root common.Hash)
177282
remove = func(root common.Hash) {
283+
clearDiff(tree.layers[root])
284+
285+
// Unlink the layer from the layer tree and cascade to its children
286+
delete(tree.descendants, root)
178287
delete(tree.layers, root)
179288
for _, child := range children[root] {
180289
remove(child)
181290
}
182291
delete(children, root)
183292
}
184-
for root, layer := range tree.layers {
185-
if dl, ok := layer.(*diskLayer); ok && dl.isStale() {
186-
remove(root)
187-
}
188-
}
293+
remove(tree.base.rootHash()) // remove the old/stale disk layer
294+
clearDiff(replaced) // remove the lookup data of the stale parent being replaced
295+
tree.base = newBase // update the base layer with newly constructed one
189296
return nil
190297
}
191298

@@ -194,17 +301,41 @@ func (tree *layerTree) bottom() *diskLayer {
194301
tree.lock.RLock()
195302
defer tree.lock.RUnlock()
196303

197-
if len(tree.layers) == 0 {
198-
return nil // Shouldn't happen, empty tree
304+
return tree.base
305+
}
306+
307+
// lookupAccount returns the layer that is guaranteed to contain the account data
308+
// corresponding to the specified state root being queried.
309+
func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash) (layer, error) {
310+
// Hold the read lock to prevent the unexpected layer changes
311+
tree.lock.RLock()
312+
defer tree.lock.RUnlock()
313+
314+
tip := tree.lookup.accountTip(accountHash, state, tree.base.root)
315+
if tip == (common.Hash{}) {
316+
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
199317
}
200-
// pick a random one as the entry point
201-
var current layer
202-
for _, layer := range tree.layers {
203-
current = layer
204-
break
318+
l := tree.layers[tip]
319+
if l == nil {
320+
return nil, fmt.Errorf("triedb layer [%#x] missing", tip)
205321
}
206-
for current.parentLayer() != nil {
207-
current = current.parentLayer()
322+
return l, nil
323+
}
324+
325+
// lookupStorage returns the layer that is guaranteed to contain the storage slot
326+
// data corresponding to the specified state root being queried.
327+
func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Hash, state common.Hash) (layer, error) {
328+
// Hold the read lock to prevent the unexpected layer changes
329+
tree.lock.RLock()
330+
defer tree.lock.RUnlock()
331+
332+
tip := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root)
333+
if tip == (common.Hash{}) {
334+
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
335+
}
336+
l := tree.layers[tip]
337+
if l == nil {
338+
return nil, fmt.Errorf("triedb layer [%#x] missing", tip)
208339
}
209-
return current.(*diskLayer)
340+
return l, nil
210341
}

0 commit comments

Comments
 (0)