Skip to content

Commit 18a5918

Browse files
core: reduce peak memory usage during reorg (#30600)
~~Opening this as a draft to have a discussion.~~ Pressed the wrong button I had [a previous PR ](#24616 long time ago which reduced the peak memory used during reorgs by not accumulating all transactions and logs. This PR reduces the peak memory further by not storing the blocks in memory. However this means we need to pull the blocks back up from storage multiple times during the reorg. I collected the following numbers on peak memory usage: // Master: BenchmarkReorg-8 10000 899591 ns/op 820154 B/op 1440 allocs/op 1549443072 bytes of heap used // WithoutOldChain: BenchmarkReorg-8 10000 1147281 ns/op 943163 B/op 1564 allocs/op 1163870208 bytes of heap used // WithoutNewChain: BenchmarkReorg-8 10000 1018922 ns/op 943580 B/op 1564 allocs/op 1171890176 bytes of heap used Each block contains a transaction with ~50k bytes and we're doing a 10k block reorg, so the chain should be ~500MB in size --------- Co-authored-by: Péter Szilágyi <[email protected]>
1 parent 368e16f commit 18a5918

File tree

2 files changed

+139
-97
lines changed

2 files changed

+139
-97
lines changed

core/blockchain.go

Lines changed: 106 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io"
2424
"math/big"
2525
"runtime"
26+
"slices"
2627
"strings"
2728
"sync"
2829
"sync/atomic"
@@ -1435,7 +1436,7 @@ func (bc *BlockChain) writeBlockWithoutState(block *types.Block, td *big.Int) (e
14351436
func (bc *BlockChain) writeKnownBlock(block *types.Block) error {
14361437
current := bc.CurrentBlock()
14371438
if block.ParentHash() != current.Hash() {
1438-
if err := bc.reorg(current, block); err != nil {
1439+
if err := bc.reorg(current, block.Header()); err != nil {
14391440
return err
14401441
}
14411442
}
@@ -1541,7 +1542,7 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types
15411542

15421543
// Reorganise the chain if the parent is not the head block
15431544
if block.ParentHash() != currentBlock.Hash() {
1544-
if err := bc.reorg(currentBlock, block); err != nil {
1545+
if err := bc.reorg(currentBlock, block.Header()); err != nil {
15451546
return NonStatTy, err
15461547
}
15471548
}
@@ -2154,8 +2155,8 @@ func (bc *BlockChain) recoverAncestors(block *types.Block, makeWitness bool) (co
21542155
return block.Hash(), nil
21552156
}
21562157

2157-
// collectLogs collects the logs that were generated or removed during
2158-
// the processing of a block. These logs are later announced as deleted or reborn.
2158+
// collectLogs collects the logs that were generated or removed during the
2159+
// processing of a block. These logs are later announced as deleted or reborn.
21592160
func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log {
21602161
var blobGasPrice *big.Int
21612162
excessBlobGas := b.ExcessBlobGas()
@@ -2181,70 +2182,55 @@ func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log {
21812182
// reorg takes two blocks, an old chain and a new chain and will reconstruct the
21822183
// blocks and inserts them to be part of the new canonical chain and accumulates
21832184
// potential missing transactions and post an event about them.
2185+
//
21842186
// Note the new head block won't be processed here, callers need to handle it
21852187
// externally.
2186-
func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error {
2188+
func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error {
21872189
var (
2188-
newChain types.Blocks
2189-
oldChain types.Blocks
2190-
commonBlock *types.Block
2191-
2192-
deletedTxs []common.Hash
2193-
addedTxs []common.Hash
2190+
newChain []*types.Header
2191+
oldChain []*types.Header
2192+
commonBlock *types.Header
21942193
)
2195-
oldBlock := bc.GetBlock(oldHead.Hash(), oldHead.Number.Uint64())
2196-
if oldBlock == nil {
2197-
return errors.New("current head block missing")
2198-
}
2199-
newBlock := newHead
2200-
22012194
// Reduce the longer chain to the same number as the shorter one
2202-
if oldBlock.NumberU64() > newBlock.NumberU64() {
2195+
if oldHead.Number.Uint64() > newHead.Number.Uint64() {
22032196
// Old chain is longer, gather all transactions and logs as deleted ones
2204-
for ; oldBlock != nil && oldBlock.NumberU64() != newBlock.NumberU64(); oldBlock = bc.GetBlock(oldBlock.ParentHash(), oldBlock.NumberU64()-1) {
2205-
oldChain = append(oldChain, oldBlock)
2206-
for _, tx := range oldBlock.Transactions() {
2207-
deletedTxs = append(deletedTxs, tx.Hash())
2208-
}
2197+
for ; oldHead != nil && oldHead.Number.Uint64() != newHead.Number.Uint64(); oldHead = bc.GetHeader(oldHead.ParentHash, oldHead.Number.Uint64()-1) {
2198+
oldChain = append(oldChain, oldHead)
22092199
}
22102200
} else {
22112201
// New chain is longer, stash all blocks away for subsequent insertion
2212-
for ; newBlock != nil && newBlock.NumberU64() != oldBlock.NumberU64(); newBlock = bc.GetBlock(newBlock.ParentHash(), newBlock.NumberU64()-1) {
2213-
newChain = append(newChain, newBlock)
2202+
for ; newHead != nil && newHead.Number.Uint64() != oldHead.Number.Uint64(); newHead = bc.GetHeader(newHead.ParentHash, newHead.Number.Uint64()-1) {
2203+
newChain = append(newChain, newHead)
22142204
}
22152205
}
2216-
if oldBlock == nil {
2206+
if oldHead == nil {
22172207
return errInvalidOldChain
22182208
}
2219-
if newBlock == nil {
2209+
if newHead == nil {
22202210
return errInvalidNewChain
22212211
}
22222212
// Both sides of the reorg are at the same number, reduce both until the common
22232213
// ancestor is found
22242214
for {
22252215
// If the common ancestor was found, bail out
2226-
if oldBlock.Hash() == newBlock.Hash() {
2227-
commonBlock = oldBlock
2216+
if oldHead.Hash() == newHead.Hash() {
2217+
commonBlock = oldHead
22282218
break
22292219
}
22302220
// Remove an old block as well as stash away a new block
2231-
oldChain = append(oldChain, oldBlock)
2232-
for _, tx := range oldBlock.Transactions() {
2233-
deletedTxs = append(deletedTxs, tx.Hash())
2234-
}
2235-
newChain = append(newChain, newBlock)
2221+
oldChain = append(oldChain, oldHead)
2222+
newChain = append(newChain, newHead)
22362223

22372224
// Step back with both chains
2238-
oldBlock = bc.GetBlock(oldBlock.ParentHash(), oldBlock.NumberU64()-1)
2239-
if oldBlock == nil {
2225+
oldHead = bc.GetHeader(oldHead.ParentHash, oldHead.Number.Uint64()-1)
2226+
if oldHead == nil {
22402227
return errInvalidOldChain
22412228
}
2242-
newBlock = bc.GetBlock(newBlock.ParentHash(), newBlock.NumberU64()-1)
2243-
if newBlock == nil {
2229+
newHead = bc.GetHeader(newHead.ParentHash, newHead.Number.Uint64()-1)
2230+
if newHead == nil {
22442231
return errInvalidNewChain
22452232
}
22462233
}
2247-
22482234
// Ensure the user sees large reorgs
22492235
if len(oldChain) > 0 && len(newChain) > 0 {
22502236
logFn := log.Info
@@ -2253,63 +2239,120 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error {
22532239
msg = "Large chain reorg detected"
22542240
logFn = log.Warn
22552241
}
2256-
logFn(msg, "number", commonBlock.Number(), "hash", commonBlock.Hash(),
2242+
logFn(msg, "number", commonBlock.Number, "hash", commonBlock.Hash(),
22572243
"drop", len(oldChain), "dropfrom", oldChain[0].Hash(), "add", len(newChain), "addfrom", newChain[0].Hash())
22582244
blockReorgAddMeter.Mark(int64(len(newChain)))
22592245
blockReorgDropMeter.Mark(int64(len(oldChain)))
22602246
blockReorgMeter.Mark(1)
22612247
} else if len(newChain) > 0 {
22622248
// Special case happens in the post merge stage that current head is
22632249
// the ancestor of new head while these two blocks are not consecutive
2264-
log.Info("Extend chain", "add", len(newChain), "number", newChain[0].Number(), "hash", newChain[0].Hash())
2250+
log.Info("Extend chain", "add", len(newChain), "number", newChain[0].Number, "hash", newChain[0].Hash())
22652251
blockReorgAddMeter.Mark(int64(len(newChain)))
22662252
} else {
22672253
// len(newChain) == 0 && len(oldChain) > 0
22682254
// rewind the canonical chain to a lower point.
2269-
log.Error("Impossible reorg, please file an issue", "oldnum", oldBlock.Number(), "oldhash", oldBlock.Hash(), "oldblocks", len(oldChain), "newnum", newBlock.Number(), "newhash", newBlock.Hash(), "newblocks", len(newChain))
2255+
log.Error("Impossible reorg, please file an issue", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "oldblocks", len(oldChain), "newnum", newHead.Number, "newhash", newHead.Hash(), "newblocks", len(newChain))
22702256
}
22712257
// Acquire the tx-lookup lock before mutation. This step is essential
22722258
// as the txlookups should be changed atomically, and all subsequent
22732259
// reads should be blocked until the mutation is complete.
22742260
bc.txLookupLock.Lock()
22752261

2276-
// Insert the new chain segment in incremental order, from the old
2277-
// to the new. The new chain head (newChain[0]) is not inserted here,
2278-
// as it will be handled separately outside of this function
2279-
for i := len(newChain) - 1; i >= 1; i-- {
2280-
// Insert the block in the canonical way, re-writing history
2281-
bc.writeHeadBlock(newChain[i])
2262+
// Reorg can be executed, start reducing the chain's old blocks and appending
2263+
// the new blocks
2264+
var (
2265+
deletedTxs []common.Hash
2266+
rebirthTxs []common.Hash
22822267

2283-
// Collect the new added transactions.
2284-
for _, tx := range newChain[i].Transactions() {
2285-
addedTxs = append(addedTxs, tx.Hash())
2268+
deletedLogs []*types.Log
2269+
rebirthLogs []*types.Log
2270+
)
2271+
// Deleted log emission on the API uses forward order, which is borked, but
2272+
// we'll leave it in for legacy reasons.
2273+
//
2274+
// TODO(karalabe): This should be nuked out, no idea how, deprecate some APIs?
2275+
{
2276+
for i := len(oldChain) - 1; i >= 0; i-- {
2277+
block := bc.GetBlock(oldChain[i].Hash(), oldChain[i].Number.Uint64())
2278+
if block == nil {
2279+
return errInvalidOldChain // Corrupt database, mostly here to avoid weird panics
2280+
}
2281+
if logs := bc.collectLogs(block, true); len(logs) > 0 {
2282+
deletedLogs = append(deletedLogs, logs...)
2283+
}
2284+
if len(deletedLogs) > 512 {
2285+
bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
2286+
deletedLogs = nil
2287+
}
2288+
}
2289+
if len(deletedLogs) > 0 {
2290+
bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
22862291
}
22872292
}
2293+
// Undo old blocks in reverse order
2294+
for i := 0; i < len(oldChain); i++ {
2295+
// Collect all the deleted transactions
2296+
block := bc.GetBlock(oldChain[i].Hash(), oldChain[i].Number.Uint64())
2297+
if block == nil {
2298+
return errInvalidOldChain // Corrupt database, mostly here to avoid weird panics
2299+
}
2300+
for _, tx := range block.Transactions() {
2301+
deletedTxs = append(deletedTxs, tx.Hash())
2302+
}
2303+
// Collect deleted logs and emit them for new integrations
2304+
if logs := bc.collectLogs(block, true); len(logs) > 0 {
2305+
// Emit revertals latest first, older then
2306+
slices.Reverse(logs)
22882307

2308+
// TODO(karalabe): Hook into the reverse emission part
2309+
}
2310+
}
2311+
// Apply new blocks in forward order
2312+
for i := len(newChain) - 1; i >= 1; i-- {
2313+
// Collect all the included transactions
2314+
block := bc.GetBlock(newChain[i].Hash(), newChain[i].Number.Uint64())
2315+
if block == nil {
2316+
return errInvalidNewChain // Corrupt database, mostly here to avoid weird panics
2317+
}
2318+
for _, tx := range block.Transactions() {
2319+
rebirthTxs = append(rebirthTxs, tx.Hash())
2320+
}
2321+
// Collect inserted logs and emit them
2322+
if logs := bc.collectLogs(block, false); len(logs) > 0 {
2323+
rebirthLogs = append(rebirthLogs, logs...)
2324+
}
2325+
if len(rebirthLogs) > 512 {
2326+
bc.logsFeed.Send(rebirthLogs)
2327+
rebirthLogs = nil
2328+
}
2329+
// Update the head block
2330+
bc.writeHeadBlock(block)
2331+
}
2332+
if len(rebirthLogs) > 0 {
2333+
bc.logsFeed.Send(rebirthLogs)
2334+
}
22892335
// Delete useless indexes right now which includes the non-canonical
22902336
// transaction indexes, canonical chain indexes which above the head.
2291-
var (
2292-
indexesBatch = bc.db.NewBatch()
2293-
diffs = types.HashDifference(deletedTxs, addedTxs)
2294-
)
2295-
for _, tx := range diffs {
2296-
rawdb.DeleteTxLookupEntry(indexesBatch, tx)
2337+
batch := bc.db.NewBatch()
2338+
for _, tx := range types.HashDifference(deletedTxs, rebirthTxs) {
2339+
rawdb.DeleteTxLookupEntry(batch, tx)
22972340
}
22982341
// Delete all hash markers that are not part of the new canonical chain.
22992342
// Because the reorg function does not handle new chain head, all hash
23002343
// markers greater than or equal to new chain head should be deleted.
2301-
number := commonBlock.NumberU64()
2344+
number := commonBlock.Number
23022345
if len(newChain) > 1 {
2303-
number = newChain[1].NumberU64()
2346+
number = newChain[1].Number
23042347
}
2305-
for i := number + 1; ; i++ {
2348+
for i := number.Uint64() + 1; ; i++ {
23062349
hash := rawdb.ReadCanonicalHash(bc.db, i)
23072350
if hash == (common.Hash{}) {
23082351
break
23092352
}
2310-
rawdb.DeleteCanonicalHash(indexesBatch, i)
2353+
rawdb.DeleteCanonicalHash(batch, i)
23112354
}
2312-
if err := indexesBatch.Write(); err != nil {
2355+
if err := batch.Write(); err != nil {
23132356
log.Crit("Failed to delete useless indexes", "err", err)
23142357
}
23152358
// Reset the tx lookup cache to clear stale txlookup cache.
@@ -2318,40 +2361,6 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error {
23182361
// Release the tx-lookup lock after mutation.
23192362
bc.txLookupLock.Unlock()
23202363

2321-
// Send out events for logs from the old canon chain, and 'reborn'
2322-
// logs from the new canon chain. The number of logs can be very
2323-
// high, so the events are sent in batches of size around 512.
2324-
2325-
// Deleted logs + blocks:
2326-
var deletedLogs []*types.Log
2327-
for i := len(oldChain) - 1; i >= 0; i-- {
2328-
// Collect deleted logs for notification
2329-
if logs := bc.collectLogs(oldChain[i], true); len(logs) > 0 {
2330-
deletedLogs = append(deletedLogs, logs...)
2331-
}
2332-
if len(deletedLogs) > 512 {
2333-
bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
2334-
deletedLogs = nil
2335-
}
2336-
}
2337-
if len(deletedLogs) > 0 {
2338-
bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
2339-
}
2340-
2341-
// New logs:
2342-
var rebirthLogs []*types.Log
2343-
for i := len(newChain) - 1; i >= 1; i-- {
2344-
if logs := bc.collectLogs(newChain[i], false); len(logs) > 0 {
2345-
rebirthLogs = append(rebirthLogs, logs...)
2346-
}
2347-
if len(rebirthLogs) > 512 {
2348-
bc.logsFeed.Send(rebirthLogs)
2349-
rebirthLogs = nil
2350-
}
2351-
}
2352-
if len(rebirthLogs) > 0 {
2353-
bc.logsFeed.Send(rebirthLogs)
2354-
}
23552364
return nil
23562365
}
23572366

@@ -2389,7 +2398,7 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) {
23892398
// Run the reorg if necessary and set the given block as new head.
23902399
start := time.Now()
23912400
if head.ParentHash() != bc.CurrentBlock().Hash() {
2392-
if err := bc.reorg(bc.CurrentBlock(), head); err != nil {
2401+
if err := bc.reorg(bc.CurrentBlock(), head.Header()); err != nil {
23932402
return common.Hash{}, err
23942403
}
23952404
}

core/blockchain_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4231,3 +4231,36 @@ func TestPragueRequests(t *testing.T) {
42314231
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
42324232
}
42334233
}
4234+
4235+
func BenchmarkReorg(b *testing.B) {
4236+
chainLength := b.N
4237+
4238+
dir := b.TempDir()
4239+
db, err := rawdb.NewLevelDBDatabase(dir, 128, 128, "", false)
4240+
if err != nil {
4241+
b.Fatalf("cannot create temporary database: %v", err)
4242+
}
4243+
defer db.Close()
4244+
gspec := &Genesis{
4245+
Config: params.TestChainConfig,
4246+
Alloc: types.GenesisAlloc{benchRootAddr: {Balance: math.BigPow(2, 254)}},
4247+
}
4248+
blockchain, _ := NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil)
4249+
defer blockchain.Stop()
4250+
4251+
// Insert an easy and a difficult chain afterwards
4252+
easyBlocks, _ := GenerateChain(params.TestChainConfig, blockchain.GetBlockByHash(blockchain.CurrentBlock().Hash()), ethash.NewFaker(), db, chainLength, genValueTx(50000))
4253+
diffBlocks, _ := GenerateChain(params.TestChainConfig, blockchain.GetBlockByHash(blockchain.CurrentBlock().Hash()), ethash.NewFaker(), db, chainLength, genValueTx(50000))
4254+
4255+
if _, err := blockchain.InsertChain(easyBlocks); err != nil {
4256+
b.Fatalf("failed to insert easy chain: %v", err)
4257+
}
4258+
b.ResetTimer()
4259+
if _, err := blockchain.InsertChain(diffBlocks); err != nil {
4260+
b.Fatalf("failed to insert difficult chain: %v", err)
4261+
}
4262+
}
4263+
4264+
// Master: BenchmarkReorg-8 10000 899591 ns/op 820154 B/op 1440 allocs/op 1549443072 bytes of heap used
4265+
// WithoutOldChain: BenchmarkReorg-8 10000 1147281 ns/op 943163 B/op 1564 allocs/op 1163870208 bytes of heap used
4266+
// WithoutNewChain: BenchmarkReorg-8 10000 1018922 ns/op 943580 B/op 1564 allocs/op 1171890176 bytes of heap used

0 commit comments

Comments
 (0)