macro_rules! args {
($($key:expr => $value:expr),*) => {{
[$(($key, $value),)*].iter().map(|(k, v): &(&str, &str)| (*k, *v))
}};
}
#[cfg(feature = "nameattr")]
mod attrs;
pub mod color;
mod merge;
mod rand;
mod svg;
use std::fs::File;
use std::io::prelude::*;
use std::io::{self, BufReader};
use std::iter;
use std::path::PathBuf;
use std::str::FromStr;
use log::{error, warn};
use num_format::Locale;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Writer;
use str_stack::StrStack;
#[cfg(feature = "nameattr")]
use self::attrs::FrameAttrs;
#[cfg(feature = "nameattr")]
pub use self::attrs::FuncFrameAttrsMap;
pub use self::color::Palette;
use self::color::{Color, SearchColor};
use self::svg::{Dimension, StyleOptions};
const XPAD: usize = 10;
const FRAMEPAD: usize = 1;
const DEFAULT_IMAGE_WIDTH: usize = 1200;
pub mod defaults {
macro_rules! doc {
($str:expr, $($def:tt)*) => {
#[doc = $str]
$($def)*
};
}
macro_rules! define {
($($name:ident : $t:ty = $val:tt),*) => {
$(
doc!(
stringify!($val),
pub const $name: $t = $val;
);
)*
#[doc(hidden)]
pub mod str {
use lazy_static::lazy_static;
$(
lazy_static! {
pub static ref $name: String = ($val).to_string();
}
)*
}
}
}
define! {
COLORS: &str = "hot",
SEARCH_COLOR: &str = "#e600e6",
TITLE: &str = "Flame Graph",
CHART_TITLE: &str = "Flame Chart",
FRAME_HEIGHT: usize = 16,
MIN_WIDTH: f64 = 0.1,
FONT_TYPE: &str = "Verdana",
FONT_SIZE: usize = 12,
FONT_WIDTH: f64 = 0.59,
COUNT_NAME: &str = "samples",
NAME_TYPE: &str = "Function:",
FACTOR: f64 = 1.0
}
}
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct Options<'a> {
pub colors: color::Palette,
pub bgcolors: Option<color::BackgroundColor>,
pub hash: bool,
pub palette_map: Option<&'a mut color::PaletteMap>,
#[cfg(feature = "nameattr")]
pub func_frameattrs: FuncFrameAttrsMap,
pub direction: Direction,
pub search_color: SearchColor,
pub title: String,
pub subtitle: Option<String>,
pub image_width: Option<usize>,
pub frame_height: usize,
pub min_width: f64,
pub font_type: String,
pub font_size: usize,
pub font_width: f64,
pub text_truncate_direction: TextTruncateDirection,
pub count_name: String,
pub name_type: String,
pub notes: String,
pub negate_differentials: bool,
pub factor: f64,
pub pretty_xml: bool,
pub no_sort: bool,
pub reverse_stack_order: bool,
#[doc(hidden)]
pub no_javascript: bool,
pub color_diffusion: bool,
pub flame_chart: bool,
}
impl<'a> Options<'a> {
pub(super) fn ypad1(&self) -> usize {
let subtitle_height = if self.subtitle.is_some() {
self.font_size * 2
} else {
0
};
if self.direction == Direction::Straight {
self.font_size * 3 + subtitle_height
} else {
self.font_size * 4 + subtitle_height + 4
}
}
pub(super) fn ypad2(&self) -> usize {
if self.direction == Direction::Straight {
self.font_size * 2 + 10
} else {
self.font_size + 10
}
}
}
impl<'a> Default for Options<'a> {
fn default() -> Self {
Options {
colors: Palette::from_str(defaults::COLORS).unwrap(),
search_color: SearchColor::from_str(defaults::SEARCH_COLOR).unwrap(),
title: defaults::TITLE.to_string(),
frame_height: defaults::FRAME_HEIGHT,
min_width: defaults::MIN_WIDTH,
font_type: defaults::FONT_TYPE.to_string(),
font_size: defaults::FONT_SIZE,
font_width: defaults::FONT_WIDTH,
text_truncate_direction: Default::default(),
count_name: defaults::COUNT_NAME.to_string(),
name_type: defaults::NAME_TYPE.to_string(),
factor: defaults::FACTOR,
image_width: Default::default(),
notes: Default::default(),
subtitle: Default::default(),
bgcolors: Default::default(),
hash: Default::default(),
palette_map: Default::default(),
direction: Default::default(),
negate_differentials: Default::default(),
pretty_xml: Default::default(),
no_sort: Default::default(),
reverse_stack_order: Default::default(),
no_javascript: Default::default(),
color_diffusion: Default::default(),
flame_chart: Default::default(),
#[cfg(feature = "nameattr")]
func_frameattrs: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction {
Straight,
Inverted,
}
impl Default for Direction {
fn default() -> Self {
Direction::Straight
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum TextTruncateDirection {
Left,
Right,
}
impl Default for TextTruncateDirection {
fn default() -> Self {
TextTruncateDirection::Left
}
}
struct Rectangle {
x1_pct: f64,
y1: usize,
x2_pct: f64,
y2: usize,
}
impl Rectangle {
fn width_pct(&self) -> f64 {
self.x2_pct - self.x1_pct
}
fn height(&self) -> usize {
self.y2 - self.y1
}
}
#[allow(clippy::cognitive_complexity)]
pub fn from_lines<'a, I, W>(opt: &mut Options<'_>, lines: I, writer: W) -> quick_xml::Result<()>
where
I: IntoIterator<Item = &'a str>,
W: Write,
{
let mut reversed = StrStack::new();
let (mut frames, time, ignored, delta_max) = if opt.reverse_stack_order {
if opt.no_sort {
warn!(
"Input lines are always sorted when `reverse_stack_order` is `true`. \
The `no_sort` option is being ignored."
);
}
let mut stack = String::new();
for line in lines {
stack.clear();
let samples_idx = merge::rfind_samples(line)
.map(|(i, _)| i)
.unwrap_or_else(|| line.len());
let samples_idx = merge::rfind_samples(&line[..samples_idx - 1])
.map(|(i, _)| i)
.unwrap_or(samples_idx);
for (i, func) in line[..samples_idx].trim().split(';').rev().enumerate() {
if i != 0 {
stack.push(';');
}
stack.push_str(func);
}
stack.push(' ');
stack.push_str(&line[samples_idx..]);
reversed.push(&stack);
}
let mut reversed: Vec<&str> = reversed.iter().collect();
reversed.sort_unstable();
merge::frames(reversed, false)?
} else if opt.flame_chart {
let mut lines: Vec<&str> = lines.into_iter().collect();
lines.reverse();
merge::frames(lines, true)?
} else if opt.no_sort {
merge::frames(lines, false)?
} else {
let mut lines: Vec<&str> = lines.into_iter().collect();
lines.sort_unstable();
merge::frames(lines, false)?
};
if ignored != 0 {
warn!("Ignored {} lines with invalid format", ignored);
}
let mut buffer = StrStack::new();
let mut svg = if opt.pretty_xml {
Writer::new_with_indent(writer, b' ', 4)
} else {
Writer::new(writer)
};
if time == 0 {
error!("No stack counts found");
let imageheight = opt.font_size * 5;
svg::write_header(&mut svg, imageheight, &opt)?;
svg::write_str(
&mut svg,
&mut buffer,
svg::TextItem {
x: Dimension::Percent(50.0),
y: (opt.font_size * 2) as f64,
text: "ERROR: No valid input provided to flamegraph".into(),
extra: None,
},
)?;
svg.write_event(Event::End(BytesEnd::borrowed(b"svg")))?;
svg.write_event(Event::Eof)?;
return Err(quick_xml::Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
"No stack counts found",
)));
}
let image_width = opt.image_width.unwrap_or(DEFAULT_IMAGE_WIDTH) as f64;
let timemax = time;
let widthpertime_pct = 100.0 / timemax as f64;
let minwidth_time = opt.min_width / widthpertime_pct;
let mut depthmax = 0;
frames.retain(|frame| {
if ((frame.end_time - frame.start_time) as f64) < minwidth_time {
false
} else {
depthmax = std::cmp::max(depthmax, frame.location.depth);
true
}
});
let imageheight = ((depthmax + 1) * opt.frame_height) + opt.ypad1() + opt.ypad2();
svg::write_header(&mut svg, imageheight, &opt)?;
let (bgcolor1, bgcolor2) = color::bgcolor_for(opt.bgcolors, opt.colors);
let style_options = StyleOptions {
imageheight,
bgcolor1,
bgcolor2,
};
svg::write_prelude(&mut svg, &style_options, &opt)?;
let mut thread_rng = rand::thread_rng();
let mut cache_g = Event::Start(BytesStart::owned_name("g"));
let mut cache_a = Event::Start(BytesStart::owned_name("a"));
let mut cache_rect = Event::Empty(BytesStart::owned_name("rect"));
let cache_g_end = Event::End(BytesEnd::borrowed(b"g"));
let cache_a_end = Event::End(BytesEnd::borrowed(b"a"));
let container_x = format!("{}", XPAD);
let container_width = format!("{}", image_width as usize - XPAD - XPAD);
svg.write_event(Event::Start(
BytesStart::borrowed_name(b"svg").with_attributes(vec![
("id", "frames"),
("x", &container_x),
("width", &container_width),
]),
))?;
let mut samples_txt_buffer = num_format::Buffer::default();
for frame in frames {
let x1_pct = frame.start_time as f64 * widthpertime_pct;
let x2_pct = frame.end_time as f64 * widthpertime_pct;
let (y1, y2) = match opt.direction {
Direction::Straight => {
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 = opt.ypad1() + frame.location.depth * opt.frame_height;
let y2 = opt.ypad1() + (frame.location.depth + 1) * opt.frame_height - FRAMEPAD;
(y1, y2)
}
};
let rect = Rectangle {
x1_pct,
y1,
x2_pct,
y2,
};
let samples = ((frame.end_time - frame.start_time) as f64 * opt.factor).round() as usize;
let _ = samples_txt_buffer.write_formatted(&samples, &Locale::en);
let samples_txt = samples_txt_buffer.as_str();
let info = if frame.location.function.is_empty() && frame.location.depth == 0 {
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,
"{} ({} {}, {:.2}%)",
function, samples_txt, opt.count_name, pct
),
Some(delta) if delta == 0 => write!(
buffer,
"{} ({} {}, {:.2}%; 0.00%)",
function, samples_txt, opt.count_name, pct,
),
Some(mut delta) => {
if opt.negate_differentials {
delta = -delta;
}
let delta_pct = (100 * delta) as f64 / (timemax as f64 * opt.factor);
write!(
buffer,
"{} ({} {}, {:.2}%; {:+.2}%)",
function, samples_txt, opt.count_name, pct, delta_pct
)
}
}
};
let (has_href, title) = write_container_start(
opt,
&mut svg,
&mut cache_a,
&mut cache_g,
&frame,
&buffer[info],
)?;
svg.write_event(Event::Start(BytesStart::borrowed_name(b"title")))?;
svg.write_event(Event::Text(BytesText::from_plain_str(title)))?;
svg.write_event(Event::End(BytesEnd::borrowed(b"title")))?;
let color = if frame.location.function == "--" {
color::VDGREY
} else if frame.location.function == "-" {
color::DGREY
} else if opt.color_diffusion {
color::color_scale((((x2_pct - x1_pct) / 100.0).sqrt() * 2000.0) as isize, 2000)
} 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) = opt.palette_map {
let colors = opt.colors;
let hash = opt.hash;
palette_map.find_color_for(&frame.location.function, |name| {
color::color(colors, hash, name, &mut thread_rng)
})
} else {
color::color(
opt.colors,
opt.hash,
frame.location.function,
&mut thread_rng,
)
};
filled_rectangle(&mut svg, &mut buffer, &rect, color, &mut cache_rect)?;
let fitchars = (rect.width_pct() as f64
/ (100.0 * opt.font_size as f64 * opt.font_width / image_width))
.trunc() as usize;
let text: svg::TextArgument<'_> = if fitchars >= 3 {
let f = deannotate(&frame.location.function);
if f.len() < fitchars {
f.into()
} else {
use std::fmt::Write;
let mut w = buffer.writer();
for c in f.chars().take(fitchars - 2) {
w.write_char(c).expect("writing to buffer shouldn't fail");
}
w.write_str("..").expect("writing to buffer shouldn't fail");
w.finish().into()
}
} else {
"".into()
};
svg::write_str(
&mut svg,
&mut buffer,
svg::TextItem {
x: Dimension::Percent(rect.x1_pct + 100.0 * 3.0 / image_width),
y: 3.0 + (rect.y1 + rect.y2) as f64 / 2.0,
text,
extra: None,
},
)?;
buffer.clear();
if has_href {
svg.write_event(&cache_a_end)?;
} else {
svg.write_event(&cache_g_end)?;
}
}
svg.write_event(Event::End(BytesEnd::borrowed(b"svg")))?;
svg.write_event(Event::End(BytesEnd::borrowed(b"svg")))?;
svg.write_event(Event::Eof)?;
Ok(())
}
#[cfg(feature = "nameattr")]
fn write_container_start<'a, W: Write>(
opt: &'a Options<'a>,
svg: &mut Writer<W>,
cache_a: &mut Event<'_>,
cache_g: &mut Event<'_>,
frame: &merge::TimedFrame<'_>,
mut title: &'a str,
) -> quick_xml::Result<(bool, &'a str)> {
let frame_attributes = opt
.func_frameattrs
.frameattrs_for_func(frame.location.function);
let mut has_href = false;
if let Some(frame_attributes) = frame_attributes {
if frame_attributes.attrs.contains_key("xlink:href") {
write_container_attributes(cache_a, &frame_attributes);
svg.write_event(&cache_a)?;
has_href = true;
} else {
write_container_attributes(cache_g, &frame_attributes);
svg.write_event(&cache_g)?;
}
if let Some(ref t) = frame_attributes.title {
title = t.as_str();
}
} else if let Event::Start(ref mut c) = cache_g {
c.clear_attributes();
svg.write_event(&cache_g)?;
}
Ok((has_href, title))
}
#[cfg(not(feature = "nameattr"))]
fn write_container_start<'a, W: Write>(
_opt: &Options<'_>,
svg: &mut Writer<W>,
_cache_a: &mut Event<'_>,
cache_g: &mut Event<'_>,
_frame: &merge::TimedFrame<'_>,
title: &'a str,
) -> quick_xml::Result<(bool, &'a str)> {
if let Event::Start(ref mut c) = cache_g {
c.clear_attributes();
svg.write_event(&cache_g)?;
}
Ok((false, title))
}
#[cfg(feature = "nameattr")]
fn write_container_attributes(event: &mut Event<'_>, frame_attributes: &FrameAttrs) {
if let Event::Start(ref mut c) = event {
c.clear_attributes();
c.extend_attributes(
frame_attributes
.attrs
.iter()
.map(|(k, v)| (k.as_str(), v.as_str())),
);
} else {
unreachable!("cache wrapper was of wrong type: {:?}", event);
}
}
pub fn from_reader<R, W>(opt: &mut Options<'_>, reader: R, writer: W) -> quick_xml::Result<()>
where
R: Read,
W: Write,
{
from_readers(opt, iter::once(reader), writer)
}
pub fn from_readers<R, W>(opt: &mut Options<'_>, readers: R, writer: W) -> quick_xml::Result<()>
where
R: IntoIterator,
R::Item: Read,
W: Write,
{
let mut input = String::new();
for mut reader in readers {
reader
.read_to_string(&mut input)
.map_err(quick_xml::Error::Io)?;
}
from_lines(opt, input.lines(), writer)
}
pub fn from_files<W: Write>(
opt: &mut Options<'_>,
files: &[PathBuf],
writer: W,
) -> quick_xml::Result<()> {
if files.is_empty() || files.len() == 1 && files[0].to_str() == Some("-") {
let stdin = io::stdin();
let r = BufReader::with_capacity(128 * 1024, stdin.lock());
from_reader(opt, r, writer)
} else if files.len() == 1 {
let r = File::open(&files[0]).map_err(quick_xml::Error::Io)?;
from_reader(opt, r, writer)
} else {
let stdin = io::stdin();
let mut stdin_added = false;
let mut readers: Vec<Box<dyn Read>> = Vec::with_capacity(files.len());
for infile in files.iter() {
if infile.to_str() == Some("-") {
if !stdin_added {
let r = BufReader::with_capacity(128 * 1024, stdin.lock());
readers.push(Box::new(r));
stdin_added = true;
}
} else {
let r = File::open(infile).map_err(quick_xml::Error::Io)?;
readers.push(Box::new(r));
}
}
from_readers(opt, readers, writer)
}
}
fn deannotate(f: &str) -> &str {
if f.ends_with(']') {
if let Some(ai) = f.rfind("_[") {
if f[ai..].len() == 4 && "kwij".contains(&f[ai + 2..ai + 3]) {
return &f[..ai];
}
}
}
f
}
fn filled_rectangle<W: Write>(
svg: &mut Writer<W>,
buffer: &mut StrStack,
rect: &Rectangle,
color: Color,
cache_rect: &mut Event<'_>,
) -> quick_xml::Result<usize> {
let x = write!(buffer, "{:.4}%", rect.x1_pct);
let y = write_usize(buffer, rect.y1);
let width = write!(buffer, "{:.4}%", rect.width_pct());
let height = write_usize(buffer, rect.height());
let color = write!(buffer, "rgb({},{},{})", color.r, color.g, color.b);
if let Event::Empty(bytes_start) = cache_rect {
bytes_start.clear_attributes();
bytes_start.extend_attributes(args!(
"x" => &buffer[x],
"y" => &buffer[y],
"width" => &buffer[width],
"height" => &buffer[height],
"fill" => &buffer[color]
));
} else {
unreachable!("cache wrapper was of wrong type: {:?}", cache_rect);
}
svg.write_event(&cache_rect)
}
fn write_usize(buffer: &mut StrStack, value: usize) -> usize {
let mut writer = buffer.writer();
itoa::fmt(&mut writer, value).unwrap();
writer.finish()
}
#[cfg(test)]
mod tests {
use super::{Direction, Options};
#[test]
fn top_ypadding_adjusts_for_subtitle() {
let height_without_subtitle = Options {
..Default::default()
}
.ypad1();
let height_with_subtitle = Options {
subtitle: Some(String::from("hello!")),
..Default::default()
}
.ypad1();
assert!(height_with_subtitle > height_without_subtitle);
}
#[test]
fn ypadding_adjust_for_inverted_mode() {
let regular = Options {
..Default::default()
};
let inverted = Options {
direction: Direction::Inverted,
..Default::default()
};
assert!(inverted.ypad1() > regular.ypad1());
assert!(inverted.ypad2() < regular.ypad2());
}
}