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, Style, Text, Title, SVG};
11use svg::Document;
12
13const PLOT_STYLE: &str = r#"
14:root {
15 color-scheme: light dark;
16 --plot-background: #ffffff;
17 --plot-foreground: #111827;
18}
19
20@media (prefers-color-scheme: dark) {
21 :root {
22 --plot-background: #111827;
23 --plot-foreground: #e5e7eb;
24 }
25}
26
27.plot-background {
28 fill: var(--plot-background);
29}
30
31text {
32 fill: var(--plot-foreground);
33}
34"#;
35
36#[derive(Deserialize, Clone)]
38pub struct OwnedSpanInfo {
39 pub id: u64,
40 pub name: String,
41 pub start: Duration,
42 pub end: Duration,
43 #[allow(dead_code)]
44 pub parents: Option<Vec<u64>>,
45 pub is_main_thread: bool,
46 pub fields: Option<HashMap<String, String>>,
47}
48
49impl OwnedSpanInfo {
50 fn secs(&self) -> f32 {
51 (self.end - self.start).as_secs_f32()
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct PlotConfig {
58 pub multi_lane: bool,
60 pub min_length: Option<Duration>,
62 pub remove: Option<HashSet<String>>,
64 pub inline_field: bool,
68 pub color_top_blocking: String,
70 pub color_top_threadpool: String,
73 pub color_bottom: String,
75 pub skip_top: bool,
77 pub skip_bottom: bool,
79}
80
81impl Default for PlotConfig {
82 fn default() -> Self {
83 PlotConfig {
84 multi_lane: false,
85 min_length: None,
86 remove: None,
87 inline_field: false,
88 color_top_blocking: "#E69F0088".to_string(),
90 color_top_threadpool: "#009E7388".to_string(),
91 color_bottom: "#56B4E988".to_string(),
92 skip_top: false,
93 skip_bottom: false,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct PlotLayout {
101 pub padding_top: usize,
103 pub padding_bottom: usize,
105 pub padding_left: usize,
107 pub padding_right: usize,
109 pub text_col_width: usize,
111 pub content_col_width: usize,
113 pub bar_height: usize,
115 pub multi_lane_padding: usize,
117 pub section_padding_height: usize,
119}
120
121impl Default for PlotLayout {
122 fn default() -> Self {
123 PlotLayout {
124 padding_top: 5,
125 padding_bottom: 5,
126 padding_left: 5,
127 padding_right: 5,
128 text_col_width: 250,
129 content_col_width: 850,
130 bar_height: 20,
131 multi_lane_padding: 1,
132 section_padding_height: 10,
133 }
134 }
135}
136
137pub fn plot(
141 spans: &[OwnedSpanInfo],
142 end: Duration,
143 config: &PlotConfig,
144 layout: &PlotLayout,
145) -> SVG {
146 let spans = if let Some(remove) = &config.remove {
148 spans
149 .iter()
150 .filter(|span| !remove.contains(&span.name))
151 .cloned()
152 .collect::<Vec<_>>()
153 } else {
154 spans.to_vec()
155 };
156
157 let mut full_spans: FxHashMap<u64, OwnedSpanInfo> = FxHashMap::default();
160 for span in &spans {
161 full_spans.entry(span.id).or_insert(span.clone()).end = span.end;
164 }
165
166 let (spans, full_spans) = if let Some(min_length) = config.min_length {
169 let mut removed_ids = HashSet::new();
170 for (id, full_span) in &full_spans {
171 if full_span.end - full_span.start < min_length {
172 removed_ids.insert(*id);
173 }
174 }
175 let spans = spans
176 .iter()
177 .filter(|span| !removed_ids.contains(&span.id))
178 .cloned()
179 .collect::<Vec<_>>();
180 for removed_id in removed_ids {
181 full_spans.remove(&removed_id);
182 }
183 (spans, full_spans)
184 } else {
185 (spans.to_vec(), full_spans)
186 };
187
188 let mut earliest_starts: FxHashMap<&str, Duration> = FxHashMap::default();
189 for span in &spans {
190 match earliest_starts.entry(&span.name) {
192 Entry::Occupied(mut entry) => {
193 if entry.get() > &span.start {
194 entry.insert(span.start);
195 }
196 }
197 Entry::Vacant(entry) => {
198 entry.insert(span.start);
199 }
200 }
201 }
202
203 let mut lanes_end: HashMap<&str, Vec<Duration>> = HashMap::new();
206 let mut span_lanes = HashMap::new();
207 let mut full_spans_sorted: Vec<_> = full_spans.values().collect();
208 full_spans_sorted.sort_by_key(|span| span.start);
209 for full_span in full_spans_sorted {
210 if config.multi_lane {
211 let lanes = lanes_end.entry(&full_span.name).or_default();
212 if let Some((idx, lane_end)) = lanes
213 .iter_mut()
214 .enumerate()
215 .find(|(_idx, end)| &full_span.start > end)
216 {
217 span_lanes.insert(full_span.id, idx);
218 *lane_end = full_span.end;
219 } else {
220 span_lanes.insert(full_span.id, lanes.len());
221 lanes.push(full_span.end)
222 }
223 } else {
224 span_lanes.insert(full_span.id, 0);
225 lanes_end
226 .entry(&full_span.name)
227 .or_insert_with(|| vec![full_span.end])[0] = full_span.end;
228 }
229 }
230
231 let extra_lane_height = layout.bar_height / 2 + layout.multi_lane_padding;
232
233 let mut earliest_starts: Vec<_> = earliest_starts.into_iter().collect();
234 earliest_starts.sort_by_key(|(_name, duration)| *duration);
235 let name_offsets: FxHashMap<&str, usize> = earliest_starts
236 .iter()
237 .enumerate()
238 .map(|(idx, (name, _earliest_start))| (*name, idx + 1))
240 .collect();
241
242 let mut extra_lanes_cur = 0;
244 let mut extra_lanes_cumulative = HashMap::new();
245 for (name, _start) in earliest_starts {
246 extra_lanes_cumulative.insert(name, extra_lanes_cur);
247 extra_lanes_cur += lanes_end.get(name).map_or(0, |lanes| lanes.len() - 1);
248 }
249
250 let total_width = layout.padding_left
251 + layout.text_col_width
252 + layout.content_col_width
253 + layout.padding_right;
254 let total_height = layout.padding_top
256 + (layout.bar_height + layout.section_padding_height) * (name_offsets.len() + 1)
257 + extra_lane_height * extra_lanes_cur
258 + layout.padding_bottom;
259
260 let mut document = Document::new()
261 .set("width", total_width)
262 .set("height", total_height)
263 .set("viewBox", (0, 0, total_width, total_height))
264 .add(Style::new(PLOT_STYLE))
265 .add(
266 Rectangle::new()
267 .set("class", "plot-background")
268 .set("x", 0)
269 .set("y", 0)
270 .set("width", total_width)
271 .set("height", total_height)
272 .set("fill", "#ffffff"),
273 );
274
275 document = document
277 .add(
278 Text::new("0s")
279 .set("x", layout.text_col_width)
280 .set("y", layout.padding_top + layout.bar_height / 2)
281 .set("dominant-baseline", "middle")
282 .set("text-anchor", "start"),
283 )
284 .add(
285 Text::new(format!("{:.3}s", end.as_secs_f32()))
286 .set("x", layout.text_col_width + layout.content_col_width)
287 .set("y", layout.padding_top + layout.bar_height / 2)
288 .set("dominant-baseline", "middle")
289 .set("text-anchor", "end"),
290 );
291
292 if let Some(min_length) = config.min_length {
294 let text = format!("only spans >{}s", min_length.as_secs_f32());
295 document = document.add(
296 Text::new(text)
297 .set("x", layout.padding_left)
298 .set("y", layout.padding_top + layout.bar_height / 2)
299 .set("dominant-baseline", "middle")
300 .set("text-anchor", "start"),
301 );
302 }
303
304 for (name, offset) in &name_offsets {
306 document = document.add(
307 Text::new(name.to_string())
308 .set("x", layout.padding_left)
309 .set(
310 "y",
311 layout.padding_top
312 + layout.bar_height / 2
313 + offset * (layout.bar_height + layout.section_padding_height)
314 + extra_lane_height * extra_lanes_cumulative[name],
315 )
316 .set("dominant-baseline", "middle"),
317 );
318 }
319
320 let format_tooltip = |span: &OwnedSpanInfo| {
321 let fields = span
322 .fields
323 .iter()
324 .flatten()
325 .map(|(key, value)| format!("{key}: {value}"))
326 .join("\n");
327 format!("{} {:.3}s\n{}", span.name, span.secs(), fields)
328 };
329
330 if !config.skip_top {
332 for span in &spans {
333 let offset = name_offsets[span.name.as_str()];
334 let color = if span.is_main_thread {
335 config.color_top_blocking.clone()
336 } else {
337 config.color_top_threadpool.clone()
338 };
339 document = document.add(
340 Rectangle::new()
341 .set(
342 "x",
343 layout.text_col_width as f32
344 + layout.content_col_width as f32 * span.start.as_secs_f32()
345 / end.as_secs_f32(),
346 )
347 .set(
348 "y",
349 offset * (layout.bar_height + layout.section_padding_height)
350 + extra_lane_height * extra_lanes_cumulative[span.name.as_str()],
351 )
352 .set(
353 "width",
354 layout.content_col_width as f32 * span.secs() / end.as_secs_f32(),
355 )
356 .set("height", layout.bar_height / 2)
357 .set("fill", color)
358 .add(Title::new(format_tooltip(span))),
360 )
361 }
362 }
363
364 if !config.skip_bottom {
366 for full_span in full_spans.values() {
367 let x = layout.text_col_width as f32
368 + layout.content_col_width as f32 * full_span.start.as_secs_f32()
369 / end.as_secs_f32();
370 let y = name_offsets[full_span.name.as_str()]
371 * (layout.bar_height + layout.section_padding_height)
372 + extra_lane_height * extra_lanes_cumulative[full_span.name.as_str()]
373 + extra_lane_height * span_lanes[&full_span.id]
374 + layout.bar_height / 2;
375 let width = layout.content_col_width as f32
376 * (full_span.end - full_span.start).as_secs_f32()
377 / end.as_secs_f32();
378 let height = layout.bar_height / 2;
379 document = document.add(
380 Rectangle::new()
381 .set("x", x)
382 .set("y", y)
383 .set("width", width)
384 .set("height", height)
385 .set("fill", config.color_bottom.to_string())
386 .add(Title::new(format_tooltip(full_span))),
388 );
389 let mut fields = full_span
390 .fields
391 .as_ref()
392 .map(|map| map.values())
393 .into_iter()
394 .flatten();
395 if let Some(value) = fields.next() {
396 if config.inline_field && fields.next().is_none() {
397 document = document.add(
398 Text::new(value)
399 .set("x", x)
400 .set("y", y + height / 2)
401 .set("font-size", "0.7em")
402 .set("dominant-baseline", "middle")
403 .set("text-anchor", "start"),
404 )
405 }
406 }
407 }
408 }
409
410 document
411}