feat: add samply profiler support#338
Conversation
468b6c8 to
0e6e4ea
Compare
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.
6ad33b5 to
f1d7ea2
Compare
9a18ed3 to
b55c9b2
Compare
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.
b55c9b2 to
9a2ab1b
Compare
GuillaumeLagrange
left a comment
There was a problem hiding this comment.
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<()> { |
There was a problem hiding this comment.
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?
| FifoCommand::GetIntegrationMode => { | ||
| return Ok(Some(FifoCommand::IntegrationModeResponse( | ||
| IntegrationMode::Perf, | ||
| IntegrationMode::Walltime, |
There was a problem hiding this comment.
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
GuillaumeLagrange
left a comment
There was a problem hiding this comment.
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; | |||
There was a problem hiding this comment.
Seen together, this commit is pretty much useless since smaply handles the symbolification
|
|
||
| pub struct WallTimeExecutor { | ||
| perf: Option<PerfRunner>, | ||
| profiler: Option<Box<dyn Profiler>>, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
| WallTimeExecutor::walltime_bench_cmd(&execution_context.config, execution_context)?; | ||
|
|
||
| let status = match self.profiler.as_mut() { | ||
| Some(profiler) if execution_context.config.enable_perf => { |
There was a problem hiding this comment.
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
| @@ -152,16 +152,34 @@ pub enum UnwindingMode { | |||
|
|
|||
| #[derive(Args, Debug, Clone)] | |||
| pub struct PerfRunArgs { | |||
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Nit: could be commonized with perf error message
There was a problem hiding this comment.
potentially through a trait function, called inside finalize, since the detection condition seem extremely similar
There was a problem hiding this comment.
but it may be overkill, at least just commonize the message
| /// Run the bundled samply profiler. Args are forwarded to samply. | ||
| #[command(disable_help_flag = true, disable_help_subcommand = true)] | ||
| Samply(samply::SamplyArgs), | ||
| } |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
fixup with previous commit
No description provided.