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 pub skip_top: bool,
54 pub skip_bottom: bool,
56}
57
58impl Default for PlotConfig {
59 fn default() -> Self {
60 PlotConfig {
61 multi_lane: false,
62 min_length: None,
63 remove: None,
64 inline_field: false,
65 color_top_blocking: "#E69F0088".to_string(),
67 color_top_threadpool: "#009E7388".to_string(),
68 color_bottom: "#56B4E988".to_string(),
69 skip_top: false,
70 skip_bottom: false,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct PlotLayout {
78 pub padding_top: usize,
80 pub padding_bottom: usize,
82 pub padding_left: usize,
84 pub padding_right: usize,
86 pub text_col_width: usize,
88 pub content_col_width: usize,
90 pub bar_height: usize,
92 pub multi_lane_padding: usize,
94 pub section_padding_height: usize,
96}
97
98impl Default for PlotLayout {
99 fn default() -> Self {
100 PlotLayout {
101 padding_top: 5,
102 padding_bottom: 5,
103 padding_left: 5,
104 padding_right: 5,
105 text_col_width: 250,
106 content_col_width: 850,
107 bar_height: 20,
108 multi_lane_padding: 1,
109 section_padding_height: 10,
110 }
111 }
112}
113
114pub fn plot(
118 spans: &[OwnedSpanInfo],
119 end: Duration,
120 config: &PlotConfig,
121 layout: &PlotLayout,
122) -> SVG {
123 let spans = if let Some(remove) = &config.remove {
125 spans
126 .iter()
127 .filter(|span| !remove.contains(&span.name))
128 .cloned()
129 .collect::<Vec<_>>()
130 } else {
131 spans.to_vec()
132 };
133
134 let mut full_spans: FxHashMap<u64, OwnedSpanInfo> = FxHashMap::default();
137 for span in &spans {
138 full_spans.entry(span.id).or_insert(span.clone()).end = span.end;
141 }
142
143 let (spans, full_spans) = if let Some(min_length) = config.min_length {
146 let mut removed_ids = HashSet::new();
147 for (id, full_span) in &full_spans {
148 if full_span.end - full_span.start < min_length {
149 removed_ids.insert(*id);
150 }
151 }
152 let spans = spans
153 .iter()
154 .filter(|span| !removed_ids.contains(&span.id))
155 .cloned()
156 .collect::<Vec<_>>();
157 for removed_id in removed_ids {
158 full_spans.remove(&removed_id);
159 }
160 (spans, full_spans)
161 } else {
162 (spans.to_vec(), full_spans)
163 };
164
165 let mut earliest_starts: FxHashMap<&str, Duration> = FxHashMap::default();
166 for span in &spans {
167 match earliest_starts.entry(&span.name) {
169 Entry::Occupied(mut entry) => {
170 if entry.get() > &span.start {
171 entry.insert(span.start);
172 }
173 }
174 Entry::Vacant(entry) => {
175 entry.insert(span.start);
176 }
177 }
178 }
179
180 let mut lanes_end: HashMap<&str, Vec<Duration>> = HashMap::new();
183 let mut span_lanes = HashMap::new();
184 let mut full_spans_sorted: Vec<_> = full_spans.values().collect();
185 full_spans_sorted.sort_by_key(|span| span.start);
186 for full_span in full_spans_sorted {
187 if config.multi_lane {
188 let lanes = lanes_end.entry(&full_span.name).or_default();
189 if let Some((idx, lane_end)) = lanes
190 .iter_mut()
191 .enumerate()
192 .find(|(_idx, end)| &full_span.start > end)
193 {
194 span_lanes.insert(full_span.id, idx);
195 *lane_end = full_span.end;
196 } else {
197 span_lanes.insert(full_span.id, lanes.len());
198 lanes.push(full_span.end)
199 }
200 } else {
201 span_lanes.insert(full_span.id, 0);
202 lanes_end
203 .entry(&full_span.name)
204 .or_insert_with(|| vec![full_span.end])[0] = full_span.end;
205 }
206 }
207
208 let extra_lane_height = layout.bar_height / 2 + layout.multi_lane_padding;
209
210 let mut earliest_starts: Vec<_> = earliest_starts.into_iter().collect();
211 earliest_starts.sort_by_key(|(_name, duration)| *duration);
212 let name_offsets: FxHashMap<&str, usize> = earliest_starts
213 .iter()
214 .enumerate()
215 .map(|(idx, (name, _earliest_start))| (*name, idx + 1))
217 .collect();
218
219 let mut extra_lanes_cur = 0;
221 let mut extra_lanes_cumulative = HashMap::new();
222 for (name, _start) in earliest_starts {
223 extra_lanes_cumulative.insert(name, extra_lanes_cur);
224 extra_lanes_cur += lanes_end.get(name).map_or(0, |lanes| lanes.len() - 1);
225 }
226
227 let total_width = layout.padding_left
228 + layout.text_col_width
229 + layout.content_col_width
230 + layout.padding_right;
231 let total_height = layout.padding_top
233 + (layout.bar_height + layout.section_padding_height) * (name_offsets.len() + 1)
234 + extra_lane_height * extra_lanes_cur
235 + layout.padding_bottom;
236
237 let mut document = Document::new()
238 .set("width", total_width)
239 .set("height", total_height)
240 .set("viewBox", (0, 0, total_width, total_height));
241
242 document = document
244 .add(
245 Text::new("0s")
246 .set("x", layout.text_col_width)
247 .set("y", layout.padding_top + layout.bar_height / 2)
248 .set("dominant-baseline", "middle")
249 .set("text-anchor", "start"),
250 )
251 .add(
252 Text::new(format!("{:.3}s", end.as_secs_f32()))
253 .set("x", layout.text_col_width + layout.content_col_width)
254 .set("y", layout.padding_top + layout.bar_height / 2)
255 .set("dominant-baseline", "middle")
256 .set("text-anchor", "end"),
257 );
258
259 if let Some(min_length) = config.min_length {
261 let text = format!("only spans >{}s", min_length.as_secs_f32());
262 document = document.add(
263 Text::new(text)
264 .set("x", layout.padding_left)
265 .set("y", layout.padding_top + layout.bar_height / 2)
266 .set("dominant-baseline", "middle")
267 .set("text-anchor", "start"),
268 );
269 }
270
271 for (name, offset) in &name_offsets {
273 document = document.add(
274 Text::new(name.to_string())
275 .set("x", layout.padding_left)
276 .set(
277 "y",
278 layout.padding_top
279 + layout.bar_height / 2
280 + offset * (layout.bar_height + layout.section_padding_height)
281 + extra_lane_height * extra_lanes_cumulative[name],
282 )
283 .set("dominant-baseline", "middle"),
284 );
285 }
286
287 let format_tooltip = |span: &OwnedSpanInfo| {
288 let fields = span
289 .fields
290 .iter()
291 .flatten()
292 .map(|(key, value)| format!("{key}: {value}"))
293 .join("\n");
294 format!("{} {:.3}s\n{}", span.name, span.secs(), fields)
295 };
296
297 if !config.skip_top {
299 for span in &spans {
300 let offset = name_offsets[span.name.as_str()];
301 let color = if span.is_main_thread {
302 config.color_top_blocking.clone()
303 } else {
304 config.color_top_threadpool.clone()
305 };
306 document = document.add(
307 Rectangle::new()
308 .set(
309 "x",
310 layout.text_col_width as f32
311 + layout.content_col_width as f32 * span.start.as_secs_f32()
312 / end.as_secs_f32(),
313 )
314 .set(
315 "y",
316 offset * (layout.bar_height + layout.section_padding_height)
317 + extra_lane_height * extra_lanes_cumulative[span.name.as_str()],
318 )
319 .set(
320 "width",
321 layout.content_col_width as f32 * span.secs() / end.as_secs_f32(),
322 )
323 .set("height", layout.bar_height / 2)
324 .set("fill", color)
325 .add(Title::new(format_tooltip(span))),
327 )
328 }
329 }
330
331 if !config.skip_bottom {
333 for full_span in full_spans.values() {
334 let x = layout.text_col_width as f32
335 + layout.content_col_width as f32 * full_span.start.as_secs_f32()
336 / end.as_secs_f32();
337 let y = name_offsets[full_span.name.as_str()]
338 * (layout.bar_height + layout.section_padding_height)
339 + extra_lane_height * extra_lanes_cumulative[full_span.name.as_str()]
340 + extra_lane_height * span_lanes[&full_span.id]
341 + layout.bar_height / 2;
342 let width = layout.content_col_width as f32
343 * (full_span.end - full_span.start).as_secs_f32()
344 / end.as_secs_f32();
345 let height = layout.bar_height / 2;
346 document = document.add(
347 Rectangle::new()
348 .set("x", x)
349 .set("y", y)
350 .set("width", width)
351 .set("height", height)
352 .set("fill", config.color_bottom.to_string())
353 .add(Title::new(format_tooltip(full_span))),
355 );
356 let mut fields = full_span
357 .fields
358 .as_ref()
359 .map(|map| map.values())
360 .into_iter()
361 .flatten();
362 if let Some(value) = fields.next() {
363 if config.inline_field && fields.next().is_none() {
364 document = document.add(
365 Text::new(value)
366 .set("x", x)
367 .set("y", y + height / 2)
368 .set("font-size", "0.7em")
369 .set("dominant-baseline", "middle")
370 .set("text-anchor", "start"),
371 )
372 }
373 }
374 }
375 }
376
377 document
378}