From ea87e77ede43afd6c6482b5f8056c3138838bc94 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 18 Aug 2026 19:27:37 +0200 Subject: [PATCH 1/3] refactor(executor): move linux_sysctl into executor helpers The sysctl read-then-write-with-sudo primitive is not walltime-specific; memory mode needs it too. --- src/executor/{wall_time/profiler => helpers}/linux_sysctl.rs | 2 +- src/executor/helpers/mod.rs | 1 + src/executor/wall_time/profiler/mod.rs | 1 - src/executor/wall_time/profiler/perf/mod.rs | 2 +- src/executor/wall_time/profiler/samply/mod.rs | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/executor/{wall_time/profiler => helpers}/linux_sysctl.rs (95%) diff --git a/src/executor/wall_time/profiler/linux_sysctl.rs b/src/executor/helpers/linux_sysctl.rs similarity index 95% rename from src/executor/wall_time/profiler/linux_sysctl.rs rename to src/executor/helpers/linux_sysctl.rs index 657581ecb..98ddac71e 100644 --- a/src/executor/wall_time/profiler/linux_sysctl.rs +++ b/src/executor/helpers/linux_sysctl.rs @@ -18,7 +18,7 @@ pub fn ensure_linux_profiling_sysctls() -> Result<()> { } #[cfg(target_os = "linux")] -fn ensure_sysctl(name: &str, target_value: i64) -> Result<()> { +pub(crate) fn ensure_sysctl(name: &str, target_value: i64) -> Result<()> { if sysctl_read(name)? == target_value { return Ok(()); } diff --git a/src/executor/helpers/mod.rs b/src/executor/helpers/mod.rs index a372a0d99..721ec697e 100644 --- a/src/executor/helpers/mod.rs +++ b/src/executor/helpers/mod.rs @@ -11,6 +11,7 @@ pub mod harvest_perf_maps_for_pids; pub mod homebrew; pub mod introspected_golang; pub mod introspected_nodejs; +pub mod linux_sysctl; pub mod profile_folder; pub mod run_command_with_log_pipe; pub mod run_with_env; diff --git a/src/executor/wall_time/profiler/mod.rs b/src/executor/wall_time/profiler/mod.rs index ab7f62cb6..07258986a 100644 --- a/src/executor/wall_time/profiler/mod.rs +++ b/src/executor/wall_time/profiler/mod.rs @@ -4,7 +4,6 @@ //! (perf, samply, instruments, ...) and produces a unified set of artifacts //! in the profile folder. -mod linux_sysctl; pub mod perf; pub mod samply; diff --git a/src/executor/wall_time/profiler/perf/mod.rs b/src/executor/wall_time/profiler/perf/mod.rs index 2c5514d24..9d799ffe7 100644 --- a/src/executor/wall_time/profiler/perf/mod.rs +++ b/src/executor/wall_time/profiler/perf/mod.rs @@ -8,13 +8,13 @@ use crate::executor::helpers::detect_executable::command_has_executable; use crate::executor::helpers::env::is_codspeed_debug_enabled; use crate::executor::helpers::env::suppress_go_perf_unwinding_warning; use crate::executor::helpers::harvest_perf_maps_for_pids::harvest_perf_maps_for_pids; +use crate::executor::helpers::linux_sysctl::ensure_linux_profiling_sysctls; use crate::executor::helpers::run_with_sudo::wrap_with_sudo; use crate::executor::shared::fifo::FifoBenchmarkData; use crate::executor::wall_time::profiler::NO_BENCHMARKS_DETECTED_WARNING; use crate::executor::wall_time::profiler::Profiler; use crate::executor::wall_time::profiler::SAMPLING_RATE_HZ; use crate::executor::wall_time::profiler::WALLTIME_METADATA_CURRENT_VERSION; -use crate::executor::wall_time::profiler::linux_sysctl::ensure_linux_profiling_sysctls; use crate::executor::wall_time::profiler::perf::perf_executable::get_working_perf_executable; use crate::prelude::*; use crate::system::SystemInfo; diff --git a/src/executor/wall_time/profiler/samply/mod.rs b/src/executor/wall_time/profiler/samply/mod.rs index 97b5bffd9..b77209ceb 100644 --- a/src/executor/wall_time/profiler/samply/mod.rs +++ b/src/executor/wall_time/profiler/samply/mod.rs @@ -4,10 +4,10 @@ use crate::cli::InternalCommands; use crate::cli::samply::SamplyArgs; use crate::executor::ExecutorConfig; use crate::executor::helpers::command::CommandBuilder; +use crate::executor::helpers::linux_sysctl::ensure_linux_profiling_sysctls; use crate::executor::helpers::run_with_sudo::wrap_with_sudo; use crate::executor::shared::fifo::FifoBenchmarkData; use crate::executor::wall_time::profiler::Profiler; -use crate::executor::wall_time::profiler::linux_sysctl::ensure_linux_profiling_sysctls; use crate::prelude::*; use crate::system::SystemInfo; use async_trait::async_trait; From 3ac6ace4258b6bc6692a673329be311cb3540b4a Mon Sep 17 00:00:00 2001 From: not-matthias Date: Wed, 19 Aug 2026 16:43:35 +0200 Subject: [PATCH 2/3] feat(memory): apply kernel memory tunables before memory-mode runs Disables transparent huge pages, sets vm.compaction_proactiveness, vm.swappiness and kernel.numa_balancing to 0, disables swap and drops the page cache, so benchmark repos no longer need a hand-written CI step. Applied only in CI, best-effort: a knob that cannot be set is a warning. swapoff is skipped on zram devices and whenever the swapped pages would not fit in available memory. --- src/executor/helpers/linux_sysctl.rs | 13 ++- src/executor/memory/executor.rs | 3 + src/executor/memory/mod.rs | 1 + src/executor/memory/tunables.rs | 138 +++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 src/executor/memory/tunables.rs diff --git a/src/executor/helpers/linux_sysctl.rs b/src/executor/helpers/linux_sysctl.rs index 98ddac71e..7e97629dd 100644 --- a/src/executor/helpers/linux_sysctl.rs +++ b/src/executor/helpers/linux_sysctl.rs @@ -17,14 +17,19 @@ pub fn ensure_linux_profiling_sysctls() -> Result<()> { Ok(()) } +/// Sets a sysctl, returning the value it held before, or `None` when it was +/// already at `target_value` and nothing was written. #[cfg(target_os = "linux")] -pub(crate) fn ensure_sysctl(name: &str, target_value: i64) -> Result<()> { - if sysctl_read(name)? == target_value { - return Ok(()); +pub(crate) fn ensure_sysctl(name: &str, target_value: i64) -> Result> { + let current_value = sysctl_read(name)?; + if current_value == target_value { + return Ok(None); } let assignment = format!("{name}={target_value}"); - run_with_sudo("sysctl", ["-w", assignment.as_str()]) + run_with_sudo("sysctl", ["-w", assignment.as_str()])?; + + Ok(Some(current_value)) } #[cfg(target_os = "linux")] diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index e12625646..b8c9a3985 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -8,6 +8,7 @@ use crate::executor::helpers::get_bench_command::get_bench_command; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe_and_callback; use crate::executor::helpers::run_with_env::prefix_command_with_env; use crate::executor::helpers::run_with_sudo::is_root_user; +use crate::executor::memory::tunables::MemoryTunables; use crate::executor::shared::fifo::RunnerFifo; use crate::executor::{ExecutionContext, Executor}; use crate::instruments::mongo_tracer::MongoTracer; @@ -159,6 +160,8 @@ impl Executor for MemoryExecutor { execution_context: &ExecutionContext, _mongo_tracer: &Option, ) -> Result<()> { + let _tunables = MemoryTunables::apply(); + // Create the results/ directory inside the profile folder to avoid having memtrack create it with wrong permissions std::fs::create_dir_all(execution_context.profile_folder.join("results"))?; diff --git a/src/executor/memory/mod.rs b/src/executor/memory/mod.rs index e0ac4745c..2d17547d1 100644 --- a/src/executor/memory/mod.rs +++ b/src/executor/memory/mod.rs @@ -1,2 +1,3 @@ pub mod executor; pub(crate) mod setup; +pub(crate) mod tunables; diff --git a/src/executor/memory/tunables.rs b/src/executor/memory/tunables.rs new file mode 100644 index 000000000..060619eb1 --- /dev/null +++ b/src/executor/memory/tunables.rs @@ -0,0 +1,138 @@ +//! Kernel controls that stabilise memory measurements: +//! +//! - [transparent huge pages](https://docs.kernel.org/admin-guide/mm/transhuge.html) +//! can allocate a 2 MiB page when a benchmark touches a small part of a +//! mapping, making its RSS depend on page-promotion timing. +//! - [`vm.drop_caches`](https://docs.kernel.org/admin-guide/sysctl/vm.html) +//! clears clean page cache and reclaimable slab objects, giving each run the +//! same cache state. +//! +//! [`MemoryTunables`] captures the previous THP setting and restores it on +//! drop, so a host that only looks like CI — `CI=true` inside a container +//! sharing the host's non-namespaced knobs, say — is left as it was. + +use crate::executor::helpers::run_with_sudo::{can_elevate_without_prompt, run_with_sudo}; +use crate::prelude::*; +use std::fs::read_to_string; + +/// Guard holding the previous THP setting when it was changed. +/// Empty when THP was not changed, making [`Drop`] a no-op. +#[derive(Debug)] +#[must_use = "the knobs are restored as soon as the guard is dropped"] +pub struct MemoryTunables { + /// THP knob path -> the mode it held before. + thp: Vec<(String, String)>, +} + +impl MemoryTunables { + /// Applies the controls on a best-effort basis: a control that cannot be + /// set is warned about, never fatal. + pub fn apply() -> Option { + // Blocking the run on an interactive password prompt would be worse than + // measuring without the knobs. + if !can_elevate_without_prompt() { + warn!( + "Cannot elevate privileges without a password prompt, skipping kernel memory tunables" + ); + return None; + } + + start_group!("Applying kernel memory tunables"); + let tunables = Self { + thp: Self::set_thp_enabled("never"), + }; + Self::drop_page_cache(); + end_group!(); + + Some(tunables) + } + + /// Drops the page cache. Nothing to restore: the node is a write-only + /// trigger and the kernel refills the cache on demand. + fn drop_page_cache() { + // drop_caches only reclaims clean objects; flush dirty buffers first. + nix::unistd::sync(); + if let Err(error) = write_root_file("/proc/sys/vm/drop_caches", "3") { + warn!("Failed to drop the page cache: {error}"); + } + } + + /// Sets the THP default mode, returning the prior mode when it changed. + fn set_thp_enabled(value: &str) -> Vec<(String, String)> { + let mut previous = Vec::new(); + + let path = "/sys/kernel/mm/transparent_hugepage/enabled"; + let Some(active) = read_thp_mode(path) else { + debug!("{path} is missing or has no active mode, skipping"); + return previous; + }; + if active == value { + return previous; + } + + match write_root_file(path, value) { + Ok(()) => previous.push((path.to_string(), active)), + Err(error) => warn!("Failed to set transparent huge pages ({path}): {error}"), + } + + previous + } +} + +impl Drop for MemoryTunables { + fn drop(&mut self) { + if self.thp.is_empty() { + return; + } + + start_group!("Restoring kernel memory tunables"); + for (path, value) in &self.thp { + if let Err(error) = write_root_file(path, value) { + warn!("Failed to restore transparent huge pages ({path}) to {value}: {error}"); + } + } + end_group!(); + } +} + +/// The active mode of a THP knob, whose value reads as `always [madvise] never`. +fn read_thp_mode(path: &str) -> Option { + let content = read_to_string(path).ok()?; + let mode = content + .split_whitespace() + .find_map(|token| token.strip_prefix('[')?.strip_suffix(']'))?; + + Some(mode.to_string()) +} + +/// Write to a root-owned /proc or /sys node. `run_with_sudo` cannot pipe stdin, +/// so the redirect happens inside a shell instead of `sudo tee`. +fn write_root_file(path: &str, value: &str) -> Result<()> { + run_with_sudo("sh", ["-c", &format!("printf '%s' {value} > {path}")]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reads_the_active_thp_mode() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("enabled"); + std::fs::write(&path, "always [madvise] never\n").unwrap(); + + assert_eq!( + read_thp_mode(path.to_str().unwrap()), + Some("madvise".to_string()) + ); + } + + #[test] + fn reports_no_thp_mode_when_none_is_active() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("enabled"); + std::fs::write(&path, "always madvise never\n").unwrap(); + + assert_eq!(read_thp_mode(path.to_str().unwrap()), None); + } +} From 87a02e97f3ca501f43bff03f5eebae1baaa08157 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 24 Aug 2026 14:58:39 +0200 Subject: [PATCH 3/3] fix(walltime): restore profiling sysctls Keep the original profiler sysctl values in the walltime executor and restore them when it is dropped. This prevents a local or containerized run from leaving host-global profiling access enabled. --- src/executor/helpers/linux_sysctl.rs | 60 +++++++++++++++++-- src/executor/tests.rs | 15 ++--- src/executor/wall_time/executor.rs | 17 +++++- src/executor/wall_time/profiler/perf/mod.rs | 3 +- src/executor/wall_time/profiler/samply/mod.rs | 3 - 5 files changed, 76 insertions(+), 22 deletions(-) diff --git a/src/executor/helpers/linux_sysctl.rs b/src/executor/helpers/linux_sysctl.rs index 7e97629dd..bde3641b8 100644 --- a/src/executor/helpers/linux_sysctl.rs +++ b/src/executor/helpers/linux_sysctl.rs @@ -7,14 +7,64 @@ use anyhow::Context; #[cfg(target_os = "linux")] use std::process::Command; -pub fn ensure_linux_profiling_sysctls() -> Result<()> { +/// Restores a sysctl to its initial value when dropped. +#[derive(Debug)] +#[must_use = "the sysctl is restored when this guard is dropped"] +pub(crate) struct LinuxSysctl { #[cfg(target_os = "linux")] - { - ensure_sysctl("kernel.kptr_restrict", 0)?; - ensure_sysctl("kernel.perf_event_paranoid", -1)?; + name: &'static str, + #[cfg(target_os = "linux")] + previous: Option, +} + +#[cfg(target_os = "linux")] +impl LinuxSysctl { + pub(crate) fn set(name: &'static str, target_value: i64) -> Result { + let previous = ensure_sysctl(name, target_value)?; + + Ok(Self { name, previous }) + } + + pub(crate) fn is_changed(&self) -> bool { + self.previous.is_some() + } +} + +impl Drop for LinuxSysctl { + fn drop(&mut self) { + #[cfg(target_os = "linux")] + { + let Some(value) = self.previous else { + return; + }; + + if let Err(error) = ensure_sysctl(self.name, value) { + warn!("Failed to restore {}={value}: {error}", self.name); + } + } } +} + +#[cfg(target_os = "linux")] +pub fn ensure_linux_profiling_sysctls() -> Result> { + let mut sysctls = Vec::new(); + + for (name, target_value) in [ + ("kernel.kptr_restrict", 0), + ("kernel.perf_event_paranoid", -1), + ] { + let sysctl = LinuxSysctl::set(name, target_value)?; + if sysctl.is_changed() { + sysctls.push(sysctl); + } + } + + Ok(sysctls) +} - Ok(()) +#[cfg(not(target_os = "linux"))] +pub fn ensure_linux_profiling_sysctls() -> Result> { + Ok(Vec::new()) } /// Sets a sysctl, returning the value it held before, or `None` when it was diff --git a/src/executor/tests.rs b/src/executor/tests.rs index d562c6c90..65507fcac 100644 --- a/src/executor/tests.rs +++ b/src/executor/tests.rs @@ -256,17 +256,8 @@ mod walltime { use crate::executor::wall_time::executor::WallTimeExecutor; async fn get_walltime_executor() -> (SemaphorePermit<'static>, WallTimeExecutor) { - static WALLTIME_INIT: OnceCell<()> = OnceCell::const_new(); static WALLTIME_SEMAPHORE: OnceCell = OnceCell::const_new(); - WALLTIME_INIT - .get_or_init(|| async { - let executor = WallTimeExecutor::new(None); - let system_info = SystemInfo::new().unwrap(); - executor.setup(&system_info, None).await.unwrap(); - }) - .await; - // We can't execute multiple walltime executors in parallel because perf isn't thread-safe (yet). We have to // use a semaphore to limit concurrent access. let semaphore = WALLTIME_SEMAPHORE @@ -274,7 +265,11 @@ mod walltime { .await; let permit = semaphore.acquire().await.unwrap(); - (permit, WallTimeExecutor::new(None)) + let executor = WallTimeExecutor::new(None); + let system_info = SystemInfo::new().unwrap(); + executor.setup(&system_info, None).await.unwrap(); + + (permit, executor) } fn walltime_config(command: &str, enable_profiler: bool) -> ExecutorConfig { diff --git a/src/executor/wall_time/executor.rs b/src/executor/wall_time/executor.rs index 810049ebd..a419f7bc3 100644 --- a/src/executor/wall_time/executor.rs +++ b/src/executor/wall_time/executor.rs @@ -10,6 +10,7 @@ use crate::executor::config::WalltimeProfiler; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; +use crate::executor::helpers::linux_sysctl::{LinuxSysctl, ensure_linux_profiling_sysctls}; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe_and_callback; use crate::executor::helpers::run_with_env::wrap_with_env; @@ -37,6 +38,8 @@ pub struct WallTimeExecutor { /// Stashed by [`Executor::run`] and consumed by [`Executor::teardown`] to /// hand the run's outputs to [`Profiler::finalize`]. benchmark_state: OnceCell<(FifoBenchmarkData, ExecutionTimestamps)>, + + profiling_sysctls: OnceCell>, } fn select_profiler(profiler_override: Option) -> Option> { @@ -52,6 +55,7 @@ impl WallTimeExecutor { Self { profiler: select_profiler(profiler_override), benchmark_state: OnceCell::new(), + profiling_sysctls: OnceCell::new(), } } @@ -107,8 +111,16 @@ impl Executor for WallTimeExecutor { } async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { - if let Some(profiler) = &self.profiler { - profiler.setup(system_info, setup_cache_dir).await?; + let Some(profiler) = &self.profiler else { + return Ok(()); + }; + + profiler.setup(system_info, setup_cache_dir).await?; + if self.profiling_sysctls.get().is_none() { + let sysctls = ensure_linux_profiling_sysctls()?; + self.profiling_sysctls + .set(sysctls) + .map_err(|_| anyhow!("profiling sysctls were initialized concurrently"))?; } Ok(()) } @@ -134,6 +146,7 @@ impl Executor for WallTimeExecutor { let Self { profiler, benchmark_state, + .. } = self; let status = match profiler.as_mut() { diff --git a/src/executor/wall_time/profiler/perf/mod.rs b/src/executor/wall_time/profiler/perf/mod.rs index 9d799ffe7..8816e5fb0 100644 --- a/src/executor/wall_time/profiler/perf/mod.rs +++ b/src/executor/wall_time/profiler/perf/mod.rs @@ -8,7 +8,6 @@ use crate::executor::helpers::detect_executable::command_has_executable; use crate::executor::helpers::env::is_codspeed_debug_enabled; use crate::executor::helpers::env::suppress_go_perf_unwinding_warning; use crate::executor::helpers::harvest_perf_maps_for_pids::harvest_perf_maps_for_pids; -use crate::executor::helpers::linux_sysctl::ensure_linux_profiling_sysctls; use crate::executor::helpers::run_with_sudo::wrap_with_sudo; use crate::executor::shared::fifo::FifoBenchmarkData; use crate::executor::wall_time::profiler::NO_BENCHMARKS_DETECTED_WARNING; @@ -83,7 +82,7 @@ impl Profiler for PerfProfiler { setup_cache_dir: Option<&Path>, ) -> anyhow::Result<()> { setup::install_perf(system_info, setup_cache_dir).await?; - ensure_linux_profiling_sysctls() + Ok(()) } async fn wrap_command( diff --git a/src/executor/wall_time/profiler/samply/mod.rs b/src/executor/wall_time/profiler/samply/mod.rs index b77209ceb..3d77e7ade 100644 --- a/src/executor/wall_time/profiler/samply/mod.rs +++ b/src/executor/wall_time/profiler/samply/mod.rs @@ -4,7 +4,6 @@ use crate::cli::InternalCommands; use crate::cli::samply::SamplyArgs; use crate::executor::ExecutorConfig; use crate::executor::helpers::command::CommandBuilder; -use crate::executor::helpers::linux_sysctl::ensure_linux_profiling_sysctls; use crate::executor::helpers::run_with_sudo::wrap_with_sudo; use crate::executor::shared::fifo::FifoBenchmarkData; use crate::executor::wall_time::profiler::Profiler; @@ -58,8 +57,6 @@ impl Profiler for SamplyProfiler { _system_info: &SystemInfo, _setup_cache_dir: Option<&Path>, ) -> anyhow::Result<()> { - ensure_linux_profiling_sysctls()?; - // samply can't profile Apple-signed bash. Only do the brew dance if the // bash that samply would actually exec (the first `bash` on PATH) is // signed; if a compatible (ad-hoc-signed) bash is already first on PATH,