Skip to content

Commit 14f2a9f

Browse files
committed
Make jemalloc memory profiling opt-in via --enable-memory-profiling flag
1 parent 163e65e commit 14f2a9f

15 files changed

Lines changed: 304 additions & 100 deletions

File tree

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ARG binaries=
1616
ARG copy=${binaries:+_copy}
1717
ARG build_flag=--release
1818
ARG build_folder=release
19-
ARG build_features=scylladb,metrics,memory-profiling
19+
ARG build_features=scylladb,metrics,jemalloc
2020
ARG rustflags="-C force-frame-pointers=yes"
2121

2222
FROM rust:1.74-slim-bookworm AS builder

linera-faucet/server/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ where
807807
pending_requests: Arc<Mutex<VecDeque<PendingRequest>>>,
808808
request_notifier: Arc<Notify>,
809809
max_batch_size: usize,
810+
enable_memory_profiling: bool,
810811
}
811812

812813
impl<C> Clone for FaucetService<C>
@@ -833,6 +834,7 @@ where
833834
pending_requests: Arc::clone(&self.pending_requests),
834835
request_notifier: Arc::clone(&self.request_notifier),
835836
max_batch_size: self.max_batch_size,
837+
enable_memory_profiling: self.enable_memory_profiling,
836838
}
837839
}
838840
}
@@ -848,6 +850,7 @@ pub struct FaucetConfig {
848850
pub chain_listener_config: ChainListenerConfig,
849851
pub storage_path: PathBuf,
850852
pub max_batch_size: usize,
853+
pub enable_memory_profiling: bool,
851854
}
852855

853856
impl<C> FaucetService<C>
@@ -901,6 +904,7 @@ where
901904
pending_requests,
902905
request_notifier,
903906
max_batch_size: config.max_batch_size,
907+
enable_memory_profiling: config.enable_memory_profiling,
904908
})
905909
}
906910

@@ -937,7 +941,12 @@ where
937941
let index_handler = axum::routing::get(graphiql).post(Self::index_handler);
938942

939943
#[cfg(feature = "metrics")]
940-
monitoring_server::start_metrics(self.metrics_address(), cancellation_token.clone());
944+
monitoring_server::start_metrics_with_profiling(
945+
self.metrics_address(),
946+
cancellation_token.clone(),
947+
self.enable_memory_profiling,
948+
)
949+
.await;
941950

942951
let app = Router::new()
943952
.route("/", index_handler)

linera-metrics/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ version.workspace = true
1515
workspace = true
1616

1717
[features]
18-
memory-profiling = ["jemalloc_pprof"]
18+
jemalloc = ["jemalloc_pprof"]
1919

2020
[dependencies]
2121
anyhow.workspace = true

linera-metrics/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
66
pub mod monitoring_server;
77

8-
#[cfg(feature = "memory-profiling")]
8+
#[cfg(feature = "jemalloc")]
99
pub mod memory_profiler;

linera-metrics/src/memory_profiler.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,33 @@ pub enum MemoryProfilerError {
2222
ProfCtlNotAvailable,
2323
#[error("another profiler is already running")]
2424
AnotherProfilerAlreadyRunning,
25+
#[error("failed to activate jemalloc profiling: {0}")]
26+
ActivationFailed(String),
2527
}
2628

2729
/// Memory profiler using safe jemalloc_pprof wrapper (pull model only)
2830
pub struct MemoryProfiler;
2931

3032
impl MemoryProfiler {
33+
/// Activates jemalloc profiling at runtime.
34+
///
35+
/// This enables sampling (prof_active) which is off by default to avoid
36+
/// the libgcc DWARF unwinder livelock (jemalloc/jemalloc#2282).
37+
pub async fn activate() -> Result<(), MemoryProfilerError> {
38+
if let Some(prof_ctl) = PROF_CTL.as_ref() {
39+
let mut prof_ctl = prof_ctl.lock().await;
40+
41+
prof_ctl
42+
.activate()
43+
.map_err(|e| MemoryProfilerError::ActivationFailed(e.to_string()))?;
44+
45+
info!("jemalloc memory profiling activated");
46+
Ok(())
47+
} else {
48+
Err(MemoryProfilerError::ProfCtlNotAvailable)
49+
}
50+
}
51+
3152
pub fn check_prof_ctl() -> Result<(), MemoryProfilerError> {
3253
// Check if jemalloc profiling is available
3354
if let Some(prof_ctl) = PROF_CTL.as_ref() {

linera-metrics/src/monitoring_server.rs

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,81 @@ use tokio::net::ToSocketAddrs;
88
use tokio_util::sync::CancellationToken;
99
use tracing::info;
1010

11-
#[cfg(feature = "memory-profiling")]
11+
#[cfg(feature = "jemalloc")]
1212
use crate::memory_profiler::MemoryProfiler;
1313

14-
pub fn start_metrics(
15-
address: impl ToSocketAddrs + Debug + Send + 'static,
16-
shutdown_signal: CancellationToken,
17-
) {
18-
#[cfg(feature = "memory-profiling")]
19-
let app = {
20-
// Try to add memory profiling endpoint
21-
match MemoryProfiler::check_prof_ctl() {
22-
Ok(()) => {
23-
info!("Memory profiling available, enabling /debug/pprof and /debug/flamegraph endpoints");
24-
Router::new()
25-
.route("/metrics", get(serve_metrics))
26-
.route("/debug/pprof", get(MemoryProfiler::heap_profile))
27-
.route("/debug/flamegraph", get(MemoryProfiler::heap_flamegraph))
28-
}
14+
/// Whether memory profiling endpoints should be enabled on the metrics server.
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16+
pub enum MemoryProfiling {
17+
Enabled,
18+
Disabled,
19+
}
20+
21+
impl From<bool> for MemoryProfiling {
22+
fn from(enabled: bool) -> Self {
23+
if enabled {
24+
MemoryProfiling::Enabled
25+
} else {
26+
MemoryProfiling::Disabled
27+
}
28+
}
29+
}
30+
31+
impl MemoryProfiling {
32+
/// Attempts to activate jemalloc memory profiling if requested.
33+
///
34+
/// Returns `MemoryProfiling::Enabled` only if activation succeeds. If the flag is
35+
/// false, or jemalloc is not compiled in, or activation fails (e.g. `MALLOC_CONF` not
36+
/// set), returns `MemoryProfiling::Disabled` with an appropriate warning.
37+
#[cfg(feature = "jemalloc")]
38+
pub async fn try_activate(requested: bool) -> Self {
39+
if !requested {
40+
return MemoryProfiling::Disabled;
41+
}
42+
match MemoryProfiler::activate().await {
43+
Ok(()) => MemoryProfiling::Enabled,
2944
Err(e) => {
3045
tracing::warn!(
31-
"Memory profiling not available: {}, serving metrics-only",
46+
"--enable-memory-profiling was passed but profiling could not be activated: {}. \
47+
Set MALLOC_CONF=\"prof:true,prof_active:false,lg_prof_sample:19\" before starting the binary.",
3248
e
3349
);
34-
Router::new().route("/metrics", get(serve_metrics))
50+
MemoryProfiling::Disabled
3551
}
3652
}
37-
};
53+
}
3854

39-
#[cfg(not(feature = "memory-profiling"))]
40-
let app = Router::new().route("/metrics", get(serve_metrics));
55+
/// Non-jemalloc fallback: always returns `Disabled`, warns if profiling was requested.
56+
#[cfg(not(feature = "jemalloc"))]
57+
pub async fn try_activate(requested: bool) -> Self {
58+
if requested {
59+
tracing::warn!(
60+
"--enable-memory-profiling was passed but the jemalloc feature is not compiled in"
61+
);
62+
}
63+
MemoryProfiling::Disabled
64+
}
65+
}
66+
67+
/// Activates memory profiling if requested and starts the metrics server.
68+
///
69+
/// This is the single entry point that handles both activation and server startup,
70+
/// avoiding duplication across binaries.
71+
pub async fn start_metrics_with_profiling(
72+
address: impl ToSocketAddrs + Debug + Send + 'static,
73+
shutdown_signal: CancellationToken,
74+
enable_memory_profiling: bool,
75+
) {
76+
let memory_profiling = MemoryProfiling::try_activate(enable_memory_profiling).await;
77+
start_metrics(address, shutdown_signal, memory_profiling);
78+
}
79+
80+
pub fn start_metrics(
81+
address: impl ToSocketAddrs + Debug + Send + 'static,
82+
shutdown_signal: CancellationToken,
83+
memory_profiling: MemoryProfiling,
84+
) {
85+
let app = metrics_router(memory_profiling);
4186

4287
tokio::spawn(async move {
4388
let listener = tokio::net::TcpListener::bind(address)
@@ -55,6 +100,36 @@ pub fn start_metrics(
55100
});
56101
}
57102

103+
fn metrics_router(memory_profiling: MemoryProfiling) -> Router {
104+
#[cfg(feature = "jemalloc")]
105+
if memory_profiling == MemoryProfiling::Enabled {
106+
match MemoryProfiler::check_prof_ctl() {
107+
Ok(()) => {
108+
info!("Memory profiling enabled, registering /debug/pprof and /debug/flamegraph endpoints");
109+
return Router::new()
110+
.route("/metrics", get(serve_metrics))
111+
.route("/debug/pprof", get(MemoryProfiler::heap_profile))
112+
.route("/debug/flamegraph", get(MemoryProfiler::heap_flamegraph));
113+
}
114+
Err(e) => {
115+
tracing::warn!(
116+
"Memory profiling requested but not available: {}, serving metrics-only",
117+
e
118+
);
119+
}
120+
}
121+
}
122+
123+
#[cfg(not(feature = "jemalloc"))]
124+
if memory_profiling == MemoryProfiling::Enabled {
125+
tracing::warn!(
126+
"Memory profiling requested but jemalloc feature is not compiled in, serving metrics-only"
127+
);
128+
}
129+
130+
Router::new().route("/metrics", get(serve_metrics))
131+
}
132+
58133
async fn serve_metrics() -> Result<String, AxumError> {
59134
let metric_families = prometheus::gather();
60135
Ok(prometheus::TextEncoder::new()

linera-service/Cargo.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,10 @@ metrics = [
5252
"linera-faucet-server/metrics",
5353
"linera-metrics",
5454
]
55-
jemalloc = ["tikv-jemallocator"]
56-
memory-profiling = [
57-
"metrics",
58-
"jemalloc",
55+
jemalloc = [
56+
"tikv-jemallocator",
5957
"tikv-jemallocator/profiling",
60-
"linera-metrics/memory-profiling",
58+
"linera-metrics/jemalloc",
6159
]
6260
storage-service = ["linera-storage-service"]
6361
opentelemetry = ["linera-rpc/opentelemetry"]

linera-service/src/cli/main.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,6 @@
88
#[global_allocator]
99
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
1010

11-
// jemalloc configuration for memory profiling with jemalloc_pprof
12-
// prof:true,prof_active:true - Enable profiling from start
13-
// lg_prof_sample:19 - Sample every 512KB for good detail/overhead balance
14-
15-
// Linux/other platforms: use unprefixed malloc (with unprefixed_malloc_on_supported_platforms)
16-
#[cfg(all(feature = "memory-profiling", not(target_os = "macos")))]
17-
#[allow(non_upper_case_globals)]
18-
#[export_name = "malloc_conf"]
19-
pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
20-
21-
// macOS: use prefixed malloc (without unprefixed_malloc_on_supported_platforms)
22-
#[cfg(all(feature = "memory-profiling", target_os = "macos"))]
23-
#[allow(non_upper_case_globals)]
24-
#[export_name = "_rjem_malloc_conf"]
25-
pub static malloc_conf: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
26-
2711
mod options;
2812
use std::{
2913
collections::{BTreeMap, BTreeSet},
@@ -827,6 +811,7 @@ impl Runnable for Job {
827811
monitoring_server::start_metrics(
828812
metrics_address,
829813
shutdown_notifier.clone(),
814+
monitoring_server::MemoryProfiling::Disabled,
830815
);
831816
}
832817

@@ -1135,6 +1120,7 @@ impl Runnable for Job {
11351120
monitoring_server::start_metrics(
11361121
metrics_address,
11371122
shutdown_notifier.clone(),
1123+
monitoring_server::MemoryProfiling::Disabled,
11381124
);
11391125
}
11401126

@@ -1319,6 +1305,7 @@ impl Runnable for Job {
13191305
query_cache_size,
13201306
query_subscriptions,
13211307
cancellation_token.clone(),
1308+
options.enable_memory_profiling(),
13221309
);
13231310
service.run(cancellation_token, command_receiver).await?;
13241311
}
@@ -1351,6 +1338,7 @@ impl Runnable for Job {
13511338
u64::try_from(et.timestamp_micros()).expect("End timestamp before 1970");
13521339
Timestamp::from(micros)
13531340
});
1341+
13541342
let config = FaucetConfig {
13551343
port,
13561344
#[cfg(with_metrics)]
@@ -1362,6 +1350,7 @@ impl Runnable for Job {
13621350
chain_listener_config: config,
13631351
storage_path,
13641352
max_batch_size,
1353+
enable_memory_profiling: options.enable_memory_profiling(),
13651354
};
13661355
let faucet = FaucetService::new(config, context).await?;
13671356
let cancellation_token = CancellationToken::new();

linera-service/src/cli/options.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ pub struct Options {
9090
)]
9191
pub execution_state_cache_size: usize,
9292

93+
/// Enable jemalloc memory profiling endpoints on the metrics server.
94+
#[cfg(feature = "jemalloc")]
95+
#[arg(long, env = "LINERA_ENABLE_MEMORY_PROFILING")]
96+
pub enable_memory_profiling: bool,
97+
9398
/// Subcommand.
9499
#[command(subcommand)]
95100
pub command: ClientCommand,
@@ -100,6 +105,17 @@ impl Options {
100105
<Options as clap::Parser>::parse()
101106
}
102107

108+
pub fn enable_memory_profiling(&self) -> bool {
109+
#[cfg(feature = "jemalloc")]
110+
{
111+
self.enable_memory_profiling
112+
}
113+
#[cfg(not(feature = "jemalloc"))]
114+
{
115+
false
116+
}
117+
}
118+
103119
pub async fn create_client_context<S, Si>(
104120
&self,
105121
storage: S,

0 commit comments

Comments
 (0)