Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `CANDIDATE` transaction status is deprecated and will never be returned by gateway or pathfinder. Using it as a filter in `subscribeNewTransactions` will fall back to `PRE_CONFIRMED` with a warning.
- The `blockifier` and `starknet_api` crates have been upgraded to 0.19.0-rc.2.
- The default value of `--rpc.compiler.concurrency-limit` is now bound to system memory via this equation: `min(floor((total_ram - margin) / compiler_max_memory_usage), num_cpu_cores)`, where:
- `margin` is the value of the new CLI option `--compiler.concurrency-memory-margin-mib`, which defaults to 4GiB,
- `compiler_max_memory_usage` is the value of the `--compiler.max-memory-usage-mib` CLI option, which defaults to 4GiB.

## [0.22.5] - 2026-06-08

Expand Down
92 changes: 92 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ starknet-types-core = "=0.2.4"
# This one needs to match the version used by blockifier
starknet_api = { version = "0.19.0-rc.2" }
syn = "2.0"
sysinfo = "0.39.3"
tempfile = "3.27"
test-log = { version = "0.2.20", features = ["trace"] }
thiserror = "2.0.18"
Expand Down
1 change: 1 addition & 0 deletions crates/pathfinder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ sha3 = { workspace = true }
starknet-gateway-client = { path = "../gateway-client" }
starknet-gateway-types = { path = "../gateway-types" }
starknet_api = { workspace = true }
sysinfo = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tikv-jemallocator = { workspace = true }
Expand Down
162 changes: 157 additions & 5 deletions crates/pathfinder/src/bin/pathfinder/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![deny(rust_2018_idioms)]

use std::net::SocketAddr;
use std::num::NonZeroU32;
use std::num::{NonZeroU32, NonZeroUsize};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
Expand Down Expand Up @@ -181,7 +181,7 @@ Hint: This is usually caused by exceeding the file descriptor limit of your syst
)?;

let execution_storage_pool_size = config.execution_concurrency.unwrap_or_else(|| {
std::num::NonZeroU32::new(available_parallelism.get() as u32)
NonZeroU32::new(available_parallelism.get() as u32)
.expect("The number of CPU cores should be non-zero")
});
let execution_storage = storage_manager
Expand Down Expand Up @@ -250,6 +250,9 @@ Hint: This is usually caused by exceeding the file descriptor limit of your syst
PendingDataCache::new().with_inactivity_timeout(config.pre_confirmed_idle_timeout),
);

let compiler_concurrency_limit =
determine_compiler_concurrency_limit(&config, available_parallelism)?;

let rpc_config = pathfinder_rpc::context::RpcConfig {
request_max_size: config.rpc_request_max_size,
request_timeout: config.rpc_request_timeout,
Expand All @@ -268,9 +271,7 @@ Hint: This is usually caused by exceeding the file descriptor limit of your syst
submission_tracker_time_limit: config.submission_tracker_time_limit,
submission_tracker_size_limit: config.submission_tracker_size_limit,
block_trace_cache_size: config.rpc_block_trace_cache_size,
compiler_concurrency_limit: config
.compiler_concurrency_limit
.unwrap_or(available_parallelism),
compiler_concurrency_limit,
compiler_resource_limits: config.compiler_resource_limits,
blockifier_libfuncs: config.blockifier_libfuncs,
};
Expand Down Expand Up @@ -612,6 +613,84 @@ fn compile_main(config: CompileConfig) -> anyhow::Result<()> {
.context("writing CASM to stdout")
}

fn determine_compiler_concurrency_limit(
config: &config::Config,
available_parallelism: NonZeroUsize,
) -> anyhow::Result<NonZeroUsize> {
if let Some(limit) = config.compiler_concurrency_limit {
return Ok(limit);
}

let mut system = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing()
.with_memory(sysinfo::MemoryRefreshKind::nothing().with_ram()),
);
system.refresh_memory_specifics(sysinfo::MemoryRefreshKind::nothing().with_ram());
let total_memory_bytes = system.total_memory();

compute_compiler_concurrency_limit(
total_memory_bytes,
config.compiler_resource_limits.memory_usage,
config.compiler_concurrency_memory_margin,
available_parallelism,
config.is_rpc_enabled,
)
}

/// Computes the compiler concurrency limit from the available memory and CPUs.
///
/// `concurrency_limit = min(floor((system_ram - margin) / compiler_ram_limit),
/// n_cpus)`
///
/// Returns an error only when the computed limit is 0 and the RPC server is
/// enabled, since a running RPC server requires at least one compiler. When the
/// limit is 0 but RPC is disabled, a dummy value of 1 is returned, because a
/// valid RPC config still needs to be constructed even though the RPC server
/// will not be started.
fn compute_compiler_concurrency_limit(
total_memory_bytes: u64,
compiler_memory_limit_bytes: u64,
compiler_concurrency_memory_margin_bytes: u64,
available_parallelism: NonZeroUsize,
is_rpc_enabled: bool,
) -> anyhow::Result<NonZeroUsize> {
// concurrency_limit = min(floor((system_ram - margin) / compiler_ram_limit),
// n_cpus)
let concurrency_limit = total_memory_bytes
.saturating_sub(compiler_concurrency_memory_margin_bytes)
/ compiler_memory_limit_bytes;
let num_cpus = u64::try_from(available_parallelism.get())?;
let concurrency_limit = usize::try_from(num_cpus.min(concurrency_limit))?;

const BYTES_IN_MIB: u64 = 1024 * 1024;
let total_memory_mib = total_memory_bytes / BYTES_IN_MIB;
let compiler_memory_limit_mib = compiler_memory_limit_bytes / BYTES_IN_MIB;
let margin_mib = compiler_concurrency_memory_margin_bytes / BYTES_IN_MIB;

if concurrency_limit == 0 {
anyhow::ensure!(
!is_rpc_enabled,
"Computed compiler concurrency limit is 0, which is insufficient for a running RPC \
server. Please decrease the compiler memory usage limit ({compiler_memory_limit_mib} \
MiB), increase system memory ({total_memory_mib} MiB), decrease normal operation \
margin ({margin_mib} MiB), set a fixed compiler concurrency limit via \
--rpc.compiler.concurrency-limit, or disable the RPC server via --rpc.enable=false."
);

// Use a dummy value, the RPC server will not be started anyway, yet we need to
// construct a valid RPC config beforehand
Ok(NonZeroUsize::new(1).expect("1>0"))
} else {
tracing::info!(
"Computed compiler concurrency limit: {concurrency_limit}, based on total memory \
({total_memory_mib} MiB), compiler memory limit ({compiler_memory_limit_mib} MiB), \
normal operation margin ({margin_mib} MiB), and number of CPUs ({num_cpus})"
);

Ok(NonZeroUsize::new(concurrency_limit).expect("Nonzero value"))
}
}

#[cfg(feature = "tokio-console")]
fn setup_tracing(color: config::Color, pretty_log: bool, json_log: bool) {
use tracing_subscriber::prelude::*;
Expand Down Expand Up @@ -1207,3 +1286,76 @@ fn handle_critical_task_result(
}
}
}

#[cfg(test)]
mod tests {
use std::num::NonZeroUsize;

use super::compute_compiler_concurrency_limit;

const GIB: u64 = 1024 * 1024 * 1024;

/// The default compiler concurrency memory margin in the config.
const MARGIN: u64 = 4 * GIB;

fn nonzero(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).unwrap()
}

#[test]
fn limited_by_memory() {
// (31 - 4) / 4 = 6, less than the 32 CPUs.
let limit =
compute_compiler_concurrency_limit(31 * GIB, 4 * GIB, MARGIN, nonzero(32), true)
.unwrap();
assert_eq!(limit.get(), 6);
}

#[test]
fn limited_by_cpus() {
// (256 - 4) / 4 = 63, but only 8 CPUs are available.
let limit =
compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, MARGIN, nonzero(8), true)
.unwrap();
assert_eq!(limit.get(), 8);
}

#[test]
fn zero_limit_with_rpc_enabled_returns_error() {
// (5 - 4) / 4 = 0, and RPC is enabled, so this is an error.
compute_compiler_concurrency_limit(5 * GIB, 4 * GIB, MARGIN, nonzero(32), true)
.unwrap_err();
}

#[test]
fn zero_limit_with_rpc_disabled_returns_dummy() {
// (5 - 4) / 4 = 0, but RPC is disabled, so a dummy value of 1 is returned.
let limit =
compute_compiler_concurrency_limit(5 * GIB, 4 * GIB, MARGIN, nonzero(32), false)
.unwrap();
assert_eq!(limit.get(), 1);
}

#[test]
fn memory_below_margin_with_rpc_enabled_returns_error() {
// Total memory below the margin saturates to 0, yielding a 0 limit.
compute_compiler_concurrency_limit(3 * GIB, 4 * GIB, MARGIN, nonzero(32), true)
.unwrap_err();
}

#[test]
fn memory_below_margin_with_rpc_disabled_returns_dummy() {
let limit =
compute_compiler_concurrency_limit(3 * GIB, 4 * GIB, MARGIN, nonzero(32), false)
.unwrap();
assert_eq!(limit.get(), 1);
}

#[test]
fn single_cpu_caps_limit() {
let limit =
compute_compiler_concurrency_limit(256 * GIB, 4 * GIB, MARGIN, nonzero(1), true)
.unwrap();
assert_eq!(limit.get(), 1);
}
}
Loading
Loading