diff --git a/trie/database.go b/trie/database.go index 0186e98f98..effd7708f6 100644 --- a/trie/database.go +++ b/trie/database.go @@ -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))) - 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) + // 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)) + }() 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)) diff --git a/trie/database_pruning.go b/trie/database_pruning.go index ebea4a033f..eb3d42c722 100644 --- a/trie/database_pruning.go +++ b/trie/database_pruning.go @@ -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 - batch ethdb.Batch // Write batch to minimize database trashing + + 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{ - db: db, - state: &traverserState{ - node: hashNode(root[:]), - hash: root, - }, - }) - } - } - // Assemble and return the pruner +// referenced in-memory nodes. +func (db *Database) newPruner() *pruner { return &pruner{ db: db, - tries: traversers, batch: db.diskdb.NewBatch(), } } -// flush commits any pending database writes. -func (p *pruner) flush() error { - if err := p.batch.Write(); err != nil { - return err +// 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, + }, + }) } - p.batch.Reset() - return nil + // 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. It does not reset the batch since +// we only ever supposed to commit once per prune run. +func (p *pruner) flush() error { + 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) }