Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for differential flame graphs #60

Merged
merged 12 commits into from
Feb 25, 2019
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;
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
(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;
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
(c, c, 255)
}
}

fn default_bg_color_for(palette: Palette) -> BackgroundColor {
match palette {
Palette::Basic(BasicPalette::Mem) => BackgroundColor::Green,
Expand Down
99 changes: 70 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,50 @@ 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,
delta,
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
};

//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 +110,26 @@ 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(s2) = parse_nsamples(&mut line) {
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
// See if there's also a differential column present
let nsamples1 = parse_nsamples(&mut line);
if let Some(s1) = nsamples1 {
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
delta = Some(s2 as isize - s1 as isize);
delta_max = std::cmp::max(delta.unwrap().abs() as usize, delta_max);
}
s2
} else {
ignored += 1;
continue;
Expand All @@ -124,7 +146,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 +155,7 @@ where
iter::once("").chain(last.split(';')),
this,
time,
delta,
);
}

Expand All @@ -148,8 +171,26 @@ 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);
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
// 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)
}
59 changes: 48 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 {
colors: Default::default(),
bgcolors: Default::default(),
hash: Default::default(),
consistent_palette: Default::default(),
func_frameattrs: Default::default(),
direction: Default::default(),
title: "Flame Graph".to_string(),
negate_differentials: Default::default(),
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
Expand Down Expand Up @@ -156,7 +172,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 +270,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.
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
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 +357,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