Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ On Linux, samply uses perf events. You can grant temporary access by running:
echo '-1' | sudo tee /proc/sys/kernel/perf_event_paranoid
```

On Linux, use `--unwinding-mode fp` to record kernel frame-pointer callchains without copying user stack bytes or registers:

```sh
samply record --unwinding-mode fp ./my-application my-arguments
```

The default, `--unwinding-mode dwarf`, keeps stack/register capture and framehop unwinding with the kernel frame-pointer callchain as a fallback.

Frame-pointer recording requires the application and its libraries to preserve frame pointers. For Rust, build with `RUSTFLAGS="-C force-frame-pointers=yes"`; for C/C++, use `-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer`. Missing frame pointers can truncate the stack. Kernel callchain support depends on the target architecture and kernel, and callchain depth is bounded by `/proc/sys/kernel/perf_event_max_stack`.

On Windows, you can use `samply record -a` to record all processes. You'll usually also want to use some symbol servers, most importantly the Microsoft Symbol Server so that you can see symbols for Windows libraries. Here's a command which supports symbols for Windows, Firefox and Chrome:

```
Expand Down
109 changes: 108 additions & 1 deletion samply/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::server::{PortSelection, ServerProps};
use super::shared::included_processes::IncludedProcesses;
use super::shared::prop_types::{
CoreClrProfileProps, ImportProps, ProcessLaunchProps, ProfileCreationProps, RecordingMode,
RecordingProps, SymbolProps,
RecordingProps, SymbolProps, UnwindingMode, DEFAULT_UNWINDING_STACK_SIZE,
};

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -133,6 +133,17 @@ pub struct RecordArgs {
#[arg(short, long, default_value = "1000")]
pub rate: f64,

/// Stack unwinding method (Linux only).
#[cfg(target_os = "linux")]
#[arg(long, value_enum, default_value = "dwarf")]
pub unwinding_mode: UnwindingMode,

/// Bytes of user stack copied with each sample for DWARF unwinding (Linux only).
/// Must be a multiple of 8 and below 65536. Ignored with `--unwinding-mode fp`.
#[cfg(target_os = "linux")]
#[arg(long, default_value_t = DEFAULT_UNWINDING_STACK_SIZE, value_parser = parse_unwinding_stack_size)]
pub unwinding_stack_size: u32,

/// Limit the recorded time to the specified number of seconds
#[arg(short, long)]
pub duration: Option<f64>,
Expand Down Expand Up @@ -456,6 +467,14 @@ impl RecordArgs {
output_file: self.output.clone(),
time_limit,
interval,
#[cfg(target_os = "linux")]
unwinding_mode: self.unwinding_mode,
#[cfg(not(target_os = "linux"))]
unwinding_mode: UnwindingMode::Dwarf,
#[cfg(target_os = "linux")]
unwinding_stack_size: self.unwinding_stack_size,
#[cfg(not(target_os = "linux"))]
unwinding_stack_size: DEFAULT_UNWINDING_STACK_SIZE,
perf_events,
gfx: self.gfx,
browsers: self.browsers,
Expand Down Expand Up @@ -614,6 +633,17 @@ impl SymbolArgs {
}
}

/// The kernel rejects `sample_stack_user` values that are not 8-byte aligned
/// or do not fit in a `u16`.
#[cfg(target_os = "linux")]
fn parse_unwinding_stack_size(s: &str) -> Result<u32, String> {
let size: u32 = s.parse().map_err(|e| format!("{e}"))?;
if size == 0 || size % 8 != 0 || size > u32::from(u16::MAX) {
return Err("must be a non-zero multiple of 8 below 65536".to_string());
}
Ok(size)
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -648,4 +678,81 @@ mod test {
let opt_res = Opt::try_parse_from(["samply", "record", "-p", "1234", "rustup"]);
assert!(opt_res.is_err());
}

#[cfg(target_os = "linux")]
#[test]
fn record_fp_unwinding_with_launched_command() {
let opt = Opt::try_parse_from([
"samply",
"record",
"--unwinding-mode",
"fp",
"rustup",
"--version",
])
.unwrap();
let Action::Record(args) = opt.action else {
panic!("expected record action");
};
assert_eq!(args.unwinding_mode, UnwindingMode::Fp);
assert_eq!(args.command, ["rustup", "--version"]);
assert!(matches!(args.recording_mode(), RecordingMode::Launch(_)));
}

#[cfg(target_os = "linux")]
#[test]
fn record_fp_unwinding_with_pid_attach() {
let opt = Opt::try_parse_from([
"samply",
"record",
"--unwinding-mode",
"fp",
"--pid",
"1234",
])
.unwrap();
let Action::Record(args) = opt.action else {
panic!("expected record action");
};
assert_eq!(args.unwinding_mode, UnwindingMode::Fp);
assert!(matches!(args.recording_mode(), RecordingMode::Pid(1234)));
}

#[cfg(target_os = "linux")]
#[test]
fn record_rejects_unsupported_unwinding_mode() {
let error = Opt::try_parse_from([
"samply",
"record",
"--unwinding-mode",
"unsupported",
"--pid",
"1234",
])
.unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
}

#[cfg(target_os = "linux")]
#[test]
fn record_unwinding_stack_size() {
let parse = |size: &str| {
Opt::try_parse_from([
"samply",
"record",
"--unwinding-stack-size",
size,
"--pid",
"1234",
])
};
let Action::Record(args) = parse("65528").unwrap().action else {
panic!("expected record action");
};
assert_eq!(args.unwinding_stack_size, 65528);
for invalid in ["0", "1001", "65536"] {
let error = parse(invalid).unwrap_err();
assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
}
}
}
10 changes: 3 additions & 7 deletions samply/src/linux/perf_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,13 +505,9 @@ impl PerfBuilder {
attr.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID;
}

// Always request callchain so the kernel walks frame pointers at sample
// time. The user portion serves as a fallback when DWARF unwinding
// truncates (e.g. due to the 32 KB captured stack window).
// FP-only recording uses the callchain directly; DWARF recording uses
// its user portion as a fallback when the captured stack is too short.
attr.sample_type |= PERF_SAMPLE_CALLCHAIN;
if exclude_kernel {
attr.flags |= PERF_ATTR_FLAG_EXCLUDE_CALLCHAIN_KERNEL;
}

attr.sample_regs_user = reg_mask;
attr.sample_stack_user = stack_size;
Expand All @@ -533,7 +529,7 @@ impl PerfBuilder {
}

if exclude_kernel {
attr.flags |= PERF_ATTR_FLAG_EXCLUDE_KERNEL;
attr.flags |= PERF_ATTR_FLAG_EXCLUDE_KERNEL | PERF_ATTR_FLAG_EXCLUDE_CALLCHAIN_KERNEL;
}

if inherit {
Expand Down
33 changes: 27 additions & 6 deletions samply/src/linux/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::linux_shared::{
};
use crate::shared::ctrl_c::CtrlC;
use crate::shared::prop_types::{
ProcessLaunchProps, ProfileCreationProps, RecordingMode, RecordingProps,
ProcessLaunchProps, ProfileCreationProps, RecordingMode, RecordingProps, UnwindingMode,
};

#[cfg(target_arch = "x86_64")]
Expand Down Expand Up @@ -93,6 +93,8 @@ pub fn run(

// Launch the observer thread. This thread will manage the perf events.
let interval = recording_props.interval;
let unwinding_mode = recording_props.unwinding_mode;
let unwinding_stack_size = recording_props.unwinding_stack_size;
let time_limit = recording_props.time_limit;
// Resolve the extra event names before launching anything, so that a
// configuration error aborts immediately.
Expand All @@ -117,7 +119,15 @@ pub fn run(
};

// Create the perf events, setting ENABLE_ON_EXEC.
let perf_group = init_profiler(interval, pid, attach_mode, extra_events, &mut converter);
let perf_group = init_profiler(
interval,
unwinding_mode,
unwinding_stack_size,
pid,
attach_mode,
extra_events,
&mut converter,
);

// Tell the main thread to tell the child process to begin executing.
profile_another_pid_reply_sender.send(true).unwrap();
Expand Down Expand Up @@ -281,8 +291,15 @@ fn start_profiling_pid(
else {
panic!("The first message should be a StartProfilingAnotherProcess")
};
let perf_group =
init_profiler(interval, pid, attach_mode, extra_events, &mut converter);
let perf_group = init_profiler(
interval,
recording_props.unwinding_mode,
recording_props.unwinding_stack_size,
pid,
attach_mode,
extra_events,
&mut converter,
);

// Tell the main thread that we are now executing.
profile_another_pid_reply_sender.send(true).unwrap();
Expand Down Expand Up @@ -403,6 +420,8 @@ fn resolve_extra_events(specs: &[String]) -> Vec<ExtraEvent> {

fn init_profiler(
interval: Duration,
unwinding_mode: UnwindingMode,
unwinding_stack_size: u32,
pid: u32,
attach_mode: AttachMode,
extra_events: Vec<ExtraEvent>,
Expand All @@ -417,8 +436,10 @@ fn init_profiler(
};

let frequency = (1_000_000_000 / interval_nanos) as u32;
let stack_size = 32000;
let regs_mask = ConvertRegsNative::regs_mask();
let (stack_size, regs_mask) = match unwinding_mode {
UnwindingMode::Dwarf => (unwinding_stack_size, ConvertRegsNative::regs_mask()),
UnwindingMode::Fp => (0, 0),
};

let mut perf = PerfGroup::open(
pid,
Expand Down
Loading
Loading