-
Notifications
You must be signed in to change notification settings - Fork 28
feat(memory): apply kernel memory tunables before memory-mode runs #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| 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; | ||
|
|
||
| /// 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")] | ||
| name: &'static str, | ||
| #[cfg(target_os = "linux")] | ||
| previous: Option<i64>, | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| impl LinuxSysctl { | ||
| pub(crate) fn set(name: &'static str, target_value: i64) -> Result<Self> { | ||
| 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<Vec<LinuxSysctl>> { | ||
| 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) | ||
| } | ||
|
|
||
| #[cfg(not(target_os = "linux"))] | ||
| pub fn ensure_linux_profiling_sysctls() -> Result<Vec<LinuxSysctl>> { | ||
| Ok(Vec::new()) | ||
| } | ||
|
|
||
| /// 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<Option<i64>> { | ||
| 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)) | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| fn sysctl_read(name: &str) -> Result<i64> { | ||
| 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<i64> { | ||
| let (_, value) = output | ||
| .split_once('=') | ||
| .context("Couldn't find the value in sysctl output")?; | ||
|
|
||
| Ok(value.trim().parse::<i64>()?) | ||
| } | ||
|
|
||
| #[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()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| pub mod executor; | ||
| pub(crate) mod setup; | ||
| pub(crate) mod tunables; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Self> { | ||
| // Blocking the run on an interactive password prompt would be worse than | ||
| // measuring without the knobs. | ||
| if !can_elevate_without_prompt() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a developer runs memory mode locally as root or with non-interactive sudo, Knowledge Base Used: Prompt To Fix With AIThis is a comment left during a code review.
Path: src/executor/memory/tunables.rs
Line: 33
Comment:
**Local runs mutate host memory**
When a developer runs memory mode locally as root or with non-interactive sudo, `MemoryTunables::apply` treats privilege elevation as sufficient authorization and changes THP before irreversibly dropping the host page cache, affecting unrelated local workloads despite the CI-only contract.
**Knowledge Base Used:**
- [Benchmark execution engine](https://app.greptile.com/codspeed/-/custom-context/knowledge-base/codspeedhq/codspeed/-/docs/benchmark-execution.md)
- [Run environment detection](https://app.greptile.com/codspeed/-/custom-context/knowledge-base/codspeedhq/codspeed/-/docs/run-environment-detection.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| 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"), | ||
| }; | ||
|
GuillaumeLagrange marked this conversation as resolved.
Comment on lines
+40
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a CI host has enabled swap, Knowledge Base Used: Memory benchmarking Prompt To Fix With AIThis is a comment left during a code review.
Path: src/executor/memory/tunables.rs
Line: 40-43
Comment:
**Swap remains enabled during measurement**
If a CI host has enabled swap, `MemoryTunables::apply` changes only THP and drops the page cache without disabling or recording swap entries, so paging can continue perturbing memory results even though the runner reports that kernel memory tunables were applied.
**Knowledge Base Used:** [Memory benchmarking](https://app.greptile.com/codspeed/-/custom-context/knowledge-base/codspeedhq/codspeed/-/docs/memory-benchmarking.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| 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(); | ||
|
not-matthias marked this conversation as resolved.
|
||
| 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<String> { | ||
| 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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to gate this whole mod behind the feature flag but not every single line, because now we end up gating almost every single line with
#[cfg(target_os = "linux")]😅