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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions