From b2494f3edc1b89ceabcdcdacfe881aceb12ddba7 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Fri, 30 Jan 2026 17:13:40 +0100 Subject: [PATCH 1/4] chore: import instrument-hooks bindings from codspeed-rust --- .github/workflows/ci.yml | 8 + .gitmodules | 3 + Cargo.lock | 12 +- Cargo.toml | 7 +- crates/instrument-hooks-bindings/Cargo.toml | 10 + crates/instrument-hooks-bindings/build.rs | 50 ++++ .../instrument-hooks | 1 + .../instrument-hooks-bindings/src/bindings.rs | 60 +++++ crates/instrument-hooks-bindings/src/ffi.rs | 13 + crates/instrument-hooks-bindings/src/lib.rs | 222 ++++++++++++++++++ crates/instrument-hooks-bindings/src/mod.rs | 220 +++++++++++++++++ .../src/update-bindings.sh | 8 + 12 files changed, 611 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 100644 crates/instrument-hooks-bindings/Cargo.toml create mode 100644 crates/instrument-hooks-bindings/build.rs create mode 160000 crates/instrument-hooks-bindings/instrument-hooks create mode 100644 crates/instrument-hooks-bindings/src/bindings.rs create mode 100644 crates/instrument-hooks-bindings/src/ffi.rs create mode 100644 crates/instrument-hooks-bindings/src/lib.rs create mode 100644 crates/instrument-hooks-bindings/src/mod.rs create mode 100755 crates/instrument-hooks-bindings/src/update-bindings.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f05994b..8f718de1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: true - uses: moonrepo/setup-rust@v1 with: components: rustfmt, clippy @@ -23,6 +25,7 @@ jobs: - uses: actions/checkout@v3 with: lfs: true + submodules: true - name: "Install rust-toolchain.toml" run: rustup toolchain install @@ -45,6 +48,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: true - uses: moonrepo/setup-rust@v1 - name: Run tests run: cargo test -p exec-harness @@ -55,6 +60,7 @@ jobs: - uses: actions/checkout@v3 with: lfs: true + submodules: true - uses: moonrepo/setup-rust@v1 - name: Install dependencies required for libbpf-sys (vendored feature) run: sudo apt-get update && sudo apt-get install -y autopoint bison flex @@ -76,6 +82,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: true - name: "Install rust-toolchain.toml" run: rustup toolchain install diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5744bd75 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/instrument-hooks-bindings/instrument-hooks"] + path = crates/instrument-hooks-bindings/instrument-hooks + url = https://github.com/CodSpeedHQ/instrument-hooks.git diff --git a/Cargo.lock b/Cargo.lock index 4900ae26..5b208511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,6 +1588,14 @@ dependencies = [ "web-sys", ] +[[package]] +name = "instrument-hooks-bindings" +version = "0.1.0" +dependencies = [ + "cc", + "nix 0.30.1", +] + [[package]] name = "ipc-channel" version = "0.18.3" @@ -1766,9 +1774,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.178" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libgit2-sys" diff --git a/Cargo.toml b/Cargo.toml index eb3d16e4..e8a4cb07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,12 @@ rstest_reuse = "0.7.0" shell-quote = "0.7.2" [workspace] -members = ["crates/runner-shared", "crates/memtrack", "crates/exec-harness"] +members = [ + "crates/runner-shared", + "crates/memtrack", + "crates/exec-harness", + "crates/instrument-hooks-bindings", +] [workspace.dependencies] anyhow = "1.0" diff --git a/crates/instrument-hooks-bindings/Cargo.toml b/crates/instrument-hooks-bindings/Cargo.toml new file mode 100644 index 00000000..8643104c --- /dev/null +++ b/crates/instrument-hooks-bindings/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "instrument-hooks-bindings" +version = "0.1.0" +edition = "2024" + +[dependencies] +nix = { version = "0.30.1", features = ["time"] } + +[build-dependencies] +cc = "1.0" diff --git a/crates/instrument-hooks-bindings/build.rs b/crates/instrument-hooks-bindings/build.rs new file mode 100644 index 00000000..63b46664 --- /dev/null +++ b/crates/instrument-hooks-bindings/build.rs @@ -0,0 +1,50 @@ +fn main() { + println!("cargo:rustc-check-cfg=cfg(use_instrument_hooks)"); + + println!("cargo:rerun-if-changed=instrument-hooks/dist/core.c"); + println!("cargo:rerun-if-changed=instrument-hooks/includes/core.h"); + println!("cargo:rerun-if-changed=build.rs"); + + let mut build = cc::Build::new(); + build + .flag("-std=c11") + .file("instrument-hooks/dist/core.c") + .include("instrument-hooks/includes") + // We generated the C code from Zig, which contains some warnings + // that can be safely ignored. + .flag("-Wno-format") + .flag("-Wno-format-security") + .flag("-Wno-unused-but-set-variable") + .flag("-Wno-unused-const-variable") + .flag("-Wno-type-limits") + .flag("-Wno-uninitialized") + // Ignore warnings when cross-compiling: + .flag("-Wno-overflow") + .flag("-Wno-unused-function") + .flag("-Wno-constant-conversion") + .flag("-Wno-incompatible-pointer-types") + // Disable warnings, as we will have lots of them + .warnings(false) + .extra_warnings(false) + .cargo_warnings(false) + .opt_level(3); + + let result = build.try_compile("instrument_hooks"); + match result { + Ok(_) => println!("cargo:rustc-cfg=use_instrument_hooks"), + Err(e) => { + let compiler = build.try_get_compiler().expect("Failed to get C compiler"); + + eprintln!("\n\nWARNING: Failed to compile instrument-hooks native library with cc-rs."); + eprintln!( + "The library will still compile, but instrument-hooks functionality will be disabled." + ); + eprintln!("Compiler information: {compiler:?}"); + eprintln!("Compilation error: {e}\n"); + + println!( + "cargo:warning=Failed to compile instrument-hooks native library with cc-rs. Continuing with noop implementation." + ); + } + } +} diff --git a/crates/instrument-hooks-bindings/instrument-hooks b/crates/instrument-hooks-bindings/instrument-hooks new file mode 160000 index 00000000..89fb72a0 --- /dev/null +++ b/crates/instrument-hooks-bindings/instrument-hooks @@ -0,0 +1 @@ +Subproject commit 89fb72a076ec71c9eca6eee9bca98bada4b4dfb4 diff --git a/crates/instrument-hooks-bindings/src/bindings.rs b/crates/instrument-hooks-bindings/src/bindings.rs new file mode 100644 index 00000000..b7af600d --- /dev/null +++ b/crates/instrument-hooks-bindings/src/bindings.rs @@ -0,0 +1,60 @@ +/* automatically generated by rust-bindgen 0.72.1 */ + +pub const MARKER_TYPE_SAMPLE_START: u32 = 0; +pub const MARKER_TYPE_SAMPLE_END: u32 = 1; +pub const MARKER_TYPE_BENCHMARK_START: u32 = 2; +pub const MARKER_TYPE_BENCHMARK_END: u32 = 3; +pub type InstrumentHooks = *mut u64; +unsafe extern "C" { + pub fn instrument_hooks_init() -> *mut InstrumentHooks; +} +unsafe extern "C" { + pub fn instrument_hooks_deinit(arg1: *mut InstrumentHooks); +} +unsafe extern "C" { + pub fn instrument_hooks_is_instrumented(arg1: *mut InstrumentHooks) -> bool; +} +unsafe extern "C" { + pub fn instrument_hooks_start_benchmark(arg1: *mut InstrumentHooks) -> u8; +} +unsafe extern "C" { + pub fn instrument_hooks_stop_benchmark(arg1: *mut InstrumentHooks) -> u8; +} +unsafe extern "C" { + pub fn instrument_hooks_set_executed_benchmark( + arg1: *mut InstrumentHooks, + pid: i32, + uri: *const ::std::os::raw::c_char, + ) -> u8; +} +unsafe extern "C" { + pub fn instrument_hooks_executed_benchmark( + arg1: *mut InstrumentHooks, + pid: i32, + uri: *const ::std::os::raw::c_char, + ) -> u8; +} +unsafe extern "C" { + pub fn instrument_hooks_set_integration( + arg1: *mut InstrumentHooks, + name: *const ::std::os::raw::c_char, + version: *const ::std::os::raw::c_char, + ) -> u8; +} +unsafe extern "C" { + pub fn instrument_hooks_add_marker( + arg1: *mut InstrumentHooks, + pid: u32, + marker_type: u8, + timestamp: u64, + ) -> u8; +} +unsafe extern "C" { + pub fn instrument_hooks_current_timestamp() -> u64; +} +pub const instrument_hooks_feature_t_FEATURE_DISABLE_CALLGRIND_MARKERS: instrument_hooks_feature_t = + 0; +pub type instrument_hooks_feature_t = ::std::os::raw::c_uint; +unsafe extern "C" { + pub fn instrument_hooks_set_feature(feature: instrument_hooks_feature_t, enabled: bool); +} diff --git a/crates/instrument-hooks-bindings/src/ffi.rs b/crates/instrument-hooks-bindings/src/ffi.rs new file mode 100644 index 00000000..db7350ff --- /dev/null +++ b/crates/instrument-hooks-bindings/src/ffi.rs @@ -0,0 +1,13 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused)] + +// Use pre-generated bindings instead of generating at build time so that downstream +// users don't need to install `libclang`. +// +// To regenerate bindings, run: +// ``` +// ./update-bindings.sh +// ``` +include!("bindings.rs"); diff --git a/crates/instrument-hooks-bindings/src/lib.rs b/crates/instrument-hooks-bindings/src/lib.rs new file mode 100644 index 00000000..f5591280 --- /dev/null +++ b/crates/instrument-hooks-bindings/src/lib.rs @@ -0,0 +1,222 @@ +#[cfg(use_instrument_hooks)] +mod ffi; + +#[cfg(use_instrument_hooks)] +mod linux_impl { + + use super::ffi; + use std::ffi::CString; + use std::sync::OnceLock; + + #[derive(PartialEq)] + pub struct InstrumentHooks(*mut ffi::InstrumentHooks); + + unsafe impl Send for InstrumentHooks {} + unsafe impl Sync for InstrumentHooks {} + + impl InstrumentHooks { + #[inline(always)] + pub fn new() -> Option { + let ptr = unsafe { ffi::instrument_hooks_init() }; + if ptr.is_null() { + None + } else { + Some(InstrumentHooks(ptr)) + } + } + + /// Returns a singleton instance of `InstrumentHooks`. + #[inline(always)] + pub fn instance() -> &'static Self { + static INSTANCE: OnceLock = OnceLock::new(); + INSTANCE.get_or_init(|| { + let instance = + InstrumentHooks::new().expect("Failed to initialize InstrumentHooks"); + instance + .set_integration("codspeed-rust", env!("CARGO_PKG_VERSION")) + .expect("Failed to set integration"); + instance + }) + } + + #[inline(always)] + pub fn is_instrumented(&self) -> bool { + unsafe { ffi::instrument_hooks_is_instrumented(self.0) } + } + + #[inline(always)] + pub fn start_benchmark(&self) -> Result<(), u8> { + let result = unsafe { ffi::instrument_hooks_start_benchmark(self.0) }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn stop_benchmark(&self) -> Result<(), u8> { + let result = unsafe { ffi::instrument_hooks_stop_benchmark(self.0) }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn set_executed_benchmark(&self, uri: &str) -> Result<(), u8> { + let pid = std::process::id() as i32; + let c_uri = CString::new(uri).map_err(|_| 1u8)?; + let result = unsafe { + ffi::instrument_hooks_set_executed_benchmark(self.0, pid, c_uri.as_ptr()) + }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn set_integration(&self, name: &str, version: &str) -> Result<(), u8> { + let c_name = CString::new(name).map_err(|_| 1u8)?; + let c_version = CString::new(version).map_err(|_| 1u8)?; + let result = unsafe { + ffi::instrument_hooks_set_integration(self.0, c_name.as_ptr(), c_version.as_ptr()) + }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn add_benchmark_timestamps(&self, start: u64, end: u64) { + let pid = std::process::id(); + + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_BENCHMARK_START as u8, + start, + ) + }; + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_BENCHMARK_END as u8, + end, + ) + }; + } + + #[inline(always)] + pub fn add_sample_timestamps(&self, start: u64, end: u64) { + let pid = std::process::id(); + + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_SAMPLE_START as u8, + start, + ) + }; + + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_SAMPLE_END as u8, + end, + ) + }; + } + + #[inline(always)] + pub fn current_timestamp() -> u64 { + #[cfg(target_os = "linux")] + { + use nix::sys::time::TimeValLike; + + nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC) + .expect("Failed to get current time") + .num_nanoseconds() as u64 + } + + #[cfg(not(target_os = "linux"))] + unsafe { + ffi::instrument_hooks_current_timestamp() + } + } + + pub fn disable_callgrind_markers() { + unsafe { + ffi::instrument_hooks_set_feature( + ffi::instrument_hooks_feature_t_FEATURE_DISABLE_CALLGRIND_MARKERS, + true, + ) + }; + } + } + + impl Drop for InstrumentHooks { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { ffi::instrument_hooks_deinit(self.0) }; + } + } + } +} + +#[cfg(not(use_instrument_hooks))] +mod other_impl { + #[derive(PartialEq)] + pub struct InstrumentHooks; + + impl InstrumentHooks { + pub fn instance() -> &'static Self { + static INSTANCE: InstrumentHooks = InstrumentHooks; + &INSTANCE + } + + pub fn is_instrumented(&self) -> bool { + false + } + + pub fn start_benchmark(&self) -> Result<(), u8> { + Ok(()) + } + + pub fn stop_benchmark(&self) -> Result<(), u8> { + Ok(()) + } + + pub fn set_executed_benchmark(&self, _uri: &str) -> Result<(), u8> { + Ok(()) + } + + pub fn set_integration(&self, _name: &str, _version: &str) -> Result<(), u8> { + Ok(()) + } + + pub fn add_benchmark_timestamps(&self, _start: u64, _end: u64) {} + + pub fn current_timestamp() -> u64 { + 0 + } + + pub fn disable_callgrind_markers() {} + } +} + +#[cfg(use_instrument_hooks)] +pub use linux_impl::InstrumentHooks; + +#[cfg(not(use_instrument_hooks))] +pub use other_impl::InstrumentHooks; + +#[cfg(test)] +mod tests { + use super::InstrumentHooks; + + #[test] + fn test_instrument_hooks() { + let hooks = InstrumentHooks::instance(); + assert!(!hooks.is_instrumented() || hooks.start_benchmark().is_ok()); + assert!(hooks.set_executed_benchmark("test_uri").is_ok()); + assert!(hooks.set_integration("test_integration", "1.0.0").is_ok()); + let start = InstrumentHooks::current_timestamp(); + let end = start + 1_000_000; // Simulate 1ms later + hooks.add_benchmark_timestamps(start, end); + assert!(!hooks.is_instrumented() || hooks.stop_benchmark().is_ok()); + } +} diff --git a/crates/instrument-hooks-bindings/src/mod.rs b/crates/instrument-hooks-bindings/src/mod.rs new file mode 100644 index 00000000..fd6c6dda --- /dev/null +++ b/crates/instrument-hooks-bindings/src/mod.rs @@ -0,0 +1,220 @@ +mod ffi; + +mod linux_impl { + + use super::ffi; + use std::ffi::CString; + use std::sync::OnceLock; + + #[derive(PartialEq)] + pub struct InstrumentHooks(*mut ffi::InstrumentHooks); + + unsafe impl Send for InstrumentHooks {} + unsafe impl Sync for InstrumentHooks {} + + impl InstrumentHooks { + #[inline(always)] + pub fn new() -> Option { + let ptr = unsafe { ffi::instrument_hooks_init() }; + if ptr.is_null() { + None + } else { + Some(InstrumentHooks(ptr)) + } + } + + /// Returns a singleton instance of `InstrumentHooks`. + #[inline(always)] + pub fn instance() -> &'static Self { + static INSTANCE: OnceLock = OnceLock::new(); + INSTANCE.get_or_init(|| { + let instance = + InstrumentHooks::new().expect("Failed to initialize InstrumentHooks"); + instance + .set_integration("codspeed-rust", env!("CARGO_PKG_VERSION")) + .expect("Failed to set integration"); + instance + }) + } + + #[inline(always)] + pub fn is_instrumented(&self) -> bool { + unsafe { ffi::instrument_hooks_is_instrumented(self.0) } + } + + #[inline(always)] + pub fn start_benchmark(&self) -> Result<(), u8> { + let result = unsafe { ffi::instrument_hooks_start_benchmark(self.0) }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn stop_benchmark(&self) -> Result<(), u8> { + let result = unsafe { ffi::instrument_hooks_stop_benchmark(self.0) }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn set_executed_benchmark(&self, uri: &str) -> Result<(), u8> { + let pid = std::process::id() as i32; + let c_uri = CString::new(uri).map_err(|_| 1u8)?; + let result = unsafe { + ffi::instrument_hooks_set_executed_benchmark(self.0, pid, c_uri.as_ptr()) + }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn set_integration(&self, name: &str, version: &str) -> Result<(), u8> { + let c_name = CString::new(name).map_err(|_| 1u8)?; + let c_version = CString::new(version).map_err(|_| 1u8)?; + let result = unsafe { + ffi::instrument_hooks_set_integration(self.0, c_name.as_ptr(), c_version.as_ptr()) + }; + if result == 0 { Ok(()) } else { Err(result) } + } + + #[inline(always)] + pub fn add_benchmark_timestamps(&self, start: u64, end: u64) { + let pid = std::process::id(); + + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_BENCHMARK_START as u8, + start, + ) + }; + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_BENCHMARK_END as u8, + end, + ) + }; + } + + #[inline(always)] + pub fn add_sample_timestamps(&self, start: u64, end: u64) { + let pid = std::process::id(); + + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_SAMPLE_START as u8, + start, + ) + }; + + unsafe { + ffi::instrument_hooks_add_marker( + self.0, + pid, + ffi::MARKER_TYPE_SAMPLE_END as u8, + end, + ) + }; + } + + #[inline(always)] + pub fn current_timestamp() -> u64 { + #[cfg(target_os = "linux")] + { + use nix::sys::time::TimeValLike; + + nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC) + .expect("Failed to get current time") + .num_nanoseconds() as u64 + } + + #[cfg(not(target_os = "linux"))] + unsafe { + ffi::instrument_hooks_current_timestamp() + } + } + + pub fn disable_callgrind_markers() { + unsafe { + ffi::instrument_hooks_set_feature( + ffi::instrument_hooks_feature_t_FEATURE_DISABLE_CALLGRIND_MARKERS, + true, + ) + }; + } + } + + impl Drop for InstrumentHooks { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { ffi::instrument_hooks_deinit(self.0) }; + } + } + } +} + +#[cfg(not(use_instrument_hooks))] +mod other_impl { + #[derive(PartialEq)] + pub struct InstrumentHooks; + + impl InstrumentHooks { + pub fn instance() -> &'static Self { + static INSTANCE: InstrumentHooks = InstrumentHooks; + &INSTANCE + } + + pub fn is_instrumented(&self) -> bool { + false + } + + pub fn start_benchmark(&self) -> Result<(), u8> { + Ok(()) + } + + pub fn stop_benchmark(&self) -> Result<(), u8> { + Ok(()) + } + + pub fn set_executed_benchmark(&self, _uri: &str) -> Result<(), u8> { + Ok(()) + } + + pub fn set_integration(&self, _name: &str, _version: &str) -> Result<(), u8> { + Ok(()) + } + + pub fn add_benchmark_timestamps(&self, _start: u64, _end: u64) {} + + pub fn current_timestamp() -> u64 { + 0 + } + + pub fn disable_callgrind_markers() {} + } +} + +#[cfg(use_instrument_hooks)] +pub use linux_impl::InstrumentHooks; + +#[cfg(not(use_instrument_hooks))] +pub use other_impl::InstrumentHooks; + +#[cfg(test)] +mod tests { + use super::InstrumentHooks; + + #[test] + fn test_instrument_hooks() { + let hooks = InstrumentHooks::instance(); + assert!(!hooks.is_instrumented() || hooks.start_benchmark().is_ok()); + assert!(hooks.set_executed_benchmark("test_uri").is_ok()); + assert!(hooks.set_integration("test_integration", "1.0.0").is_ok()); + let start = InstrumentHooks::current_timestamp(); + let end = start + 1_000_000; // Simulate 1ms later + hooks.add_benchmark_timestamps(start, end); + assert!(!hooks.is_instrumented() || hooks.stop_benchmark().is_ok()); + } +} diff --git a/crates/instrument-hooks-bindings/src/update-bindings.sh b/crates/instrument-hooks-bindings/src/update-bindings.sh new file mode 100755 index 00000000..ce2e001b --- /dev/null +++ b/crates/instrument-hooks-bindings/src/update-bindings.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +bindgen ../instrument-hooks/includes/core.h \ + -o bindings.rs \ + --rust-target 1.74 \ + --allowlist-function "instrument_hooks_.*" \ + --allowlist-var "MARKER_TYPE_.*" From 150ff2f990cafaf1359e158cd02b5f69bef00a6a Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Fri, 30 Jan 2026 17:30:39 +0100 Subject: [PATCH 2/4] feat(exec-harness): use the new instrument-hooks bindings rather than codspeed core --- Cargo.lock | 3 +- crates/exec-harness/Cargo.toml | 3 +- crates/exec-harness/build.rs | 58 ++++--------------- crates/exec-harness/src/analysis/mod.rs | 6 +- .../src/walltime/benchmark_loop.rs | 5 +- crates/exec-harness/src/walltime/mod.rs | 14 ++++- crates/instrument-hooks-bindings/src/lib.rs | 6 +- .../runner-shared/src/walltime_results/mod.rs | 9 +-- 8 files changed, 37 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b208511..c9339be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -888,12 +888,11 @@ name = "exec-harness" version = "1.1.0" dependencies = [ "anyhow", - "cargo_metadata", "cc", "clap", - "codspeed", "env_logger", "humantime", + "instrument-hooks-bindings", "log", "object 0.36.7", "runner-shared", diff --git a/crates/exec-harness/Cargo.toml b/crates/exec-harness/Cargo.toml index 88f521f9..30534ab5 100644 --- a/crates/exec-harness/Cargo.toml +++ b/crates/exec-harness/Cargo.toml @@ -10,8 +10,8 @@ name = "exec-harness" path = "src/main.rs" [dependencies] +instrument-hooks-bindings = { path = "../instrument-hooks-bindings" } anyhow = { workspace = true } -codspeed = "4.2.0" log = { workspace = true } env_logger = { workspace = true } clap = { workspace = true } @@ -23,7 +23,6 @@ tempfile = { workspace = true } object = { workspace = true } [build-dependencies] -cargo_metadata = "0.19" cc = "1" [package.metadata.dist] diff --git a/crates/exec-harness/build.rs b/crates/exec-harness/build.rs index 6fb18014..113857b2 100644 --- a/crates/exec-harness/build.rs +++ b/crates/exec-harness/build.rs @@ -3,16 +3,9 @@ //! This script compiles the `libcodspeed_preload.so` shared library that is used //! to inject instrumentation into child processes via LD_PRELOAD. //! -//! The library is built using the `core.c` and headers from the `codspeed` crate's -//! `instrument-hooks` directory. -//! -//! # Environment Variables -//! -//! - `CODSPEED_INSTRUMENT_HOOKS_DIR`: Optional override for the instrument-hooks -//! source directory. If not set, the build script will locate it from the -//! `codspeed` crate in the cargo registry. +//! The library is built using the `core.c` and headers from the `instrument-hooks-bindings` +//! crate's `instrument-hooks` directory. -use cargo_metadata::MetadataCommand; use std::env; use std::path::PathBuf; @@ -25,7 +18,6 @@ struct PreloadConstants { /// Integration name reported to CodSpeed. integration_name: &'static str, /// Integration version reported to CodSpeed. - /// This should match the version of the `codspeed` crate dependency. integration_version: &'static str, /// Filename for the preload shared library. preload_lib_filename: &'static str, @@ -58,11 +50,11 @@ fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); // Try to get the instrument-hooks directory from the environment variable first, - // otherwise locate it from the codspeed crate - let instrument_hooks_dir = match env::var("CODSPEED_INSTRUMENT_HOOKS_DIR") { - Ok(dir) => PathBuf::from(dir), - Err(_) => find_codspeed_instrument_hooks_dir(), - }; + // otherwise use the one from the instrument-hooks-bindings crate + let instrument_hooks_dir = manifest_dir + .parent() + .unwrap() + .join("instrument-hooks-bindings/instrument-hooks"); // Build the preload shared library let paths = PreloadBuildPaths { @@ -130,40 +122,12 @@ fn build_shared_library(paths: &PreloadBuildPaths, constants: &PreloadConstants) } } -/// Find the instrument-hooks directory from the codspeed crate using cargo_metadata -fn find_codspeed_instrument_hooks_dir() -> PathBuf { - let metadata = MetadataCommand::new() - .exec() - .expect("Failed to run cargo metadata"); - - // Find the codspeed package in the resolved dependencies - let codspeed_pkg = metadata - .packages - .iter() - .find(|p| p.name == "codspeed") - .expect("codspeed crate not found in dependencies"); - - let codspeed_dir = codspeed_pkg - .manifest_path - .parent() - .expect("Failed to get codspeed crate directory"); - - let instrument_hooks_dir = codspeed_dir.join("instrument-hooks"); - - if !instrument_hooks_dir.exists() { - panic!("instrument-hooks directory not found at {instrument_hooks_dir}"); - } - - instrument_hooks_dir.into_std_path_buf() -} - impl Default for PreloadConstants { - // TODO(COD-1736): Stop impersonating codspeed-rust 🥸 fn default() -> Self { Self { uri_env: "CODSPEED_BENCH_URI", - integration_name: "codspeed-rust", - integration_version: "4.2.0", + integration_name: "exec-harness", + integration_version: env!("CARGO_PKG_VERSION"), preload_lib_filename: "libcodspeed_preload.so", } } @@ -185,13 +149,13 @@ impl PreloadBuildPaths { fn check_sources_exist(&self) { if !self.core_c.exists() { panic!( - "core.c not found at {}. Make sure the codspeed crate is available.", + "core.c not found at {}. Make sure the instrument-hooks-bindings crate is available.", self.core_c.display() ); } if !self.includes_dir.exists() { panic!( - "includes directory not found at {}. Make sure the codspeed crate is available.", + "includes directory not found at {}. Make sure the instrument-hooks-bindings crate is available.", self.includes_dir.display() ); } diff --git a/crates/exec-harness/src/analysis/mod.rs b/crates/exec-harness/src/analysis/mod.rs index 49442d1c..65e220d7 100644 --- a/crates/exec-harness/src/analysis/mod.rs +++ b/crates/exec-harness/src/analysis/mod.rs @@ -1,16 +1,18 @@ +use crate::constants::INTEGRATION_NAME; +use crate::constants::INTEGRATION_VERSION; use crate::prelude::*; use crate::BenchmarkCommand; use crate::constants; use crate::uri; -use codspeed::instrument_hooks::InstrumentHooks; +use instrument_hooks_bindings::InstrumentHooks; use std::process::Command; mod ld_preload_check; mod preload_lib_file; pub fn perform(commands: Vec) -> Result<()> { - let hooks = InstrumentHooks::instance(); + let hooks = InstrumentHooks::instance(INTEGRATION_NAME, INTEGRATION_VERSION); for benchmark_cmd in commands { let name_and_uri = uri::generate_name_and_uri(&benchmark_cmd.name, &benchmark_cmd.command); diff --git a/crates/exec-harness/src/walltime/benchmark_loop.rs b/crates/exec-harness/src/walltime/benchmark_loop.rs index 9ea14b47..b18038f7 100644 --- a/crates/exec-harness/src/walltime/benchmark_loop.rs +++ b/crates/exec-harness/src/walltime/benchmark_loop.rs @@ -1,7 +1,8 @@ use super::ExecutionOptions; use super::config::RoundOrTime; +use crate::constants::{INTEGRATION_NAME, INTEGRATION_VERSION}; use crate::prelude::*; -use codspeed::instrument_hooks::InstrumentHooks; +use instrument_hooks_bindings::InstrumentHooks; use std::process::Command; use std::time::Duration; @@ -11,7 +12,7 @@ pub fn run_rounds( config: &ExecutionOptions, ) -> Result> { let warmup_time_ns = config.warmup_time_ns; - let hooks = InstrumentHooks::instance(); + let hooks = InstrumentHooks::instance(INTEGRATION_NAME, INTEGRATION_VERSION); let do_one_round = || -> Result<(u64, u64)> { let mut child = Command::new(&command[0]) diff --git a/crates/exec-harness/src/walltime/mod.rs b/crates/exec-harness/src/walltime/mod.rs index b121063d..d52870c9 100644 --- a/crates/exec-harness/src/walltime/mod.rs +++ b/crates/exec-harness/src/walltime/mod.rs @@ -3,10 +3,13 @@ mod config; pub use config::ExecutionOptions; pub use config::WalltimeExecutionArgs; +use runner_shared::walltime_results::Creator; use runner_shared::walltime_results::WalltimeBenchmark; pub use runner_shared::walltime_results::WalltimeResults; use crate::BenchmarkCommand; +use crate::constants::INTEGRATION_NAME; +use crate::constants::INTEGRATION_VERSION; use crate::prelude::*; use crate::uri::NameAndUri; use crate::uri::generate_name_and_uri; @@ -42,8 +45,15 @@ pub fn perform(commands: Vec) -> Result<()> { walltime_benchmarks.push(walltime_benchmark); } - let walltime_results = WalltimeResults::from_benchmarks(walltime_benchmarks) - .expect("Failed to create walltime results"); + let walltime_results = WalltimeResults::new( + Creator { + name: INTEGRATION_NAME.to_string(), + version: INTEGRATION_VERSION.to_string(), + pid: std::process::id(), + }, + walltime_benchmarks, + ) + .expect("Failed to create walltime results"); walltime_results .save_to_file( diff --git a/crates/instrument-hooks-bindings/src/lib.rs b/crates/instrument-hooks-bindings/src/lib.rs index f5591280..c4a12aaa 100644 --- a/crates/instrument-hooks-bindings/src/lib.rs +++ b/crates/instrument-hooks-bindings/src/lib.rs @@ -27,13 +27,13 @@ mod linux_impl { /// Returns a singleton instance of `InstrumentHooks`. #[inline(always)] - pub fn instance() -> &'static Self { + pub fn instance(integration: &str, version: &str) -> &'static Self { static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| { let instance = InstrumentHooks::new().expect("Failed to initialize InstrumentHooks"); instance - .set_integration("codspeed-rust", env!("CARGO_PKG_VERSION")) + .set_integration(integration, version) .expect("Failed to set integration"); instance }) @@ -210,7 +210,7 @@ mod tests { #[test] fn test_instrument_hooks() { - let hooks = InstrumentHooks::instance(); + let hooks = InstrumentHooks::instance("test_integration", "1.0.0"); assert!(!hooks.is_instrumented() || hooks.start_benchmark().is_ok()); assert!(hooks.set_executed_benchmark("test_uri").is_ok()); assert!(hooks.set_integration("test_integration", "1.0.0").is_ok()); diff --git a/crates/runner-shared/src/walltime_results/mod.rs b/crates/runner-shared/src/walltime_results/mod.rs index 45e570c1..e10e8cf5 100644 --- a/crates/runner-shared/src/walltime_results/mod.rs +++ b/crates/runner-shared/src/walltime_results/mod.rs @@ -4,17 +4,12 @@ mod stats; pub use interfaces::*; impl WalltimeResults { - pub fn from_benchmarks(benchmarks: Vec) -> anyhow::Result { + pub fn new(creator: Creator, benchmarks: Vec) -> anyhow::Result { Ok(WalltimeResults { instrument: Instrument { type_: "walltime".to_string(), }, - creator: Creator { - // TODO(COD-1736): Stop impersonating codspeed-rust 🥸 - name: "codspeed-rust".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - pid: std::process::id(), - }, + creator, benchmarks, }) } From 0411285148f1c24f8089f0ae2bf946730ef080a6 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 2 Feb 2026 17:55:03 +0100 Subject: [PATCH 3/4] fix(runner): check benchmark URIs rather than memtrack events Memory profiling can be empty, if the benchmark has no allocations. We shouldn't fail in those cases. --- crates/runner-shared/src/artifacts/mod.rs | 11 +++++++-- src/executor/memory/executor.rs | 27 +++++++++++------------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/runner-shared/src/artifacts/mod.rs b/crates/runner-shared/src/artifacts/mod.rs index 7a95ebca..6df5ff35 100644 --- a/crates/runner-shared/src/artifacts/mod.rs +++ b/crates/runner-shared/src/artifacts/mod.rs @@ -1,6 +1,8 @@ +use std::io::BufReader; + use libc::pid_t; use log::debug; -use serde::Serialize; +use serde::{Deserialize, Serialize}; mod execution_timestamps; mod memtrack; @@ -10,7 +12,7 @@ pub use memtrack::*; pub trait ArtifactExt where - Self: Sized + Serialize, + Self: Sized + Serialize + for<'de> Deserialize<'de>, { /// WARNING: This doesn't support generic types fn name() -> &'static str { @@ -25,6 +27,11 @@ where } } + fn decode_from_reader(reader: R) -> anyhow::Result { + let reader = BufReader::new(reader); + rmp_serde::from_read(reader).map_err(anyhow::Error::from) + } + fn encode_to_writer(&self, mut writer: W) -> anyhow::Result<()> { let encoded = rmp_serde::to_vec_named(self)?; writer.write_all(&encoded)?; diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index 388f7193..53688db0 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -16,7 +16,7 @@ use async_trait::async_trait; use ipc_channel::ipc; use memtrack::MemtrackIpcClient; use memtrack::MemtrackIpcServer; -use runner_shared::artifacts::{ArtifactExt, ExecutionTimestamps, MemtrackArtifact}; +use runner_shared::artifacts::{ArtifactExt, ExecutionTimestamps}; use runner_shared::fifo::Command as FifoCommand; use runner_shared::fifo::IntegrationMode; use semver::Version; @@ -121,28 +121,27 @@ impl Executor for MemoryExecutor { } async fn teardown(&self, execution_context: &ExecutionContext) -> Result<()> { - let files: Vec<_> = std::fs::read_dir(execution_context.profile_folder.join("results"))? + let results_dir = execution_context.profile_folder.join("results"); + let has_benchmarks = std::fs::read_dir(&results_dir)? .filter_map(Result::ok) - // Filter out non-memtrack files: + // Filter out non-ExecutionTimestamps files: .filter(|entry| { entry .file_name() .to_string_lossy() - .contains(MemtrackArtifact::name()) + .contains(ExecutionTimestamps::name()) }) - .flat_map(|f| std::fs::File::open(f.path())) - .filter(|file| !MemtrackArtifact::is_empty(file)) - .collect(); + .filter_map(|entry| { + let file = std::fs::File::open(entry.path()).ok()?; + ExecutionTimestamps::decode_from_reader(file).ok() + }) + .any(|artifact| !artifact.uri_by_ts.is_empty()); - if files.is_empty() { + if !has_benchmarks { if !execution_context.config.allow_empty { - bail!( - "No memtrack artifact files found. Does the integration support memory profiling?" - ); + bail!("No memory results found in profile folder: {results_dir:?}."); } else { - info!( - "No memtrack artifact files found. Does the integration support memory profiling?" - ); + info!("No memory results found in profile folder: {results_dir:?}."); } } From c4b1532456f7a57aebd8e439317f706f4b6682ca Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Tue, 3 Feb 2026 16:25:24 +0100 Subject: [PATCH 4/4] feat: allow memory instrument with exec-harness's integration hook --- src/executor/memory/executor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index 53688db0..327a7e3c 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -177,6 +177,7 @@ impl MemoryExecutor { "codspeed-rust" => Version::new(4, 2, 0), "codspeed-cpp" => Version::new(2, 1, 0), "pytest-codspeed" => Version::new(4, 3, 0), + "exec-harness" => Version::new(1, 0, 0), _ => { panic!("{INVALID_INTEGRATION_ERROR}") }