Skip to content

Commit

Permalink
Add support for differential flame graphs (#60)
Browse files Browse the repository at this point in the history
This adds the ability to easily compare before and after profiles when
benchmarking code changes. If the folded profile file has an extra
samples column at the end it will be interpreted as a differential
profile and the delta between the before and after will be calculated.
The resulting flame graph is drawn using the after profile, but is
colorized based on the delta. It gets a red hue where the after profile
spends more time, and a blue hue where it spends less time. You can
also pass the --negate flag to switch the red and blue.

To create the differential profile files that this feature expects, you
can use the difffolded.pl script from upstream FlameGraph. We have not
yet ported this to Rust.
  • Loading branch information
jasonrhansen committed Feb 25, 2019
1 parent ff4ae74 commit 65587d8
Show file tree
Hide file tree
Showing 9 changed files with 828 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/perf*
*.log
/*.svg
.DS_Store
43 changes: 20 additions & 23 deletions src/bin/flamegraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,33 @@ struct Opt {
/// use consistent palette (palette.map)
#[structopt(long = "cp")]
cp: bool,

/// switch differential hues (green<->red)
#[structopt(long = "negate")]
negate: bool,
}

impl Into<Options> for Opt {
fn into(self) -> Options {
let func_frameattrs = match self.nameattr_file {
Some(file) => match FuncFrameAttrsMap::from_file(&file) {
Ok(n) => n,
let mut options = Options::default();
options.colors = self.colors;
options.bgcolors = self.bgcolors;
options.hash = self.hash;
options.consistent_palette = self.cp;
if let Some(file) = self.nameattr_file {
match FuncFrameAttrsMap::from_file(&file) {
Ok(m) => {
options.func_frameattrs = m;
}
Err(e) => panic!("Error reading {}: {:?}", file.display(), e),
},
None => FuncFrameAttrsMap::default(),
};
let direction = if self.inverted {
Direction::Inverted
} else {
Direction::Straight
};
let title = if self.inverted {
"Icicle Graph".to_string()
} else {
"Flame Graph".to_string()
}
};
Options {
colors: self.colors,
bgcolors: self.bgcolors,
hash: self.hash,
consistent_palette: self.cp,
func_frameattrs,
direction,
title,
if self.inverted {
options.direction = Direction::Inverted;
options.title = "Icicle Graph".to_string();
}
options.negate_differentials = self.negate;
options
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/flamegraph/color/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,22 @@ pub(super) fn color(
rgb_components_for_palette(palette, name, v1, v2, v3)
}

pub(super) fn color_scale(value: isize, max: usize) -> (u8, u8, u8) {
if value == 0 {
(255, 255, 255)
} else if value > 0 {
// A positive value indicates _more_ samples,
// and hence more time spent, so we give it a red hue.
let c = (210 * (max as isize - value) / max as isize) as u8;
(255, c, c)
} else {
// A negative value indicates _fewer_ samples,
// or a speed-up, so we give it a green hue.
let c = (210 * (max as isize + value) / max as isize) as u8;
(c, c, 255)
}
}

fn default_bg_color_for(palette: Palette) -> BackgroundColor {
match palette {
Palette::Basic(BasicPalette::Mem) => BackgroundColor::Green,
Expand Down
101 changes: 72 additions & 29 deletions src/flamegraph/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ pub(super) struct TimedFrame<'a> {
pub(super) location: Frame<'a>,
pub(super) start_time: usize,
pub(super) end_time: usize,
pub(super) delta: Option<isize>,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(super) struct FrameTime {
pub(super) start_time: usize,
pub(super) delta: Option<isize>,
}

fn flow<'a, LI, TI>(
tmp: &mut HashMap<Frame<'a>, usize>,
tmp: &mut HashMap<Frame<'a>, FrameTime>,
frames: &mut Vec<TimedFrame<'a>>,
last: LI,
this: TI,
time: usize,
delta: Option<isize>,
) where
LI: IntoIterator<Item = &'a str>,
TI: IntoIterator<Item = &'a str>,
Expand Down Expand Up @@ -50,31 +58,52 @@ fn flow<'a, LI, TI>(
};

//eprintln!("at {} ending frame {:?}", time, key);
let start_time = tmp.remove(&key).unwrap_or_else(|| {
let frame_time = tmp.remove(&key).unwrap_or_else(|| {
unreachable!("did not have start time for {:?}", key);
});

let key = TimedFrame {
let frame = TimedFrame {
location: key,
start_time,
start_time: frame_time.start_time,
end_time: time,
delta: frame_time.delta,
};
frames.push(key);
frames.push(frame);
}

for (i, func) in this.enumerate() {
let mut i = 0;
while this.peek().is_some() {
let func = this.next().unwrap();
let key = Frame {
function: func,
depth: shared_depth + i,
};

let is_last = this.peek().is_none();
let delta = match delta {
Some(_) if !is_last => Some(0),
d => d,
};
let frame_time = FrameTime {
start_time: time,
// For some reason the Perl version does a `+=` for `delta`, but I can't figure out why.
// See https://github.com/brendangregg/FlameGraph/blob/1b1c6deede9c33c5134c920bdb7a44cc5528e9a7/flamegraph.pl#L588
delta,
};

//eprintln!("stored tmp for time {}: {:?}", time, key);
if let Some(start_time) = tmp.insert(key, time) {
unreachable!("start time {} already registered for frame", start_time);
if let Some(frame_time) = tmp.insert(key, frame_time) {
unreachable!(
"start time {} already registered for frame",
frame_time.start_time
);
}

i += 1;
}
}

pub(super) fn frames<'a, I>(lines: I) -> (Vec<TimedFrame<'a>>, usize, usize)
pub(super) fn frames<'a, I>(lines: I) -> (Vec<TimedFrame<'a>>, usize, usize, usize)
where
I: IntoIterator<Item = &'a str>,
{
Expand All @@ -83,31 +112,25 @@ where
let mut last = "";
let mut tmp = Default::default();
let mut frames = Default::default();
let mut delta = None;
let mut delta_max = 1;
for line in lines {
let mut line = line.trim();
if line.is_empty() {
continue;
}

let nsamples = if let Some(samplesi) = line.rfind(' ') {
let mut samples = &line[(samplesi + 1)..];
// strip fractional part (if any);
// foobar 1.klwdjlakdj
if let Some(doti) = samples.find('.') {
samples = &samples[..doti];
}
match samples.parse::<usize>() {
Ok(nsamples) => {
// remove nsamples part we just parsed from line
line = line[..samplesi].trim_end();
// give out the sample count
nsamples
}
Err(_) => {
ignored += 1;
continue;
}
// Parse the number of samples for the purpose of computing overall time passed.
// Usually there will only be one samples column at the end of a line,
// but for differentials there will be two. When there are two we compute the
// delta between them and use the second one.
let nsamples = if let Some(samples) = parse_nsamples(&mut line) {
// See if there's also a differential column present
if let Some(original_samples) = parse_nsamples(&mut line) {
delta = Some(samples as isize - original_samples as isize);
delta_max = std::cmp::max(delta.unwrap().abs() as usize, delta_max);
}
samples
} else {
ignored += 1;
continue;
Expand All @@ -124,7 +147,7 @@ where
if last.is_empty() {
// need to special-case this, because otherwise iter("") + "".split(';') == ["", ""]
//eprintln!("flow(_, {}, {})", stack, time);
flow(&mut tmp, &mut frames, None, this, time);
flow(&mut tmp, &mut frames, None, this, time, delta);
} else {
//eprintln!("flow({}, {}, {})", last, stack, time);
flow(
Expand All @@ -133,6 +156,7 @@ where
iter::once("").chain(last.split(';')),
this,
time,
delta,
);
}

Expand All @@ -148,8 +172,27 @@ where
iter::once("").chain(last.split(';')),
None,
time,
delta,
);
}

(frames, time, ignored)
(frames, time, ignored, delta_max)
}

// Parse and remove the number of samples from the end of a line.
fn parse_nsamples(line: &mut &str) -> Option<usize> {
let samplesi = line.rfind(' ')?;
let mut samples = &line[(samplesi + 1)..];

// strip fractional part (if any);
// foobar 1.klwdjlakdj
// TODO: Properly handle fractional samples (see issue #43)
if let Some(doti) = samples.find('.') {
samples = &samples[..doti];
}

let nsamples = samples.parse::<usize>().ok()?;
// remove nsamples part we just parsed from line
*line = line[..samplesi].trim_end();
Some(nsamples)
}
60 changes: 49 additions & 11 deletions src/flamegraph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const XPAD: usize = 10; // pad lefm and right
const FRAMEPAD: usize = 1; // vertical padding for frames
const PALETTE_FILE: &str = "palette.map";

#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Options {
pub colors: color::Palette,
pub bgcolors: Option<color::BackgroundColor>,
Expand All @@ -41,6 +41,22 @@ pub struct Options {
pub func_frameattrs: FuncFrameAttrsMap,
pub direction: Direction,
pub title: String,
pub negate_differentials: bool,
}

impl Default for Options {
fn default() -> Self {
Options {
title: "Flame Graph".to_string(),
colors: Default::default(),
bgcolors: Default::default(),
hash: Default::default(),
consistent_palette: Default::default(),
func_frameattrs: Default::default(),
direction: Default::default(),
negate_differentials: Default::default(),
}
}
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
Expand Down Expand Up @@ -142,6 +158,7 @@ impl Rectangle {
}
}

#[allow(clippy::cyclomatic_complexity)]
pub fn from_sorted_lines<'a, I, W>(opt: Options, lines: I, writer: W) -> quick_xml::Result<()>
where
I: IntoIterator<Item = &'a str>,
Expand All @@ -156,7 +173,7 @@ where
let (bgcolor1, bgcolor2) = color::bgcolor_for(opt.bgcolors, opt.colors);

let mut buffer = StrStack::new();
let (mut frames, time, ignored) = merge::frames(lines);
let (mut frames, time, ignored, delta_max) = merge::frames(lines);
if ignored != 0 {
warn!("Ignored {} lines with invalid format", ignored);
}
Expand Down Expand Up @@ -254,15 +271,31 @@ where
write!(buffer, "all ({} samples, 100%)", samples_txt)
} else {
let pct = (100 * samples) as f64 / timemax as f64;

// strip any annotation
write!(
buffer,
"{} ({} samples, {:.2}%)",
deannotate(&frame.location.function),
samples_txt,
pct
)
let function = deannotate(&frame.location.function);
match frame.delta {
None => write!(
buffer,
"{} ({} samples, {:.2}%)",
function, samples_txt, pct
),
// Special case delta == 0 so we don't format percentage with a + sign.
Some(delta) if delta == 0 => write!(
buffer,
"{} ({} samples, {:.2}%; 0.00%)",
function, samples_txt, pct,
),
Some(mut delta) => {
if opt.negate_differentials {
delta = -delta;
}
let delta_pct = (100 * delta) as f64 / timemax as f64;
write!(
buffer,
"{} ({} samples, {:.2}%; {:+.2}%)",
function, samples_txt, pct, delta_pct
)
}
}
};

let frame_attributes = opt
Expand Down Expand Up @@ -325,6 +358,11 @@ where
color::VDGREY
} else if frame.location.function == "-" {
color::DGREY
} else if let Some(mut delta) = frame.delta {
if opt.negate_differentials {
delta = -delta;
}
color::color_scale(delta, delta_max)
} else if let Some(ref mut palette_map) = palette_map {
palette_map.find_color_for(&frame.location.function, |name| {
color::color(opt.colors, opt.hash, name, &mut thread_rng)
Expand Down
Loading

0 comments on commit 65587d8

Please sign in to comment.