diff --git a/Cargo.lock b/Cargo.lock index 13dbb7e3..1ec1c042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -55,6 +70,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if 1.0.4", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -656,6 +672,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if 1.0.4", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base-x" version = "0.2.11" @@ -1835,6 +1866,25 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1953,6 +2003,7 @@ dependencies = [ "spawned-concurrency", "spawned-rt", "thiserror 2.0.17", + "tikv-jemallocator", "tokio", "tracing", "tracing-subscriber", @@ -2048,6 +2099,7 @@ dependencies = [ "ethlambda-storage", "ethlambda-types", "http-body-util", + "jemalloc_pprof", "serde", "serde_json", "tokio", @@ -2482,6 +2534,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2711,6 +2773,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "git2" version = "0.20.3" @@ -3250,6 +3318,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inferno" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90807d610575744524d9bdc69f3885d96f0e6c3354565b0828354a7ff2a262b8" +dependencies = [ + "ahash", + "clap", + "crossbeam-channel 0.5.15", + "crossbeam-utils 0.8.21", + "dashmap", + "env_logger", + "indexmap", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml", + "rgb", + "str_stack", +] + [[package]] name = "inout" version = "0.1.4" @@ -3335,6 +3425,23 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jemalloc_pprof" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d44c349cfe2654897fadcb9de4f0bfbf48288ec344f700b2bd59f152dd209" +dependencies = [ + "anyhow", + "libc", + "mappings", + "once_cell", + "pprof_util", + "tempfile", + "tikv-jemalloc-ctl", + "tokio", + "tracing", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4397,6 +4504,19 @@ dependencies = [ "malachite-nz", ] +[[package]] +name = "mappings" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bab1e61a4b76757edb59cd81fcaa7f3ba9018d43b527d9abfad877b4c6c60f2" +dependencies = [ + "anyhow", + "libc", + "once_cell", + "pprof_util", + "tracing", +] + [[package]] name = "match-lookup" version = "0.1.2" @@ -4466,6 +4586,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -4694,6 +4824,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.3.3" @@ -4715,12 +4859,31 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4730,6 +4893,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4781,6 +4966,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -5535,6 +5729,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pprof_util" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea0cc524de808a6d98d192a3d99fe95617031ad4a52ec0a0f987ef4432e8fe1" +dependencies = [ + "anyhow", + "backtrace", + "flate2", + "inferno", + "num", + "paste", + "prost", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -5652,6 +5861,29 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "protobuf" version = "3.7.2" @@ -5728,6 +5960,15 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -6088,6 +6329,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" @@ -6223,6 +6473,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -6615,6 +6871,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simdutf8" version = "0.1.5" @@ -6817,6 +7079,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + [[package]] name = "strength_reduce" version = "0.2.4" @@ -7044,6 +7312,37 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tikv-jemalloc-ctl" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "661f1f6a57b3a36dc9174a2c10f19513b4866816e13425d3e418b11cc37bc24c" +dependencies = [ + "libc", + "paste", + "tikv-jemalloc-sys", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + [[package]] name = "time" version = "0.3.45" diff --git a/Cargo.toml b/Cargo.toml index 1648a2bd..c5f2fc35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,3 +74,7 @@ rand = "0.9" rocksdb = "0.24" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } eyre = "0.6" + +# Allocator + heap profiling +tikv-jemallocator = { version = "0.6", features = ["stats", "unprefixed_malloc_on_supported_platforms", "profiling"] } +jemalloc_pprof = { version = "0.8", features = ["flamegraph"] } diff --git a/bin/ethlambda/Cargo.toml b/bin/ethlambda/Cargo.toml index 7a897680..2ecb780d 100644 --- a/bin/ethlambda/Cargo.toml +++ b/bin/ethlambda/Cargo.toml @@ -28,5 +28,7 @@ reqwest.workspace = true thiserror.workspace = true eyre.workspace = true +tikv-jemallocator.workspace = true + [build-dependencies] vergen-git2.workspace = true diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 7b68abad..984b20f2 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -1,6 +1,15 @@ mod checkpoint_sync; mod version; +#[cfg(not(target_env = "msvc"))] +#[global_allocator] +static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + +#[cfg(not(target_env = "msvc"))] +#[allow(non_upper_case_globals)] +#[unsafe(export_name = "malloc_conf")] +static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0"; + use std::{ collections::{BTreeMap, HashMap}, net::{IpAddr, SocketAddr}, @@ -80,6 +89,10 @@ async fn main() -> eyre::Result<()> { println!("{ASCII_ART}"); info!(version = version::CLIENT_VERSION, "Starting ethlambda"); + #[cfg(not(target_env = "msvc"))] + info!("Using jemalloc allocator with heap profiling enabled"); + #[cfg(target_env = "msvc")] + info!("Using system allocator (MSVC target)"); info!(node_key=?options.node_key, "got node key"); diff --git a/crates/net/rpc/Cargo.toml b/crates/net/rpc/Cargo.toml index 65690623..191aaecc 100644 --- a/crates/net/rpc/Cargo.toml +++ b/crates/net/rpc/Cargo.toml @@ -19,6 +19,7 @@ ethlambda-storage.workspace = true ethlambda-types.workspace = true serde.workspace = true serde_json.workspace = true +jemalloc_pprof.workspace = true [dev-dependencies] ethlambda-types.workspace = true diff --git a/crates/net/rpc/src/heap_profiling.rs b/crates/net/rpc/src/heap_profiling.rs new file mode 100644 index 00000000..cadef653 --- /dev/null +++ b/crates/net/rpc/src/heap_profiling.rs @@ -0,0 +1,74 @@ +//! Heap profiling endpoints backed by jemalloc's built-in profiler. +//! +//! Returns pprof-format heap profiles at `/debug/pprof/allocs` and interactive +//! SVG flamegraphs at `/debug/pprof/allocs/flamegraph`. Only functional on Linux; +//! other platforms return 501 Not Implemented. + +#[cfg(target_os = "linux")] +mod inner { + use axum::{http::StatusCode, response::IntoResponse}; + + pub async fn handle_get_heap() -> impl IntoResponse { + let Some(prof_ctl) = jemalloc_pprof::PROF_CTL.as_ref() else { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Heap profiling not enabled", + ) + .into_response(); + }; + let mut guard = prof_ctl.lock().await; + match guard.dump_pprof() { + Ok(pprof) => ( + StatusCode::OK, + [("content-type", "application/octet-stream")], + pprof, + ) + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to dump heap profile: {err}"), + ) + .into_response(), + } + } + + pub async fn handle_get_heap_flamegraph() -> impl IntoResponse { + let Some(prof_ctl) = jemalloc_pprof::PROF_CTL.as_ref() else { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Heap profiling not enabled", + ) + .into_response(); + }; + let mut guard = prof_ctl.lock().await; + match guard.dump_flamegraph() { + Ok(svg) => (StatusCode::OK, [("content-type", "image/svg+xml")], svg).into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to dump flamegraph: {err}"), + ) + .into_response(), + } + } +} + +#[cfg(not(target_os = "linux"))] +mod inner { + use axum::{http::StatusCode, response::IntoResponse}; + + pub async fn handle_get_heap() -> impl IntoResponse { + ( + StatusCode::NOT_IMPLEMENTED, + "Heap profiling is only available on Linux", + ) + } + + pub async fn handle_get_heap_flamegraph() -> impl IntoResponse { + ( + StatusCode::NOT_IMPLEMENTED, + "Heap profiling is only available on Linux", + ) + } +} + +pub use inner::{handle_get_heap, handle_get_heap_flamegraph}; diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index 40048aee..487af7fd 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -8,13 +8,18 @@ pub(crate) const JSON_CONTENT_TYPE: &str = "application/json; charset=utf-8"; pub(crate) const SSZ_CONTENT_TYPE: &str = "application/octet-stream"; mod fork_choice; +mod heap_profiling; pub mod metrics; pub async fn start_rpc_server(address: SocketAddr, store: Store) -> Result<(), std::io::Error> { let metrics_router = metrics::start_prometheus_metrics_api(); let api_router = build_api_router(store); + let debug_router = build_debug_router(); - let app = Router::new().merge(metrics_router).merge(api_router); + let app = Router::new() + .merge(metrics_router) + .merge(api_router) + .merge(debug_router); let listener = tokio::net::TcpListener::bind(address).await?; axum::serve(listener, app).await?; @@ -38,6 +43,16 @@ fn build_api_router(store: Store) -> Router { .with_state(store) } +/// Build the debug router for profiling endpoints. +fn build_debug_router() -> Router { + Router::new() + .route("/debug/pprof/allocs", get(heap_profiling::handle_get_heap)) + .route( + "/debug/pprof/allocs/flamegraph", + get(heap_profiling::handle_get_heap_flamegraph), + ) +} + async fn get_latest_finalized_state( axum::extract::State(store): axum::extract::State, ) -> impl IntoResponse {