diff --git a/src/bin/flamegraph.rs b/src/bin/flamegraph.rs index 8298d45f..652d8439 100644 --- a/src/bin/flamegraph.rs +++ b/src/bin/flamegraph.rs @@ -2,7 +2,7 @@ use std::io; use std::path::{Path, PathBuf}; use env_logger::Env; -use inferno::flamegraph::color::{BackgroundColor, PaletteMap, SearchColor}; +use inferno::flamegraph::color::{BackgroundColor, PaletteMap, SearchColor, StrokeColor}; use inferno::flamegraph::{self, defaults, Direction, Options, Palette, TextTruncateDirection}; #[cfg(feature = "nameattr")] @@ -174,6 +174,14 @@ struct Opt { )] search_color: SearchColor, + /// Adds an outline to every frame + #[clap( + long = "stroke-color", + default_value = defaults::STROKE_COLOR, + value_name = "STRING" + )] + stroke_color: StrokeColor, + /// Second level title (optional) #[clap(long = "subtitle", value_name = "STRING")] subtitle: Option, @@ -255,6 +263,7 @@ impl<'a> Opt { options.negate_differentials = self.negate; options.factor = self.factor; options.search_color = self.search_color; + options.stroke_color = self.stroke_color; (self.infiles, options) } diff --git a/src/flamegraph/color/mod.rs b/src/flamegraph/color/mod.rs index ceac0d4c..1cf1fe8f 100644 --- a/src/flamegraph/color/mod.rs +++ b/src/flamegraph/color/mod.rs @@ -186,6 +186,28 @@ impl fmt::Display for SearchColor { } } +/// `StrokeColor::default()` is `None`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StrokeColor { + /// Color of the stroke + Color(Color), + /// No color for the stroke + None, +} + +impl FromStr for StrokeColor { + type Err = String; + + fn from_str(s: &str) -> Result { + if s == "none" { + return Ok(StrokeColor::None); + } + parse_flat_bgcolor(s) + .map(|c| StrokeColor::Color(c)) + .ok_or_else(|| format!("unknown color: {}", s)) + } +} + impl FromStr for Palette { type Err = String; diff --git a/src/flamegraph/mod.rs b/src/flamegraph/mod.rs index a2067ed3..f8ee45b7 100644 --- a/src/flamegraph/mod.rs +++ b/src/flamegraph/mod.rs @@ -32,7 +32,7 @@ use self::attrs::FrameAttrs; pub use self::attrs::FuncFrameAttrsMap; pub use self::color::Palette; -use self::color::{Color, SearchColor}; +use self::color::{Color, SearchColor, StrokeColor}; use self::svg::{Dimension, StyleOptions}; const XPAD: usize = 10; // pad left and right @@ -76,6 +76,7 @@ pub mod defaults { define! { COLORS: &str = "hot", SEARCH_COLOR: &str = "#e600e6", + STROKE_COLOR: &str = "none", TITLE: &str = "Flame Graph", CHART_TITLE: &str = "Flame Chart", FRAME_HEIGHT: usize = 16, @@ -137,6 +138,11 @@ pub struct Options<'a> { /// [Default value](defaults::SEARCH_COLOR). pub search_color: SearchColor, + /// The stroke color for flame graph. + /// + /// [Default value](defaults::STROKE_COLOR). + pub stroke_color: StrokeColor, + /// The title for the flame graph. /// /// [Default value](defaults::TITLE). @@ -285,6 +291,7 @@ impl<'a> Default for Options<'a> { Options { colors: Palette::from_str(defaults::COLORS).unwrap(), search_color: SearchColor::from_str(defaults::SEARCH_COLOR).unwrap(), + stroke_color: StrokeColor::from_str(defaults::STROKE_COLOR).unwrap(), title: defaults::TITLE.to_string(), frame_height: defaults::FRAME_HEIGHT, min_width: defaults::MIN_WIDTH, @@ -500,10 +507,15 @@ where svg::write_header(&mut svg, imageheight, opt)?; let (bgcolor1, bgcolor2) = color::bgcolor_for(opt.bgcolors, opt.colors); + let strokecolor = match opt.stroke_color { + StrokeColor::Color(c) => Some(c.to_string()), + StrokeColor::None => None, + }; let style_options = StyleOptions { imageheight, bgcolor1, bgcolor2, + strokecolor, }; svg::write_prelude(&mut svg, &style_options, opt)?; diff --git a/src/flamegraph/svg.rs b/src/flamegraph/svg.rs index 67f5684e..bf57475b 100644 --- a/src/flamegraph/svg.rs +++ b/src/flamegraph/svg.rs @@ -51,6 +51,7 @@ pub(super) struct StyleOptions<'a> { pub(super) imageheight: usize, pub(super) bgcolor1: Cow<'a, str>, pub(super) bgcolor2: Cow<'a, str>, + pub(super) strokecolor: Option, } pub fn write_header( @@ -130,11 +131,17 @@ where " text {{ font-family:{}; font-size:{}px; fill:rgb(0,0,0); }} #title {{ text-anchor:middle; font-size:{}px; }} -{}", - font_type, - &opt.font_size, - titlesize, - include_str!("flamegraph.css") +", + font_type, &opt.font_size, titlesize, + ))))?; + if let Some(strokecolor) = &style_options.strokecolor { + svg.write_event(Event::Text(BytesText::from_escaped_str(&format!( + "#frames > g > rect {{ stroke:{}; stroke-width:1; }}\n", + strokecolor + ))))?; + } + svg.write_event(Event::Text(BytesText::from_escaped_str(include_str!( + "flamegraph.css" ))))?; svg.write_event(Event::End(BytesEnd::borrowed(b"style")))?; diff --git a/tests/data/flamegraph/options/stroke_color.svg b/tests/data/flamegraph/options/stroke_color.svg new file mode 100644 index 00000000..591af1ec --- /dev/null +++ b/tests/data/flamegraph/options/stroke_color.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + Flame Graph + + Reset Zoom + Search + + + + _start (56 samples, 10.92%; 0.00%) + + _start + + + __libc_start_main (56 samples, 10.92%; 0.00%) + + __libc_start_main + + + main (56 samples, 10.92%; 0.00%) + + main + + + cksum (56 samples, 10.92%; +4.87%) + + cksum + + + cksum (5 samples, 0.97%; -0.78%) + + + + + __GI___fread_unlocked (3 samples, 0.58%; 0.00%) + + + + + _IO_file_xsgetn (3 samples, 0.58%; 0.00%) + + + + + _IO_file_read (3 samples, 0.58%; 0.00%) + + + + + entry_SYSCALL_64_fastpath (3 samples, 0.58%; 0.00%) + + + + + sys_read (3 samples, 0.58%; 0.00%) + + + + + vfs_read (3 samples, 0.58%; 0.00%) + + + + + __vfs_read (3 samples, 0.58%; 0.00%) + + + + + ext4_file_read_iter (3 samples, 0.58%; +0.39%) + + + + + cksum (96 samples, 18.71%; 0.00%) + + cksum + + + main (35 samples, 6.82%; 0.00%) + + main + + + cksum (35 samples, 6.82%; +3.12%) + + cksum + + + [unknown] (2 samples, 0.39%; 0.00%) + + + + + all (513 samples, 100%) + + + + + noploop (417 samples, 81.29%; 0.00%) + + noploop + + + main (415 samples, 80.90%; +27.49%) + + main + + + diff --git a/tests/flamegraph.rs b/tests/flamegraph.rs index b9cd2ba2..9929cfa9 100644 --- a/tests/flamegraph.rs +++ b/tests/flamegraph.rs @@ -710,6 +710,18 @@ fn search_color_non_default() { test_flamegraph(input_file, expected_result_file, options).unwrap(); } +#[test] +fn stroke_color_non_default() { + let input_file = + "./tests/data/flamegraph/differential/perf-cycles-instructions-01-collapsed-all-diff.txt"; + let expected_result_file = "./tests/data/flamegraph/options/stroke_color.svg"; + + let mut options = flamegraph::Options::default(); + options.stroke_color = "#7d7d7d".parse().unwrap(); + + test_flamegraph(input_file, expected_result_file, options).unwrap(); +} + #[test] fn flamegraph_sorted_input_file() { let input_file = "./flamegraph/test/results/perf-vertx-stacks-01-collapsed-all.txt";