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/mod.rs b/src/executor/wall_time/perf/mod.rs index 40dcec09..ef31a3ba 100644 --- a/src/executor/wall_time/perf/mod.rs +++ b/src/executor/wall_time/perf/mod.rs @@ -18,6 +18,8 @@ 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; use runner_shared::artifacts::ExecutionTimestamps; @@ -131,11 +133,20 @@ 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 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. diff --git a/src/executor/wall_time/perf/perf_executable.rs b/src/executor/wall_time/perf/perf_executable.rs index 6da51cd6..2fd1c531 100644 --- a/src/executor/wall_time/perf/perf_executable.rs +++ b/src/executor/wall_time/perf/perf_executable.rs @@ -1,4 +1,7 @@ +use runner_shared::perf_event::PerfEvent; + use crate::prelude::*; +use std::path::Path; use std::{ffi::OsString, process::Command}; @@ -62,3 +65,78 @@ 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> { + let perf_events = PerfEvent::all_events(); + + 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 = perf_events + .iter() + .filter(|&&event| { + !stdout.lines().any(|line| { + line.split_whitespace() + .any(|word| word == event.to_perf_string()) + }) + }) + .copied() + .collect(); + + if !missing_events.is_empty() { + warn!( + "Not all required perf events available. Missing: [{}], using default events", + missing_events.into_iter().join(", ") + ); + return Ok(None); + } + + let events_string = perf_events.into_iter().join(","); + debug!("All required perf events available: {events_string}",); + Ok(Some(format!("-e {{{events_string}}}"))) +} + +pub fn get_compression_flags>(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) + } +}