core/state, trie, light: improve commit paralellization

This commit is contained in:
Martin Holst Swende 2020-01-01 23:31:39 +01:00
parent 478d0ee00d
commit 8bfb58e30d
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
8 changed files with 112 additions and 35 deletions

View file

@ -83,6 +83,8 @@ type Trie interface {
// and external (for account tries) references. // and external (for account tries) references.
Commit(onleaf trie.LeafCallback) (common.Hash, error) Commit(onleaf trie.LeafCallback) (common.Hash, error)
CommitTo(onleaf trie.LeafCallback, dbi *trie.DbInserter) (common.Hash, error)
// NodeIterator returns an iterator that returns nodes of the trie. Iteration // NodeIterator returns an iterator that returns nodes of the trie. Iteration
// starts at the key after the given start key. // starts at the key after the given start key.
NodeIterator(startKey []byte) trie.NodeIterator NodeIterator(startKey []byte) trie.NodeIterator

View file

@ -19,6 +19,7 @@ package state
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/trie"
"io" "io"
"math/big" "math/big"
"time" "time"
@ -272,7 +273,7 @@ func (s *stateObject) finalise() {
} }
// updateTrie writes cached storage modifications into the object's storage trie. // updateTrie writes cached storage modifications into the object's storage trie.
func (s *stateObject) updateTrie(db Database) Trie { func (s *stateObject) updateTrie(tr Trie) Trie {
// Make sure all dirty slots are finalized into the pending storage area // Make sure all dirty slots are finalized into the pending storage area
s.finalise() s.finalise()
@ -281,7 +282,6 @@ func (s *stateObject) updateTrie(db Database) Trie {
defer func(start time.Time) { s.db.StorageUpdates += time.Since(start) }(time.Now()) defer func(start time.Time) { s.db.StorageUpdates += time.Since(start) }(time.Now())
} }
// Insert all the pending updates into the trie // Insert all the pending updates into the trie
tr := s.getTrie(db)
for key, value := range s.pendingStorage { for key, value := range s.pendingStorage {
// Skip noop changes, persist actual changes // Skip noop changes, persist actual changes
if value == s.originStorage[key] { if value == s.originStorage[key] {
@ -304,8 +304,8 @@ func (s *stateObject) updateTrie(db Database) Trie {
} }
// UpdateRoot sets the trie root to the current root hash of // UpdateRoot sets the trie root to the current root hash of
func (s *stateObject) updateRoot(db Database) { func (s *stateObject) updateRoot(tr Trie) {
s.updateTrie(db) s.updateTrie(tr)
// Track the amount of time wasted on hashing the storge trie // Track the amount of time wasted on hashing the storge trie
if metrics.EnabledExpensive { if metrics.EnabledExpensive {
@ -316,8 +316,8 @@ func (s *stateObject) updateRoot(db Database) {
// CommitTrie the storage trie of the object to db. // CommitTrie the storage trie of the object to db.
// This updates the trie root. // This updates the trie root.
func (s *stateObject) CommitTrie(db Database) error { func (s *stateObject) CommitTrie(tr Trie) error {
s.updateTrie(db) s.updateTrie(tr)
if s.dbErr != nil { if s.dbErr != nil {
return s.dbErr return s.dbErr
} }
@ -332,6 +332,22 @@ func (s *stateObject) CommitTrie(db Database) error {
return err return err
} }
func (s *stateObject) CommitTrieTo(tr Trie, inserter *trie.DbInserter) error {
s.updateTrie(tr)
if s.dbErr != nil {
return s.dbErr
}
// Track the amount of time wasted on committing the storge trie
if metrics.EnabledExpensive {
defer func(start time.Time) { s.db.StorageCommits += time.Since(start) }(time.Now())
}
root, err := s.trie.CommitTo(nil, inserter)
if err == nil {
s.data.Root = root
}
return err
}
// AddBalance removes amount from c's balance. // AddBalance removes amount from c's balance.
// It is used to add funds to the destination account of a transfer. // It is used to add funds to the destination account of a transfer.
func (s *stateObject) AddBalance(amount *big.Int) { func (s *stateObject) AddBalance(amount *big.Int) {

View file

@ -18,6 +18,7 @@
package state package state
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -330,7 +331,7 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie {
return nil return nil
} }
cpy := stateObject.deepCopy(s) cpy := stateObject.deepCopy(s)
return cpy.updateTrie(s.db) return cpy.updateTrie(cpy.getTrie(s.db))
} }
func (s *StateDB) HasSuicided(addr common.Address) bool { func (s *StateDB) HasSuicided(addr common.Address) bool {
@ -694,7 +695,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
if obj.deleted { if obj.deleted {
s.deleteStateObject(obj) s.deleteStateObject(obj)
} else { } else {
obj.updateRoot(s.db) obj.updateRoot(obj.getTrie(s.db))
s.updateStateObject(obj) s.updateStateObject(obj)
} }
} }
@ -730,20 +731,30 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
// Finalize any pending changes and merge everything into the tries // Finalize any pending changes and merge everything into the tries
s.IntermediateRoot(deleteEmptyObjects) s.IntermediateRoot(deleteEmptyObjects)
// The commit phase. We start by committing the account storage tries
//
// Start the dedicated inserter
dbi := trie.StartDBInserter(s.db.TrieDB())
// Commit objects to the trie, measuring the elapsed time // Commit objects to the trie, measuring the elapsed time
for addr := range s.stateObjectsDirty { for addr := range s.stateObjectsDirty {
if obj := s.stateObjects[addr]; !obj.deleted { if obj := s.stateObjects[addr]; !obj.deleted {
// Write any contract code associated with the state object // Write any contract code associated with the state object
if obj.code != nil && obj.dirtyCode { if obj.code != nil && obj.dirtyCode {
s.db.TrieDB().InsertBlob(common.BytesToHash(obj.CodeHash()), obj.code) dbi.InsertBlob(obj.code, common.BytesToHash(obj.CodeHash()))
//s.db.TrieDB().InsertBlob(common.BytesToHash(obj.CodeHash()), obj.code)
obj.dirtyCode = false obj.dirtyCode = false
} }
// Write any storage changes in the state object to its storage trie // Write any storage changes in the state object to its storage trie
if err := obj.CommitTrie(s.db); err != nil { if err := obj.CommitTrieTo(obj.getTrie(s.db), dbi); err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
} }
} }
// Wait for storage update to punch through
//dbi.WaitForEmpty()
// .. or not
if len(s.stateObjectsDirty) > 0 { if len(s.stateObjectsDirty) > 0 {
s.stateObjectsDirty = make(map[common.Address]struct{}) s.stateObjectsDirty = make(map[common.Address]struct{})
} }
@ -754,17 +765,21 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) {
// The onleaf func is called _serially_, so we can reuse the same account // The onleaf func is called _serially_, so we can reuse the same account
// for unmarshalling every time. // for unmarshalling every time.
var account Account var account Account
return s.trie.Commit(func(leaf []byte, parent common.Hash) error { var trieDb = s.db.TrieDB()
h, e := s.trie.CommitTo(func(leaf []byte, parent common.Hash) error {
if err := rlp.DecodeBytes(leaf, &account); err != nil { if err := rlp.DecodeBytes(leaf, &account); err != nil {
return nil return nil
} }
if account.Root != emptyRoot { if account.Root != emptyRoot {
s.db.TrieDB().Reference(account.Root, parent) trieDb.Reference(account.Root, parent)
} }
code := common.BytesToHash(account.CodeHash) if !bytes.Equal(emptyCodeHash, account.CodeHash) {
if code != emptyCode { trieDb.Reference(common.BytesToHash(account.CodeHash), parent)
s.db.TrieDB().Reference(code, parent)
} }
return nil return nil
}) }, dbi)
// Close it, and wait for empty
dbi.Close()
return h, e
} }

View file

@ -129,6 +129,13 @@ func (t *odrTrie) Commit(onleaf trie.LeafCallback) (common.Hash, error) {
return t.trie.Commit(onleaf) return t.trie.Commit(onleaf)
} }
func (t *odrTrie) CommitTo(onleaf trie.LeafCallback, dbi *trie.DbInserter) (common.Hash, error) {
if t.trie == nil {
return t.id.Root, nil
}
return t.trie.Commit(onleaf)
}
func (t *odrTrie) Hash() common.Hash { func (t *odrTrie) Hash() common.Hash {
if t.trie == nil { if t.trie == nil {
return t.id.Root return t.id.Root

View file

@ -19,6 +19,7 @@ package trie
import ( import (
"errors" "errors"
"fmt" "fmt"
"math/rand"
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -32,6 +33,7 @@ type Leaf struct {
hash common.Hash // hash of rlp data hash common.Hash // hash of rlp data
node node // the node to commit node node // the node to commit
vnodes bool // set to true if the node (possibly) contains a valueNode vnodes bool // set to true if the node (possibly) contains a valueNode
onleaf LeafCallback
} }
type committer struct { type committer struct {
@ -52,10 +54,11 @@ var committerPool = sync.Pool{
}, },
} }
func newCommitter(onleaf LeafCallback) *committer { func newCommitter(onleaf LeafCallback, leafCh chan *Leaf) *committer {
h := committerPool.Get().(*committer) h := committerPool.Get().(*committer)
h.onleaf = onleaf h.onleaf = onleaf
if onleaf != nil { h.leafCh = leafCh
if onleaf != nil && leafCh == nil {
h.leafCh = make(chan *Leaf, 200) // arbitrary number h.leafCh = make(chan *Leaf, 200) // arbitrary number
} }
return h return h
@ -182,6 +185,7 @@ func (h *committer) store(n node, db *Database, force bool, hasVnodeChildren boo
hash: common.BytesToHash(hash), hash: common.BytesToHash(hash),
node: n, node: n,
vnodes: hasVnodeChildren, vnodes: hasVnodeChildren,
onleaf: h.onleaf,
} }
} else if db != nil { } else if db != nil {
// No leaf-callback used, but there's still a database. Do serial // No leaf-callback used, but there's still a database. Do serial
@ -275,7 +279,7 @@ have to do the waitgroup-wait between each trie commit.
The code below is a rough sketch, it needs to be integrated nicely without causing The code below is a rough sketch, it needs to be integrated nicely without causing
dependency cycles between state, core and trie. dependency cycles between state, core and trie.
**/
type DbInserter struct { type DbInserter struct {
inputCh chan *Leaf // This is where input to database is sent inputCh chan *Leaf // This is where input to database is sent
@ -293,7 +297,7 @@ func (dbi *DbInserter) run() {
size = item.size size = item.size
n = item.node n = item.node
hasVnodes = item.vnodes hasVnodes = item.vnodes
onleaf = item.onLeaf onleaf = item.onleaf
) )
if size < 0 { if size < 0 {
// This is an end-marker object. // This is an end-marker object.
@ -334,14 +338,14 @@ func (dbi *DbInserter) Insert(leaf *Leaf) {
// channel has been handled // channel has been handled
func (dbi *DbInserter) WaitForEmpty() { func (dbi *DbInserter) WaitForEmpty() {
// Send an arbitrary id there // Send an arbitrary id there
checksum := rand.Uint32() checksum := rand.Int()
dbi.inputCh <- &trie.Leaf{ dbi.inputCh <- &Leaf{
size: -checksum, size: -checksum,
} }
// And wait for it to come back // And wait for it to come back
for { for {
select { select {
case retval <- dbi.reportCh: case retval := <-dbi.reportCh:
if retval == checksum { if retval == checksum {
return return
} }
@ -351,7 +355,7 @@ func (dbi *DbInserter) WaitForEmpty() {
} }
func (dbi *DbInserter) InsertBlob(blob []byte, blobHash common.Hash) { func (dbi *DbInserter) InsertBlob(blob []byte, blobHash common.Hash) {
dbi.inputCh <- &trie.Leaf{ dbi.inputCh <- &Leaf{
size: len(blob), size: len(blob),
hash: blobHash, hash: blobHash,
node: rawNode(blob), node: rawNode(blob),
@ -366,8 +370,7 @@ func StartDBInserter(db *Database) *DbInserter {
reportCh: make(chan int), reportCh: make(chan int),
db: db, db: db,
} }
dbi.wg.Add(1)
go dbi.run() go dbi.run()
return dbi
} }
*/

View file

@ -161,6 +161,20 @@ func (t *SecureTrie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
return t.trie.Commit(onleaf) return t.trie.Commit(onleaf)
} }
func (t *SecureTrie) CommitTo(onleaf LeafCallback, inserter *DbInserter ) (root common.Hash, err error) {
// Write all the pre-images to the actual disk database
if len(t.getSecKeyCache()) > 0 {
t.trie.db.lock.Lock()
for hk, key := range t.secKeyCache {
t.trie.db.insertPreimage(common.BytesToHash([]byte(hk)), key)
}
t.trie.db.lock.Unlock()
t.secKeyCache = make(map[string][]byte)
}
return t.trie.CommitTo(onleaf, inserter)
}
// Hash returns the root hash of SecureTrie. It does not write to the // Hash returns the root hash of SecureTrie. It does not write to the
// database and can be used even if the trie doesn't have one. // database and can be used even if the trie doesn't have one.
func (t *SecureTrie) Hash() common.Hash { func (t *SecureTrie) Hash() common.Hash {

View file

@ -187,10 +187,10 @@ func (t *Trie) TryUpdate(key, value []byte) error {
return nil return nil
} }
func (t *Trie) batchStart(){ func (t *Trie) batchStart() {
t.batchMode = true t.batchMode = true
} }
func (t *Trie) batchEnd(){ func (t *Trie) batchEnd() {
t.batchMode = false t.batchMode = false
} }
@ -240,14 +240,14 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
// When we modify a node, we only copy it in case it is an old committed // When we modify a node, we only copy it in case it is an old committed
// node. // node.
// If the node is "new", we just update in place. // If the node is "new", we just update in place.
if t.batchMode{ if t.batchMode {
if h, dirty := n.cache(); !dirty || h != nil{ if h, dirty := n.cache(); !dirty || h != nil {
// This node is either not dirty, or already hashed. We copy it // This node is either not dirty, or already hashed. We copy it
n = n.copy() n = n.copy()
}else{ } else {
// No copy // No copy
} }
}else{ } else {
n = n.copy() n = n.copy()
} }
n.flags = t.newFlag() n.flags = t.newFlag()
@ -468,7 +468,7 @@ func (t *Trie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
return emptyRoot, nil return emptyRoot, nil
} }
rootHash := t.Hash() rootHash := t.Hash()
h := newCommitter(onleaf) h := newCommitter(onleaf, nil)
defer returnCommitterToPool(h) defer returnCommitterToPool(h)
var wg sync.WaitGroup var wg sync.WaitGroup
if onleaf != nil { if onleaf != nil {
@ -487,6 +487,24 @@ func (t *Trie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
return rootHash, nil return rootHash, nil
} }
func (t *Trie) CommitTo(onleaf LeafCallback, dbi *DbInserter) (root common.Hash, err error) {
if t.db == nil {
panic("commit called on trie with nil database")
}
if t.root == nil {
return emptyRoot, nil
}
rootHash := t.Hash()
h := newCommitter(onleaf, dbi.inputCh)
h.leafCh = dbi.inputCh
_, err = h.commit(t.root, t.db, true)
if err != nil {
return common.Hash{}, err
}
t.dirtyCount = 0
return rootHash, nil
}
// oldHashRoot is the old implementation of hashRoot, which uses the regular hasher // oldHashRoot is the old implementation of hashRoot, which uses the regular hasher
func (t *Trie) oldHashRoot(db *Database, onleaf LeafCallback) (node, node, error) { func (t *Trie) oldHashRoot(db *Database, onleaf LeafCallback) (node, node, error) {
if t.root == nil { if t.root == nil {

View file

@ -507,10 +507,12 @@ func benchUpdate(b *testing.B, e binary.ByteOrder) *Trie {
trie := newEmpty() trie := newEmpty()
k := make([]byte, 32) k := make([]byte, 32)
b.ReportAllocs() b.ReportAllocs()
trie.batchStart()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
e.PutUint64(k, uint64(i)) e.PutUint64(k, uint64(i))
trie.Update(k, k) trie.Update(k, k)
} }
trie.batchEnd()
return trie return trie
} }