From 3362ac74fb1711e764380a72560a22df3953506e Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Fri, 7 Nov 2025 11:52:37 +0100 Subject: [PATCH 1/4] feat: add more events to perf monitoring --- src/executor/wall_time/perf/mod.rs | 7 ++- .../wall_time/perf/perf_executable.rs | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/executor/wall_time/perf/mod.rs b/src/executor/wall_time/perf/mod.rs index 40dcec09..a3088a1b 100644 --- a/src/executor/wall_time/perf/mod.rs +++ b/src/executor/wall_time/perf/mod.rs @@ -18,6 +18,7 @@ use crate::run::UnwindingMode; use anyhow::Context; use fifo::PerfFifo; use libc::pid_t; +use perf_executable::get_event_flags; use perf_map::ProcessSymbols; use runner_shared::artifacts::ArtifactExt; use runner_shared::artifacts::ExecutionTimestamps; @@ -131,11 +132,15 @@ impl PerfRunner { let working_perf_executable = get_working_perf_executable().context("Failed to find a working perf executable")?; - let mut perf_wrapper_builder = CommandBuilder::new(working_perf_executable); + let mut perf_wrapper_builder = CommandBuilder::new(&working_perf_executable); perf_wrapper_builder.arg("record"); if !is_codspeed_debug_enabled() { perf_wrapper_builder.arg("--quiet"); } + // Add events flag if all required events are available + if let Some(events_flag) = get_event_flags(&working_perf_executable)? { + perf_wrapper_builder.arg(events_flag); + } perf_wrapper_builder.args([ "--timestamp", // Required for matching the markers and URIs to the samples. diff --git a/src/executor/wall_time/perf/perf_executable.rs b/src/executor/wall_time/perf/perf_executable.rs index 6da51cd6..191b6e2e 100644 --- a/src/executor/wall_time/perf/perf_executable.rs +++ b/src/executor/wall_time/perf/perf_executable.rs @@ -62,3 +62,61 @@ pub fn get_working_perf_executable() -> Option { debug!("perf is installed but not functioning correctly"); None } + +/// Detects if the required perf events are available on this system. +/// Returns the flags to pass to perf record command if they are available, otherwise returns None. +pub fn get_event_flags(perf_executable: &OsString) -> anyhow::Result> { + const CYCLES_EVENT_NAME: &str = "cycles"; + const CACHE_REFERENCES_EVENT_NAME: &str = "cache-references"; + const CACHE_MISSES_EVENT_NAME: &str = "cache-misses"; + + let perf_events = [ + CYCLES_EVENT_NAME, + CACHE_REFERENCES_EVENT_NAME, + CACHE_MISSES_EVENT_NAME, + ]; + + let output = Command::new(perf_executable) + .arg("list") + .output() + .context("Failed to run perf list")?; + + let stdout = String::from_utf8_lossy(&output.stdout); + + // Check if all required events are available + // Expected format in `perf list` output: + // + // List of pre-defined events (to be used in -e or -M): + // + // branch-instructions OR branches [Hardware event] + // branch-misses [Hardware event] + // bus-cycles [Hardware event] + // cache-misses [Hardware event] + // cache-references [Hardware event] + // cpu-cycles OR cycles [Hardware event] + // instructions [Hardware event] + // ref-cycles [Hardware event] + let missing_events: Vec<&str> = perf_events + .iter() + .filter(|&&event| { + !stdout + .lines() + .any(|line| line.split_whitespace().any(|word| word == event)) + }) + .copied() + .collect(); + + if !missing_events.is_empty() { + debug!( + "Not all required perf events available. Missing: [{}], using default events", + missing_events.join(", ") + ); + return Ok(None); + } + + debug!( + "All required perf events available: {}", + perf_events.join(", ") + ); + Ok(Some(format!("-e {{{}}}", perf_events.join(",")))) +} From bb624970141d8f927c4db1d48367537a5b449ad1 Mon Sep 17 00:00:00 2001 From: Arthur Pastel Date: Sat, 8 Nov 2025 13:47:38 +0100 Subject: [PATCH 2/4] feat: enable perf data compression --- src/executor/wall_time/perf/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/executor/wall_time/perf/mod.rs b/src/executor/wall_time/perf/mod.rs index a3088a1b..a2f4821c 100644 --- a/src/executor/wall_time/perf/mod.rs +++ b/src/executor/wall_time/perf/mod.rs @@ -149,6 +149,7 @@ impl PerfRunner { "--freq=997", // Use a prime number to avoid synchronization with periodic tasks "--delay=-1", "-g", + "--compression-level=3", // 3 is a widely adopted default level (AWS Athena, Python, ...) "--user-callchains", &format!("--call-graph={cg_mode}"), &format!( From 9b3b64bda8c1764635a44834f6be78e50fe4198e Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Wed, 24 Dec 2025 10:27:53 +0100 Subject: [PATCH 3/4] feat: check for perf compression and enable multi-events conditionally --- src/executor/wall_time/perf/mod.rs | 13 ++++++--- .../wall_time/perf/perf_executable.rs | 29 ++++++++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/executor/wall_time/perf/mod.rs b/src/executor/wall_time/perf/mod.rs index a2f4821c..ef31a3ba 100644 --- a/src/executor/wall_time/perf/mod.rs +++ b/src/executor/wall_time/perf/mod.rs @@ -18,6 +18,7 @@ use crate::run::UnwindingMode; use anyhow::Context; use fifo::PerfFifo; use libc::pid_t; +use perf_executable::get_compression_flags; use perf_executable::get_event_flags; use perf_map::ProcessSymbols; use runner_shared::artifacts::ArtifactExt; @@ -137,10 +138,15 @@ impl PerfRunner { if !is_codspeed_debug_enabled() { perf_wrapper_builder.arg("--quiet"); } - // Add events flag if all required events are available - if let Some(events_flag) = get_event_flags(&working_perf_executable)? { - perf_wrapper_builder.arg(events_flag); + // Add compression if available + if let Some(compression_flags) = get_compression_flags(&working_perf_executable)? { + perf_wrapper_builder.arg(compression_flags); + // Add events flag if all required events are available + if let Some(events_flag) = get_event_flags(&working_perf_executable)? { + perf_wrapper_builder.arg(events_flag); + } } + perf_wrapper_builder.args([ "--timestamp", // Required for matching the markers and URIs to the samples. @@ -149,7 +155,6 @@ impl PerfRunner { "--freq=997", // Use a prime number to avoid synchronization with periodic tasks "--delay=-1", "-g", - "--compression-level=3", // 3 is a widely adopted default level (AWS Athena, Python, ...) "--user-callchains", &format!("--call-graph={cg_mode}"), &format!( diff --git a/src/executor/wall_time/perf/perf_executable.rs b/src/executor/wall_time/perf/perf_executable.rs index 191b6e2e..e60e39c1 100644 --- a/src/executor/wall_time/perf/perf_executable.rs +++ b/src/executor/wall_time/perf/perf_executable.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use std::path::Path; use std::{ffi::OsString, process::Command}; @@ -107,7 +108,7 @@ pub fn get_event_flags(perf_executable: &OsString) -> anyhow::Result anyhow::Result>(perf_executable: S) -> Result> { + let output = Command::new(perf_executable.as_ref()) + .arg("version") + .arg("--build-options") + .output() + .context("Failed to run perf version --build-options")?; + + let stdout = String::from_utf8_lossy(&output.stdout); + debug!("Perf version build options:\n{stdout}"); + + // Look for zstd compression support in the build options + // Expected format: " zstd: [ on ] # HAVE_ZSTD_SUPPORT" + let has_zstd = stdout + .lines() + .any(|line| line.to_lowercase().contains("zstd: [ on")); + + if has_zstd { + debug!("perf supports zstd compression"); + // 3 is a widely adopted default level (AWS Athena, Python, ...) + Ok(Some("--compression-level=3".to_string())) + } else { + warn!("perf does not support zstd compression"); + Ok(None) + } +} From 7ad6ef72c306d3785ea62b4195e39ca45b411600 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Tue, 6 Jan 2026 14:36:19 +0100 Subject: [PATCH 4/4] chore: add instructions event, and use runner_shared for monitored events --- crates/runner-shared/src/lib.rs | 1 + crates/runner-shared/src/perf_event.rs | 44 +++++++++++++++++++ .../wall_time/perf/perf_executable.rs | 31 +++++-------- 3 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 crates/runner-shared/src/perf_event.rs diff --git a/crates/runner-shared/src/lib.rs b/crates/runner-shared/src/lib.rs index 95950044..381a8a04 100644 --- a/crates/runner-shared/src/lib.rs +++ b/crates/runner-shared/src/lib.rs @@ -2,5 +2,6 @@ pub mod artifacts; pub mod debug_info; pub mod fifo; pub mod metadata; +pub mod perf_event; pub mod unwind_data; pub mod walltime_results; diff --git a/crates/runner-shared/src/perf_event.rs b/crates/runner-shared/src/perf_event.rs new file mode 100644 index 00000000..8f2b7d4e --- /dev/null +++ b/crates/runner-shared/src/perf_event.rs @@ -0,0 +1,44 @@ +/// Subset of perf events that CodSpeed supports. +#[derive(Debug, Clone, Copy)] +pub enum PerfEvent { + CpuCycles, + CacheReferences, + CacheMisses, + Instructions, +} + +impl PerfEvent { + pub fn to_perf_string(&self) -> &'static str { + match self { + PerfEvent::CpuCycles => "cpu-cycles", + PerfEvent::CacheReferences => "cache-references", + PerfEvent::CacheMisses => "cache-misses", + PerfEvent::Instructions => "instructions", + } + } + + pub fn from_perf_string(event: &str) -> Option { + match event { + "cpu-cycles" => Some(PerfEvent::CpuCycles), + "cache-references" => Some(PerfEvent::CacheReferences), + "cache-misses" => Some(PerfEvent::CacheMisses), + "instructions" => Some(PerfEvent::Instructions), + _ => None, + } + } + + pub fn all_events() -> Vec { + vec![ + PerfEvent::CpuCycles, + PerfEvent::CacheReferences, + PerfEvent::CacheMisses, + PerfEvent::Instructions, + ] + } +} + +impl std::fmt::Display for PerfEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_perf_string()) + } +} diff --git a/src/executor/wall_time/perf/perf_executable.rs b/src/executor/wall_time/perf/perf_executable.rs index e60e39c1..2fd1c531 100644 --- a/src/executor/wall_time/perf/perf_executable.rs +++ b/src/executor/wall_time/perf/perf_executable.rs @@ -1,3 +1,5 @@ +use runner_shared::perf_event::PerfEvent; + use crate::prelude::*; use std::path::Path; @@ -67,15 +69,7 @@ pub fn get_working_perf_executable() -> Option { /// Detects if the required perf events are available on this system. /// Returns the flags to pass to perf record command if they are available, otherwise returns None. pub fn get_event_flags(perf_executable: &OsString) -> anyhow::Result> { - const CYCLES_EVENT_NAME: &str = "cycles"; - const CACHE_REFERENCES_EVENT_NAME: &str = "cache-references"; - const CACHE_MISSES_EVENT_NAME: &str = "cache-misses"; - - let perf_events = [ - CYCLES_EVENT_NAME, - CACHE_REFERENCES_EVENT_NAME, - CACHE_MISSES_EVENT_NAME, - ]; + let perf_events = PerfEvent::all_events(); let output = Command::new(perf_executable) .arg("list") @@ -97,12 +91,13 @@ pub fn get_event_flags(perf_executable: &OsString) -> anyhow::Result = perf_events + let missing_events: Vec = perf_events .iter() .filter(|&&event| { - !stdout - .lines() - .any(|line| line.split_whitespace().any(|word| word == event)) + !stdout.lines().any(|line| { + line.split_whitespace() + .any(|word| word == event.to_perf_string()) + }) }) .copied() .collect(); @@ -110,16 +105,14 @@ pub fn get_event_flags(perf_executable: &OsString) -> anyhow::Result>(perf_executable: S) -> Result> {