eth/fetcher: lazy-allocate hashes slice in scheduleFetches

scheduleFetches.func1 is the single biggest allocator in the Pyroscope
profile of a busy node (~13.5 GB/hr, 8% of total alloc_space). Each
peer-iteration pre-allocated 'make([]common.Hash, 0, maxTxRetrievals)'
= 8 KB, even for peers that end up collecting no new hashes (all their
announces were already being fetched by someone else).

Defer the slice allocation to the first append. Peers that collect zero
hashes now pay zero allocation, which is the common case on the
timeoutTrigger path where all peers with any announces are iterated.

New benchmarks BenchmarkScheduleFetches_{100peers_10new,
100peers_allFetching, 500peers_3new} (benchstat, 6 samples):

  scenario            ns/op       B/op        allocs/op
  100p/10new          unchanged   unchanged   unchanged   (fast path)
  100p/allFetching   -62%        -92%        -20%
  500p/3new          -22%        -44%         -7%
  geomean            -33%        -65%         -9%
This commit is contained in:
Sina Mahmoodi 2026-04-22 17:55:43 +00:00
parent 8e2107dc39
commit 2ca74d2ef9

View file

@ -991,8 +991,10 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{},
if len(f.announces[peer]) == 0 {
return // continue in the for-each
}
// hashes is allocated lazily: peers that collect no new hashes
// (all announces already being fetched) skip the 8KB allocation.
var (
hashes = make([]common.Hash, 0, maxTxRetrievals)
hashes []common.Hash
bytes uint64
)
f.forEachAnnounce(f.announces[peer], func(hash common.Hash, meta txMetadata) bool {
@ -1009,6 +1011,9 @@ func (f *TxFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{},
f.alternates[hash] = f.announced[hash]
delete(f.announced, hash)
if hashes == nil {
hashes = make([]common.Hash, 0, maxTxRetrievals)
}
// Accumulate the hash and stop if the limit was reached
hashes = append(hashes, hash)
if len(hashes) >= maxTxRetrievals {