From 3575389ad3597a9aed67f6c237f166b81b6498b1 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 6efbf5cc8..5318ccbaa 100644 --- a/src/executor/helpers/mod.rs +++ b/src/executor/helpers/mod.rs @@ -10,6 +10,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 8cb3ef421787fb58d37eda5c32d437b077d0150a 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 | 332 +++++++++++++++++++++++++++ 4 files changed, 345 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..439b4ee1c --- /dev/null +++ b/src/executor/memory/tunables.rs @@ -0,0 +1,332 @@ +//! Kernel knobs that stabilise memory measurements: transparent huge pages, +//! compaction/swap/NUMA-balancing sysctls, swap and the page cache. +//! +//! The selected controls follow the semantics documented by Linux: +//! +//! - [transparent huge pages](https://docs.kernel.org/admin-guide/mm/transhuge.html) +//! affect anonymous-memory page sizes and allocation behavior. +//! - [`vm.compaction_proactiveness`](https://docs.kernel.org/admin-guide/sysctl/vm.html) +//! controls proactive background compaction. +//! - [`vm.swappiness`](https://docs.kernel.org/admin-guide/sysctl/vm.html) controls +//! the VM's relative preference for swap versus filesystem paging. +//! - [`kernel.numa_balancing`](https://docs.kernel.org/admin-guide/sysctl/kernel.html) +//! controls automatic NUMA page migration. +//! - [`vm.drop_caches`](https://docs.kernel.org/admin-guide/sysctl/vm.html) drops +//! clean page cache and reclaimable slab objects. +//! +//! [`MemoryTunables`] captures the previous value of every knob it changes 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::linux_sysctl::LinuxSysctl; +use crate::executor::helpers::run_with_sudo::{can_elevate_without_prompt, run_with_sudo}; +use crate::prelude::*; +use std::fs::read_to_string; +use std::path::Path; + +/// Guard holding the previous value of every knob that was actually changed. +/// Empty when the knobs were not applied at all, 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)>, + sysctls: Vec, + /// Swap entries that were active before `swapoff -a`. + swap: Vec, +} + +impl MemoryTunables { + /// Applies the knobs on a best-effort basis: a knob 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("never"), + sysctls: Self::set_sysctls(0), + swap: Self::set_swap(false, &[]), + }; + 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}"); + } + } + + /// Writes `value` to every THP knob, returning the modes they held before, + /// keyed by path. Knobs that are absent, unreadable or already at `value` + /// are left out. + fn set_thp(value: &str) -> Vec<(String, String)> { + let mut previous = Vec::new(); + + for knob in ["enabled", "defrag"] { + let path = format!("/sys/kernel/mm/transparent_hugepage/{knob}"); + let Some(active) = read_thp_mode(&path) else { + debug!("{path} is missing or has no active mode, skipping"); + continue; + }; + if active == value { + continue; + } + + match write_root_file(&path, value) { + Ok(()) => previous.push((path, active)), + Err(error) => warn!("Failed to set transparent huge pages ({path}): {error}"), + } + } + + previous + } + + /// Sets every stabilising sysctl to `value`, returning the guards for the + /// ones that were not already there. + fn set_sysctls(value: i64) -> Vec { + let mut names = vec!["vm.compaction_proactiveness", "vm.swappiness"]; + // Absent on single-node hosts. + if Path::new("/proc/sys/kernel/numa_balancing").exists() { + names.push("kernel.numa_balancing"); + } + + let mut sysctls = Vec::new(); + for name in names { + match LinuxSysctl::set(name, value) { + Ok(sysctl) if sysctl.is_changed() => sysctls.push(sysctl), + Ok(_) => {} + Err(error) => warn!("Failed to set {name}={value}: {error}"), + } + } + + sysctls + } + + /// Disables swap one entry at a time, returning only entries that were + /// successfully disabled, or re-enables exactly `paths`. + /// + /// Restoring path by path rather than with `swapon -a` covers a swap file + /// that was activated manually and is absent from `/etc/fstab`. + fn set_swap(enabled: bool, paths: &[String]) -> Vec { + if enabled { + for path in paths { + if let Err(error) = run_with_sudo("swapon", [path]) { + warn!("Failed to re-enable swap on {path}: {error}"); + } + } + return Vec::new(); + } + + let (Ok(swaps), Ok(meminfo)) = ( + read_to_string("/proc/swaps"), + read_to_string("/proc/meminfo"), + ) else { + debug!("Leaving swap enabled: could not read /proc/swaps or /proc/meminfo"); + return Vec::new(); + }; + + let active = match swapoff_decision(&swaps, &meminfo) { + SwapoffDecision::Skip(reason) => { + debug!("Leaving swap enabled: {reason}"); + return Vec::new(); + } + SwapoffDecision::Proceed(active) => active, + }; + + let mut disabled = Vec::with_capacity(active.len()); + for path in active { + if let Err(error) = run_with_sudo("swapoff", [path.as_str()]) { + warn!("Failed to disable swap on {path}: {error}"); + break; + } + disabled.push(path); + } + + disabled + } +} + +impl Drop for MemoryTunables { + fn drop(&mut self) { + if self.thp.is_empty() && self.sysctls.is_empty() && self.swap.is_empty() { + return; + } + + start_group!("Restoring kernel memory tunables"); + Self::set_swap(true, &self.swap); + + self.sysctls.clear(); + 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}")]) +} + +#[derive(Debug, PartialEq, Eq)] +enum SwapoffDecision { + /// Swap can be disabled; carries the active entries, to re-enable them later. + Proceed(Vec), + Skip(String), +} + +/// `swapoff -a` faults every swapped page back into RAM and permanently breaks a +/// zram device (its `disksize` resets to 0 and a later `swapon` fails), so it is +/// only safe on a plain swap file/partition whose used pages fit in free memory. +/// +/// `/proc/swaps` columns: `Filename Type Size Used Priority`, first line is a header. +fn swapoff_decision(swaps: &str, meminfo: &str) -> SwapoffDecision { + let skip = |reason: &str| SwapoffDecision::Skip(reason.to_string()); + + let rows: Vec<&str> = swaps + .lines() + .skip(1) + .filter(|l| !l.trim().is_empty()) + .collect(); + if rows.is_empty() { + return skip("no swap is active"); + } + + let mut used_kib: u64 = 0; + let mut active = Vec::with_capacity(rows.len()); + for row in rows { + let fields: Vec<&str> = row.split_whitespace().collect(); + let (Some(filename), Some(used)) = (fields.first(), fields.get(3)) else { + return skip("could not determine swap usage"); + }; + if filename.contains("zram") { + return skip("zram swap device present"); + } + let Ok(used) = used.parse::() else { + return skip("could not determine swap usage"); + }; + used_kib += used; + active.push(filename.to_string()); + } + + let Some(available_kib) = parse_mem_available_kib(meminfo) else { + return skip("could not determine swap usage"); + }; + + if used_kib >= available_kib { + return skip("swapped pages do not fit in available memory"); + } + + SwapoffDecision::Proceed(active) +} + +fn parse_mem_available_kib(meminfo: &str) -> Option { + meminfo + .lines() + .find_map(|line| line.strip_prefix("MemAvailable:"))? + .split_whitespace() + .next()? + .parse() + .ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + const MEMINFO_8G: &str = "MemTotal: 16000000 kB\nMemAvailable: 8000000 kB\n"; + + #[test] + fn skips_when_no_swap_is_active() { + let swaps = "Filename\t\t\t\tType\t\tSize\t\tUsed\t\tPriority\n"; + assert_eq!( + swapoff_decision(swaps, MEMINFO_8G), + SwapoffDecision::Skip("no swap is active".to_string()) + ); + } + + #[test] + fn skips_zram_swap_devices() { + let swaps = "Filename\tType\tSize\tUsed\tPriority\n/dev/zram0 partition 8000000 1024 100\n"; + assert_eq!( + swapoff_decision(swaps, MEMINFO_8G), + SwapoffDecision::Skip("zram swap device present".to_string()) + ); + } + + #[test] + fn skips_when_swapped_pages_do_not_fit_in_memory() { + let swaps = "Filename\tType\tSize\tUsed\tPriority\n/swapfile file 16000000 8000000 -2\n"; + assert_eq!( + swapoff_decision(swaps, "MemAvailable: 4000000 kB\n"), + SwapoffDecision::Skip("swapped pages do not fit in available memory".to_string()) + ); + } + + #[test] + fn skips_malformed_rows() { + let swaps = "Filename\tType\tSize\tUsed\tPriority\n/swapfile file\n"; + assert_eq!( + swapoff_decision(swaps, MEMINFO_8G), + SwapoffDecision::Skip("could not determine swap usage".to_string()) + ); + } + + #[test] + fn reports_the_active_entries_to_restore() { + let swaps = "Filename\tType\tSize\tUsed\tPriority\n/swapfile file 16000000 1024 -2\n/swap2 file 16000000 512 -3\n"; + assert_eq!( + swapoff_decision(swaps, MEMINFO_8G), + SwapoffDecision::Proceed(vec!["/swapfile".to_string(), "/swap2".to_string()]) + ); + } + + #[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 8bfb28b25db5a6d58716eb156498a0b00ba95116 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 | 63 +++++++++++++++++-- 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, 79 insertions(+), 22 deletions(-) diff --git a/src/executor/helpers/linux_sysctl.rs b/src/executor/helpers/linux_sysctl.rs index 7e97629dd..3efb8223e 100644 --- a/src/executor/helpers/linux_sysctl.rs +++ b/src/executor/helpers/linux_sysctl.rs @@ -7,14 +7,67 @@ 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, + previous: Option, +} + +impl LinuxSysctl { + pub(crate) fn set(name: &'static str, target_value: i64) -> Result { + #[cfg(target_os = "linux")] + let previous = ensure_sysctl(name, target_value)?; + #[cfg(not(target_os = "linux"))] + let previous = { + let _ = (name, target_value); + None + }; + + Ok(Self { + #[cfg(target_os = "linux")] + 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); + } + } + } +} + +pub fn ensure_linux_profiling_sysctls() -> Result> { + let mut sysctls = Vec::new(); + + #[cfg(target_os = "linux")] + 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(()) + Ok(sysctls) } /// 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,