Skip to content

Commit b14ecfa

Browse files
added rate limiting to address biased trie cache preloading (#2029)
* added rate limiting to address biased trie cache preloading * updated default limit to 512KB/sec and a small update in limiter * added unit tests * updated default to 1MB/s and updated comments * reduced code duplication * triedb/pathdb: skip the node in preloading if it's size is > burst size
1 parent f5ee550 commit b14ecfa

File tree

10 files changed

+586
-22
lines changed

10 files changed

+586
-22
lines changed

core/blockchain.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ type BlockChainConfig struct {
198198
// Maps account address to cache size in bytes
199199
AddressCacheSizes map[common.Address]int
200200

201+
// PreloadRateLimit limits cache preload I/O in bytes per second per address.
202+
// This prevents preloading from overwhelming the disk during sync.
203+
// 0 = unlimited (legacy behavior), default = 1MB/s
204+
PreloadRateLimit int64
205+
201206
// State snapshot related options
202207
SnapshotLimit int // Memory allowance (MB) to use for caching snapshot entries in memory
203208
SnapshotNoBuild bool // Whether the background generation is allowed
@@ -302,6 +307,7 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
302307
WriteBufferSize: cfg.TrieDirtyLimit * 1024 * 1024,
303308
NoAsyncFlush: cfg.TrieNoAsyncFlush,
304309
AddressCacheSizes: cfg.AddressCacheSizes,
310+
PreloadRateLimit: cfg.PreloadRateLimit,
305311
}
306312
}
307313
return config

eth/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
278278
ChainHistoryMode: config.HistoryMode,
279279
TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)),
280280
AddressCacheSizes: config.AddressCacheSizes,
281+
PreloadRateLimit: config.PreloadRateLimit,
281282
VmConfig: vm.Config{
282283
EnablePreimageRecording: config.EnablePreimageRecording,
283284
EnableWitnessStats: config.EnableWitnessStats,

eth/ethconfig/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ type Config struct {
162162
// Maps account address to cache size in bytes
163163
AddressCacheSizes map[common.Address]int
164164

165+
// PreloadRateLimit limits cache preload I/O in bytes per second per address.
166+
// This prevents preloading from overwhelming the disk during sync.
167+
// 0 = unlimited (legacy behavior), default = 1MB/s
168+
PreloadRateLimit int64
169+
165170
// Mining options
166171
Miner miner.Config
167172

internal/cli/server/config.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,11 @@ type CacheConfig struct {
679679
AddressCacheSizesRaw string `hcl:"addresscachesizes,optional" toml:"addresscachesizes,optional"`
680680
AddressCacheSizes map[string]string `hcl:"-,optional" toml:"-"`
681681

682+
// PreloadRateLimit limits cache preload I/O in bytes per second per address.
683+
// This prevents preloading from overwhelming the disk during sync.
684+
// Accepts values like "500KB", "1MB", "0" (for unlimited). Default: 1MB/s
685+
PreloadRateLimit string `hcl:"preloadratelimit,optional" toml:"preloadratelimit,optional"`
686+
682687
// GC settings
683688
// GoMemLimit sets the soft memory limit for the runtime
684689
GoMemLimit string `hcl:"gomemlimit,optional" toml:"gomemlimit,optional"`
@@ -1436,6 +1441,20 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (*
14361441
n.AddressCacheSizes = addressCacheSizes
14371442
}
14381443
}
1444+
1445+
// Parse preload rate limit (default: 1MB/s per address)
1446+
if c.Cache.PreloadRateLimit != "" {
1447+
rateLimitBytes, err := parseByteSize(c.Cache.PreloadRateLimit)
1448+
if err != nil {
1449+
log.Warn("Failed to parse preload rate limit, using default 1MB/s per address", "error", err)
1450+
n.PreloadRateLimit = 1024 * 1024
1451+
} else {
1452+
n.PreloadRateLimit = rateLimitBytes
1453+
}
1454+
} else {
1455+
// Default to 1MB/s per address if not specified
1456+
n.PreloadRateLimit = 1024 * 1024
1457+
}
14391458
}
14401459

14411460
// History
@@ -1598,6 +1617,46 @@ func parseAddressCacheSizes(input string) (map[common.Address]int, error) {
15981617
return result, nil
15991618
}
16001619

1620+
// parseByteSize parses a byte size string like "5MB", "10MB", "1GB", or "0" (for unlimited)
1621+
// Returns the size in bytes. Supported suffixes: B, KB, MB, GB (case insensitive)
1622+
func parseByteSize(input string) (int64, error) {
1623+
input = strings.TrimSpace(input)
1624+
if input == "" || input == "0" {
1625+
return 0, nil
1626+
}
1627+
1628+
input = strings.ToUpper(input)
1629+
1630+
var multiplier int64 = 1
1631+
var numStr string
1632+
1633+
switch {
1634+
case strings.HasSuffix(input, "GB"):
1635+
multiplier = 1024 * 1024 * 1024
1636+
numStr = strings.TrimSuffix(input, "GB")
1637+
case strings.HasSuffix(input, "MB"):
1638+
multiplier = 1024 * 1024
1639+
numStr = strings.TrimSuffix(input, "MB")
1640+
case strings.HasSuffix(input, "KB"):
1641+
multiplier = 1024
1642+
numStr = strings.TrimSuffix(input, "KB")
1643+
case strings.HasSuffix(input, "B"):
1644+
multiplier = 1
1645+
numStr = strings.TrimSuffix(input, "B")
1646+
default:
1647+
// Assume bytes if no suffix
1648+
numStr = input
1649+
}
1650+
1651+
numStr = strings.TrimSpace(numStr)
1652+
num, err := strconv.ParseInt(numStr, 10, 64)
1653+
if err != nil {
1654+
return 0, fmt.Errorf("invalid byte size: %s", input)
1655+
}
1656+
1657+
return num * multiplier, nil
1658+
}
1659+
16011660
var (
16021661
clientIdentifier = "bor"
16031662
gitCommit = "" // Git SHA1 commit hash of the release (set via linker flags)

internal/cli/server/config_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,93 @@ func TestSealerBothGasParametersConfig(t *testing.T) {
292292
})
293293
}
294294

295+
// TestParseByteSize tests the parseByteSize helper function for parsing
296+
// human-readable byte size strings (e.g., "500KB", "5MB", "1GB").
297+
func TestParseByteSize(t *testing.T) {
298+
tests := []struct {
299+
name string
300+
input string
301+
expected int64
302+
wantErr bool
303+
}{
304+
// Valid inputs with different suffixes
305+
{"bytes", "1024B", 1024, false},
306+
{"kilobytes", "500KB", 500 * 1024, false},
307+
{"megabytes", "5MB", 5 * 1024 * 1024, false},
308+
{"gigabytes", "1GB", 1 * 1024 * 1024 * 1024, false},
309+
310+
// Case insensitivity
311+
{"lowercase kb", "500kb", 500 * 1024, false},
312+
{"lowercase mb", "5mb", 5 * 1024 * 1024, false},
313+
{"mixed case", "5Mb", 5 * 1024 * 1024, false},
314+
315+
// No suffix (assumes bytes)
316+
{"no suffix", "1024", 1024, false},
317+
318+
// Zero and empty
319+
{"zero string", "0", 0, false},
320+
{"empty string", "", 0, false},
321+
322+
// Whitespace handling
323+
{"leading space", " 500KB", 500 * 1024, false},
324+
{"trailing space", "500KB ", 500 * 1024, false},
325+
{"space before suffix", "500 KB", 500 * 1024, false},
326+
327+
// Invalid inputs
328+
{"invalid suffix", "500XB", 0, true},
329+
{"non-numeric", "abcMB", 0, true},
330+
{"float", "5.5MB", 0, true},
331+
332+
// Note: negative values are technically parsed by ParseInt,
333+
// but produce negative results which are invalid for byte sizes.
334+
// The calling code should validate the result is non-negative.
335+
}
336+
337+
for _, tt := range tests {
338+
t.Run(tt.name, func(t *testing.T) {
339+
result, err := parseByteSize(tt.input)
340+
if tt.wantErr {
341+
assert.Error(t, err, "expected error for input: %s", tt.input)
342+
} else {
343+
assert.NoError(t, err, "unexpected error for input: %s", tt.input)
344+
assert.Equal(t, tt.expected, result, "unexpected result for input: %s", tt.input)
345+
}
346+
})
347+
}
348+
}
349+
350+
// TestPreloadRateLimitConfig tests the preload rate limit configuration parsing.
351+
func TestPreloadRateLimitConfig(t *testing.T) {
352+
tests := []struct {
353+
name string
354+
input string
355+
expected int64
356+
}{
357+
{"empty string defaults to 1MB/s", "", 1024 * 1024},
358+
{"explicit 1MB", "1MB", 1024 * 1024},
359+
{"unlimited (0)", "0", 0},
360+
{"invalid falls back to 1MB/s", "invalid", 1024 * 1024},
361+
{"500KB", "500KB", 500 * 1024},
362+
{"2MB", "2MB", 2 * 1024 * 1024},
363+
{"lowercase 100kb", "100kb", 100 * 1024},
364+
{"lowercase 10mb", "10mb", 10 * 1024 * 1024},
365+
}
366+
367+
for _, tt := range tests {
368+
t.Run(tt.name, func(t *testing.T) {
369+
config := DefaultConfig()
370+
config.Cache.PreloadRateLimit = tt.input
371+
372+
assert.NoError(t, config.loadChain())
373+
374+
ethConfig, err := config.buildEth(nil, nil)
375+
assert.NoError(t, err)
376+
377+
assert.Equal(t, tt.expected, ethConfig.PreloadRateLimit, "input: %s", tt.input)
378+
})
379+
}
380+
}
381+
295382
// TestDeveloperModeGasParameters tests the developer mode specific code path
296383
// for setting TargetGasPercentage and BaseFeeChangeDenominator (lines 1293-1304 in config.go).
297384
// The default config uses mainnet which has Bor config, so these tests actually execute lines 1293-1304.

internal/cli/server/flags.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,13 @@ func (c *Command) Flags(config *Config) *flagset.Flagset {
559559
Default: c.cliConfig.Cache.AddressCacheSizesRaw,
560560
Group: "Cache",
561561
})
562+
f.StringFlag(&flagset.StringFlag{
563+
Name: "cache.preloadratelimit",
564+
Usage: "Rate limit per address for cache preloading (e.g. 500KB, 1MB, 0 for unlimited). Limits I/O during sync. Default: 1MB",
565+
Value: &c.cliConfig.Cache.PreloadRateLimit,
566+
Default: c.cliConfig.Cache.PreloadRateLimit,
567+
Group: "Cache",
568+
})
562569
f.Uint64Flag(&flagset.Uint64Flag{
563570
Name: "cache.triesinmemory",
564571
Usage: "Number of block states (tries) to keep in memory",

triedb/pathdb/biased_fastcache.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/ethereum/go-ethereum/ethdb"
1414
"github.com/ethereum/go-ethereum/log"
1515
"github.com/ethereum/go-ethereum/metrics"
16+
"golang.org/x/time/rate"
1617
)
1718

1819
var (
@@ -44,20 +45,25 @@ type AddressBiasedCache struct {
4445
ctx stdcontext.Context
4546
cancel stdcontext.CancelFunc
4647
wg sync.WaitGroup // Wait for all preloads to finish
48+
49+
// Rate limiting for preload operations (bytes per second, 0 = unlimited)
50+
rateLimitBPS int64
4751
}
4852

4953
// NewAddressBiasedCache creates a new address-biased cache with preloading.
5054
// It scans the database for storage trie nodes of the specified addresses and
5155
// loads them into dedicated caches. The addressCacheSizes maps each address to
5256
// its desired cache size in bytes. The commonCacheSize specifies the size
53-
// of the cache for non-preloaded data.
57+
// of the cache for non-preloaded data. The rateLimitBPS limits preload I/O
58+
// in bytes per second (0 = unlimited).
5459
// Preloading happens asynchronously in the background.
55-
func NewAddressBiasedCache(db ethdb.Database, addressCacheSizes map[common.Address]int, commonCacheSize int) (*AddressBiasedCache, error) {
60+
func NewAddressBiasedCache(db ethdb.Database, addressCacheSizes map[common.Address]int, commonCacheSize int, rateLimitBPS int64) (*AddressBiasedCache, error) {
5661
ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
5762
cache := &AddressBiasedCache{
58-
commonCache: fastcache.New(commonCacheSize),
59-
ctx: ctx,
60-
cancel: cancel,
63+
commonCache: fastcache.New(commonCacheSize),
64+
ctx: ctx,
65+
cancel: cancel,
66+
rateLimitBPS: rateLimitBPS,
6167
}
6268

6369
// Initialize caches synchronously, but preload asynchronously
@@ -86,6 +92,7 @@ func (c *AddressBiasedCache) initAddressCache(addr common.Address, cacheSize int
8692
// BFS traversal, prioritizing shallow nodes (most frequently accessed) until
8793
// the cache is full. This naturally loads nodes by depth, filling the cache
8894
// with as many upper-level nodes as possible. This function runs asynchronously.
95+
// Rate limiting is applied to prevent overwhelming the disk during sync.
8996
func (c *AddressBiasedCache) preloadAddressAsync(db ethdb.Database, addr common.Address, cacheSize int) {
9097
defer c.wg.Done()
9198
startTime := time.Now()
@@ -100,14 +107,25 @@ func (c *AddressBiasedCache) preloadAddressAsync(db ethdb.Database, addr common.
100107
}
101108
addrCache := cacheValue.(*fastcache.Cache)
102109

110+
// Create rate limiter if configured (burst of 64KB for smoother throttling)
111+
var limiter *rate.Limiter
112+
if c.rateLimitBPS > 0 {
113+
limiter = rate.NewLimiter(rate.Limit(c.rateLimitBPS), 64*1024)
114+
}
115+
103116
// Local stats for logging progress
104117
var entriesLoaded int
105118
var totalBytesLoaded uint64
106119

120+
rateLimitStr := "unlimited"
121+
if c.rateLimitBPS > 0 {
122+
rateLimitStr = fmt.Sprintf("%s/s", common.StorageSize(c.rateLimitBPS))
123+
}
107124
log.Info("Starting storage trie preload",
108125
"address", addr.Hex(),
109126
"account hash", accountHash.Hex(),
110-
"cache size", common.StorageSize(cacheSize).String())
127+
"cache size", common.StorageSize(cacheSize).String(),
128+
"rate limit", rateLimitStr)
111129

112130
var maxDepthReached int
113131
const logInterval = 100000
@@ -156,6 +174,27 @@ func (c *AddressBiasedCache) preloadAddressAsync(db ethdb.Database, addr common.
156174
continue
157175
}
158176

177+
// Apply rate limiting after reading, based on actual bytes read
178+
if limiter != nil {
179+
if err := limiter.WaitN(c.ctx, len(nodeData)); err != nil {
180+
if c.ctx.Err() != nil {
181+
log.Info("Preload interrupted during shutdown",
182+
"account hash", accountHash.Hex(),
183+
"entries", entriesLoaded,
184+
"max depth", maxDepthReached,
185+
"size", common.StorageSize(totalBytesLoaded).String(),
186+
"elapsed", time.Since(startTime))
187+
return
188+
}
189+
// Node exceeds burst size — skip it and continue preloading
190+
log.Warn("Preload skipping oversized node",
191+
"account hash", accountHash.Hex(),
192+
"node size", len(nodeData),
193+
"burst", limiter.Burst())
194+
continue
195+
}
196+
}
197+
159198
// Check if adding this node would exceed cache size
160199
// Key format: owner (32 bytes) + path
161200
nodeSize := uint64(common.HashLength + len(item.path) + len(nodeData))

0 commit comments

Comments
 (0)