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 "fluid" drawing #136

Merged
merged 5 commits into from
Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
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
102 changes: 76 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;
"use strict"
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
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]);
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
}

// 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,18 @@ 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 framesWidth = frames.width.baseVal.value;
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
var w = parseFloat(r.attributes.width.value) * framesWidth / 100 - 3;
var txt = find_child(e, "title").textContent.replace(/\([^(]*\)$/,"");
t.attributes.x.value = format_percent((parseFloat(r.attributes.x.value) + (100 * 3 / framesWidth)));
// 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)
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
return;
for (var x = txt.length - 2; x > 0; x--) {
if (t.getSubStringLength(0, x + 2) <= w) {
Expand All @@ -143,6 +185,11 @@ function update_text(e) {
}
t.textContent = "";
}
function update_search_elements() {
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
var svgWidth = svg.width.baseVal.value;
searchbtn.attributes.x.value = svgWidth - xpad - 100;
matchedtxt.attributes.x.value = svgWidth - xpad - 100;
}
// zoom
function zoom_reset(e) {
if (e.attributes != undefined) {
Expand All @@ -158,29 +205,29 @@ 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;
e.attributes.x.value = format_percent((parseFloat(e.attributes.x.value) - x) * ratio);
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
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(find_child(e.parentNode, "rect[x]").attributes.x.value) + 3);
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
}
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 +239,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;
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
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 +283,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 +319,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 +390,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.
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
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![
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
("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);
jonhoo marked this conversation as resolved.
Show resolved Hide resolved
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