core/state: improve prefetcher

This commit is contained in:
Gary Rong 2024-03-05 15:17:18 +08:00 committed by Jared Wasinger
parent 8d1074fc37
commit 287cead19f
2 changed files with 88 additions and 71 deletions

View file

@ -17,6 +17,7 @@
package state package state
import ( import (
"errors"
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -27,6 +28,9 @@ import (
var ( var (
// triePrefetchMetricsPrefix is the prefix under which to publish the metrics. // triePrefetchMetricsPrefix is the prefix under which to publish the metrics.
triePrefetchMetricsPrefix = "trie/prefetch/" triePrefetchMetricsPrefix = "trie/prefetch/"
// errTerminated is returned if any invocation is applied on a terminated fetcher.
errTerminated = errors.New("fetcher is already terminated")
) )
// triePrefetcher is an active prefetcher, which receives accounts or storage // triePrefetcher is an active prefetcher, which receives accounts or storage
@ -43,17 +47,15 @@ type triePrefetcher struct {
deliveryMissMeter metrics.Meter deliveryMissMeter metrics.Meter
accountLoadMeter metrics.Meter accountLoadMeter metrics.Meter
accountDupMeter metrics.Meter accountDupMeter metrics.Meter
accountSkipMeter metrics.Meter
accountWasteMeter metrics.Meter accountWasteMeter metrics.Meter
storageLoadMeter metrics.Meter storageLoadMeter metrics.Meter
storageDupMeter metrics.Meter storageDupMeter metrics.Meter
storageSkipMeter metrics.Meter
storageWasteMeter metrics.Meter storageWasteMeter metrics.Meter
} }
func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePrefetcher { func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePrefetcher {
prefix := triePrefetchMetricsPrefix + namespace prefix := triePrefetchMetricsPrefix + namespace
p := &triePrefetcher{ return &triePrefetcher{
db: db, db: db,
root: root, root: root,
fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map
@ -61,20 +63,20 @@ func newTriePrefetcher(db Database, root common.Hash, namespace string) *triePre
deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil), deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil),
accountLoadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load", nil), accountLoadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load", nil),
accountDupMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup", nil), accountDupMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup", nil),
accountSkipMeter: metrics.GetOrRegisterMeter(prefix+"/account/skip", nil),
accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil), accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil),
storageLoadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load", nil), storageLoadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load", nil),
storageDupMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup", nil), storageDupMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup", nil),
storageSkipMeter: metrics.GetOrRegisterMeter(prefix+"/storage/skip", nil),
storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil), storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil),
} }
return p
} }
// close iterates over all the subfetchers, waits on any that were left spinning // close iterates over all the subfetchers, waits on any that were left spinning
// and reports the stats to the metrics subsystem. close should not be called // and reports the stats to the metrics subsystem.
// more than once on a triePrefetcher instance.
func (p *triePrefetcher) close() { func (p *triePrefetcher) close() {
// Short circuit if the fetcher is already closed.
if p.closed {
return
}
for _, fetcher := range p.fetchers { for _, fetcher := range p.fetchers {
fetcher.close() fetcher.close()
@ -82,8 +84,6 @@ func (p *triePrefetcher) close() {
if fetcher.root == p.root { if fetcher.root == p.root {
p.accountLoadMeter.Mark(int64(len(fetcher.seen))) p.accountLoadMeter.Mark(int64(len(fetcher.seen)))
p.accountDupMeter.Mark(int64(fetcher.dups)) p.accountDupMeter.Mark(int64(fetcher.dups))
p.accountSkipMeter.Mark(int64(len(fetcher.tasks)))
for _, key := range fetcher.used { for _, key := range fetcher.used {
delete(fetcher.seen, string(key)) delete(fetcher.seen, string(key))
} }
@ -91,8 +91,6 @@ func (p *triePrefetcher) close() {
} else { } else {
p.storageLoadMeter.Mark(int64(len(fetcher.seen))) p.storageLoadMeter.Mark(int64(len(fetcher.seen)))
p.storageDupMeter.Mark(int64(fetcher.dups)) p.storageDupMeter.Mark(int64(fetcher.dups))
p.storageSkipMeter.Mark(int64(len(fetcher.tasks)))
for _, key := range fetcher.used { for _, key := range fetcher.used {
delete(fetcher.seen, string(key)) delete(fetcher.seen, string(key))
} }
@ -101,50 +99,54 @@ func (p *triePrefetcher) close() {
} }
} }
p.closed = true p.closed = true
p.fetchers = nil
} }
// prefetch schedules a batch of trie items to prefetch. After the prefetcher is closed, all the following tasks scheduled will not be executed. // prefetch schedules a batch of trie items to prefetch. After the prefetcher is
// closed, all the following tasks scheduled will not be executed and an error
// will be returned.
// //
// prefetch is called from two locations: // prefetch is called from two locations:
//
// 1. Finalize of the state-objects storage roots. This happens at the end // 1. Finalize of the state-objects storage roots. This happens at the end
// of every transaction, meaning that if several transactions touches // of every transaction, meaning that if several transactions touches
// upon the same contract, the parameters invoking this method may be // upon the same contract, the parameters invoking this method may be
// repeated. // repeated.
// 2. Finalize of the main account trie. This happens only once per block. // 2. Finalize of the main account trie. This happens only once per block.
func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, keys [][]byte) { func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, keys [][]byte) error {
if p.closed {
return errTerminated
}
id := p.trieID(owner, root) id := p.trieID(owner, root)
fetcher := p.fetchers[id] fetcher := p.fetchers[id]
if fetcher == nil { if fetcher == nil {
fetcher = newSubfetcher(p.db, p.root, owner, root, addr) fetcher = newSubfetcher(p.db, p.root, owner, root, addr)
p.fetchers[id] = fetcher p.fetchers[id] = fetcher
} }
fetcher.schedule(keys) return fetcher.schedule(keys)
} }
// trie returns the trie matching the root hash, or nil if the prefetcher doesn't // trie returns the trie matching the root hash, or nil if either the fetcher
// have it. trie is not safe to call concurrently // is terminated or the trie is not available.
func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie { func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie {
if p.closed {
return nil
}
// Bail if no trie was prefetched for this root // Bail if no trie was prefetched for this root
fetcher := p.fetchers[p.trieID(owner, root)] fetcher := p.fetchers[p.trieID(owner, root)]
if fetcher == nil || fetcher.trie == nil { if fetcher == nil {
p.deliveryMissMeter.Mark(1) p.deliveryMissMeter.Mark(1)
return nil return nil
} }
if p.closed { return fetcher.peek()
return fetcher.db.CopyTrie(fetcher.trie)
}
trieChan := make(chan Trie)
fetcher.copy <- trieChan
select {
case fetcher.wake <- true:
default:
}
return <-trieChan
} }
// used marks a batch of state items used to allow creating statistics as to // used marks a batch of state items used to allow creating statistics as to
// how useful or wasteful the prefetcher is. // how useful or wasteful the fetcher is.
func (p *triePrefetcher) used(owner common.Hash, root common.Hash, used [][]byte) { func (p *triePrefetcher) used(owner common.Hash, root common.Hash, used [][]byte) {
if p.closed {
return
}
if fetcher := p.fetchers[p.trieID(owner, root)]; fetcher != nil { if fetcher := p.fetchers[p.trieID(owner, root)]; fetcher != nil {
fetcher.used = used fetcher.used = used
} }
@ -173,7 +175,8 @@ type subfetcher struct {
tasks [][]byte // Items queued up for retrieval tasks [][]byte // Items queued up for retrieval
lock sync.Mutex // Lock protecting the task queue lock sync.Mutex // Lock protecting the task queue
wake chan bool // Wake channel if a new task is scheduled, true if the subfetcher should continue running when there are no pending tasks wake chan struct{} // Wake channel if a new task is scheduled
stop chan struct{} // Channel to interrupt processing
term chan struct{} // Channel to signal interruption term chan struct{} // Channel to signal interruption
copy chan chan Trie // channel for retrieving copies of the subfetcher's trie copy chan chan Trie // channel for retrieving copies of the subfetcher's trie
@ -191,9 +194,10 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo
owner: owner, owner: owner,
root: root, root: root,
addr: addr, addr: addr,
wake: make(chan bool, 1), wake: make(chan struct{}),
copy: make(chan chan Trie, 1), stop: make(chan struct{}),
term: make(chan struct{}), term: make(chan struct{}),
copy: make(chan chan Trie),
seen: make(map[string]struct{}), seen: make(map[string]struct{}),
} }
go sf.loop() go sf.loop()
@ -201,24 +205,41 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo
} }
// schedule adds a batch of trie keys to the queue to prefetch. // schedule adds a batch of trie keys to the queue to prefetch.
func (sf *subfetcher) schedule(keys [][]byte) { func (sf *subfetcher) schedule(keys [][]byte) error {
// Append the tasks to the current queue // Append the tasks to the current queue
sf.lock.Lock() sf.lock.Lock()
sf.tasks = append(sf.tasks, keys...) sf.tasks = append(sf.tasks, keys...)
sf.lock.Unlock() sf.lock.Unlock()
// Notify the prefetcher, it's fine if it's already terminated
// Notify the background thread to execute scheduled tasks
select { select {
case sf.wake <- true: case sf.wake <- struct{}{}:
default: return nil
case <-sf.term:
return errTerminated
}
}
// peek tries to retrieve a deep copy of the fetcher's trie. Nil is returned
// if the fetcher is already terminated, or the associated trie is failing
// for opening.
func (sf *subfetcher) peek() Trie {
ch := make(chan Trie)
select {
case sf.copy <- ch:
return <-ch
case <-sf.term:
return nil
} }
} }
// close waits for the subfetcher to finish its tasks. It cannot be called multiple times // close waits for the subfetcher to finish its tasks. It cannot be called multiple times
func (sf *subfetcher) close() { func (sf *subfetcher) close() {
// Notify the prefetcher. The wake-chan is buffered, so this is async. select {
sf.wake <- false case <-sf.stop:
// Wait for it to terminate default:
close(sf.stop)
}
<-sf.term <-sf.term
} }
@ -228,8 +249,7 @@ func (sf *subfetcher) loop() {
// No matter how the loop stops, signal anyone waiting that it's terminated // No matter how the loop stops, signal anyone waiting that it's terminated
defer close(sf.term) defer close(sf.term)
// Any calls to trie // Start by opening the trie and stop processing if it fails.
// start by opening the trie and stop processing if it fails.
if sf.owner == (common.Hash{}) { if sf.owner == (common.Hash{}) {
trie, err := sf.db.OpenTrie(sf.root) trie, err := sf.db.OpenTrie(sf.root)
if err != nil { if err != nil {
@ -238,8 +258,6 @@ func (sf *subfetcher) loop() {
} }
sf.trie = trie sf.trie = trie
} else { } else {
// The trie argument can be nil as verkle doesn't support prefetching
// yet. TODO FIX IT(rjl493456442), otherwise code will panic here.
trie, err := sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root, nil) trie, err := sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root, nil)
if err != nil { if err != nil {
log.Warn("Trie prefetcher failed opening trie", "root", sf.root, "err", err) log.Warn("Trie prefetcher failed opening trie", "root", sf.root, "err", err)
@ -248,14 +266,15 @@ func (sf *subfetcher) loop() {
sf.trie = trie sf.trie = trie
} }
// Trie opened successfully, keep prefetching items // Trie opened successfully, keep prefetching items
for keepRunning := range sf.wake { for {
// Subfetcher was woken up, retrieve any tasks to avoid spinning the lock select {
case <-sf.wake:
// Execute all remaining tasks in single run
sf.lock.Lock() sf.lock.Lock()
tasks := sf.tasks tasks := sf.tasks
sf.tasks = nil sf.tasks = nil
sf.lock.Unlock() sf.lock.Unlock()
// Prefetch all tasks
for _, task := range tasks { for _, task := range tasks {
if _, ok := sf.seen[string(task)]; ok { if _, ok := sf.seen[string(task)]; ok {
sf.dups++ sf.dups++
@ -268,14 +287,12 @@ func (sf *subfetcher) loop() {
} }
sf.seen[string(task)] = struct{}{} sf.seen[string(task)] = struct{}{}
} }
// if any trie retrieval request is made, ensure it is completed
// after pending tasks have been processed.
select {
case ch := <-sf.copy: case ch := <-sf.copy:
// Somebody wants a copy of the current trie, grant them.
ch <- sf.db.CopyTrie(sf.trie) ch <- sf.db.CopyTrie(sf.trie)
default:
} case <-sf.stop:
if !keepRunning { // Termination is requested, abort
return return
} }
} }

View file

@ -55,7 +55,7 @@ func TestUseAfterClose(t *testing.T) {
if a == nil { if a == nil {
t.Fatal("Prefetching before close should not return nil") t.Fatal("Prefetching before close should not return nil")
} }
if b == nil { if b != nil {
t.Fatal("Trie after close should not return nil") t.Fatal("Trie after close should return nil")
} }
} }