Skip to content

feat: add samply profiler support#338

Open
not-matthias wants to merge 16 commits intomainfrom
cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format
Open

feat: add samply profiler support#338
not-matthias wants to merge 16 commits intomainfrom
cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format

Conversation

@not-matthias
Copy link
Copy Markdown
Member

No description provided.

@not-matthias not-matthias force-pushed the cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format branch from 468b6c8 to 0e6e4ea Compare May 5, 2026 14:16
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 5, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks


Comparing cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format (8bbdf4f) with main (fd8701d)

Open in CodSpeed

Move ELF/DWARF artifact extraction (elf_helper, module_symbols,
unwind_data, debug_info, jit_dump) and the LoadedModule struct out
of perf/ into a shared symbolication/ module. These helpers are
agnostic to which sampling tool produced the module list and will
be reused by future Linux profilers.
Output filename also changes from perf.metadata to walltime.metadata.
This is a wire-breaking change that requires a coordinated server
rollout to read the new filename.
PerfRunner becomes PerfProfiler implementing the Profiler trait. The
RunnerFifo loop moves out of the perf module and into WallTimeExecutor,
which now drives the trait callbacks (on_start_benchmark / on_stop_benchmark
/ on_ping / GetIntegrationMode) generically.

Profiler::wrap stashes the perf control fifo and output path on the
profiler instance; finalize harvests the perf.data artifacts and writes
walltime.metadata. The OnceCell<BenchmarkData> bridge is gone — fifo
data and timestamps are passed directly into finalize.

Executor::run now takes &mut self to allow profilers to hold per-run
state on the trait object. This cascades through run_executor,
orchestrator, and the Memory/Valgrind executors (no behavioral
change for those — they don't mutate self).
…BLED

Introduce --enable-profiler / CODSPEED_PROFILER_ENABLED as the canonical
flag now that walltime profiling is profiler-agnostic. The legacy
--enable-perf / CODSPEED_PERF_ENABLED is kept as a hidden alias and
emits a deprecation warning when used, so existing configurations keep
working.
Tokio's OpenOptions::read_write is Linux-only, but the O_RDWR trick
works on every Unix and is required on macOS to avoid open() deadlocks
when both FIFO ends are opened before the peer process is spawned.
Mirror the clock used elsewhere in CodSpeed on macOS (mach_absolute_time
with cached mach_timebase_info) so runner and benchmark process
timestamps come from the exact same source. Adds mach2 as a macOS-only
dependency.
@not-matthias not-matthias force-pushed the cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format branch from 6ad33b5 to f1d7ea2 Compare May 6, 2026 16:50
@not-matthias not-matthias force-pushed the cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format branch from 9a18ed3 to b55c9b2 Compare May 7, 2026 13:37
Move the Linux profiling sysctl setup out of the perf profiler and into a shared walltime profiler helper. This lets both perf and samply prepare kernel symbol access and non-root profiling consistently while preserving the existing sudo-based behavior.
Use the samply crate directly instead of a standalone binary. Adds a
`codspeed samply` subcommand that forwards args to samply's CLI and
dispatches to `do_record_action`. The wall_time profiler re-execs the
current binary, removing the need for a PATH-installed samply.
@not-matthias not-matthias force-pushed the cod-2599-runner-migrate-perf-to-new-profiler-agnostic-data-format branch from b55c9b2 to 9a2ab1b Compare May 7, 2026 13:37
Copy link
Copy Markdown
Contributor

@GuillaumeLagrange GuillaumeLagrange left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching to github mid review, have to publish 😬

) -> anyhow::Result<CommandBuilder>;

/// The benchmarked process signaled the start of a measured region.
async fn on_start_benchmark(&mut self) -> anyhow::Result<()> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make use of the fact that we are changing stuff here to clear up the name of the markers?
We've discussed this multiple time, and it's quite a huge clusterfuck

The current names with markers is this

    /// It expects the data to be structured like this:
    /// - SampleStart
    ///    - BenchmarkStart
    ///       - N samples
    ///    - BenchmarkEnd
    /// - SampleEnd

But we also have the fifo commands StartBenchmark and StopBenchmark which actually push SampleStart/End markers

What I propose for this is just a rename that would make everything a bit clearer

The external marker, currently named SampleStart should be named ProfilerSamplingStart, or something that conveys the idea that we are starting and stopping the profiler's sampling functionality, and we should rename the FifoCommand::StartBenchmark to reflect that. Another naming possibility could be BenchmarkRegion if we want to be a bit farther from the implementation detail of starting/stopping the profiler

The internal marker would be clearer if it was something like RoundStart RoundEnd, as in practice it's what is being done.

WE SHOULD BE ABSOLUTELY EXTREMELY CAREFUL TO MAKE SURE IT DOES NOT IMPACT THE SERIALIZED IDs, it's only meant to be a naming clarification.

WDYT?

Comment thread src/executor/wall_time/perf/mod.rs Outdated
FifoCommand::GetIntegrationMode => {
return Ok(Some(FifoCommand::IntegrationModeResponse(
IntegrationMode::Perf,
IntegrationMode::Walltime,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make absolutely sure that the ser/de test inside instrument-hooks still works, and I believe explicitly updated.

Not sure which is the best order, but we need to be careful this does not get left behind as it had been before

Copy link
Copy Markdown
Contributor

@GuillaumeLagrange GuillaumeLagrange left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first pass done, let me know if you want to sync on some points

@@ -28,18 +28,13 @@ use std::path::Path;
use std::path::PathBuf;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seen together, this commit is pretty much useless since smaply handles the symbolification


pub struct WallTimeExecutor {
perf: Option<PerfRunner>,
profiler: Option<Box<dyn Profiler>>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it wont really change at runtime and is computed from static cfg asserts anyway, wouldnt it be better to have a type generic profiler rather than a Box<dyn> ?

if let Some(perf) = &self.perf
&& execution_context.config.enable_perf
{
perf.save_files_to(&execution_context.profile_folder)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason the finalize part moved from teardown step to the execute step?

I'm guessing it has no side effect, but still conceptually it makes more sense to me to have the perf parsing etc as part of the executor teardown rather than inside the run step. WDYT?

Comment thread src/executor/wall_time/executor.rs Outdated
WallTimeExecutor::walltime_bench_cmd(&execution_context.config, execution_context)?;

let status = match self.profiler.as_mut() {
Some(profiler) if execution_context.config.enable_perf => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore if it is addressed in later commits, but we should rename everything that is perf related to profiler in the CLI interface, but still accept the perf options with a dperecation message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread src/cli/shared.rs
@@ -152,16 +152,34 @@ pub enum UnwindingMode {

#[derive(Args, Debug, Clone)]
pub struct PerfRunArgs {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be renamed and the unwinding mode argument should be scoped to perf, could be done using a generic to avoid having to handle this at runtime


#[async_trait(?Send)]
impl Profiler for SamplyProfiler {
async fn wrap(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename this to wrap_command to make it slightly clearer?

) -> anyhow::Result<()> {
let Some(integration) = fifo_data.integration.clone() else {
warn!(
"Walltime profiling is enabled, but failed to detect benchmarks. If you wish to disable this warning, set CODSPEED_PROFILER_ENABLED=false"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could be commonized with perf error message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially through a trait function, called inside finalize, since the detection condition seem extremely similar

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it may be overkill, at least just commonize the message

Comment thread src/cli/mod.rs
Comment on lines +121 to 124
/// Run the bundled samply profiler. Args are forwarded to samply.
#[command(disable_help_flag = true, disable_help_subcommand = true)]
Samply(samply::SamplyArgs),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we segregate these subcommands into a dedicated enum so we now at glance which commands are real entrypoints by users and which are actually just the cli re-invoking itself?

let mut samply_builder = CommandBuilder::new("samply");
// samply is bundled into this binary as the `samply` subcommand;
// re-exec ourselves so we don't depend on a system install.
let current_exe = std::env::current_exe()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we define the subcommands that are invoked by the CLI, we could have this as a helper that would just take the enum value and build the command with args etc

Comment thread src/cli/samply.rs
cli::Action::Record(a) => ::samply::do_record_action(a),
_ => unimplemented!("Only `samply record` is supported"),
}
// samply spins up its own tokio runtime internally, so it must run on a
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixup with previous commit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants