Skip to content

Commit 682ee82

Browse files
authored
core/state: parallelise parts of state commit (#29681)
* core/state, internal/workerpool: parallelize parts of state commit * core, internal: move workerpool into syncx * core/state: use errgroups, commit accounts concurrently * core: resurrect detailed commit timers to almost-accuracy
1 parent 9f96e07 commit 682ee82

File tree

3 files changed

+89
-42
lines changed

3 files changed

+89
-42
lines changed

core/blockchain.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1963,7 +1963,7 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s
19631963
snapshotCommitTimer.Update(statedb.SnapshotCommits) // Snapshot commits are complete, we can mark them
19641964
triedbCommitTimer.Update(statedb.TrieDBCommits) // Trie database commits are complete, we can mark them
19651965

1966-
blockWriteTimer.Update(time.Since(wstart) - statedb.AccountCommits - statedb.StorageCommits - statedb.SnapshotCommits - statedb.TrieDBCommits)
1966+
blockWriteTimer.Update(time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.SnapshotCommits - statedb.TrieDBCommits)
19671967
blockInsertTimer.UpdateSince(start)
19681968

19691969
return &blockProcessingResult{usedGas: usedGas, procTime: proctime, status: status}, nil

core/state/state_object.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,9 @@ func (s *stateObject) updateRoot() {
403403
// commit obtains a set of dirty storage trie nodes and updates the account data.
404404
// The returned set can be nil if nothing to commit. This function assumes all
405405
// storage mutations have already been flushed into trie by updateRoot.
406+
//
407+
// Note, commit may run concurrently across all the state objects. Do not assume
408+
// thread-safe access to the statedb.
406409
func (s *stateObject) commit() (*trienode.NodeSet, error) {
407410
// Short circuit if trie is not even loaded, don't bother with committing anything
408411
if s.trie == nil {

core/state/statedb.go

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"math/big"
2424
"slices"
2525
"sort"
26+
"sync"
2627
"time"
2728

2829
"github.com/ethereum/go-ethereum/common"
@@ -37,6 +38,7 @@ import (
3738
"github.com/ethereum/go-ethereum/trie/trienode"
3839
"github.com/ethereum/go-ethereum/trie/triestate"
3940
"github.com/holiman/uint256"
41+
"golang.org/x/sync/errgroup"
4042
)
4143

4244
type revision struct {
@@ -1146,66 +1148,108 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er
11461148
storageTrieNodesUpdated int
11471149
storageTrieNodesDeleted int
11481150
nodes = trienode.NewMergedNodeSet()
1149-
codeWriter = s.db.DiskDB().NewBatch()
11501151
)
11511152
// Handle all state deletions first
11521153
if err := s.handleDestruction(nodes); err != nil {
11531154
return common.Hash{}, err
11541155
}
1155-
// Handle all state updates afterwards
1156+
// Handle all state updates afterwards, concurrently to one another to shave
1157+
// off some milliseconds from the commit operation. Also accumulate the code
1158+
// writes to run in parallel with the computations.
11561159
start := time.Now()
1160+
var (
1161+
code = s.db.DiskDB().NewBatch()
1162+
lock sync.Mutex
1163+
root common.Hash
1164+
workers errgroup.Group
1165+
)
1166+
// Schedule the account trie first since that will be the biggest, so give
1167+
// it the most time to crunch.
1168+
//
1169+
// TODO(karalabe): This account trie commit is *very* heavy. 5-6ms at chain
1170+
// heads, which seems excessive given that it doesn't do hashing, it just
1171+
// shuffles some data. For comparison, the *hashing* at chain head is 2-3ms.
1172+
// We need to investigate what's happening as it seems something's wonky.
1173+
// Obviously it's not an end of the world issue, just something the original
1174+
// code didn't anticipate for.
1175+
workers.Go(func() error {
1176+
// Write the account trie changes, measuring the amount of wasted time
1177+
newroot, set, err := s.trie.Commit(true)
1178+
if err != nil {
1179+
return err
1180+
}
1181+
root = newroot
1182+
1183+
// Merge the dirty nodes of account trie into global set
1184+
lock.Lock()
1185+
defer lock.Unlock()
1186+
1187+
if set != nil {
1188+
if err = nodes.Merge(set); err != nil {
1189+
return err
1190+
}
1191+
accountTrieNodesUpdated, accountTrieNodesDeleted = set.Size()
1192+
}
1193+
s.AccountCommits = time.Since(start)
1194+
return nil
1195+
})
1196+
// Schedule each of the storage tries that need to be updated, so they can
1197+
// run concurrently to one another.
1198+
//
1199+
// TODO(karalabe): Experimentally, the account commit takes approximately the
1200+
// same time as all the storage commits combined, so we could maybe only have
1201+
// 2 threads in total. But that kind of depends on the account commit being
1202+
// more expensive than it should be, so let's fix that and revisit this todo.
11571203
for addr, op := range s.mutations {
11581204
if op.isDelete() {
11591205
continue
11601206
}
1161-
obj := s.stateObjects[addr]
1162-
11631207
// Write any contract code associated with the state object
1208+
obj := s.stateObjects[addr]
11641209
if obj.code != nil && obj.dirtyCode {
1165-
rawdb.WriteCode(codeWriter, common.BytesToHash(obj.CodeHash()), obj.code)
1210+
rawdb.WriteCode(code, common.BytesToHash(obj.CodeHash()), obj.code)
11661211
obj.dirtyCode = false
11671212
}
1168-
// Write any storage changes in the state object to its storage trie
1169-
set, err := obj.commit()
1170-
if err != nil {
1171-
return common.Hash{}, err
1172-
}
1173-
// Merge the dirty nodes of storage trie into global set. It is possible
1174-
// that the account was destructed and then resurrected in the same block.
1175-
// In this case, the node set is shared by both accounts.
1176-
if set != nil {
1177-
if err := nodes.Merge(set); err != nil {
1178-
return common.Hash{}, err
1213+
// Run the storage updates concurrently to one another
1214+
workers.Go(func() error {
1215+
// Write any storage changes in the state object to its storage trie
1216+
set, err := obj.commit()
1217+
if err != nil {
1218+
return err
11791219
}
1180-
updates, deleted := set.Size()
1181-
storageTrieNodesUpdated += updates
1182-
storageTrieNodesDeleted += deleted
1183-
}
1220+
// Merge the dirty nodes of storage trie into global set. It is possible
1221+
// that the account was destructed and then resurrected in the same block.
1222+
// In this case, the node set is shared by both accounts.
1223+
lock.Lock()
1224+
defer lock.Unlock()
1225+
1226+
if set != nil {
1227+
if err = nodes.Merge(set); err != nil {
1228+
return err
1229+
}
1230+
updates, deleted := set.Size()
1231+
storageTrieNodesUpdated += updates
1232+
storageTrieNodesDeleted += deleted
1233+
}
1234+
s.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime
1235+
return nil
1236+
})
11841237
}
1185-
s.StorageCommits += time.Since(start)
1186-
1187-
if codeWriter.ValueSize() > 0 {
1188-
if err := codeWriter.Write(); err != nil {
1189-
log.Crit("Failed to commit dirty codes", "error", err)
1238+
// Schedule the code commits to run concurrently too. This shouldn't really
1239+
// take much since we don't often commit code, but since it's disk access,
1240+
// it's always yolo.
1241+
workers.Go(func() error {
1242+
if code.ValueSize() > 0 {
1243+
if err := code.Write(); err != nil {
1244+
log.Crit("Failed to commit dirty codes", "error", err)
1245+
}
11901246
}
1191-
}
1192-
// Write the account trie changes, measuring the amount of wasted time
1193-
start = time.Now()
1194-
1195-
root, set, err := s.trie.Commit(true)
1196-
if err != nil {
1247+
return nil
1248+
})
1249+
// Wait for everything to finish and update the metrics
1250+
if err := workers.Wait(); err != nil {
11971251
return common.Hash{}, err
11981252
}
1199-
// Merge the dirty nodes of account trie into global set
1200-
if set != nil {
1201-
if err := nodes.Merge(set); err != nil {
1202-
return common.Hash{}, err
1203-
}
1204-
accountTrieNodesUpdated, accountTrieNodesDeleted = set.Size()
1205-
}
1206-
// Report the commit metrics
1207-
s.AccountCommits += time.Since(start)
1208-
12091253
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
12101254
storageUpdatedMeter.Mark(int64(s.StorageUpdated))
12111255
accountDeletedMeter.Mark(int64(s.AccountDeleted))

0 commit comments

Comments
 (0)