Skip to content

Commit

Permalink
Add support for "fluid" drawing
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonrhansen committed Jul 11, 2019
1 parent 9d7657a commit b9479dc
Show file tree
Hide file tree
Showing 37 changed files with 19,428 additions and 23,781 deletions.
6 changes: 3 additions & 3 deletions src/bin/flamegraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ struct Opt {
subtitle: Option<String>,

/// Width of image
#[structopt(long = "width", raw(default_value = "&defaults::str::IMAGE_WIDTH"))]
image_width: usize,
#[structopt(long = "width")]
image_width: Option<usize>,

/// Height of each frame
#[structopt(long = "height", raw(default_value = "&defaults::str::FRAME_HEIGHT"))]
Expand Down Expand Up @@ -304,7 +304,7 @@ mod tests {
colors: Palette::from_str("purple").unwrap(),
search_color: color::SearchColor::from_str("#203040").unwrap(),
title: "Test Title".to_string(),
image_width: 100,
image_width: Some(100),
frame_height: 500,
min_width: 90.1,
font_type: "Helvetica".to_string(),
Expand Down
97 changes: 71 additions & 26 deletions src/flamegraph/flamegraph.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,63 @@
"use strict";
var details, searchbtn, unzoombtn, matchedtxt, svg, searching;
var details, searchbtn, unzoombtn, matchedtxt, svg, searching, frames;
function init(evt) {
details = document.getElementById("details").firstChild;
searchbtn = document.getElementById("search");
unzoombtn = document.getElementById("unzoom");
matchedtxt = document.getElementById("matched");
svg = document.getElementsByTagName("svg")[0];
frames = document.getElementById("frames");
searching = 0;

// use GET parameters to restore a flamegraphs state.
var params = get_params();
if (params.x && params.y)
zoom(find_group(document.querySelector('[x="' + params.x + '"][y="' + params.y + '"]')));
if (params.s)
search(params.s);
}
// Use GET parameters to restore a flamegraph's state.
var restore_state = function() {
var params = get_params();
if (params.x && params.y)
zoom(find_group(document.querySelector('[x="' + params.x + '"][y="' + params.y + '"]')));
if (params.s)
search(params.s);
};

if (fluiddrawing) {
// Make width dynamic so the SVG fits its parent's width.
svg.removeAttribute("width");
// Edge requires us to have a viewBox that gets updated with size changes.
var isEdge = /Edge\/\d./i.test(navigator.userAgent);
if (!isEdge) {
svg.removeAttribute("viewBox");
}
var update_for_width_change = function() {
if (isEdge) {
svg.attributes.viewBox.value = "0 0 " + svg.width.baseVal.value + " " + svg.height.baseVal.value;
}

// Keep consistent padding on left and right of frames container.
frames.attributes.width.value = svg.width.baseVal.value - xpad * 2;

// Text truncation needs to be adjusted for the current width.
var el = frames.children;
for(var i = 0; i < el.length; i++) {
update_text(el[i]);
}

// Keep search elements at a fixed distance from right edge.
var svgWidth = svg.width.baseVal.value;
searchbtn.attributes.x.value = svgWidth - xpad - 100;
matchedtxt.attributes.x.value = svgWidth - xpad - 100;
};
window.addEventListener('resize', function() {
update_for_width_change();
});
// This needs to be done asynchronously for Safari to work.
setTimeout(function() {
unzoom();
update_for_width_change();
restore_state();
}, 0);
} else {
restore_state();
}
}
// event listeners
window.addEventListener("click", function(e) {
var target = find_group(e.target);
Expand Down Expand Up @@ -47,7 +89,6 @@ window.addEventListener("click", function(e) {
}
else if (e.target.id == "search") search_prompt();
}, false)

// mouse-over for info
// show
window.addEventListener("mouseover", function(e) {
Expand Down Expand Up @@ -123,17 +164,17 @@ function g_to_func(e) {
function update_text(e) {
var r = find_child(e, "rect");
var t = find_child(e, "text");
var w = parseFloat(r.attributes.width.value) -3;
var txt = find_child(e, "title").textContent.replace(/\\([^(]*\\)\$/,"");
t.attributes.x.value = parseFloat(r.attributes.x.value) + 3;
var w = parseFloat(r.attributes.width.value) * frames.attributes.width.value / 100 - 3;
var txt = find_child(e, "title").textContent.replace(/\([^(]*\)$/,"");
t.attributes.x.value = format_percent((parseFloat(r.attributes.x.value) + (100 * 3 / frames.attributes.width.value)));
// Smaller than this size won't fit anything
if (w < 2 * fontsize * fontwidth) {
t.textContent = "";
return;
}
t.textContent = txt;
// Fit in full text width
if (/^ *\$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
if (/^ *\$/.test(txt) || t.getComputedTextLength() < w)
return;
for (var x = txt.length - 2; x > 0; x--) {
if (t.getSubStringLength(0, x + 2) <= w) {
Expand All @@ -158,29 +199,30 @@ function zoom_child(e, x, ratio) {
if (e.attributes != undefined) {
if (e.attributes.x != undefined) {
orig_save(e, "x");
e.attributes.x.value = (parseFloat(e.attributes.x.value) - x - xpad) * ratio + xpad;
if(e.tagName == "text")
e.attributes.x.value = find_child(e.parentNode, "rect[x]").attributes.x.value + 3;
e.attributes.x.value = format_percent((parseFloat(e.attributes.x.value) - x) * ratio);
if (e.tagName == "text") {
e.attributes.x.value = format_percent(parseFloat(find_child(e.parentNode, "rect[x]").attributes.x.value) + (100 * 3 / frames.attributes.width.value));
}
}
if (e.attributes.width != undefined) {
orig_save(e, "width");
e.attributes.width.value = parseFloat(e.attributes.width.value) * ratio;
e.attributes.width.value = format_percent(parseFloat(e.attributes.width.value) * ratio);
}
}
if (e.childNodes == undefined) return;
for(var i = 0, c = e.childNodes; i < c.length; i++) {
zoom_child(c[i], x - xpad, ratio);
zoom_child(c[i], x, ratio);
}
}
function zoom_parent(e) {
if (e.attributes) {
if (e.attributes.x != undefined) {
orig_save(e, "x");
e.attributes.x.value = xpad;
e.attributes.x.value = "0.0%";
}
if (e.attributes.width != undefined) {
orig_save(e, "width");
e.attributes.width.value = parseInt(svg.width.baseVal.value) - (xpad*2);
e.attributes.width.value = "100.0%";
}
}
if (e.childNodes == undefined) return;
Expand All @@ -192,13 +234,13 @@ function zoom(node) {
var attr = find_child(node, "rect").attributes;
var width = parseFloat(attr.width.value);
var xmin = parseFloat(attr.x.value);
var xmax = parseFloat(xmin + width);
var xmax = xmin + width;
var ymin = parseFloat(attr.y.value);
var ratio = (svg.width.baseVal.value - 2 * xpad) / width;
var ratio = 100 / width;
// XXX: Workaround for JavaScript float issues (fix me)
var fudge = 0.0001;
var fudge = 0.001;
unzoombtn.classList.remove("hide");
var el = document.getElementById("frames").children;
var el = frames.children;
for (var i = 0; i < el.length; i++) {
var e = el[i];
var a = find_child(e, "rect").attributes;
Expand Down Expand Up @@ -236,7 +278,7 @@ function zoom(node) {
}
function unzoom() {
unzoombtn.classList.add("hide");
var el = document.getElementById("frames").children;
var el = frames.children;
for(var i = 0; i < el.length; i++) {
el[i].classList.remove("parent");
el[i].classList.remove("hide");
Expand Down Expand Up @@ -272,7 +314,7 @@ function search_prompt() {
}
function search(term) {
var re = new RegExp(term);
var el = document.getElementById("frames").children;
var el = frames.children;
var matches = new Object();
var maxwidth = 0;
for (var i = 0; i < el.length; i++) {
Expand Down Expand Up @@ -343,3 +385,6 @@ function search(term) {
if (pct != 100) pct = pct.toFixed(1);
matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
}
function format_percent(n) {
return n.toFixed(4) + "%";
}
73 changes: 46 additions & 27 deletions src/flamegraph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ use std::iter;
use std::path::PathBuf;
use std::str::FromStr;
use str_stack::StrStack;
use svg::StyleOptions;
use svg::{Dimension, StyleOptions};

const XPAD: usize = 10; // pad lefm and right
const XPAD: usize = 10; // pad left and right
const FRAMEPAD: usize = 1; // vertical padding for frames

// If no image width is given, this will be the initial width, but the embedded JavaScript will set
// the width to 100% when it loads to make the width "fluid". The reason we give an initial width
// even when the width will be "fluid" is so it looks good in previewers or viewers that don't run
// the embedded JavaScript.
const DEFAULT_IMAGE_WIDTH: usize = 1200;

/// Default values for [`Options`].
pub mod defaults {
macro_rules! doc {
Expand Down Expand Up @@ -66,7 +72,6 @@ pub mod defaults {
COLORS: &str = "hot",
SEARCH_COLOR: &str = "#e600e6",
TITLE: &str = "Flame Graph",
IMAGE_WIDTH: usize = 1200,
FRAME_HEIGHT: usize = 16,
MIN_WIDTH: f64 = 0.1,
FONT_TYPE: &str = "Verdana",
Expand Down Expand Up @@ -130,10 +135,10 @@ pub struct Options<'a> {
/// Defaults to None.
pub subtitle: Option<String>,

/// Width of for the flame graph
/// Width of the flame graph
///
/// [Default value](defaults::IMAGE_WIDTH).
pub image_width: usize,
/// Defaults to None, which means the width will be "fluid".
pub image_width: Option<usize>,

/// Height of each frame.
///
Expand Down Expand Up @@ -235,7 +240,6 @@ impl<'a> Default for Options<'a> {
colors: Palette::from_str(defaults::COLORS).unwrap(),
search_color: SearchColor::from_str(defaults::SEARCH_COLOR).unwrap(),
title: defaults::TITLE.to_string(),
image_width: defaults::IMAGE_WIDTH,
frame_height: defaults::FRAME_HEIGHT,
min_width: defaults::MIN_WIDTH,
font_type: defaults::FONT_TYPE.to_string(),
Expand All @@ -244,6 +248,7 @@ impl<'a> Default for Options<'a> {
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(),
Expand Down Expand Up @@ -281,14 +286,15 @@ impl Default for Direction {
}

struct Rectangle {
x1: usize,
x1_pct: f64,
y1: usize,
x2: usize,
x2_pct: f64,
y2: usize,
}

impl Rectangle {
fn width(&self) -> usize {
self.x2 - self.x1
fn width_pct(&self) -> f64 {
self.x2_pct - self.x1_pct
}
fn height(&self) -> usize {
self.y2 - self.y1
Expand Down Expand Up @@ -379,7 +385,7 @@ where
&mut svg,
&mut buffer,
svg::TextItem {
x: (opt.image_width / 2) as f64,
x: Dimension::Percent(50.0),
y: (opt.font_size * 2) as f64,
text: "ERROR: No valid input provided to flamegraph".into(),
extra: None,
Expand All @@ -393,9 +399,10 @@ where
)));
}

let image_width = opt.image_width.unwrap_or(DEFAULT_IMAGE_WIDTH) as f64;
let timemax = time;
let widthpertime = (opt.image_width - 2 * XPAD) as f64 / timemax as f64;
let minwidth_time = opt.min_width / widthpertime;
let widthpertime_pct = 100.0 / timemax as f64;
let minwidth_time = opt.min_width / widthpertime_pct;

// prune blocks that are too narrow
let mut depthmax = 0;
Expand Down Expand Up @@ -434,16 +441,21 @@ where
let cache_a_end = Event::End(BytesEnd::borrowed(b"a"));

// create frames container
if let Event::Start(ref mut g) = cache_g {
g.extend_attributes(std::iter::once(("id", "frames")));
}
svg.write_event(&cache_g)?;
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),
]),
))?;

// draw frames
let mut samples_txt_buffer = num_format::Buffer::default();
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 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 => {
Expand All @@ -458,7 +470,13 @@ where
(y1, y2)
}
};
let rect = Rectangle { x1, y1, x2, y2 };

let rect = Rectangle {
x1_pct,
y1,
x2_pct,
y2,
};

// The rounding here can differ from the Perl version when the fractional part is `0.5`.
// The Perl version does `my $samples = sprintf "%.0f", ($etime - $stime) * $factor;`,
Expand Down Expand Up @@ -556,8 +574,9 @@ where
};
filled_rectangle(&mut svg, &mut buffer, &rect, color, &mut cache_rect)?;

let fitchars =
(rect.width() as f64 / (opt.font_size as f64 * opt.font_width)).trunc() as usize;
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 {
// room for one char plus two dots
let f = deannotate(&frame.location.function);
Expand Down Expand Up @@ -586,7 +605,7 @@ where
&mut svg,
&mut buffer,
svg::TextItem {
x: rect.x1 as f64 + 3.0,
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,
Expand All @@ -601,7 +620,7 @@ where
}
}

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)?;

Expand Down Expand Up @@ -711,9 +730,9 @@ fn filled_rectangle<W: Write>(
color: Color,
cache_rect: &mut Event<'_>,
) -> quick_xml::Result<usize> {
let x = write_usize(buffer, rect.x1);
let x = write!(buffer, "{:.4}%", rect.x1_pct);
let y = write_usize(buffer, rect.y1);
let width = write_usize(buffer, rect.width());
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);

Expand Down
Loading

0 comments on commit b9479dc

Please sign in to comment.