diff --git a/src/executor/helpers/linux_sysctl.rs b/src/executor/helpers/linux_sysctl.rs new file mode 100644 index 000000000..892408497 --- /dev/null +++ b/src/executor/helpers/linux_sysctl.rs @@ -0,0 +1,108 @@ +use crate::executor::helpers::run_with_sudo::run_with_sudo; +use crate::prelude::*; +use anyhow::Context; +use std::process::Command; + +/// 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 { + name: &'static str, + previous: Option, +} + +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) { + 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> { + if !cfg!(target_os = "linux") { + return Ok(Vec::new()); + } + + 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) +} + +/// Sets a sysctl, returning the value it held before, or `None` when it was +/// already at `target_value` and nothing was written. +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()])?; + + Ok(Some(current_value)) +} + +fn sysctl_read(name: &str) -> Result { + let output = Command::new("sysctl").arg(name).output()?; + let output = String::from_utf8(output.stdout)?; + + parse_sysctl_value(&output) +} + +fn parse_sysctl_value(output: &str) -> Result { + let (_, value) = output + .split_once('=') + .context("Couldn't find the value in sysctl output")?; + + Ok(value.trim().parse::()?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_sysctl_value() { + assert_eq!(parse_sysctl_value("kernel.kptr_restrict = 0\n").unwrap(), 0); + } + + #[test] + fn parses_negative_sysctl_value() { + assert_eq!( + parse_sysctl_value("kernel.perf_event_paranoid = -1\n").unwrap(), + -1 + ); + } + + #[test] + fn rejects_sysctl_output_without_value_separator() { + assert!(parse_sysctl_value("kernel.kptr_restrict 0\n").is_err()); + } +} 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/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..928424840 --- /dev/null +++ b/src/executor/memory/tunables.rs @@ -0,0 +1,357 @@ +//! 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. +//! - swap lets the kernel evict anonymous pages, so a benchmark's resident set +//! reflects reclaim decisions rather than what it allocated. +//! - [`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 settings and restores them 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 settings that were changed. +/// Empty when nothing was 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)>, + /// Swap areas that were turned off, re-enabled one by one on drop. + swap: Vec, +} + +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"), + swap: Self::disable_swap(), + }; + // After swapoff: faulting the swapped-out pages back in dirties the cache. + 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 + } + + /// Turns off the swap areas that are safe to turn off, returning the ones + /// that have to be turned back on. + fn disable_swap() -> Vec { + let Ok(swaps) = read_to_string(PROC_SWAPS) else { + debug!("{PROC_SWAPS} is missing, skipping swap"); + return Vec::new(); + }; + let areas = SwapArea::parse_all(&swaps); + if areas.is_empty() { + debug!("No swap area is active, skipping swap"); + return Vec::new(); + } + + let Some(available_kib) = read_to_string("/proc/meminfo") + .ok() + .as_deref() + .and_then(mem_available_kib) + else { + warn!("Cannot read MemAvailable, leaving swap enabled"); + return Vec::new(); + }; + + let mut disabled = Vec::new(); + for area in SwapArea::to_disable(areas, available_kib) { + match run_with_sudo("swapoff", [&area.path]) { + Ok(()) => disabled.push(area), + Err(error) => warn!("Failed to disable swap on {}: {error}", area.path), + } + } + + disabled + } +} + +impl Drop for MemoryTunables { + fn drop(&mut self) { + if self.thp.is_empty() && self.swap.is_empty() { + return; + } + + start_group!("Restoring kernel memory tunables"); + // `swapon -a` would only cover the areas listed in /etc/fstab, so a swap + // file activated by hand would never come back. Restoring in the order + // /proc/swaps listed them also gives the areas whose priority the kernel + // picks back their original relative order. + for area in &self.swap { + let argv = area.swapon_argv(); + if let Err(error) = run_with_sudo("swapon", &argv) { + warn!( + "Failed to re-enable swap on {}, re-run manually with `sudo swapon {}`: {error}", + area.path, + argv.join(" ") + ); + } + } + 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!(); + } +} + +const PROC_SWAPS: &str = "/proc/swaps"; + +/// An active swap area, as listed by `/proc/swaps`. +#[derive(Debug, PartialEq, Eq)] +struct SwapArea { + path: String, + /// How much of the area currently holds evicted pages. + used_kib: u64, + /// Reclaim order: higher areas are used first. Negative when the kernel + /// assigned it rather than the caller. + priority: i32, +} + +impl SwapArea { + /// Parses `/proc/swaps`, whose rows read as `Filename Type Size Used Priority`. + fn parse_all(content: &str) -> Vec { + content + .lines() + .skip(1) + .filter_map(|line| { + let mut columns = line.split_whitespace(); + let path = columns.next()?.to_string(); + // Type and Size sit between Filename and Used. + let used_kib = columns.nth(2)?.parse().ok()?; + let priority = columns.next()?.parse().ok()?; + + Some(Self { + path, + used_kib, + priority, + }) + }) + .collect() + } + + /// `swapon` arguments re-enabling the area with the priority it had. + /// `SWAP_FLAG_PREFER` only encodes `0..=32767`, so a kernel-assigned + /// negative priority cannot be asked for and is left to be assigned again. + fn swapon_argv(&self) -> Vec { + let mut argv = Vec::new(); + if self.priority >= 0 { + argv.push("-p".to_string()); + argv.push(self.priority.to_string()); + } + argv.push(self.path.clone()); + + argv + } + + /// Selects the areas that can be turned off without risking the host: + /// `swapoff` faults every evicted page back into RAM, so it needs the + /// resident set to fit. + fn to_disable(areas: Vec, mem_available_kib: u64) -> Vec { + let (candidates, zram): (Vec<_>, Vec<_>) = + areas.into_iter().partition(|area| !area.is_zram()); + for area in &zram { + warn!( + "Leaving zram swap {} enabled: disabling it would reset its size", + area.path + ); + } + + let used_kib: u64 = candidates.iter().map(|area| area.used_kib).sum(); + if used_kib >= mem_available_kib { + warn!( + "Leaving swap enabled: faulting {used_kib} kB of evicted pages back in does not fit in {mem_available_kib} kB of available memory" + ); + return Vec::new(); + } + + candidates + } + + /// zram areas are compressed swap backed by RAM. `swapoff` resets their + /// `disksize` to 0, after which a plain `swapon` fails with `EINVAL` and + /// only restarting the zram unit brings them back. + fn is_zram(&self) -> bool { + self.path.starts_with("/dev/zram") + } +} + +/// The `MemAvailable` line of `/proc/meminfo`, which reads as `MemAvailable: 123 kB`. +fn mem_available_kib(meminfo: &str) -> Option { + meminfo + .lines() + .find_map(|line| line.strip_prefix("MemAvailable:"))? + .split_whitespace() + .next()? + .parse() + .ok() +} + +/// 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::*; + + fn area(path: &str, used_kib: u64, priority: i32) -> SwapArea { + SwapArea { + path: path.to_string(), + used_kib, + priority, + } + } + + #[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); + } + + #[test] + fn parses_the_active_swap_areas() { + let content = "Filename\t\t\t\tType\t\tSize\t\tUsed\t\tPriority\n\ + /swapfile file\t\t8388604\t\t131072\t\t-2\n\ + /dev/zram0 partition\t4194300\t\t0\t\t100\n"; + + assert_eq!( + SwapArea::parse_all(content), + vec![area("/swapfile", 131072, -2), area("/dev/zram0", 0, 100)] + ); + } + + #[test] + fn reports_no_swap_area_when_swap_is_off() { + let content = "Filename\t\t\t\tType\t\tSize\t\tUsed\t\tPriority\n"; + + assert_eq!(SwapArea::parse_all(content), vec![]); + } + + #[test] + fn reads_the_available_memory() { + let meminfo = "MemTotal: 16316360 kB\nMemAvailable: 9583756 kB\n"; + + assert_eq!(mem_available_kib(meminfo), Some(9583756)); + } + + #[test] + fn disables_a_swap_file_whose_pages_fit_in_memory() { + let areas = vec![area("/swapfile", 131072, -2)]; + + assert_eq!( + SwapArea::to_disable(areas, 9583756), + vec![area("/swapfile", 131072, -2)] + ); + } + + #[test] + fn never_disables_zram_swap() { + let areas = vec![area("/dev/zram0", 0, 100)]; + + assert_eq!(SwapArea::to_disable(areas, 9583756), vec![]); + } + + #[test] + fn keeps_swap_enabled_when_its_pages_do_not_fit_in_memory() { + let areas = vec![ + area("/swapfile", 4194304, -2), + area("/swapfile2", 4194304, -3), + ]; + + assert_eq!(SwapArea::to_disable(areas, 1048576), vec![]); + } + + #[test] + fn asks_for_the_priority_the_kernel_did_not_pick() { + assert_eq!( + area("/swapfile", 0, 10).swapon_argv(), + vec!["-p", "10", "/swapfile"] + ); + } + + #[test] + fn leaves_a_kernel_assigned_priority_to_be_reassigned() { + assert_eq!(area("/swapfile", 0, -2).swapon_argv(), vec!["/swapfile"]); + } +} 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/linux_sysctl.rs b/src/executor/wall_time/profiler/linux_sysctl.rs deleted file mode 100644 index 657581ecb..000000000 --- a/src/executor/wall_time/profiler/linux_sysctl.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::prelude::*; - -#[cfg(target_os = "linux")] -use crate::executor::helpers::run_with_sudo::run_with_sudo; -#[cfg(any(test, target_os = "linux"))] -use anyhow::Context; -#[cfg(target_os = "linux")] -use std::process::Command; - -pub fn ensure_linux_profiling_sysctls() -> Result<()> { - #[cfg(target_os = "linux")] - { - ensure_sysctl("kernel.kptr_restrict", 0)?; - ensure_sysctl("kernel.perf_event_paranoid", -1)?; - } - - Ok(()) -} - -#[cfg(target_os = "linux")] -fn ensure_sysctl(name: &str, target_value: i64) -> Result<()> { - if sysctl_read(name)? == target_value { - return Ok(()); - } - - let assignment = format!("{name}={target_value}"); - run_with_sudo("sysctl", ["-w", assignment.as_str()]) -} - -#[cfg(target_os = "linux")] -fn sysctl_read(name: &str) -> Result { - let output = Command::new("sysctl").arg(name).output()?; - let output = String::from_utf8(output.stdout)?; - - parse_sysctl_value(&output) -} - -#[cfg(any(test, target_os = "linux"))] -fn parse_sysctl_value(output: &str) -> Result { - let (_, value) = output - .split_once('=') - .context("Couldn't find the value in sysctl output")?; - - Ok(value.trim().parse::()?) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_sysctl_value() { - assert_eq!(parse_sysctl_value("kernel.kptr_restrict = 0\n").unwrap(), 0); - } - - #[test] - fn parses_negative_sysctl_value() { - assert_eq!( - parse_sysctl_value("kernel.perf_event_paranoid = -1\n").unwrap(), - -1 - ); - } - - #[test] - fn rejects_sysctl_output_without_value_separator() { - assert!(parse_sysctl_value("kernel.kptr_restrict 0\n").is_err()); - } -} 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..8816e5fb0 100644 --- a/src/executor/wall_time/profiler/perf/mod.rs +++ b/src/executor/wall_time/profiler/perf/mod.rs @@ -14,7 +14,6 @@ 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; @@ -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 97b5bffd9..3d77e7ade 100644 --- a/src/executor/wall_time/profiler/samply/mod.rs +++ b/src/executor/wall_time/profiler/samply/mod.rs @@ -7,7 +7,6 @@ use crate::executor::helpers::command::CommandBuilder; 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; @@ -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,