Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c692bba
execution/cache, execution/commitment, db/state: consolidate cache st…
mh0lt Jul 1, 2026
b5c5261
execution, db: trim comments to project comment policy
mh0lt Jul 2, 2026
fcfc99b
db/state, execution: fix CodeStore reorg/unwind wrong-root
mh0lt Jul 2, 2026
0950d78
execution/cache, execution/commitment: address #22154 review
mh0lt Jul 2, 2026
77c7800
execution: trim over-long comments to the load-bearing why
mh0lt Jul 2, 2026
c742ee7
Merge origin/main into mh/trunk-pin-on-cachestack
mh0lt Jul 3, 2026
7d467d0
execution/commitment, db/state: lazy trunk d4 + skip BranchCache for …
mh0lt Jul 3, 2026
8814491
execution/cache: fix CodeCache size-drift under concurrent same-key Puts
mh0lt Jul 3, 2026
681de6b
execution/commitment, db/state: byte-budget the BranchCache LRU tail
mh0lt Jul 3, 2026
94d8095
execution/commitment: demand-allocate BranchCache tiers to cut alloc …
mh0lt Jul 3, 2026
ee9e2db
execution/cache, execution/commitment, common/cachebudget, db/state: …
mh0lt Jul 3, 2026
2ca5333
execution/cache: jump-grow the CodeCache content layers
mh0lt Jul 3, 2026
fe86259
execution/commitment: trim BranchCache doc to comment policy
mh0lt Jul 6, 2026
8d6a1d7
execution/cache: make jump-grow curCap atomic to fix data race
mh0lt Jul 6, 2026
b5563e3
Merge origin/main (incl. #21536 typed-vio) into trunk-pin cachestack
mh0lt Jul 6, 2026
6266fac
execution/cache, execution/commitment: fix tailLRU curCap race + Code…
mh0lt Jul 7, 2026
f1d4cfe
db/state, execution/commitment: hoist adaptive pin controller to aggr…
mh0lt Jul 7, 2026
4e8c56a
execution/cache, execution/commitment: review cleanups (sizing, pin m…
mh0lt Jul 7, 2026
615c83e
execution/commitment, execution/cache: restore deleted tests, fix hea…
mh0lt Jul 7, 2026
9a56051
execution/cache, execution/commitment, db/state: clear post-approval …
mh0lt Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions common/cachebudget/budget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

// Package cachebudget bounds the total resident memory of the process-wide
// application caches (state, code) to a fraction of the memory actually
// available — system RAM, cgroup limit, or GOMEMLIMIT, whichever is lowest.
//
// Caches do not pre-commit their full configured size. They start small and
// grow in steps, reserving each step's bytes from one shared envelope; a step
// that would overflow the envelope is refused, so the cache stops growing and
// evicts within its current size instead. A cache with a small working set (a
// test fixture) therefore stays small regardless of its configured budget,
// while a busy production cache grows into it — and the sum across every cache
// instance in the process stays within the envelope. Release returns a cache's
// reserved bytes when it is torn down or cleared. No cache is ever disabled and
// there is no test-specific sizing.
package cachebudget

import (
"sync/atomic"

"github.com/erigontech/erigon/common/estimate"
)

// Divisor sets the single shared envelope — covering every application cache
// (state, code, and the commitment-branch LRU tail) — to this fraction of total
// available memory. It is the one knob governing aggregate cache residency:
// larger (e.g. 16) buys hit-rate on big nodes; 32 keeps a constrained 16GB CI
// runner well within bounds even under the race detector's memory multiplier.
const Divisor = 32

// Budget is a shared byte allowance drawn down by Reserve and returned by
// Release. Safe for concurrent use.
type Budget struct {
limit int64
used atomic.Int64
}

func New(limit int64) *Budget { return &Budget{limit: limit} }

// Global is the process-wide application-cache envelope.
var Global = New(int64(estimate.TotalMemory() / Divisor))

// Reserve takes exactly n bytes if the envelope has room, returning true; it
// takes nothing and returns false when full. A grow step calls this and stops
// growing on false.
func (b *Budget) Reserve(n int64) bool {
if n <= 0 {
return true
}
for {
used := b.used.Load()
if used+n > b.limit {
return false
}
if b.used.CompareAndSwap(used, used+n) {
return true
}
}
}

// Take reserves n bytes unconditionally (may push used past limit). Used for a
// cache's initial small allocation, which must always succeed so no cache is
// born disabled.
func (b *Budget) Take(n int64) {
if n > 0 {
b.used.Add(n)
}
}

// Release returns n bytes to the envelope.
func (b *Budget) Release(n int64) {
if n > 0 {
b.used.Add(-n)
}
}

func (b *Budget) Limit() int64 { return b.limit }
func (b *Budget) Used() int64 { return b.used.Load() }
64 changes: 64 additions & 0 deletions common/cachebudget/budget_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package cachebudget

import "testing"

func TestReserveStopsAtLimit(t *testing.T) {
b := New(1000)
if !b.Reserve(600) {
t.Fatal("first Reserve(600) should fit")
}
if !b.Reserve(400) {
t.Fatal("second Reserve(400) should exactly fill")
}
if b.Reserve(1) {
t.Fatal("Reserve past the limit must fail and take nothing")
}
if b.Used() != 1000 {
t.Fatalf("used: got %d want 1000", b.Used())
}
}

func TestReleaseReopensRoom(t *testing.T) {
b := New(1000)
b.Reserve(1000)
b.Release(400)
if !b.Reserve(400) {
t.Fatal("after Release(400) a Reserve(400) should fit")
}
if b.Reserve(1) {
t.Fatal("still full after regrow")
}
}

func TestTakeIsUnconditional(t *testing.T) {
b := New(100)
b.Take(500) // initial small allocation always succeeds even past limit
if b.Used() != 500 {
t.Fatalf("used: got %d want 500", b.Used())
}
if b.Reserve(1) {
t.Fatal("over-committed envelope refuses further Reserve")
}
}

func TestGlobalSizedFromMemory(t *testing.T) {
if Global.Limit() <= 0 {
t.Fatalf("Global envelope must be positive, got %d", Global.Limit())
}
}
2 changes: 2 additions & 0 deletions common/dbg/experiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ var (
CaplinEfficientReorg = EnvBool("CAPLIN_EFFICIENT_REORG", true)
UseTxDependencies = EnvBool("USE_TX_DEPENDENCIES", false)
UseStateCache = EnvBool("USE_STATE_CACHE", true)
UseCodeStore = EnvBool("USE_CODE_STORE", true)
DisableAdaptivePin = EnvBool("DISABLE_ADAPTIVE_PIN", false)
AssertStateCache = EnvBool("ASSERT_STATE_CACHE", false)
ReadAhead = EnvBool("READ_AHEAD", true)

Expand Down
6 changes: 6 additions & 0 deletions db/kv/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ const (
TblCodeHistoryVals = "CodeHistoryVals"
TblCodeIdx = "CodeIdx"

// TblCodeCache holds decompressed contract code keyed by keccak(code), the
// persistent backing tier for the in-memory code cache so reads skip the
// CodeDomain decompression across restarts. Immutable (content-addressed).
TblCodeCache = "CodeCache"

TblCommitmentVals = "CommitmentVals"
TblCommitmentHistoryKeys = "CommitmentHistoryKeys"
TblCommitmentHistoryVals = "CommitmentHistoryVals"
Expand Down Expand Up @@ -363,6 +368,7 @@ var ChaindataTables = []string{
TblCodeHistoryKeys,
TblCodeHistoryVals,
TblCodeIdx,
TblCodeCache,

TblCommitmentVals,
TblCommitmentHistoryKeys,
Expand Down
29 changes: 24 additions & 5 deletions db/state/aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ type Aggregator struct {
oldestVisible *aggregatorVisible
snapshotBuildSema *semaphore.Weighted

disableHistory bool
workers workersCfg
disableHistory bool
branchCacheDisabled bool
workers workersCfg

// To keep DB small - need move data to small files ASAP.
// It means goroutine which creating small files - can't be locked by merge or indexing.
Expand Down Expand Up @@ -411,10 +412,16 @@ func (a *Aggregator) ConfigureDomains() error {
}
a.configured = true

// Attach the aggregator-lifetime BranchCache to the commitment domain; gated by USE_STATE_CACHE, nil = disabled.
if dbg.UseStateCache {
// Attach the aggregator-lifetime BranchCache to the commitment domain; gated
// by USE_STATE_CACHE, nil = disabled. Skipped for ephemeral aggregators that
// opt out (e.g. one-shot genesis processing has no cross-block reuse).
if dbg.UseStateCache && !a.branchCacheDisabled {
if cd := a.d[kv.CommitmentDomain]; cd != nil && cd.branchCache == nil {
cd.branchCache = commitment.NewBranchCache(commitment.DefaultBranchCacheTailCapacity)
if !dbg.DisableAdaptivePin {
cd.adaptivePinController = commitment.NewAdaptivePinController(
cd.branchCache, commitment.DefaultAdaptivePinControllerConfig(), a.logger)
}
}
}

Expand Down Expand Up @@ -623,9 +630,12 @@ func (a *Aggregator) Close() {
}
a.wg.Wait()

// A closed Aggregator may linger referenced; release the cached branch data eagerly.
// A closed Aggregator may linger referenced; release the cached branch data
// eagerly and drop this cache from the active-instance count so later
// BranchCaches size their trunk depth against real concurrency.
if cd := a.d[kv.CommitmentDomain]; cd != nil && cd.branchCache != nil {
cd.branchCache.Clear()
cd.branchCache.Close()
}

a.dirtyFilesLock.Lock()
Expand Down Expand Up @@ -2477,6 +2487,15 @@ func (at *AggregatorRoTx) BranchCache() *commitment.BranchCache {
return at.d[kv.CommitmentDomain].d.branchCache
}

// AdaptivePinController attached to the commitment domain (implements
// commitment.AdaptivePinControllerProvider).
func (at *AggregatorRoTx) AdaptivePinController() *commitment.AdaptivePinController {
if at.d[kv.CommitmentDomain] == nil {
return nil
}
return at.d[kv.CommitmentDomain].d.adaptivePinController
}

// MetricsCollector exposes the aggregator-scope KV-read metrics collector,
// fetched by SharedDomains through the duck-typed kvmetrics.MetricsCollectorProvider
// (same pattern as BranchCache), so every read path folds into one process-level
Expand Down
14 changes: 10 additions & 4 deletions db/state/aggregator2.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ type AggOpts struct { //nolint:gocritic

referencesInCommitmentBranches *bool // nil = leave global schema default untouched

genSaltIfNeed bool
sanityOldNaming bool // prevent start directory with old file names
disableFsync bool // for tests speed
disableHistory bool // for temp/inmem aggregator instances
genSaltIfNeed bool
sanityOldNaming bool // prevent start directory with old file names
disableFsync bool // for tests speed
disableHistory bool // for temp/inmem aggregator instances
disableBranchCache bool // for one-shot aggregators with no cross-block reuse (e.g. genesis)
}

func New(dirs datadir.Dirs) AggOpts { //nolint:gocritic
Expand Down Expand Up @@ -74,6 +75,7 @@ func (opts AggOpts) Open(ctx context.Context, db kv.RoDB) (*Aggregator, error) {
a.erigondbDomainStepsInFrozenFile = opts.erigondbDomainStepsInFrozenFile

a.disableHistory = opts.disableHistory
a.branchCacheDisabled = opts.disableBranchCache
a.disableFsync = opts.disableFsync

a.savedSalt = salt
Expand Down Expand Up @@ -121,6 +123,10 @@ func (opts AggOpts) GenSaltIfNeed(v bool) AggOpts { opts.genSaltIfNeed = v; retu
func (opts AggOpts) Logger(l log.Logger) AggOpts { opts.logger = l; return opts } //nolint:gocritic
func (opts AggOpts) DisableFsync() AggOpts { opts.disableFsync = true; return opts } //nolint:gocritic
func (opts AggOpts) DisableHistory() AggOpts { opts.disableHistory = true; return opts } //nolint:gocritic
func (opts AggOpts) DisableBranchCache() AggOpts { //nolint:gocritic
opts.disableBranchCache = true
return opts
}
func (opts AggOpts) SanityOldNaming() AggOpts { //nolint:gocritic
opts.sanityOldNaming = true
return opts
Expand Down
9 changes: 9 additions & 0 deletions db/state/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ type Domain struct {

// Long-lived commitment-branch cache; non-nil only on the commitment domain.
branchCache *commitment.BranchCache
// Adaptive pin controller, co-located with branchCache so pin residency ages
// by block-access recency across all SharedDomains rather than per-SD.
adaptivePinController *commitment.AdaptivePinController

// _testBuildAccessorHook - test-only: called with the recsplit before the build loop in buildHashMapAccessor
_testBuildAccessorHook func(rs *recsplit.RecSplit)
Expand Down Expand Up @@ -147,6 +150,12 @@ func (d *Domain) BranchCache() *commitment.BranchCache {
return d.branchCache
}

// AdaptivePinController returns the aggregator-lifetime pin controller
// co-located with BranchCache. Non-nil only on the commitment domain.
func (d *Domain) AdaptivePinController() *commitment.AdaptivePinController {
return d.adaptivePinController
}

// kvWriteVersion is the version stamped on a new .kv file: the domain's KVWriteVersion hook if set, else DataKV.Current.
func (d *Domain) kvWriteVersion() version.Version {
if d.KVWriteVersion != nil {
Expand Down
Loading
Loading