Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
.DS_Store
.codspeed
compile_commands.json
.cache
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions crates/exec-harness/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ path = "src/main.rs"

[dependencies]
anyhow = { workspace = true }
codspeed = "4.1.0"
codspeed = "4.2.0"
log = { workspace = true }
env_logger = { workspace = true }
clap = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
humantime = "2.1"
runner-shared = { path = "../runner-shared" }

[dev-dependencies]
tempfile = { workspace = true }

[build-dependencies]
cargo_metadata = "0.19"
cc = "1"

[package.metadata.dist]
targets = ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"]
194 changes: 194 additions & 0 deletions crates/exec-harness/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//! Build script for exec-harness
//!
//! 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.

use cargo_metadata::MetadataCommand;
use std::env;
use std::path::PathBuf;

/// Shared constants for the preload library.
/// These are passed as C defines during compilation and exported as environment
/// variables for the Rust code to use via `env!()`.
struct PreloadConstants {
/// Environment variable name for the benchmark URI.
uri_env: &'static str,
/// 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,
}

// TODO(COD-1736): Stop impersonating codspeed-rust 🥸
const PRELOAD_CONSTANTS: PreloadConstants = PreloadConstants {
uri_env: "CODSPEED_BENCH_URI",
integration_name: "codspeed-rust",
integration_version: "4.2.0",
};
Comment thread
GuillaumeLagrange marked this conversation as resolved.
Outdated

/// Filename for the preload shared library.
const PRELOAD_LIB_FILENAME: &str = "libcodspeed_preload.so";

/// Paths required to build the preload shared library.
struct PreloadBuildPaths {
/// Path to the preload C source file (codspeed_preload.c).
preload_c: PathBuf,
/// Path to the core C source file from instrument-hooks.
core_c: PathBuf,
/// Path to the includes directory from instrument-hooks.
includes_dir: PathBuf,
/// Path where the output shared library will be written.
output_lib: PathBuf,
}

fn main() {
println!("cargo:rerun-if-changed=preload/codspeed_preload.c");
println!("cargo:rerun-if-env-changed=CODSPEED_INSTRUMENT_HOOKS_DIR");

// Export constants as environment variables for the Rust code
println!(
"cargo:rustc-env=CODSPEED_URI_ENV={}",
PRELOAD_CONSTANTS.uri_env
);
println!(
"cargo:rustc-env=CODSPEED_INTEGRATION_NAME={}",
PRELOAD_CONSTANTS.integration_name
);
println!(
"cargo:rustc-env=CODSPEED_INTEGRATION_VERSION={}",
PRELOAD_CONSTANTS.integration_version
);
println!("cargo:rustc-env=CODSPEED_PRELOAD_LIB_FILENAME={PRELOAD_LIB_FILENAME}");

let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
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(),
};

// Build the preload shared library
let paths = PreloadBuildPaths {
preload_c: manifest_dir.join("preload/codspeed_preload.c"),
core_c: instrument_hooks_dir.join("dist/core.c"),
includes_dir: instrument_hooks_dir.join("includes"),
output_lib: out_dir.join(PRELOAD_LIB_FILENAME),
};
paths.check_sources_exist();
build_shared_library(&paths, &PRELOAD_CONSTANTS);
}

/// Build the shared library using the cc crate
fn build_shared_library(paths: &PreloadBuildPaths, constants: &PreloadConstants) {
let uri_env_val = format!("\"{}\"", constants.uri_env);
let integration_name_val = format!("\"{}\"", constants.integration_name);
let integration_version_val = format!("\"{}\"", constants.integration_version);

let mut build = cc::Build::new();
build
.file(&paths.preload_c)
.file(&paths.core_c)
.include(&paths.includes_dir)
.pic(true)
.opt_level(2)
Comment thread
GuillaumeLagrange marked this conversation as resolved.
Outdated
.cargo_metadata(false)
Comment thread
GuillaumeLagrange marked this conversation as resolved.
// Pass constants as C defines
.define("CODSPEED_URI_ENV", uri_env_val.as_str())
.define("CODSPEED_INTEGRATION_NAME", integration_name_val.as_str())
.define(
"CODSPEED_INTEGRATION_VERSION",
integration_version_val.as_str(),
)
// Suppress warnings from generated Zig code
.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")
Comment thread
GuillaumeLagrange marked this conversation as resolved.
.flag("-Wno-overflow")
.flag("-Wno-unused-function");

// Compile source files to object files
let objects = build.compile_intermediates();

// Link object files into shared library
let compiler = build.get_compiler();
let mut link_cmd = compiler.to_command();
link_cmd
.arg("-shared")
.arg("-o")
.arg(&paths.output_lib)
.args(&objects)
.arg("-lpthread");

let status = link_cmd.status().expect("Failed to run linker");
if !status.success() {
panic!("Failed to link libcodspeed_preload.so");
}
}

/// Find the instrument-hooks directory from the codspeed crate using cargo_metadata
fn find_codspeed_instrument_hooks_dir() -> PathBuf {
Comment thread
GuillaumeLagrange marked this conversation as resolved.
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 PreloadBuildPaths {
Comment thread
GuillaumeLagrange marked this conversation as resolved.
/// Verify that all required source files and directories exist.
/// Panics with a descriptive message if any path is missing.
fn check_sources_exist(&self) {
if !self.core_c.exists() {
panic!(
"core.c not found at {}. Make sure the codspeed 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.",
self.includes_dir.display()
);
}
if !self.preload_c.exists() {
panic!(
"codspeed_preload.c not found at {}",
self.preload_c.display()
);
}
}
}
99 changes: 99 additions & 0 deletions crates/exec-harness/preload/codspeed_preload.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// LD_PRELOAD library for enabling Valgrind instrumentation in child processes
//
// This library is loaded via LD_PRELOAD into benchmark processes spawned by
// exec-harness. It enables callgrind instrumentation on load and disables it on
// exit, allowing exec-harness to measure arbitrary commands without requiring
// them to link against instrument-hooks.
//
// Environment variables:
// CODSPEED_BENCH_URI - The benchmark URI to report (required)
// CODSPEED_PRELOAD_LOCK - Set by the first process to prevent child processes
// from re-initializing instrumentation

#include <stdlib.h>
#include <unistd.h>

#include "core.h"

#ifndef RUNNING_ON_VALGRIND
// If somehow the core.h did not include the valgrind header, something is
// wrong, but still have a fallback
#warning "RUNNING_ON_VALGRIND not defined, headers may be missing"
#define RUNNING_ON_VALGRIND 0
#endif

static const char *LOCK_ENV = "CODSPEED_PRELOAD_LOCK";

// These constants are defined by the build script (build.rs) via -D flags
#ifndef CODSPEED_URI_ENV
#error "CODSPEED_URI_ENV must be defined by the build system"
#endif
Comment thread
not-matthias marked this conversation as resolved.
#ifndef CODSPEED_INTEGRATION_NAME
#error "CODSPEED_INTEGRATION_NAME must be defined by the build system"
#endif
#ifndef CODSPEED_INTEGRATION_VERSION
#error "CODSPEED_INTEGRATION_VERSION must be defined by the build system"
#endif

static const char *URI_ENV = CODSPEED_URI_ENV;
static const char *INTEGRATION_NAME = CODSPEED_INTEGRATION_NAME;
static const char *INTEGRATION_VERSION = CODSPEED_INTEGRATION_VERSION;

static InstrumentHooks *g_hooks = NULL;
static const char *g_bench_uri = NULL;

__attribute__((constructor)) static void codspeed_preload_init(void) {
// Skip initialization if not running under Valgrind yet.
// When using LD_PRELOAD with Valgrind, the constructor runs twice:
// once before Valgrind takes over, and once after. We only want to
// initialize when Valgrind is active.
//
// This is purely empirical, and is not (yet) backed up by documented
// behavior.
if (!RUNNING_ON_VALGRIND) {
return;
}

// Check if another process already owns the instrumentation
if (getenv(LOCK_ENV)) {
return;
}

// Set the lock to prevent child processes from initializing
setenv(LOCK_ENV, "1", 1);

g_bench_uri = getenv(URI_ENV);
if (!g_bench_uri) {
return;
}

g_hooks = instrument_hooks_init();
if (!g_hooks) {
return;
}

instrument_hooks_set_integration(g_hooks, INTEGRATION_NAME,
INTEGRATION_VERSION);

if (instrument_hooks_start_benchmark_inline(g_hooks) != 0) {
instrument_hooks_deinit(g_hooks);
g_hooks = NULL;
return;
}
}

__attribute__((destructor)) static void codspeed_preload_fini(void) {
// If the process is not the owner of the lock, this means g_hooks was not
// initialized
if (!g_hooks) {
Comment thread
GuillaumeLagrange marked this conversation as resolved.
return;
}

instrument_hooks_stop_benchmark_inline(g_hooks);

int32_t pid = getpid();
instrument_hooks_set_executed_benchmark(g_hooks, pid, g_bench_uri);

instrument_hooks_deinit(g_hooks);
g_hooks = NULL;
}
29 changes: 0 additions & 29 deletions crates/exec-harness/src/analysis.rs

This file was deleted.

Loading
Loading