use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use serde::Deserialize;
use svg::node::element::{Rectangle, Text, Title, SVG};
use svg::Document;
#[derive(Deserialize, Clone)]
pub struct OwnedSpanInfo {
pub id: u64,
pub name: String,
pub start: Duration,
pub end: Duration,
#[allow(dead_code)]
pub parents: Option<Vec<u64>>,
pub is_main_thread: bool,
pub fields: Option<HashMap<String, String>>,
}
impl OwnedSpanInfo {
fn secs(&self) -> f32 {
(self.end - self.start).as_secs_f32()
}
}
#[derive(Debug, Clone)]
pub struct PlotConfig {
pub multi_lane: bool,
pub min_length: Option<Duration>,
pub remove: Option<HashSet<String>>,
pub inline_field: bool,
pub color_top_blocking: String,
pub color_top_threadpool: String,
pub color_bottom: String,
pub skip_top: bool,
pub skip_bottom: bool,
}
impl Default for PlotConfig {
fn default() -> Self {
PlotConfig {
multi_lane: false,
min_length: None,
remove: None,
inline_field: false,
color_top_blocking: "#E69F0088".to_string(),
color_top_threadpool: "#009E7388".to_string(),
color_bottom: "#56B4E988".to_string(),
skip_top: false,
skip_bottom: false,
}
}
}
#[derive(Debug, Clone)]
pub struct PlotLayout {
pub padding_top: usize,
pub padding_bottom: usize,
pub padding_left: usize,
pub padding_right: usize,
pub text_col_width: usize,
pub content_col_width: usize,
pub bar_height: usize,
pub multi_lane_padding: usize,
pub section_padding_height: usize,
}
impl Default for PlotLayout {
fn default() -> Self {
PlotLayout {
padding_top: 5,
padding_bottom: 5,
padding_left: 5,
padding_right: 5,
text_col_width: 250,
content_col_width: 850,
bar_height: 20,
multi_lane_padding: 1,
section_padding_height: 10,
}
}
}
pub fn plot(
spans: &[OwnedSpanInfo],
end: Duration,
config: &PlotConfig,
layout: &PlotLayout,
) -> SVG {
let spans = if let Some(remove) = &config.remove {
spans
.iter()
.filter(|span| !remove.contains(&span.name))
.cloned()
.collect::<Vec<_>>()
} else {
spans.to_vec()
};
let mut full_spans: FxHashMap<u64, OwnedSpanInfo> = FxHashMap::default();
for span in &spans {
full_spans.entry(span.id).or_insert(span.clone()).end = span.end;
}
let (spans, full_spans) = if let Some(min_length) = config.min_length {
let mut removed_ids = HashSet::new();
for (id, full_span) in &full_spans {
if full_span.end - full_span.start < min_length {
removed_ids.insert(*id);
}
}
let spans = spans
.iter()
.filter(|span| !removed_ids.contains(&span.id))
.cloned()
.collect::<Vec<_>>();
for removed_id in removed_ids {
full_spans.remove(&removed_id);
}
(spans, full_spans)
} else {
(spans.to_vec(), full_spans)
};
let mut earliest_starts: FxHashMap<&str, Duration> = FxHashMap::default();
for span in &spans {
match earliest_starts.entry(&span.name) {
Entry::Occupied(mut entry) => {
if entry.get() > &span.start {
entry.insert(span.start);
}
}
Entry::Vacant(entry) => {
entry.insert(span.start);
}
}
}
let mut lanes_end: HashMap<&str, Vec<Duration>> = HashMap::new();
let mut span_lanes = HashMap::new();
let mut full_spans_sorted: Vec<_> = full_spans.values().collect();
full_spans_sorted.sort_by_key(|span| span.start);
for full_span in full_spans_sorted {
if config.multi_lane {
let lanes = lanes_end.entry(&full_span.name).or_default();
if let Some((idx, lane_end)) = lanes
.iter_mut()
.enumerate()
.find(|(_idx, end)| &full_span.start > end)
{
span_lanes.insert(full_span.id, idx);
*lane_end = full_span.end;
} else {
span_lanes.insert(full_span.id, lanes.len());
lanes.push(full_span.end)
}
} else {
span_lanes.insert(full_span.id, 0);
lanes_end
.entry(&full_span.name)
.or_insert_with(|| vec![full_span.end])[0] = full_span.end;
}
}
let extra_lane_height = layout.bar_height / 2 + layout.multi_lane_padding;
let mut earliest_starts: Vec<_> = earliest_starts.into_iter().collect();
earliest_starts.sort_by_key(|(_name, duration)| *duration);
let name_offsets: FxHashMap<&str, usize> = earliest_starts
.iter()
.enumerate()
.map(|(idx, (name, _earliest_start))| (*name, idx + 1))
.collect();
let mut extra_lanes_cur = 0;
let mut extra_lanes_cumulative = HashMap::new();
for (name, _start) in earliest_starts {
extra_lanes_cumulative.insert(name, extra_lanes_cur);
extra_lanes_cur += lanes_end.get(name).map_or(0, |lanes| lanes.len() - 1);
}
let total_width = layout.padding_left
+ layout.text_col_width
+ layout.content_col_width
+ layout.padding_right;
let total_height = layout.padding_top
+ (layout.bar_height + layout.section_padding_height) * (name_offsets.len() + 1)
+ extra_lane_height * extra_lanes_cur
+ layout.padding_bottom;
let mut document = Document::new()
.set("width", total_width)
.set("height", total_height)
.set("viewBox", (0, 0, total_width, total_height));
document = document
.add(
Text::new("0s")
.set("x", layout.text_col_width)
.set("y", layout.padding_top + layout.bar_height / 2)
.set("dominant-baseline", "middle")
.set("text-anchor", "start"),
)
.add(
Text::new(format!("{:.3}s", end.as_secs_f32()))
.set("x", layout.text_col_width + layout.content_col_width)
.set("y", layout.padding_top + layout.bar_height / 2)
.set("dominant-baseline", "middle")
.set("text-anchor", "end"),
);
if let Some(min_length) = config.min_length {
let text = format!("only spans >{}s", min_length.as_secs_f32());
document = document.add(
Text::new(text)
.set("x", layout.padding_left)
.set("y", layout.padding_top + layout.bar_height / 2)
.set("dominant-baseline", "middle")
.set("text-anchor", "start"),
);
}
for (name, offset) in &name_offsets {
document = document.add(
Text::new(name.to_string())
.set("x", layout.padding_left)
.set(
"y",
layout.padding_top
+ layout.bar_height / 2
+ offset * (layout.bar_height + layout.section_padding_height)
+ extra_lane_height * extra_lanes_cumulative[name],
)
.set("dominant-baseline", "middle"),
);
}
let format_tooltip = |span: &OwnedSpanInfo| {
let fields = span
.fields
.iter()
.flatten()
.map(|(key, value)| format!("{key}: {value}"))
.join("\n");
format!("{} {:.3}s\n{}", span.name, span.secs(), fields)
};
if !config.skip_top {
for span in &spans {
let offset = name_offsets[span.name.as_str()];
let color = if span.is_main_thread {
config.color_top_blocking.clone()
} else {
config.color_top_threadpool.clone()
};
document = document.add(
Rectangle::new()
.set(
"x",
layout.text_col_width as f32
+ layout.content_col_width as f32 * span.start.as_secs_f32()
/ end.as_secs_f32(),
)
.set(
"y",
offset * (layout.bar_height + layout.section_padding_height)
+ extra_lane_height * extra_lanes_cumulative[span.name.as_str()],
)
.set(
"width",
layout.content_col_width as f32 * span.secs() / end.as_secs_f32(),
)
.set("height", layout.bar_height / 2)
.set("fill", color)
.add(Title::new(format_tooltip(span))),
)
}
}
if !config.skip_bottom {
for full_span in full_spans.values() {
let x = layout.text_col_width as f32
+ layout.content_col_width as f32 * full_span.start.as_secs_f32()
/ end.as_secs_f32();
let y = name_offsets[full_span.name.as_str()]
* (layout.bar_height + layout.section_padding_height)
+ extra_lane_height * extra_lanes_cumulative[full_span.name.as_str()]
+ extra_lane_height * span_lanes[&full_span.id]
+ layout.bar_height / 2;
let width = layout.content_col_width as f32
* (full_span.end - full_span.start).as_secs_f32()
/ end.as_secs_f32();
let height = layout.bar_height / 2;
document = document.add(
Rectangle::new()
.set("x", x)
.set("y", y)
.set("width", width)
.set("height", height)
.set("fill", config.color_bottom.to_string())
.add(Title::new(format_tooltip(full_span))),
);
let mut fields = full_span
.fields
.as_ref()
.map(|map| map.values())
.into_iter()
.flatten();
if let Some(value) = fields.next() {
if config.inline_field && fields.next().is_none() {
document = document.add(
Text::new(value)
.set("x", x)
.set("y", y + height / 2)
.set("font-size", "0.7em")
.set("dominant-baseline", "middle")
.set("text-anchor", "start"),
)
}
}
}
}
document
}