trie: background pruner

This commit is contained in:
Péter Szilágyi 2019-01-09 13:25:11 +02:00
parent 7d437426af
commit 25a4d2e067
No known key found for this signature in database
GPG key ID: E9AE538CEDF8293D
2 changed files with 79 additions and 45 deletions

View file

@ -539,8 +539,8 @@ func (db *Database) Nodes() []string {
// to break genericity here and assume that parent nodes are not owned (account
// trie) whereas child nodes may be owned (storage trie or bytecode).
func (db *Database) Reference(owner common.Hash, child common.Hash, parent common.Hash) {
db.lock.RLock()
defer db.lock.RUnlock()
db.lock.Lock()
defer db.lock.Unlock()
// If the node does not exist, it's a node pulled from disk, skip
childKey := makeNodeKey(owner, child)
@ -566,17 +566,19 @@ func (db *Database) Dereference(root common.Hash, prune bool) error {
log.Error("Attempted to dereference the trie cache meta root")
return nil
}
// Obtain the write lock and garbage collect in-memory
db.lock.Lock()
defer db.lock.Unlock()
nodes, storage, start := len(db.dirties), db.dirtiesSize, time.Now()
prunetime, prunenodes, prunesize := db.prunetime, db.prunenodes, db.prunesize
pruner := db.newPruner(root)
if err := db.dereference(common.Hash{}, root, common.Hash{}, common.Hash{}, prune, nil, pruner); err != nil {
return err
// Dereference the trie and accumulate prune targets if needed
var pruner *pruner
if prune {
pruner = db.newPruner()
}
if err := pruner.flush(); err != nil {
if err := db.dereference(common.Hash{}, root, common.Hash{}, common.Hash{}, nil, pruner); err != nil {
return err
}
db.gcnodes += uint64(nodes - len(db.dirties))
@ -587,19 +589,31 @@ func (db *Database) Dereference(root common.Hash, prune bool) error {
memcacheGCSizeMeter.Mark(int64(storage - db.dirtiesSize))
memcacheGCNodesMeter.Mark(int64(nodes - len(db.dirties)))
// If pruning was requested, execute on a background thread
go func() {
db.lock.RLock()
defer db.lock.RUnlock()
if pruner != nil {
start := time.Now()
pruner.execute()
go func() {
if err := pruner.flush(); err != nil {
log.Crit("Failed to prune database", "err", err)
}
}()
db.prunetime += time.Since(start) // TODO(karalabe): unsafe, stats are off too
}
// Pruned or not, update the stats and log
memcachePruneTimeTimer.Update(db.prunetime - prunetime)
memcachePruneNodesMeter.Mark(int64(db.prunenodes - prunenodes))
memcachePruneSizeMeter.Mark(int64(db.prunesize - prunesize))
log.Debug("Dereferenced trie from memory database", "nodes", nodes-len(db.dirties), "size", storage-db.dirtiesSize, "time", common.PrettyDuration(time.Since(start)),
"gcnodes", db.gcnodes, "gcsize", db.gcsize, "gctime", common.PrettyDuration(db.gctime), "prnodes", db.prunenodes, "prsize", db.prunesize, "prtime", common.PrettyDuration(db.prunetime),
"livenodes", len(db.dirties), "livesize", db.dirtiesSize)
}()
return nil
}
// dereference is the private locked version of Dereference.
func (db *Database) dereference(childOwner common.Hash, childHash common.Hash, parentOwner common.Hash, parentHash common.Hash, prune bool, path []byte, pruner *pruner) error {
func (db *Database) dereference(childOwner common.Hash, childHash common.Hash, parentOwner common.Hash, parentHash common.Hash, path []byte, pruner *pruner) error {
// Dereference the parent-child
parentKey := makeNodeKey(parentOwner, parentHash)
parent := db.dirties[parentKey]
@ -614,10 +628,8 @@ func (db *Database) dereference(childOwner common.Hash, childHash common.Hash, p
// If the child does not exist, it's a previously committed node.
child, ok := db.dirties[childKey]
if !ok {
if prune {
start := time.Now()
pruner.prune(childOwner, childHash, path)
db.prunetime += time.Since(start)
if pruner != nil {
pruner.mark(childOwner, childHash, path)
}
return nil
}
@ -644,12 +656,12 @@ func (db *Database) dereference(childOwner common.Hash, childHash common.Hash, p
}
// Dereference all children and delete the node
child.iterateRefs(path, func(path []byte, hash common.Hash) error {
db.dereference(childOwner, hash, childOwner, childHash, prune, path, pruner)
db.dereference(childOwner, hash, childOwner, childHash, path, pruner)
return nil
})
for key := range child.children {
owner, hash := splitNodeKey(key)
db.dereference(owner, hash, childOwner, childHash, prune, nil, pruner)
db.dereference(owner, hash, childOwner, childHash, nil, pruner)
}
delete(db.dirties, childKey)
db.dirtiesSize -= common.StorageSize(common.HashLength + int(child.size))

View file

@ -33,40 +33,60 @@ import (
type pruner struct {
db *Database // Trie database for accessing dirty and clean data
tries []*traverser // Individual stateful trie traversers for fast liveness checks
marks []*prunerTarget // Nodes marked for potential pruning
batch ethdb.Batch // Write batch to minimize database trashing
}
// prunerTarget represents a single marked target for potential pruning.
type prunerTarget struct {
owner common.Hash // Owner account hash of the node to delete
path []byte // Patricia path leading to this node
hash common.Hash // Hash of the node to delete
}
// newPruner creates a new trie pruner tied to the liveness of all the currently
// referenced in-memory nodes, except the specified one (currently being pruned).
func (db *Database) newPruner(skip common.Hash) *pruner {
// Create the set of traversers based on the live tries
var traversers []*traverser
for key := range db.dirties[metaRoot].children {
if _, root := splitNodeKey(key); root != skip {
traversers = append(traversers, &traverser{
// referenced in-memory nodes.
func (db *Database) newPruner() *pruner {
return &pruner{
db: db,
batch: db.diskdb.NewBatch(),
}
}
// mark adds a new prune target to be deleted on the pruning run.
func (p *pruner) mark(owner common.Hash, hash common.Hash, path []byte) {
p.marks = append(p.marks, &prunerTarget{
owner: owner,
hash: hash,
path: common.CopyBytes(path),
})
}
// execute runs the pruning procedure, deleting everything that has no live
// reference any more.
func (p *pruner) execute() {
// Create the set of traversers based on the live tries
for key := range p.db.dirties[metaRoot].children {
_, root := splitNodeKey(key)
p.tries = append(p.tries, &traverser{
db: p.db,
state: &traverserState{
node: hashNode(root[:]),
hash: root,
},
})
}
}
// Assemble and return the pruner
return &pruner{
db: db,
tries: traversers,
batch: db.diskdb.NewBatch(),
// Iterate over all the nodes marked for pruning and delete them
for _, mark := range p.marks {
p.prune(mark.owner, mark.hash, mark.path)
}
}
// flush commits any pending database writes.
// flush commits any pending database writes. It does not reset the batch since
// we only ever supposed to commit once per prune run.
func (p *pruner) flush() error {
if err := p.batch.Write(); err != nil {
return err
}
p.batch.Reset()
return nil
return p.batch.Write()
}
// prune deletes a trie node from disk if there are no more live references to
@ -186,7 +206,9 @@ func (t *traverser) live(owner common.Hash, hash common.Hash, path []byte, unref
} else {
blob, err := t.db.diskdb.Get([]byte(key))
if blob == nil || err != nil {
panic(fmt.Sprintf("missing referenced node %x (searching for %x:%x at %x%x)", key, owner, t.state.hash, t.state.prefix, path))
log.Error("Missing referenced node", "owner", owner, "hash", t.state.hash, "path", fmt.Sprintf("%x%x", t.state.prefix, path))
return false
//panic(fmt.Sprintf("missing referenced node %x (searching for %x:%x at %x%x)", key, owner, t.state.hash, t.state.prefix, path))
}
t.state.node = mustDecodeNode(t.state.hash[:], blob, 0)
}