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

style settings configuration #52

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions src/bin/flamegraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use std::path::{Path, PathBuf};
use structopt::StructOpt;

use inferno::flamegraph::{
self, color::BackgroundColor, color::PaletteMap, Direction, FuncFrameAttrsMap, Options, Palette,
self, color::BackgroundColor, color::PaletteMap, Direction, FuncFrameAttrsMap, Options,
Palette, DEFAULT_TITLE,
};

#[derive(Debug, StructOpt)]
Expand Down Expand Up @@ -47,6 +48,50 @@ struct Opt {
#[structopt(long = "cp")]
cp: bool,

/// Change title text
#[structopt(long = "title", default_value = "Flame Graph")]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use DEFAULT_TITLE here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but structopt macro accept only string.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should work:

#[structopt(long = "title", raw(default_value = "DEFAULT_TITLE"))]

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have to use

#[structopt(long = "title", raw(default_value = DEFAULT_TITLE))]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have constants for all of the defaults, but the above trick will only work for &str constants. I don't know how we would use f64 constants and others with structopt.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, yeah, that doesn't work sadly:

error: proc-macro derive panicked
  --> src/bin/flamegraph.rs:11:17
   |
11 | #[derive(Debug, StructOpt)]
   |                 ^^^^^^^^^
   |
   = help: message: invalid structopt syntax: # [ structopt ( long = "title" , raw ( default_value = DEFAULT_TITLE ) ) ]

error: aborting due to previous error

Let's leave it for another PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEFAULT_TITLE needs to be in quotes like this.

#[structopt(long = "title", raw(default_value = "DEFAULT_TITLE"))]

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't that make the title quite literally "DEFAULT_TITLE"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with raw.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #91

title: String,
AnderEnder marked this conversation as resolved.
Show resolved Hide resolved

/// Second level title (optional)
#[structopt(long = "subtitle")]
subtitle: Option<String>,

/// Width of image
#[structopt(long = "width", default_value = "1200")]
image_width: usize,

/// Height of each frame
#[structopt(long = "height", default_value = "16")]
frame_height: usize,

/// Omit smaller functions (default 0.1 pixels)
#[structopt(long = "minwidth", default_value = "0.1")]
min_width: f64,

/// Font type
#[structopt(long = "fonttype", default_value = "Verdana")]
font_type: String,

/// Font size
#[structopt(long = "fontsize", default_value = "12")]
font_size: usize,

/// Font width
#[structopt(long = "fontwidth", default_value = "0.59")]
font_width: f64,

/// Count type label
#[structopt(long = "countname", default_value = "samples")]
count_name: String,

/// Name type label
#[structopt(long = "nametype", default_value = "Function:")]
name_type: String,

/// Set embedded notes in SVG
#[structopt(long = "notes", default_value = "")]
notes: String,

AnderEnder marked this conversation as resolved.
Show resolved Hide resolved
/// Switch differential hues (green<->red)
#[structopt(long = "negate")]
negate: bool,
Expand Down Expand Up @@ -91,12 +136,28 @@ impl<'a> Opt {
};
if self.inverted {
options.direction = Direction::Inverted;
options.title = "Icicle Graph".to_string();
if self.title == DEFAULT_TITLE {
options.title = "Icicle Graph".to_string();
}
}
options.negate_differentials = self.negate;
options.factor = self.factor;
options.pretty_xml = self.pretty_xml;
options.no_javascript = self.no_javascript;

// set style options
options.subtitle = self.subtitle;
options.image_width = self.image_width;
options.frame_height = self.frame_height;
options.min_width = self.min_width;
options.font_type = self.font_type;
options.font_size = self.font_size;
options.font_width = self.font_width;
options.count_name = self.count_name;
options.name_type = self.name_type;
options.notes = self.notes;
options.negate_differentials = self.negate;
options.factor = self.factor;
(self.infiles, options)
}
}
Expand Down
134 changes: 104 additions & 30 deletions src/flamegraph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,12 @@ use std::path::PathBuf;
use str_stack::StrStack;
use svg::StyleOptions;

const IMAGEWIDTH: usize = 1200; // max width, pixels
const FRAMEHEIGHT: usize = 16; // max height is dynamic
const FONTSIZE: usize = 12; // base text size
const FONTWIDTH: f64 = 0.59; // avg width relative to FONTSIZE
const MINWIDTH: f64 = 0.1; // min function width, pixels
const YPAD1: usize = FONTSIZE * 3; // pad top, include title
const YPAD2: usize = FONTSIZE * 2 + 10; // pad bottom, include labels
const XPAD: usize = 10; // pad lefm and right
const FRAMEPAD: usize = 1; // vertical padding for frames

/// Default title
pub const DEFAULT_TITLE: &str = "Flame Graph";

/// Configure the flame graph.
#[derive(Debug)]
pub struct Options<'a> {
Expand Down Expand Up @@ -71,6 +67,56 @@ pub struct Options<'a> {
/// Defaults to "Flame Graph".
pub title: String,

/// The subtitle for the flame chart.
///
/// Defaults to None.
pub subtitle: Option<String>,

/// Width of for the flame chart
///
/// Defaults to 1200.
pub image_width: usize,

/// Height of each frame.
///
/// Defaults to 16.
pub frame_height: usize,

/// Minimal width to omit smaller functions
///
/// Defaults to 0.1.
pub min_width: f64,

/// The font type for the flame chart.
///
/// Defaults to "Verdana".
pub font_type: String,

/// Font size for the flame chart.
///
/// Defaults to 12.
pub font_size: usize,

/// Font width for the flame chart.
///
/// Defaults to 0.59.
pub font_width: f64,

/// Count type label for the flame chart.
///
/// Defaults to "samples".
pub count_name: String,

/// Name type label for the flame chart.
///
/// Defaults to "Function:".
pub name_type: String,

/// The notes for the flame chart.
///
/// Defaults to "".
pub notes: String,

/// By default, if [differential] samples are included in the provided stacks, the resulting
/// flame graph will compute and show differentials as `sample#2 - sample#1`. If this option is
/// set, the differential is instead computed using `sample#1 - sample#2`.
Expand Down Expand Up @@ -99,10 +145,32 @@ pub struct Options<'a> {
pub no_javascript: bool,
}

impl<'a> Options<'a> {
/// Calculate pad top, including title
pub(super) fn ypad1(&self) -> usize {
self.font_size * 3
}

/// Calculate pad bottom, including labels
pub(super) fn ypad2(&self) -> usize {
self.font_size * 2 + 10
}
}

impl<'a> Default for Options<'a> {
fn default() -> Self {
Options {
title: "Flame Graph".to_string(),
title: DEFAULT_TITLE.to_string(),
subtitle: None,
image_width: 1200,
frame_height: 16,
min_width: 0.1,
font_type: "Verdana".to_owned(),
font_size: 12,
font_width: 0.59,
count_name: "samples".to_owned(),
name_type: "Function:".to_owned(),
notes: "".to_owned(),
factor: 1.0,
colors: Default::default(),
bgcolors: Default::default(),
Expand Down Expand Up @@ -269,20 +337,21 @@ where
if time == 0 {
error!("No stack counts found");
// emit an error message SVG, for tools automating flamegraph use
let imageheight = FONTSIZE * 5;
svg::write_header(&mut svg, imageheight)?;
let imageheight = opt.font_size * 5;
svg::write_header(&mut svg, imageheight, &opt)?;
svg::write_str(
&mut svg,
&mut buffer,
svg::TextItem {
color: "black",
size: FONTSIZE + 2,
x: (IMAGEWIDTH / 2) as f64,
y: (FONTSIZE * 2) as f64,
size: opt.font_size + 2,
x: (opt.image_width / 2) as f64,
y: (opt.font_size * 2) as f64,
text: "ERROR: No valid input provided to flamegraph".into(),
location: Some("middle"),
extra: None,
},
&opt.font_type,
)?;
svg.write_event(Event::End(BytesEnd::borrowed(b"svg")))?;
svg.write_event(Event::Eof)?;
Expand All @@ -293,8 +362,8 @@ where
}

let timemax = time;
let widthpertime = (IMAGEWIDTH - 2 * XPAD) as f64 / timemax as f64;
let minwidth_time = MINWIDTH / widthpertime;
let widthpertime = (opt.image_width - 2 * XPAD) as f64 / timemax as f64;
let minwidth_time = opt.min_width / widthpertime;

// prune blocks that are too narrow
let mut depthmax = 0;
Expand All @@ -308,14 +377,15 @@ where
});

// draw canvas, and embed interactive JavaScript program
let imageheight = ((depthmax + 1) * FRAMEHEIGHT) + YPAD1 + YPAD2;
svg::write_header(&mut svg, imageheight)?;
let imageheight = ((depthmax + 1) * opt.frame_height) + opt.ypad1() + opt.ypad2();
svg::write_header(&mut svg, imageheight, &opt)?;

let style_options = StyleOptions {
imageheight,
bgcolor1,
bgcolor2,
};

svg::write_prelude(&mut svg, &style_options, &opt)?;

// Used when picking color parameters at random, when no option determines how to pick these
Expand All @@ -333,15 +403,17 @@ where
for frame in frames {
let x1 = XPAD + (frame.start_time as f64 * widthpertime) as usize;
let x2 = XPAD + (frame.end_time as f64 * widthpertime) as usize;

let (y1, y2) = match opt.direction {
Direction::Straight => {
let y1 = imageheight - YPAD2 - (frame.location.depth + 1) * FRAMEHEIGHT + FRAMEPAD;
let y2 = imageheight - YPAD2 - frame.location.depth * FRAMEHEIGHT;
let y1 = imageheight - opt.ypad2() - (frame.location.depth + 1) * opt.frame_height
+ FRAMEPAD;
let y2 = imageheight - opt.ypad2() - frame.location.depth * opt.frame_height;
(y1, y2)
}
Direction::Inverted => {
let y1 = YPAD1 + frame.location.depth * FRAMEHEIGHT;
let y2 = YPAD1 + (frame.location.depth + 1) * FRAMEHEIGHT - FRAMEPAD;
let y1 = opt.ypad1() + frame.location.depth * opt.frame_height;
let y2 = opt.ypad1() + (frame.location.depth + 1) * opt.frame_height - FRAMEPAD;
(y1, y2)
}
};
Expand All @@ -360,21 +432,21 @@ where
let samples_txt = samples_txt_buffer.as_str();

let info = if frame.location.function.is_empty() && frame.location.depth == 0 {
write!(buffer, "all ({} samples, 100%)", samples_txt)
write!(buffer, "all ({} {}, 100%)", samples_txt, opt.count_name)
} else {
let pct = (100 * samples) as f64 / (timemax as f64 * opt.factor);
let function = deannotate(&frame.location.function);
match frame.delta {
None => write!(
buffer,
"{} ({} samples, {:.2}%)",
function, samples_txt, pct
"{} ({} {}, {:.2}%)",
function, samples_txt, opt.count_name, 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,
"{} ({} {}, {:.2}%; 0.00%)",
function, samples_txt, opt.count_name, pct,
),
Some(mut delta) => {
if opt.negate_differentials {
Expand All @@ -383,8 +455,8 @@ where
let delta_pct = (100 * delta) as f64 / (timemax as f64 * opt.factor);
write!(
buffer,
"{} ({} samples, {:.2}%; {:+.2}%)",
function, samples_txt, pct, delta_pct
"{} ({} {}, {:.2}%; {:+.2}%)",
function, samples_txt, opt.count_name, pct, delta_pct
)
}
}
Expand Down Expand Up @@ -471,7 +543,8 @@ where
};
filled_rectangle(&mut svg, &mut buffer, &rect, color, &mut cache_rect)?;

let fitchars = (rect.width() as f64 / (FONTSIZE as f64 * FONTWIDTH)).trunc() as usize;
let fitchars =
(rect.width() as f64 / (opt.font_size as f64 * opt.font_width)).trunc() as usize;
let text: svg::TextArgument = if fitchars >= 3 {
// room for one char plus two dots
let f = deannotate(&frame.location.function);
Expand Down Expand Up @@ -501,13 +574,14 @@ where
&mut buffer,
svg::TextItem {
color: "rgb(0, 0, 0)",
size: FONTSIZE,
size: opt.font_size,
x: rect.x1 as f64 + 3.0,
y: 3.0 + (rect.y1 + rect.y2) as f64 / 2.0,
text,
location: None,
extra: None,
},
&opt.font_type,
)?;

buffer.clear();
Expand Down
Loading