From 213ab9ba610a06025264afa917db70f9c39d0045 Mon Sep 17 00:00:00 2001 From: Dat Duong Date: Fri, 7 Aug 2026 09:12:20 +0000 Subject: [PATCH] perf(cmd): scale db-cache-size to available memory --- cmd/juno/juno.go | 23 +++++-- cmd/juno/juno_internal_test.go | 20 ++++++ cmd/juno/juno_test.go | 9 +-- docs/docs/_config-options.md | 4 +- docs/docs/tuning.md | 8 +-- docs/generate-config.js | 6 +- node/node.go | 8 ++- node/node_test.go | 4 +- node/throttled_compiler.go | 20 ++++-- node/throttled_compiler_internal_test.go | 86 ++++++++++++++++++++++++ starknet/compiler/concurrency.go | 18 ----- starknet/compiler/concurrency_test.go | 4 -- utils/memory.go | 19 ++++++ utils/memory_test.go | 12 ++++ 14 files changed, 196 insertions(+), 45 deletions(-) create mode 100644 node/throttled_compiler_internal_test.go create mode 100644 utils/memory.go create mode 100644 utils/memory_test.go diff --git a/cmd/juno/juno.go b/cmd/juno/juno.go index 942c35ef06..0dd15baf74 100644 --- a/cmd/juno/juno.go +++ b/cmd/juno/juno.go @@ -147,6 +147,8 @@ const ( defaultRemoteDB = "" defaultRPCMaxBlockScan = math.MaxUint defaultCacheSizeMb = 1024 + maxAutoCacheSizeMb = 8192 + cacheMemoryFraction = 4 defaultDBMaxHandlesFloor = 1024 defaultDBMaxHandlesCeiling = 1_048_576 defaultGwAPIKey = "" @@ -234,8 +236,10 @@ const ( maxVMQueueUsage = "Maximum number for requests to queue after reaching max-vms before starting to reject incoming requests" remoteDBUsage = "gRPC URL of a remote Juno node" rpcMaxBlockScanUsage = "Maximum number of blocks scanned in single starknet_getEvents call" - dbCacheSizeUsage = "Determines the amount of memory (in megabytes) allocated for caching data in the database." - dbMaxHandlesUsage = "A soft limit on the number of open files that can be used by the DB. " + + dbCacheSizeUsage = "Determines the amount of memory (in megabytes) allocated for " + + "caching data in the database. When not set, defaults to a quarter of total " + + "memory (host RAM or cgroup limit), between 1024 and 8192" + dbMaxHandlesUsage = "A soft limit on the number of open files that can be used by the DB. " + "When not set, defaults to half of the process fd limit (min 1024, max 1048576)" //nolint: gosec // usage text, not a credential gwAPIKeyUsage = "API key for gateway endpoints to avoid throttling" @@ -281,7 +285,8 @@ const ( "may consume; a compilation exceeding it is aborted. Enforced on Linux only. " + "0 disables the limit." maxCompilationReserveMemory = "Memory (in MB) excluded from the compilations memory budget when " + - "calculating the default for `max-concurrent-compilations`" + "calculating the default for `max-concurrent-compilations`. The DB cache size is " + + "excluded on top of this reserve" pruneModeUsage = "Enables block-data and state-history pruning. Pruning is " + "disabled by default; passing this flag (with or without a value) turns " + "it on. The value is the size of the retention window in blocks, counted " + @@ -592,7 +597,7 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr // --- Database --- junoCmd.Flags().String(dbPathF, defaultDBPath, dbPathUsage) - junoCmd.Flags().Uint(dbCacheSizeF, defaultCacheSizeMb, dbCacheSizeUsage) + junoCmd.Flags().Uint(dbCacheSizeF, defaultDBCacheSize(), dbCacheSizeUsage) junoCmd.Flags().Int(dbMaxHandlesF, defaultDBMaxHandles(), dbMaxHandlesUsage) junoCmd.Flags().String( dbCompactionConcurrencyF, defaultDBCompactionConcurrency, dbCompactionConcurrencyUsage, @@ -716,6 +721,16 @@ func NewCmd(config *node.Config, run func(*cobra.Command, []string) error) *cobr return junoCmd } +// defaultDBCacheSize gives the DB block cache a quarter of the memory this +// process can use, clamped to [defaultCacheSizeMb, maxAutoCacheSizeMb]. +func defaultDBCacheSize() uint { + return dbCacheSizeForMemoryMB(utils.AvailableMemoryMB()) +} + +func dbCacheSizeForMemoryMB(memMB uint64) uint { + return min(max(uint(memMB/cacheMemoryFraction), defaultCacheSizeMb), maxAutoCacheSizeMb) +} + // defaultDBMaxHandles gives the DB half of the process fd limit, clamped to // [defaultDBMaxHandlesFloor, defaultDBMaxHandlesCeiling]. func defaultDBMaxHandles() int { diff --git a/cmd/juno/juno_internal_test.go b/cmd/juno/juno_internal_test.go index 834ebd4e0c..4bce1f1c87 100644 --- a/cmd/juno/juno_internal_test.go +++ b/cmd/juno/juno_internal_test.go @@ -7,6 +7,26 @@ import ( "github.com/stretchr/testify/assert" ) +func TestDBCacheSizeForMemoryMB(t *testing.T) { + tests := []struct { + name string + memMB uint64 + want uint + }{ + {name: "zero memory clamps to floor", memMB: 0, want: defaultCacheSizeMb}, + {name: "quarter below floor clamps to floor", memMB: 4095, want: defaultCacheSizeMb}, + {name: "quarter equals floor", memMB: 4096, want: defaultCacheSizeMb}, + {name: "quarter within bounds", memMB: 8192, want: 2048}, + {name: "quarter equals cap", memMB: 32768, want: maxAutoCacheSizeMb}, + {name: "quarter above cap clamps to cap", memMB: 65536, want: maxAutoCacheSizeMb}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, dbCacheSizeForMemoryMB(tt.memMB)) + }) + } +} + func TestDBMaxHandlesForFDLimit(t *testing.T) { tests := []struct { name string diff --git a/cmd/juno/juno_test.go b/cmd/juno/juno_test.go index 64c819b751..9de2f5dc18 100644 --- a/cmd/juno/juno_test.go +++ b/cmd/juno/juno_test.go @@ -73,7 +73,7 @@ func TestConfigPrecedence(t *testing.T) { defaultRPCMaxConcurrentRequests := uint(256000) defaultRPCMaxRequestQueue := uint(256000) defaultRPCMaxBlockScan := uint(math.MaxUint) - defaultMaxCacheSize := uint(1024) + defaultMaxCacheSize := min(max(uint(utils.AvailableMemoryMB()/4), 1024), 8192) defaultMaxHandles := max(int(min(fdLimit/2, 1_048_576)), 1024) defaultDBMemtableSize := uint(256) defaultDBMemtableCount := uint(2) @@ -224,7 +224,7 @@ func TestConfigPrecedence(t *testing.T) { "custom network all flags": { inputArgs: []string{ "--log-level", "debug", "--http-port", "4576", "--http-host", "0.0.0.0", - "--db-path", "/home/.juno", "--pprof", "--db-cache-size", "1024", + "--db-path", "/home/.juno", "--pprof", "--cn-name", "custom", "--cn-feeder-url", "http://awesome.feeder", "--cn-gateway-url", "http://awesome.gateway", "--cn-l1-chain-id", "0x1", "--cn-l2-chain-id", "SN_AWESOME", "--cn-unverifiable-range", "0,10", @@ -416,7 +416,8 @@ http-port: 4576 "all flags without config file": { inputArgs: []string{ "--log-level", "debug", "--http-port", "4576", "--http-host", "0.0.0.0", - "--db-path", "/home/.juno", "--network", "sepolia-integration", "--pprof", "--db-cache-size", "1024", + "--db-path", "/home/.juno", "--network", "sepolia-integration", "--pprof", + "--db-cache-size", "2222", }, expectedConfig: &node.Config{ LogLevel: "debug", @@ -443,7 +444,7 @@ http-port: 4576 RPCMaxConcurrentRequests: defaultRPCMaxConcurrentRequests, RPCMaxRequestQueue: defaultRPCMaxRequestQueue, RPCMaxBlockScan: defaultRPCMaxBlockScan, - DBCacheSize: defaultMaxCacheSize, + DBCacheSize: 2222, DBMaxHandles: defaultMaxHandles, DBMemtableSize: defaultDBMemtableSize, DBMemtableCount: defaultDBMemtableCount, diff --git a/docs/docs/_config-options.md b/docs/docs/_config-options.md index 3e143bea5b..5f01ba77dc 100644 --- a/docs/docs/_config-options.md +++ b/docs/docs/_config-options.md @@ -83,7 +83,7 @@ | Config Option | Default Value | Description | | - | - | - | -| `db-cache-size` | `1024` | Determines the amount of memory (in megabytes) allocated for caching data in the database | +| `db-cache-size` | `quarter of total memory (min 1024, max 8192)` | Determines the amount of memory (in megabytes) allocated for caching data in the database. When not set, defaults to a quarter of total memory (host RAM or cgroup limit), between 1024 and 8192 | | `db-compaction-concurrency` | | DB compaction concurrency range. Format: N (lower=1, upper=N) or M,N (lower=M, upper=N). Default: 1,GOMAXPROCS/2 | | `db-compression` | `zstd` | Database compression profile. Options: zstd, snappy, minlz. Use zstd for low storage | | `db-max-handles` | `half of process fd limit (min 1024, max 1048576)` | A soft limit on the number of open files that can be used by the DB. When not set, defaults to half of the process fd limit (min 1024, max 1048576) | @@ -105,7 +105,7 @@ | `max-compilation-cpu-time` | `10` | Maximum CPU time (in seconds) each Sierra compilation process may consume; a compilation exceeding it is aborted. Enforced on Linux only. 0 disables the limit | | `max-compilation-memory` | `4 * 1024` | Maximum memory (in MB) each Sierra compilation process may use; a compilation exceeding it is aborted. Enforced on Linux only. 0 disables the limit | | `max-compilation-queue` | `2 * max-concurrent-compilations` | Maximum number of compilation requests to queue after reaching max-concurrent-compilations before starting to reject incoming requests | -| `max-concurrent-compilations` | `CPU Cores` | Maximum concurrent Sierra compilations | +| `max-concurrent-compilations` | `auto (memory-aware)` | Maximum concurrent Sierra compilations. Default is set based on available hardware resources. Derived as `min(cpu_cores, (available_memory - node_memory_reserve - db_cache_size) / max_compilation_memory)`, at least 1 | | `max-vm-queue` | `2 * max-vms` | Maximum number for requests to queue after reaching max-vms before starting to reject incoming requests | | `max-vms` | `3 * CPU Cores` | Maximum number for VM instances to be used for RPC calls concurrently | | `versioned-constants-file` | | Use custom versioned constants from provided file | diff --git a/docs/docs/tuning.md b/docs/docs/tuning.md index c1df68714d..d063581de9 100644 --- a/docs/docs/tuning.md +++ b/docs/docs/tuning.md @@ -65,9 +65,9 @@ Note that this effectively improve syncing speed while behind the tip of the cha ## Database Cache Size -Set by the `--db-cache-size` flag (default: 1024 MB), this determines the amount of memory allocated for caching frequently accessed data from the database. +Set by the `--db-cache-size` flag (default: a quarter of total memory — host RAM or cgroup limit — between 1024 and 8192 MB), this determines the amount of memory allocated for caching frequently accessed data from the database. -A larger cache reduces disk reads and improves query performance. On systems with ample memory, increasing this value (e.g., 2048 or 4096 MB) can significantly improve RPC response times and overall node performance. +A larger cache reduces disk reads and improves query performance. The default already scales with memory; on machines with more than 32 GB of RAM, raising it past the 8192 MB cap holds more of the database in memory and can improve RPC response times. ## Sierra Compilation Limits @@ -95,12 +95,12 @@ The CPU time limit counts seconds of CPU actually consumed by the compilation pr - `--max-concurrent-compilations`(default: unset): controls how many compilations run at once. Any non-negative integer is used directly (`0` disables compilations). Left unset (the default), Juno derives a safe value so concurrent compilations cannot exhaust RAM: ``` -limit = min((available_memory - node_memory_reserve) / max_compilation_memory, cpu_cores), at least 1. +limit = min((available_memory - node_memory_reserve - db_cache_size) / max_compilation_memory, cpu_cores), at least 1. ``` - `--max-compilation-queue` (default: unset): How many requests wait once the concurrency limit is reached before new ones are rejected. Unset uses twice the concurrency limit (`0` disables the queue). -- `--node-memory-reserve` (default: 4096 MB): Memory kept for the rest of the node, excluded from the compilation budget. +- `--node-memory-reserve` (default: 4096 MB): Memory kept for the rest of the node, excluded from the compilation budget. The DB cache size is excluded on top of this reserve, since the cache fills to its ceiling on a synced node. The available memory respects container limits (cgroups), so inside a memory-capped container the value reflects the container, not the host. On non-Linux, where the per-compilation memory limit does not apply, the limit is simply the CPU core count. diff --git a/docs/generate-config.js b/docs/generate-config.js index dda7c0b73f..41094f1f5f 100644 --- a/docs/generate-config.js +++ b/docs/generate-config.js @@ -105,6 +105,10 @@ const extractConfigs = (codebase) => { if (configName === "db-max-handles") { defaultValue = "half of process fd limit (min 1024, max 1048576)"; } + // Same for db-cache-size — computed from total memory at startup. + if (configName === "db-cache-size") { + defaultValue = "quarter of total memory (min 1024, max 8192)"; + } if (configName === "max-vms") { defaultValue = "3 * CPU Cores"; } @@ -116,7 +120,7 @@ const extractConfigs = (codebase) => { if (configName === "max-concurrent-compilations") { defaultValue = "auto (memory-aware)"; description += - ". Derived as `min(cpu_cores, (available_memory - node_memory_reserve) / max_compilation_memory)`, at least 1"; + ". Derived as `min(cpu_cores, (available_memory - node_memory_reserve - db_cache_size) / max_compilation_memory)`, at least 1"; } if (configName === "max-compilation-queue") { defaultValue = "2 * max-concurrent-compilations"; diff --git a/node/node.go b/node/node.go index ca00dfc5f9..b5bbafd229 100644 --- a/node/node.go +++ b/node/node.go @@ -41,6 +41,7 @@ import ( "github.com/NethermindEth/juno/starknet/compiler" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" "github.com/NethermindEth/juno/sync" + "github.com/NethermindEth/juno/utils" "github.com/NethermindEth/juno/utils/log" "github.com/NethermindEth/juno/vm" "github.com/consensys/gnark-crypto/ecc/stark-curve/ecdsa" @@ -331,7 +332,12 @@ func New(cfg *Config, version string, logLevel *log.Level) (*Node, error) { var nodeVM vm.VM var throttledVM *ThrottledVM - maxConcurrentComp, maxQueuedComp := calculateCompilerConcurrencyBudget(cfg, logger) + maxConcurrentComp, maxQueuedComp := calculateCompilerConcurrencyBudget( + cfg, + uint64(runtime.GOMAXPROCS(0)), + utils.AvailableMemoryMB(), + logger, + ) compiler := compiler.New( &compiler.Config{ MaxMemory: uint64(cfg.MaxCompilationMemory) * 1024 * 1024, diff --git a/node/node_test.go b/node/node_test.go index 28ddcd4455..7269e5c581 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -11,9 +11,9 @@ import ( statetestutils "github.com/NethermindEth/juno/core/state/testutils" "github.com/NethermindEth/juno/db/pebblev2" "github.com/NethermindEth/juno/node" - "github.com/NethermindEth/juno/starknet/compiler" adaptfeeder "github.com/NethermindEth/juno/starknetdata/feeder" "github.com/NethermindEth/juno/sync" + "github.com/NethermindEth/juno/utils" "github.com/NethermindEth/juno/utils/log" "github.com/stretchr/testify/require" ) @@ -89,7 +89,7 @@ func TestNewNodeRunsOneAtATimeOnLowMemory(t *testing.T) { // MaxConcurrentCompilations left unset: derive, then floor to 1. MaxCompilationMemory: 4096, // Reserve more than the machine has, so nothing fits. - NodeMemoryReserve: uint(compiler.AvailableMemoryMB() + 4096), + NodeMemoryReserve: uint(utils.AvailableMemoryMB() + 4096), } _, err := node.New(config, "v0.3", log.NewLevel(log.INFO)) diff --git a/node/throttled_compiler.go b/node/throttled_compiler.go index 1ac01b5789..088754d430 100644 --- a/node/throttled_compiler.go +++ b/node/throttled_compiler.go @@ -2,7 +2,6 @@ package node import ( "context" - "runtime" "github.com/NethermindEth/juno/starknet" "github.com/NethermindEth/juno/starknet/compiler" @@ -41,14 +40,24 @@ func (tc *ThrottledCompiler) Compile( // calculateCompilerConcurrencyBudget determines safe limits for concurrent compilations // if this were not explicitly set -func calculateCompilerConcurrencyBudget(cfg *Config, logger log.StructuredLogger) (uint64, uint64) { +func calculateCompilerConcurrencyBudget( + cfg *Config, + cores uint64, + availableMemoryMB uint64, + logger log.StructuredLogger, +) (uint64, uint64) { + // A remote DB allocates no local pebble cache, so none of it is reserved. + dbCacheSizeMB := uint64(cfg.DBCacheSize) + if cfg.RemoteDB != "" { + dbCacheSizeMB = 0 + } + maxConcurrency := cfg.MaxConcurrentCompilations - availableMemoryMB := compiler.AvailableMemoryMB() if !cfg.MaxConcurrentCompilationsExplicit { maxConcurrency = compiler.ConcurrencyLimit( - uint64(runtime.GOMAXPROCS(0)), + cores, availableMemoryMB, - uint64(cfg.NodeMemoryReserve), + uint64(cfg.NodeMemoryReserve)+dbCacheSizeMB, uint64(cfg.MaxCompilationMemory), ) } @@ -63,6 +72,7 @@ func calculateCompilerConcurrencyBudget(cfg *Config, logger log.StructuredLogger zap.Uint64("queueSize", queueSize), zap.Uint64("availableMemoryMB", availableMemoryMB), zap.Uint("nodeMemoryReserveMB", cfg.NodeMemoryReserve), + zap.Uint64("dbCacheSizeMB", dbCacheSizeMB), zap.Uint("maxCompilationMemoryMB", cfg.MaxCompilationMemory), ) return maxConcurrency, queueSize diff --git a/node/throttled_compiler_internal_test.go b/node/throttled_compiler_internal_test.go new file mode 100644 index 0000000000..f4c502173f --- /dev/null +++ b/node/throttled_compiler_internal_test.go @@ -0,0 +1,86 @@ +package node + +import ( + "testing" + + "github.com/NethermindEth/juno/utils/log" + "github.com/stretchr/testify/assert" +) + +func TestCalculateCompilerConcurrencyBudget(t *testing.T) { + tests := []struct { + name string + cfg Config + cores uint64 + memMB uint64 + wantConc uint64 + wantQueue uint64 + }{ + { + name: "db cache is reserved on top of node reserve", + cfg: Config{MaxCompilationMemory: 4096, NodeMemoryReserve: 4096, DBCacheSize: 8192}, + cores: 64, + memMB: 65536, + wantConc: 13, // (65536 - 4096 - 8192) / 4096 + wantQueue: 26, + }, + { + name: "remote db ignores the local cache size", + cfg: Config{ + MaxCompilationMemory: 4096, NodeMemoryReserve: 4096, DBCacheSize: 8192, + RemoteDB: "localhost:9090", + }, + cores: 64, + memMB: 65536, + wantConc: 15, // (65536 - 4096) / 4096 + wantQueue: 30, + }, + { + name: "reserve plus cache covering all memory floors to 1", + cfg: Config{MaxCompilationMemory: 4096, NodeMemoryReserve: 4096, DBCacheSize: 8192}, + cores: 64, + memMB: 12288, + wantConc: 1, + wantQueue: 2, + }, + { + name: "no compilation memory limit uses core count", + cfg: Config{MaxCompilationMemory: 0, NodeMemoryReserve: 4096, DBCacheSize: 8192}, + cores: 64, + memMB: 65536, + wantConc: 64, + wantQueue: 128, + }, + { + name: "explicit setting bypasses derivation", + cfg: Config{ + MaxConcurrentCompilations: 5, MaxConcurrentCompilationsExplicit: true, + MaxCompilationMemory: 4096, NodeMemoryReserve: 4096, DBCacheSize: 8192, + }, + cores: 64, + memMB: 65536, + wantConc: 5, + wantQueue: 10, + }, + { + name: "explicit queue bypasses derivation", + cfg: Config{ + MaxCompilationMemory: 4096, NodeMemoryReserve: 4096, DBCacheSize: 8192, + MaxCompilationQueue: 7, MaxCompilationQueueExplicit: true, + }, + cores: 64, + memMB: 65536, + wantConc: 13, + wantQueue: 7, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conc, queue := calculateCompilerConcurrencyBudget( + &tt.cfg, tt.cores, tt.memMB, log.NewNopZapLogger(), + ) + assert.Equal(t, tt.wantConc, conc) + assert.Equal(t, tt.wantQueue, queue) + }) + } +} diff --git a/starknet/compiler/concurrency.go b/starknet/compiler/concurrency.go index a4aa7efa11..3705e103a1 100644 --- a/starknet/compiler/concurrency.go +++ b/starknet/compiler/concurrency.go @@ -1,23 +1,5 @@ package compiler -import ( - "github.com/KimMachineGun/automemlimit/memlimit" - "github.com/pbnjay/memory" -) - -const megabyte = 1 << 20 - -// AvailableMemoryMB returns the RAM this process can use, in MB. -// It checks if the memory is limited by the cgroup limit, otherwise it uses the host RAM. -func AvailableMemoryMB() uint64 { - hostMemory := memory.TotalMemory() - cgroupLimit, err := memlimit.FromCgroup() - if err == nil && cgroupLimit > 0 && cgroupLimit < hostMemory { - return cgroupLimit / megabyte - } - return hostMemory / megabyte -} - // ConcurrencyLimit returns how many compilations fit in memory, capped by maxConcurrency. // Memory is in MB. A 0 maxMemoryPerCompilation means no memory limit. // Returns 1 when no compilation fits memory. diff --git a/starknet/compiler/concurrency_test.go b/starknet/compiler/concurrency_test.go index 40040eb9db..eae81a2163 100644 --- a/starknet/compiler/concurrency_test.go +++ b/starknet/compiler/concurrency_test.go @@ -76,7 +76,3 @@ func TestConcurrencyLimit(t *testing.T) { }) } } - -func TestAvailableMemoryMB(t *testing.T) { - assert.NotZero(t, compiler.AvailableMemoryMB()) -} diff --git a/utils/memory.go b/utils/memory.go new file mode 100644 index 0000000000..a8575c23e1 --- /dev/null +++ b/utils/memory.go @@ -0,0 +1,19 @@ +package utils + +import ( + "github.com/KimMachineGun/automemlimit/memlimit" + "github.com/pbnjay/memory" +) + +const megabyte = 1 << 20 + +// AvailableMemoryMB returns the RAM this process can use, in MB. +// It checks if the memory is limited by the cgroup limit, otherwise it uses the host RAM. +func AvailableMemoryMB() uint64 { + hostMemory := memory.TotalMemory() + cgroupLimit, err := memlimit.FromCgroup() + if err == nil && cgroupLimit > 0 && cgroupLimit < hostMemory { + return cgroupLimit / megabyte + } + return hostMemory / megabyte +} diff --git a/utils/memory_test.go b/utils/memory_test.go new file mode 100644 index 0000000000..de7b94164e --- /dev/null +++ b/utils/memory_test.go @@ -0,0 +1,12 @@ +package utils_test + +import ( + "testing" + + "github.com/NethermindEth/juno/utils" + "github.com/stretchr/testify/assert" +) + +func TestAvailableMemoryMB(t *testing.T) { + assert.NotZero(t, utils.AvailableMemoryMB()) +}