Skip to content

Flamegraph requires perf even if not used #437

Description

@madeckel-philips

Hi, I really like your project, but encountered some issue: I want to use --perfdata to analyze a file recorded on a different machine, but it still requires perf to be locally installed. Copilot has figured out the reason below. I may implement it, if I find time for it. Otherwise feel free to implement.

The reason is that --perfdata only skips perf record, but it does not skip perf script.

In src/bin/flamegraph.rs, --perfdata sets the workload to Workload::ReadPerf(perf_file) and then calls:

  • flamegraph::generate_flamegraph_for_workload(workload, opt.graph)
    return Ok(());
    }
    opt.graph.check()?;
    let workload = if let Some(perf_file) = opt.perf_file {
    Workload::ReadPerf(perf_file)
    } else {
    match (opt.pid.is_empty(), opt.trailing_arguments.is_empty()) {
    (false, true) => Workload::Pid(opt.pid),
    (true, false) => Workload::Command(opt.trailing_arguments.clone()),
    (false, false) => return Err(anyhow!("cannot pass in command with --pid")),
    (true, true) => return Err(anyhow!("no workload given to generate a flamegraph for")),
    }
    };
    flamegraph::generate_flamegraph_for_workload(workload, opt.graph)

Inside generate_flamegraph_for_workload (src/lib.rs):

  1. If the workload is ReadPerf, it does:

    let perf_output = if let Workload::ReadPerf(perf_file) = workload {
        Some(perf_file)
    } else {
        arch::initial_command(...)?
    };

    So it does not call arch::initial_command, and therefore it does not run the perf --help “is perf installed?” check that exists in the Linux initial_command.

    flamegraph/src/lib.rs

    Lines 507 to 524 in 3388af9

    let perf_output = if let Workload::ReadPerf(perf_file) = workload {
    Some(perf_file)
    } else {
    #[cfg(target_os = "linux")]
    let compression = opts.compression_level;
    #[cfg(not(target_os = "linux"))]
    let compression = None;
    arch::initial_command(
    workload,
    sudo,
    opts.frequency(),
    compression,
    opts.custom_cmd,
    opts.verbose,
    opts.ignore_status,
    )?
    };

  2. But right after that, it always does:

    let output = arch::output(perf_output, opts.script_no_inline, sudo)?;

    let output = arch::output(perf_output, opts.script_no_inline, sudo)?;

On Linux, arch::output(...) is implemented to invoke perf script, using either $PERF or "perf":

let perf = env::var("PERF").unwrap_or_else(|_| "perf".to_string());
let mut command = sudo_command(&perf, sudo);

command.arg("script");
command.arg("--force");

if let Some(perf_output) = perf_output {
    command.arg("-i");
    command.arg(perf_output);
}

let result = command.output().context("unable to call perf script");

So even when you pass an existing perf.data file, it still needs a local perf binary to run perf script -i <file> to turn the binary perf.data into textual stacks that inferno can collapse.

flamegraph/src/lib.rs

Lines 134 to 178 in 3388af9

pub fn output(
perf_output: Option<PathBuf>,
script_no_inline: bool,
sudo: Option<Option<&str>>,
) -> anyhow::Result<Vec<u8>> {
// We executed `perf record` with sudo, and will be executing `perf script` with sudo,
// so that we can resolve privileged kernel symbols from /proc/kallsyms.
let perf = env::var("PERF").unwrap_or_else(|_| "perf".to_string());
let mut command = sudo_command(&perf, sudo);
command.arg("script");
// Force reading perf.data owned by another uid if it happened to be created earlier.
command.arg("--force");
if script_no_inline {
command.arg("--no-inline");
}
if let Some(perf_output) = perf_output {
command.arg("-i");
command.arg(perf_output);
}
// perf script can take a long time to run. Notify the user that it is running
// by using a spinner. Note that if this function exits before calling
// spinner.finish(), then the spinner will be completely removed from the terminal.
let spinner = ProgressBar::new_spinner().with_prefix("Running perf script");
spinner.set_style(
ProgressStyle::with_template("{prefix} [{elapsed}]: {spinner:.green}").unwrap(),
);
spinner.enable_steady_tick(Duration::from_millis(500));
let result = command.output().context("unable to call perf script");
spinner.finish();
let output = result?;
if !output.status.success() {
bail!(
"unable to run 'perf script': ({}) {}",
output.status,
std::str::from_utf8(&output.stderr)?
);
}
Ok(output.stdout)
}

Practical implication

If you don’t have perf installed locally, this version of flamegraph cannot analyze a raw perf.data because it depends on perf script for decoding.

Workarounds

  • On the machine that has perf: run perf script -i perf.data > out.perfscript and move that text output.
  • Then use a workflow/tooling that accepts the script output (or “folded stacks”) directly. (This repo’s --perfdata path currently expects the binary perf.data, not already-scripted text.)

Code-search results may be incomplete due to tool limits; you can browse more in GitHub here:
https://github.com/flamegraph-rs/flamegraph/search?q=perf+script&type=code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions