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
104 changes: 73 additions & 31 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,34 +110,30 @@ 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;
// Usually there will only be one samples column at the end of a line,
// but for differentials there will be two. In the case there are two
// we compute the delta between them and use the second one as as the
// number of samples for this frame.
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
let nsamples = match (parse_nsamples(&mut line), parse_nsamples(&mut line)) {
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
(Some(s2), nsamples1) => {
delta = nsamples1.and_then(|s1| Some(s2 as isize - s1 as isize));
if let Some(delta) = delta {
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
delta_max = std::cmp::max(delta.abs() as usize, delta_max);
}
s2
}
_ => {
ignored += 1;
continue;
}
} else {
ignored += 1;
continue;
};

if line.is_empty() {
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,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)
}
65 changes: 54 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,37 @@ 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
)
match frame.delta {
None => write!(
buffer,
"{} ({} samples, {:.2}%)",
deannotate(&frame.location.function),
jasonrhansen marked this conversation as resolved.
Show resolved Hide resolved
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%)",
deannotate(&frame.location.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}%)",
deannotate(&frame.location.function),
samples_txt,
pct,
delta_pct
)
}
}
};

let frame_attributes = opt
Expand Down Expand Up @@ -325,6 +363,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