tracing_durations_export/
plot.rs1use std::collections::hash_map::Entry;
4use std::collections::{HashMap, HashSet};
5use std::time::Duration;
6
7use itertools::Itertools;
8use rustc_hash::FxHashMap;
9use serde::Deserialize;
10use svg::node::element::{Rectangle, Text, Title, SVG};
11use svg::Document;
12
13#[derive(Deserialize, Clone)]
15pub struct OwnedSpanInfo {
16 pub id: u64,
17 pub name: String,
18 pub start: Duration,
19 pub end: Duration,
20 #[allow(dead_code)]
21 pub parents: Option<Vec<u64>>,
22 pub is_main_thread: bool,
23 pub fields: Option<HashMap<String, String>>,
24}
25
26impl OwnedSpanInfo {
27 fn secs(&self) -> f32 {
28 (self.end - self.start).as_secs_f32()
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct PlotConfig {
35 pub multi_lane: bool,
37 pub min_length: Option<Duration>,
39 pub remove: Option<HashSet<String>>,
41 pub inline_field: bool,
45 pub color_top_blocking: String,
47 pub color_top_threadpool: String,
50 pub color_bottom: String,
52}
53
54impl Default for PlotConfig {
55 fn default() -> Self {
56 PlotConfig {
57 multi_lane: false,
58 min_length: None,
59 remove: None,
60 inline_field: false,
61 color_top_blocking: "#E69F0088".to_string(),
63 color_top_threadpool: "#009E7388".to_string(),
64 color_bottom: "#56B4E988".to_string(),
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct PlotLayout {
72 pub padding_top: usize,
74 pub padding_bottom: usize,
76 pub padding_left: usize,
78 pub padding_right: usize,
80 pub text_col_width: usize,
82 pub content_col_width: usize,
84 pub bar_height: usize,
86 pub multi_lane_padding: usize,
88 pub section_padding_height: usize,
90}
91
92impl Default for PlotLayout {
93 fn default() -> Self {
94 PlotLayout {
95 padding_top: 5,
96 padding_bottom: 5,
97 padding_left: 5,
98 padding_right: 5,
99 text_col_width: 250,
100 content_col_width: 850,
101 bar_height: 20,
102 multi_lane_padding: 1,
103 section_padding_height: 10,
104 }
105 }
106}
107
108pub fn plot(
112 spans: &[OwnedSpanInfo],
113 end: Duration,
114 config: &PlotConfig,
115 layout: &PlotLayout,
116) -> SVG {
117 let spans = if let Some(remove) = &config.remove {
119 spans
120 .iter()
121 .filter(|span| !remove.contains(&span.name))
122 .cloned()
123 .collect::<Vec<_>>()
124 } else {
125 spans.to_vec()
126 };
127
128 let mut full_spans: FxHashMap<u64, OwnedSpanInfo> = FxHashMap::default();
129 for span in &spans {
130 full_spans.entry(span.id).or_insert(span.clone()).end = span.end;
133 }
134
135 let (spans, full_spans) = if let Some(min_length) = config.min_length {
138 let mut removed_ids = HashSet::new();
139 for (id, full_span) in &full_spans {
140 if full_span.end - full_span.start < min_length {
141 removed_ids.insert(*id);
142 }
143 }
144 let spans = spans
145 .iter()
146 .filter(|span| !removed_ids.contains(&span.id))
147 .cloned()
148 .collect::<Vec<_>>();
149 for removed_id in removed_ids {
150 full_spans.remove(&removed_id);
151 }
152 (spans, full_spans)
153 } else {
154 (spans.to_vec(), full_spans)
155 };
156
157 let mut earliest_starts: FxHashMap<&str, Duration> = FxHashMap::default();
158 for span in &spans {
159 match earliest_starts.entry(&span.name) {
161 Entry::Occupied(mut entry) => {
162 if entry.get() > &span.start {
163 entry.insert(span.start);
164 }
165 }
166 Entry::Vacant(entry) => {
167 entry.insert(span.start);
168 }
169 }
170 }
171
172 let mut lanes_end: HashMap<&str, Vec<Duration>> = HashMap::new();
175 let mut span_lanes = HashMap::new();
176 let mut full_spans_sorted: Vec<_> = full_spans.values().collect();
177 full_spans_sorted.sort_by_key(|span| span.start);
178 for full_span in full_spans_sorted {
179 if config.multi_lane {
180 let lanes = lanes_end.entry(&full_span.name).or_default();
181 if let Some((idx, lane_end)) = lanes
182 .iter_mut()
183 .enumerate()
184 .find(|(_idx, end)| &full_span.start > end)
185 {
186 span_lanes.insert(full_span.id, idx);
187 *lane_end = full_span.end;
188 } else {
189 span_lanes.insert(full_span.id, lanes.len());
190 lanes.push(full_span.end)
191 }
192 } else {
193 span_lanes.insert(full_span.id, 0);
194 lanes_end
195 .entry(&full_span.name)
196 .or_insert_with(|| vec![full_span.end])[0] = full_span.end;
197 }
198 }
199
200 let extra_lane_height = layout.bar_height / 2 + layout.multi_lane_padding;
201
202 let mut earliest_starts: Vec<_> = earliest_starts.into_iter().collect();
203 earliest_starts.sort_by_key(|(_name, duration)| *duration);
204 let name_offsets: FxHashMap<&str, usize> = earliest_starts
205 .iter()
206 .enumerate()
207 .map(|(idx, (name, _earliest_start))| (*name, idx + 1))
209 .collect();
210
211 let mut extra_lanes_cur = 0;
213 let mut extra_lanes_cumulative = HashMap::new();
214 for (name, _start) in earliest_starts {
215 extra_lanes_cumulative.insert(name, extra_lanes_cur);
216 extra_lanes_cur += lanes_end[name].len() - 1;
217 }
218
219 let total_width = layout.padding_left
220 + layout.text_col_width
221 + layout.content_col_width
222 + layout.padding_right;
223 let total_height = layout.padding_top
225 + (layout.bar_height + layout.section_padding_height) * (name_offsets.len() + 1)
226 + extra_lane_height * extra_lanes_cur
227 + layout.padding_bottom;
228
229 let mut document = Document::new()
230 .set("width", total_width)
231 .set("height", total_height)
232 .set("viewBox", (0, 0, total_width, total_height));
233
234 document = document
235 .add(
236 Text::new("0s")
237 .set("x", layout.text_col_width)
238 .set("y", layout.padding_top + layout.bar_height / 2)
239 .set("dominant-baseline", "middle")
240 .set("text-anchor", "start"),
241 )
242 .add(
243 Text::new(format!("{:.3}s", end.as_secs_f32()))
244 .set("x", layout.text_col_width + layout.content_col_width)
245 .set("y", layout.padding_top + layout.bar_height / 2)
246 .set("dominant-baseline", "middle")
247 .set("text-anchor", "end"),
248 );
249
250 if let Some(min_length) = config.min_length {
251 let text = format!("only spans >{}s", min_length.as_secs_f32());
253 document = document.add(
254 Text::new(text)
255 .set("x", layout.padding_left)
256 .set("y", layout.padding_top + layout.bar_height / 2)
257 .set("dominant-baseline", "middle")
258 .set("text-anchor", "start"),
259 );
260 }
261
262 for (name, offset) in &name_offsets {
264 document = document.add(
265 Text::new(name.to_string())
266 .set("x", layout.padding_left)
267 .set(
268 "y",
269 layout.padding_top
270 + layout.bar_height / 2
271 + offset * (layout.bar_height + layout.section_padding_height)
272 + extra_lane_height * extra_lanes_cumulative[name],
273 )
274 .set("dominant-baseline", "middle"),
275 );
276 }
277
278 let format_tooltip = |span: &OwnedSpanInfo| {
279 let fields = span
280 .fields
281 .iter()
282 .flatten()
283 .map(|(key, value)| format!("{key}: {value}"))
284 .join("\n");
285 format!("{} {:.3}s\n{}", span.name, span.secs(), fields)
286 };
287
288 for span in &spans {
290 let offset = name_offsets[span.name.as_str()];
291 let color = if span.is_main_thread {
292 config.color_top_blocking.clone()
293 } else {
294 config.color_top_threadpool.clone()
295 };
296 document = document.add(
297 Rectangle::new()
298 .set(
299 "x",
300 layout.text_col_width as f32
301 + layout.content_col_width as f32 * span.start.as_secs_f32()
302 / end.as_secs_f32(),
303 )
304 .set(
305 "y",
306 offset * (layout.bar_height + layout.section_padding_height)
307 + extra_lane_height * extra_lanes_cumulative[span.name.as_str()],
308 )
309 .set(
310 "width",
311 layout.content_col_width as f32 * span.secs() / end.as_secs_f32(),
312 )
313 .set("height", layout.bar_height / 2)
314 .set("fill", color)
315 .add(Title::new(format_tooltip(span))),
317 )
318 }
319
320 for full_span in full_spans.values() {
322 let x = layout.text_col_width as f32
323 + layout.content_col_width as f32 * full_span.start.as_secs_f32() / end.as_secs_f32();
324 let y = name_offsets[full_span.name.as_str()]
325 * (layout.bar_height + layout.section_padding_height)
326 + extra_lane_height * extra_lanes_cumulative[full_span.name.as_str()]
327 + extra_lane_height * span_lanes[&full_span.id]
328 + layout.bar_height / 2;
329 let width = layout.content_col_width as f32
330 * (full_span.end - full_span.start).as_secs_f32()
331 / end.as_secs_f32();
332 let height = layout.bar_height / 2;
333 document = document.add(
334 Rectangle::new()
335 .set("x", x)
336 .set("y", y)
337 .set("width", width)
338 .set("height", height)
339 .set("fill", config.color_bottom.to_string())
340 .add(Title::new(format_tooltip(full_span))),
342 );
343 let mut fields = full_span
344 .fields
345 .as_ref()
346 .map(|map| map.values())
347 .into_iter()
348 .flatten();
349 if let Some(value) = fields.next() {
350 if config.inline_field && fields.next().is_none() {
351 document = document.add(
352 Text::new(value)
353 .set("x", x)
354 .set("y", y + height / 2)
355 .set("font-size", "0.7em")
356 .set("dominant-baseline", "middle")
357 .set("text-anchor", "start"),
358 )
359 }
360 }
361 }
362 document
363}