Skip to content

Commit e940047

Browse files
Port mining optimizations for veblop (ethereum#1783)
* feat: disabling gogc override * internal/cli: add GC variables in config * core,eth,internal/cli,triedb/pathdb: add maxdifflayers to config * consensus/bor: fix header timestamp when time is in the past When the calculated header timestamp based on parent time and producer delay falls in the past, ensure we add the full producer delay to the current time rather than just setting it to current time. This prevents immediate timeout issues in the commit interrupt mechanism where blocks would be interrupted immediately due to negative time delays. Previously, when header.Time < now, we would set header.Time = now, which could still result in timing issues during block building. Now we properly calculate header.Time = now + CalcProducerDelay() to ensure adequate time for block building operations. * consensus/bor,internal/cli,params: add header timestamp fix when in past * perf(core/blockstm): optimize HasReadDep for small input sizes Add performance optimization to HasReadDep function based on production metrics showing ~3M calls with median size of 15. For small inputs (<=512 elements), use direct nested loop comparison instead of map allocation to improve performance due to better cache locality and avoiding allocation overhead. Additionally: - Replace map[Key]bool with map[Key]struct{} to reduce memory usage - Add Prometheus metrics to track function calls and input sizes - Add detailed comments explaining the optimization rationale The cutoff of 512 was determined through benchmarking and captures >50% of production calls, providing significant performance improvement for the common case while maintaining O(n+m) complexity for larger inputs. * core: add hasReadDepCallCounter and readsMapSizeHist metrics * eth.internal/cli,triedb: fix lint * core,eth,internal/cli,triedb/pathdb: rm maxdifflayers config * core: add dagBuildTimer * Revert "consensus/bor,internal/cli,params: add header timestamp fix when in past" This reverts commit 76f6ac5afef4309ec18184cf07618fc25cefff55. * triedb/pathdb: rm invalid comment * internal/cli: rm unused function --------- Co-authored-by: John Hilliard <praetoriansentry@gmail.com>
1 parent eec9fa5 commit e940047

File tree

6 files changed

+201
-11
lines changed

6 files changed

+201
-11
lines changed

consensus/bor/bor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header) e
988988

989989
header.Time = parent.Time + CalcProducerDelay(number, succession, c.config)
990990
if header.Time < uint64(time.Now().Unix()) {
991-
header.Time = uint64(time.Now().Unix())
991+
header.Time = uint64(time.Now().Unix()) + CalcProducerDelay(number, succession, c.config)
992992
} else {
993993
// For primary validators, wait until the current block production window
994994
// starts. This prevents bor from starting to build next block before time

core/blockstm/dag.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import (
88
"github.com/heimdalr/dag"
99

1010
"github.com/ethereum/go-ethereum/log"
11+
"github.com/ethereum/go-ethereum/metrics"
12+
)
13+
14+
var (
15+
hasReadDepCallCounter = metrics.NewRegisteredCounter("blockstm/hasreaddep/calls", nil)
16+
readsMapSizeHist = metrics.NewRegisteredHistogram("blockstm/hasreaddep/reads/size", nil, metrics.NewExpDecaySample(1028, 0.015))
17+
dagBuildTimer = metrics.NewRegisteredTimer("blockstm/dag/build", nil)
1118
)
1219

1320
type DAG struct {
@@ -20,13 +27,39 @@ type TxDep struct {
2027
FullWriteList [][]WriteDescriptor
2128
}
2229

30+
// HasReadDep checks if there are any read dependencies between two transactions.
31+
// Performance optimization: Based on production metrics showing ~3M calls with median
32+
// size of 15 and 95th percentile of 94, we avoid map allocation for small inputs.
33+
// This optimization leverages the fact that for small lists, linear search is faster
34+
// than map construction due to avoiding allocation overhead and better cache locality.
2335
func HasReadDep(txFrom TxnOutput, txTo TxnInput) bool {
24-
reads := make(map[Key]bool)
36+
hasReadDepCallCounter.Inc(1)
37+
38+
// Cutoff determined by benchmarking: below this size, nested loops outperform map lookup
39+
// due to avoiding allocation overhead. With median=15, this captures >50% of calls.
40+
const smallCutoff = 512
41+
if len(txTo) <= smallCutoff {
42+
// For small inputs, use direct comparison (O(n*m) but with better constants)
43+
for _, rd := range txFrom {
44+
for _, v := range txTo {
45+
if rd.Path == v.Path {
46+
return true
47+
}
48+
}
49+
}
50+
return false
51+
}
2552

53+
// For larger inputs, use map for O(n+m) complexity
54+
// Using struct{} instead of bool saves memory (0 bytes vs 1 byte per entry)
55+
// since we only need set membership, not associated values
56+
reads := make(map[Key]struct{})
2657
for _, v := range txTo {
27-
reads[v.Path] = true
58+
reads[v.Path] = struct{}{}
2859
}
2960

61+
readsMapSizeHist.Update(int64(len(reads)))
62+
3063
for _, rd := range txFrom {
3164
if _, ok := reads[rd.Path]; ok {
3265
return true
@@ -37,6 +70,8 @@ func HasReadDep(txFrom TxnOutput, txTo TxnInput) bool {
3770
}
3871

3972
func BuildDAG(deps TxnInputOutput) (d DAG) {
73+
defer dagBuildTimer.UpdateSince(time.Now())
74+
4075
d = DAG{dag.NewDAG()}
4176
ids := make(map[int]string)
4277

internal/cli/server/config.go

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package server
33
import (
44
"crypto/ecdsa"
55
"fmt"
6-
"math"
6+
7+
// "math"
78
"math/big"
89
"os"
910
"path/filepath"
11+
"regexp"
1012
"runtime"
1113
"strconv"
1214
"strings"
@@ -614,6 +616,16 @@ type CacheConfig struct {
614616

615617
// Raise the open file descriptor resource limit (default = system fd limit)
616618
FDLimit int `hcl:"fdlimit,optional" toml:"fdlimit,optional"`
619+
620+
// GC settings
621+
// GoMemLimit sets the soft memory limit for the runtime
622+
GoMemLimit string `hcl:"gomemlimit,optional" toml:"gomemlimit,optional"`
623+
624+
// GoGC sets the initial garbage collection target percentage
625+
GoGC int `hcl:"gogc,optional" toml:"gogc,optional"`
626+
627+
// GoDebug sets debugging variables for the runtime
628+
GoDebug string `hcl:"godebug,optional" toml:"godebug,optional"`
617629
}
618630

619631
type ExtraDBConfig struct {
@@ -847,6 +859,9 @@ func DefaultConfig() *Config {
847859
FilterLogCacheSize: ethconfig.Defaults.FilterLogCacheSize,
848860
TrieTimeout: 60 * time.Minute,
849861
FDLimit: 0,
862+
GoMemLimit: "", // Empty means no limit
863+
GoGC: 100, // Go default is 100%
864+
GoDebug: "", // Empty means no debug flags
850865
},
851866
ExtraDB: &ExtraDBConfig{
852867
// These are LevelDB defaults, specifying here for clarity in code and in logging.
@@ -1226,11 +1241,34 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (*
12261241
cache = allowance
12271242
}
12281243
}
1229-
// Tune the garbage collector
1230-
gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024)))
1244+
// Apply configurable garbage collection settings with validation
1245+
if c.Cache.GoMemLimit != "" {
1246+
if err := validateGoMemLimit(c.Cache.GoMemLimit); err != nil {
1247+
log.Warn("Invalid GOMEMLIMIT value, skipping", "value", c.Cache.GoMemLimit, "error", err)
1248+
} else {
1249+
os.Setenv("GOMEMLIMIT", c.Cache.GoMemLimit)
1250+
log.Info("Set GOMEMLIMIT", "value", c.Cache.GoMemLimit)
1251+
}
1252+
}
1253+
1254+
// Sanitize GOGC value to reasonable bounds
1255+
sanitizedGoGC := sanitizeGoGC(c.Cache.GoGC)
1256+
if sanitizedGoGC != c.Cache.GoGC {
1257+
log.Warn("GOGC value sanitized", "original", c.Cache.GoGC, "sanitized", sanitizedGoGC)
1258+
}
1259+
if sanitizedGoGC != 100 { // Only set if different from default
1260+
godebug.SetGCPercent(sanitizedGoGC)
1261+
log.Info("Set GOGC", "percent", sanitizedGoGC)
1262+
}
12311263

1232-
log.Debug("Sanitizing Go's GC trigger", "percent", int(gogc))
1233-
godebug.SetGCPercent(int(gogc))
1264+
if c.Cache.GoDebug != "" {
1265+
if err := validateGoDebug(c.Cache.GoDebug); err != nil {
1266+
log.Warn("Invalid GODEBUG value, skipping", "value", c.Cache.GoDebug, "error", err)
1267+
} else {
1268+
os.Setenv("GODEBUG", c.Cache.GoDebug)
1269+
log.Info("Set GODEBUG", "value", c.Cache.GoDebug)
1270+
}
1271+
}
12341272

12351273
n.DatabaseCache = calcPerc(c.Cache.PercDatabase)
12361274
n.SnapshotCache = calcPerc(c.Cache.PercSnapshot)
@@ -1706,6 +1744,103 @@ func MakePasswordListFromFile(path string) ([]string, error) {
17061744
return lines, nil
17071745
}
17081746

1747+
// validateGoMemLimit validates GOMEMLIMIT values
1748+
func validateGoMemLimit(value string) error {
1749+
// GOMEMLIMIT can be:
1750+
// - A number followed by optional unit (B, KB, MB, GB, TB, PB)
1751+
// - "off" to disable soft limit
1752+
if value == "off" {
1753+
return nil
1754+
}
1755+
1756+
// Parse the value to validate format
1757+
if matched, _ := regexp.MatchString(`^[0-9]+(\.[0-9]+)?[KMGTPE]?[iB]?$`, value); !matched {
1758+
return fmt.Errorf("invalid GOMEMLIMIT format, expected number with optional unit (e.g., '8GB', '1024MB')")
1759+
}
1760+
1761+
return nil
1762+
}
1763+
1764+
// sanitizeGoGC clamps GOGC values to reasonable bounds
1765+
func sanitizeGoGC(value int) int {
1766+
const (
1767+
minGoGC = 10
1768+
maxGoGC = 2000
1769+
)
1770+
1771+
if value < minGoGC {
1772+
return minGoGC
1773+
}
1774+
if value > maxGoGC {
1775+
return maxGoGC
1776+
}
1777+
return value
1778+
}
1779+
1780+
// validateGoDebug validates GODEBUG values for known debug variables
1781+
func validateGoDebug(value string) error {
1782+
// Known GODEBUG variables (not exhaustive, but covers common ones)
1783+
knownVars := map[string]bool{
1784+
"gctrace": true, // GC trace
1785+
"gcpacertrace": true, // GC pacer trace
1786+
"madvdontneed": true, // Memory management
1787+
"scavenge": true, // Scavenger debug
1788+
"asyncpreempt": true, // Async preemption
1789+
"cgocheck": true, // CGO check
1790+
"schedtrace": true, // Scheduler trace
1791+
"scheddetail": true, // Scheduler detail
1792+
"tracebackancestors": true, // Traceback ancestors
1793+
"httpmuxgo121": true, // HTTP mux
1794+
"netdns": true, // DNS resolution
1795+
"tls13": true, // TLS 1.3
1796+
"panicnil": true, // Panic on nil
1797+
"invalidptr": true, // Invalid pointer checking
1798+
"sbrk": true, // Memory allocation
1799+
"efence": true, // Electric fence
1800+
"inittrace": true, // Init trace
1801+
"cpu.all": true, // CPU features
1802+
}
1803+
1804+
// Split by comma and validate each key=value pair
1805+
pairs := strings.Split(value, ",")
1806+
for _, pair := range pairs {
1807+
if strings.TrimSpace(pair) == "" {
1808+
continue
1809+
}
1810+
1811+
parts := strings.SplitN(pair, "=", 2)
1812+
if len(parts) != 2 {
1813+
return fmt.Errorf("invalid GODEBUG format: '%s', expected key=value", pair)
1814+
}
1815+
1816+
key := strings.TrimSpace(parts[0])
1817+
val := strings.TrimSpace(parts[1])
1818+
1819+
if key == "" {
1820+
return fmt.Errorf("empty GODEBUG variable name in: '%s'", pair)
1821+
}
1822+
1823+
// Validate value (most GODEBUG vars use 0/1 or numeric values)
1824+
if val == "" {
1825+
return fmt.Errorf("empty GODEBUG value for '%s'", key)
1826+
}
1827+
1828+
// Warn about unknown variables but don't fail (Go may add new ones)
1829+
if !knownVars[key] {
1830+
log.Warn("Unknown GODEBUG variable", "var", key, "value", val)
1831+
}
1832+
1833+
// Validate numeric values where appropriate
1834+
if key == "gctrace" || key == "gcpacertrace" || key == "schedtrace" || key == "cgocheck" {
1835+
if _, err := strconv.Atoi(val); err != nil {
1836+
return fmt.Errorf("GODEBUG variable '%s' expects numeric value, got '%s'", key, val)
1837+
}
1838+
}
1839+
}
1840+
1841+
return nil
1842+
}
1843+
17091844
// loadFilteredAddresses loads newline-separated addresses to filter from the specified file.
17101845
func loadFilteredAddresses(filePath string) (map[common.Address]struct{}, error) {
17111846
if filePath == "" {

internal/cli/server/flags.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,27 @@ func (c *Command) Flags(config *Config) *flagset.Flagset {
489489
Default: c.cliConfig.Cache.FDLimit,
490490
Group: "Cache",
491491
})
492+
f.StringFlag(&flagset.StringFlag{
493+
Name: "cache.gomemlimit",
494+
Usage: "Set GOMEMLIMIT for the runtime (e.g. '34GB', '34359738368'). Empty means no limit",
495+
Value: &c.cliConfig.Cache.GoMemLimit,
496+
Default: c.cliConfig.Cache.GoMemLimit,
497+
Group: "Cache",
498+
})
499+
f.IntFlag(&flagset.IntFlag{
500+
Name: "cache.gogc",
501+
Usage: "Set GOGC percentage for garbage collection trigger (default: 100)",
502+
Value: &c.cliConfig.Cache.GoGC,
503+
Default: c.cliConfig.Cache.GoGC,
504+
Group: "Cache",
505+
})
506+
f.StringFlag(&flagset.StringFlag{
507+
Name: "cache.godebug",
508+
Usage: "Set GODEBUG variables for runtime debugging (e.g. 'gctrace=1,gcpacertrace=1')",
509+
Value: &c.cliConfig.Cache.GoDebug,
510+
Default: c.cliConfig.Cache.GoDebug,
511+
Group: "Cache",
512+
})
492513

493514
// LevelDB options
494515
f.Uint64Flag(&flagset.Uint64Flag{

triedb/pathdb/database.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func (c *Config) sanitize() *Config {
134134
log.Warn("Sanitizing invalid node buffer size", "provided", common.StorageSize(conf.WriteBufferSize), "updated", common.StorageSize(maxBufferSize))
135135
conf.WriteBufferSize = maxBufferSize
136136
}
137+
137138
return &conf
138139
}
139140

triedb/pathdb/database_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ type tester struct {
121121
snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
122122
}
123123

124+
//nolint:unused
124125
func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool) *tester {
125126
var (
126127
disk, _ = rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{Ancient: t.TempDir()})
@@ -464,8 +465,6 @@ func TestDatabaseRollback(t *testing.T) {
464465
defer func() {
465466
maxDiffLayers = 128
466467
}()
467-
468-
// Verify state histories
469468
tester := newTester(t, 0, false, 32, false)
470469
defer tester.release()
471470

@@ -498,7 +497,6 @@ func TestDatabaseRecoverable(t *testing.T) {
498497
defer func() {
499498
maxDiffLayers = 128
500499
}()
501-
502500
var (
503501
tester = newTester(t, 0, false, 12, false)
504502
index = tester.bottomIndex()

0 commit comments

Comments
 (0)