Skip to content

Commit f5ec2e7

Browse files
committed
core/state: finish prefetching async and process storage updates async
1 parent f166ce1 commit f5ec2e7

File tree

4 files changed

+79
-103
lines changed

4 files changed

+79
-103
lines changed

core/state/state_object.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"io"
2323
"maps"
24+
"sync"
2425
"time"
2526

2627
"github.com/ethereum/go-ethereum/common"
@@ -33,6 +34,14 @@ import (
3334
"github.com/holiman/uint256"
3435
)
3536

37+
// hasherPool holds a pool of hashers used by state objects during concurrent
38+
// trie updates.
39+
var hasherPool = sync.Pool{
40+
New: func() interface{} {
41+
return crypto.NewKeccakState()
42+
},
43+
}
44+
3645
type Storage map[common.Hash]common.Hash
3746

3847
func (s Storage) Copy() Storage {
@@ -314,6 +323,9 @@ func (s *stateObject) updateTrie() (Trie, error) {
314323
// Insert all the pending storage updates into the trie
315324
usedStorage := make([][]byte, 0, len(s.pendingStorage))
316325

326+
hasher := hasherPool.Get().(crypto.KeccakState)
327+
defer hasherPool.Put(hasher)
328+
317329
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
318330
// in circumstances similar to the following:
319331
//
@@ -342,26 +354,30 @@ func (s *stateObject) updateTrie() (Trie, error) {
342354
s.db.setError(err)
343355
return nil, err
344356
}
345-
s.db.StorageUpdated += 1
357+
s.db.StorageUpdated.Add(1)
346358
} else {
347359
deletions = append(deletions, key)
348360
}
349361
// Cache the mutated storage slots until commit
350362
if storage == nil {
363+
s.db.storagesLock.Lock()
351364
if storage = s.db.storages[s.addrHash]; storage == nil {
352365
storage = make(map[common.Hash][]byte)
353366
s.db.storages[s.addrHash] = storage
354367
}
368+
s.db.storagesLock.Unlock()
355369
}
356-
khash := crypto.HashData(s.db.hasher, key[:])
370+
khash := crypto.HashData(hasher, key[:])
357371
storage[khash] = encoded // encoded will be nil if it's deleted
358372

359373
// Cache the original value of mutated storage slots
360374
if origin == nil {
375+
s.db.storagesLock.Lock()
361376
if origin = s.db.storagesOrigin[s.address]; origin == nil {
362377
origin = make(map[common.Hash][]byte)
363378
s.db.storagesOrigin[s.address] = origin
364379
}
380+
s.db.storagesLock.Unlock()
365381
}
366382
// Track the original value of slot only if it's mutated first time
367383
if _, ok := origin[khash]; !ok {
@@ -381,7 +397,7 @@ func (s *stateObject) updateTrie() (Trie, error) {
381397
s.db.setError(err)
382398
return nil, err
383399
}
384-
s.db.StorageDeleted += 1
400+
s.db.StorageDeleted.Add(1)
385401
}
386402
// If no slots were touched, issue a warning as we shouldn't have done all
387403
// the above work in the first place

core/state/statedb.go

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"slices"
2525
"sort"
2626
"sync"
27+
"sync/atomic"
2728
"time"
2829

2930
"github.com/ethereum/go-ethereum/common"
@@ -92,10 +93,12 @@ type StateDB struct {
9293

9394
// These maps hold the state changes (including the corresponding
9495
// original value) that occurred in this **block**.
95-
accounts map[common.Hash][]byte // The mutated accounts in 'slim RLP' encoding
96+
accounts map[common.Hash][]byte // The mutated accounts in 'slim RLP' encoding
97+
accountsOrigin map[common.Address][]byte // The original value of mutated accounts in 'slim RLP' encoding
98+
9699
storages map[common.Hash]map[common.Hash][]byte // The mutated slots in prefix-zero trimmed rlp format
97-
accountsOrigin map[common.Address][]byte // The original value of mutated accounts in 'slim RLP' encoding
98100
storagesOrigin map[common.Address]map[common.Hash][]byte // The original value of mutated slots in prefix-zero trimmed rlp format
101+
storagesLock sync.Mutex // Mutex protecting the maps during concurrent updates/commits
99102

100103
// This map holds 'live' objects, which will get modified while
101104
// processing a state transition.
@@ -161,9 +164,9 @@ type StateDB struct {
161164
TrieDBCommits time.Duration
162165

163166
AccountUpdated int
164-
StorageUpdated int
167+
StorageUpdated atomic.Int64
165168
AccountDeleted int
166-
StorageDeleted int
169+
StorageDeleted atomic.Int64
167170

168171
// Testing hooks
169172
onCommit func(states *triestate.Set) // Hook invoked when commit is performed
@@ -210,7 +213,7 @@ func (s *StateDB) SetLogger(l *tracing.Hooks) {
210213
// commit phase, most of the needed data is already hot.
211214
func (s *StateDB) StartPrefetcher(namespace string) {
212215
if s.prefetcher != nil {
213-
s.prefetcher.terminate()
216+
s.prefetcher.terminate(false)
214217
s.prefetcher.report()
215218
s.prefetcher = nil
216219
}
@@ -223,7 +226,7 @@ func (s *StateDB) StartPrefetcher(namespace string) {
223226
// from the gathered metrics.
224227
func (s *StateDB) StopPrefetcher() {
225228
if s.prefetcher != nil {
226-
s.prefetcher.terminate()
229+
s.prefetcher.terminate(false)
227230
s.prefetcher.report()
228231
s.prefetcher = nil
229232
}
@@ -542,9 +545,6 @@ func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common
542545

543546
// updateStateObject writes the given object to the trie.
544547
func (s *StateDB) updateStateObject(obj *stateObject) {
545-
// Track the amount of time wasted on updating the account from the trie
546-
defer func(start time.Time) { s.AccountUpdates += time.Since(start) }(time.Now())
547-
548548
// Encode the account and update the account trie
549549
addr := obj.Address()
550550
if err := s.trie.UpdateAccount(addr, &obj.data); err != nil {
@@ -573,10 +573,6 @@ func (s *StateDB) updateStateObject(obj *stateObject) {
573573

574574
// deleteStateObject removes the given object from the state trie.
575575
func (s *StateDB) deleteStateObject(addr common.Address) {
576-
// Track the amount of time wasted on deleting the account from the trie
577-
defer func(start time.Time) { s.AccountUpdates += time.Since(start) }(time.Now())
578-
579-
// Delete the account from the trie
580576
if err := s.trie.DeleteAccount(addr); err != nil {
581577
s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err))
582578
}
@@ -835,48 +831,40 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
835831
// Finalise all the dirty storage states and write them into the tries
836832
s.Finalise(deleteEmptyObjects)
837833

838-
// If there was a trie prefetcher operating, terminate it (blocking until
839-
// all tasks finish) and then proceed with the trie hashing.
840-
var subfetchers chan *subfetcher
834+
// If there was a trie prefetcher operating, terminate it async so that the
835+
// individual storage tries can be updated as soon as the disk load finishes.
841836
if s.prefetcher != nil {
842-
subfetchers = s.prefetcher.terminateAsync()
837+
s.prefetcher.terminate(true)
843838
defer func() {
844839
s.prefetcher.report()
845840
s.prefetcher = nil // Pre-byzantium, unset any used up prefetcher
846841
}()
847842
}
848-
// Although naively it makes sense to retrieve the account trie and then do
849-
// the contract storage and account updates sequentially, that short circuits
850-
// the account prefetcher. Instead, let's process all the storage updates
851-
// first, giving the account prefetches just a few more milliseconds of time
852-
// to pull useful data from disk.
853-
start := time.Now()
854-
855-
updated := make(map[common.Address]struct{})
856-
if subfetchers != nil {
857-
for f := range subfetchers {
858-
if op, ok := s.mutations[f.addr]; ok {
859-
if !op.applied && !op.isDelete() {
860-
s.stateObjects[f.addr].updateRoot()
861-
}
862-
updated[f.addr] = struct{}{}
863-
}
864-
}
865-
}
843+
// Process all storage updates concurrently. The state object update root
844+
// method will internally call a blocking trie fetch from the prefetcher,
845+
// so there's no need to explicitly wait for the prefetchers to finish.
846+
var (
847+
start = time.Now()
848+
workers errgroup.Group
849+
)
866850
for addr, op := range s.mutations {
867-
if op.applied {
868-
continue
869-
}
870-
if op.isDelete() {
851+
if op.applied || op.isDelete() {
871852
continue
872853
}
873-
s.stateObjects[addr].updateRoot()
854+
obj := s.stateObjects[addr] // closure for the task runner below
855+
workers.Go(func() error {
856+
obj.updateRoot()
857+
return nil
858+
})
874859
}
860+
workers.Wait()
875861
s.StorageUpdates += time.Since(start)
876862

877863
// Now we're about to start to write changes to the trie. The trie is so far
878864
// _untouched_. We can check with the prefetcher, if it can give us a trie
879865
// which has the same root, but also has some content loaded into it.
866+
start = time.Now()
867+
880868
if s.prefetcher != nil {
881869
if trie, err := s.prefetcher.trie(common.Hash{}, s.originalRoot); err != nil {
882870
log.Error("Failed to retrieve account pre-fetcher trie", "err", err)
@@ -916,6 +904,8 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
916904
s.deleteStateObject(deletedAddr)
917905
s.AccountDeleted += 1
918906
}
907+
s.AccountUpdates += time.Since(start)
908+
919909
if s.prefetcher != nil {
920910
s.prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs)
921911
}
@@ -1258,15 +1248,16 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er
12581248
return common.Hash{}, err
12591249
}
12601250
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
1261-
storageUpdatedMeter.Mark(int64(s.StorageUpdated))
1251+
storageUpdatedMeter.Mark(s.StorageUpdated.Load())
12621252
accountDeletedMeter.Mark(int64(s.AccountDeleted))
1263-
storageDeletedMeter.Mark(int64(s.StorageDeleted))
1253+
storageDeletedMeter.Mark(s.StorageDeleted.Load())
12641254
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
12651255
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
12661256
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
12671257
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
12681258
s.AccountUpdated, s.AccountDeleted = 0, 0
1269-
s.StorageUpdated, s.StorageDeleted = 0, 0
1259+
s.StorageUpdated.Store(0)
1260+
s.StorageDeleted.Store(0)
12701261

12711262
// If snapshotting is enabled, update the snapshot tree with this new version
12721263
if s.snap != nil {

core/state/trie_prefetcher.go

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -76,52 +76,23 @@ func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePre
7676
}
7777
}
7878

79-
// terminate iterates over all the subfetchers, waiting on any that still spin.
80-
func (p *triePrefetcher) terminate() {
79+
// terminate iterates over all the subfetchers and issues a terminateion request
80+
// to all of them. Depending on the async parameter, the method will either block
81+
// until all subfetchers spin down, or return immediately.
82+
func (p *triePrefetcher) terminate(async bool) {
8183
// Short circuit if the fetcher is already closed
8284
select {
8385
case <-p.term:
8486
return
8587
default:
8688
}
87-
// Termiante all sub-fetchers synchronously and close the main fetcher
89+
// Termiante all sub-fetchers, sync or async, depending on the request
8890
for _, fetcher := range p.fetchers {
89-
fetcher.terminate()
91+
fetcher.terminate(async)
9092
}
9193
close(p.term)
9294
}
9395

94-
// terminateAsync iterates over all the subfetchers and terminates them async,
95-
// feeding each into a result channel as they finish.
96-
func (p *triePrefetcher) terminateAsync() chan *subfetcher {
97-
// Short circuit if the fetcher is already closed
98-
select {
99-
case <-p.term:
100-
return nil
101-
default:
102-
}
103-
// Terminate all the sub-fetchers asynchronously and feed them into a result
104-
// channel as they finish
105-
var (
106-
res = make(chan *subfetcher, len(p.fetchers))
107-
pend sync.WaitGroup
108-
)
109-
for _, fetcher := range p.fetchers {
110-
pend.Add(1)
111-
go func(f *subfetcher) {
112-
f.terminate()
113-
res <- f
114-
pend.Done()
115-
}(fetcher)
116-
}
117-
go func() {
118-
pend.Wait()
119-
close(res)
120-
}()
121-
close(p.term)
122-
return res
123-
}
124-
12596
// report aggregates the pre-fetching and usage metrics and reports them.
12697
func (p *triePrefetcher) report() {
12798
if !metrics.Enabled {
@@ -173,17 +144,19 @@ func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr comm
173144
return fetcher.schedule(keys)
174145
}
175146

176-
// trie returns the trie matching the root hash, or nil if either the fetcher
177-
// is terminated or the trie is not available.
147+
// trie returns the trie matching the root hash, blocking until the fetcher of
148+
// the given trie terminates. If no fetcher exists for the request, nil will be
149+
// returned.
178150
func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) (Trie, error) {
179151
// Bail if no trie was prefetched for this root
180152
fetcher := p.fetchers[p.trieID(owner, root)]
181153
if fetcher == nil {
182-
log.Warn("Prefetcher missed to load trie", "owner", owner, "root", root)
154+
log.Error("Prefetcher missed to load trie", "owner", owner, "root", root)
183155
p.deliveryMissMeter.Mark(1)
184156
return nil, nil
185157
}
186-
return fetcher.peek()
158+
// Subfetcher exists, retrieve its trie
159+
return fetcher.peek(), nil
187160
}
188161

189162
// used marks a batch of state items used to allow creating statistics as to
@@ -269,27 +242,26 @@ func (sf *subfetcher) schedule(keys [][]byte) error {
269242

270243
// peek retrieves the fetcher's trie, populated with any pre-fetched data. The
271244
// returned trie will be a shallow copy, so modifying it will break subsequent
272-
// peeks for the original data.
273-
//
274-
// This method can only be called after closing the subfetcher.
275-
func (sf *subfetcher) peek() (Trie, error) {
276-
// Ensure the subfetcher finished operating on its trie
277-
select {
278-
case <-sf.term:
279-
default:
280-
return nil, errNotTerminated
281-
}
282-
return sf.trie, nil
245+
// peeks for the original data. The method will block until all the scheduled
246+
// data has been loaded and the fethcer terminated.
247+
func (sf *subfetcher) peek() Trie {
248+
// Block until the fertcher terminates, then retrieve the trie
249+
<-sf.term
250+
return sf.trie
283251
}
284252

285-
// terminate waits for the subfetcher to finish its tasks, after which it tears
286-
// down all the internal background loaders.
287-
func (sf *subfetcher) terminate() {
253+
// terminate requests the subfetcher to stop accepting new tasks and spin down
254+
// as soon as everything is loaded. Depending on the async parameter, the method
255+
// will either block until all disk loads finish or return immediately.
256+
func (sf *subfetcher) terminate(async bool) {
288257
select {
289258
case <-sf.stop:
290259
default:
291260
close(sf.stop)
292261
}
262+
if async {
263+
return
264+
}
293265
<-sf.term
294266
}
295267

core/state/trie_prefetcher_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,7 @@ func TestUseAfterTerminate(t *testing.T) {
5353
if err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()}); err != nil {
5454
t.Errorf("Prefetch failed before terminate: %v", err)
5555
}
56-
if _, err := prefetcher.trie(common.Hash{}, db.originalRoot); err == nil {
57-
t.Errorf("Trie retrieval succeeded before terminate")
58-
}
59-
prefetcher.terminate()
56+
prefetcher.terminate(false)
6057

6158
if err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, [][]byte{skey.Bytes()}); err == nil {
6259
t.Errorf("Prefetch succeeded after terminate: %v", err)

0 commit comments

Comments
 (0)