1use std::collections::{BTreeMap, HashMap};
4use std::io::{self, Write};
5use std::path::Path;
6use std::time::Duration;
7
8use crate::event::EventId;
9use crate::{Event, Events};
10
11const STYLE: &[u8] = include_bytes!("trace.css");
12const SCRIPT: &[u8] = include_bytes!("trace.js");
13
14pub fn write<P>(path: P, events: &Events) -> io::Result<()>
16where
17 P: AsRef<Path>,
18{
19 let path = path.as_ref();
20
21 let file_stem = path.file_stem().ok_or_else(|| {
22 io::Error::new(
23 io::ErrorKind::InvalidInput,
24 "Missing file stem from the specified path",
25 )
26 })?;
27
28 let parent = path.parent().ok_or_else(|| {
29 io::Error::new(
30 io::ErrorKind::InvalidInput,
31 "Missing parent from the specified path",
32 )
33 })?;
34
35 let css = parent.join(file_stem).with_extension("css");
36 let script = parent.join(file_stem).with_extension("js");
37
38 std::fs::write(&css, STYLE)?;
39 std::fs::write(&script, SCRIPT)?;
40
41 let css = css
42 .file_name()
43 .and_then(|name| name.to_str())
44 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid css file name"))?;
45
46 let script = script
47 .file_name()
48 .and_then(|name| name.to_str())
49 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid script file name"))?;
50
51 let mut out = std::fs::File::create(path)?;
52
53 let mut start = u64::MAX;
55 let mut end = u64::MIN;
57
58 let mut opens = BTreeMap::<_, BTreeMap<_, Vec<_>>>::new();
59 let mut children = HashMap::<_, Vec<_>>::new();
60 let mut closes = HashMap::new();
61
62 for enter in &events.enters {
63 start = start.min(enter.timestamp);
64
65 if let Some(parent) = enter.parent {
66 children.entry(parent).or_default().push(enter);
67 } else {
68 opens
69 .entry((enter.lock, enter.type_name.as_ref()))
70 .or_default()
71 .entry(enter.thread_index)
72 .or_default()
73 .push(enter);
74 }
75 }
76
77 for leave in &events.leaves {
78 end = end.max(leave.timestamp);
79 closes.insert(leave.sibling, leave.timestamp);
80 }
81
82 if start == u64::MAX || end == u64::MIN {
83 return Ok(());
84 }
85
86 writeln!(out, "<!DOCTYPE html>")?;
87 writeln!(out, "<html>")?;
88 writeln!(out, "<head>")?;
89 writeln!(out, r#"<link href="{css}" rel="stylesheet">"#)?;
90 writeln!(out, "</head>")?;
91
92 writeln!(out, "<body>")?;
93 writeln!(out, "<div id=\"traces\">")?;
94
95 for ((lock, type_name), events) in opens {
96 writeln!(out, "<div class=\"lock-instance\">")?;
97
98 let kind = lock.kind();
99 let index = lock.index();
100
101 let type_name = type_name.replace('<', "<").replace('>', ">");
102
103 writeln!(
104 out,
105 r#"<div class="title">{kind:?}<{type_name}> (lock index: {index})</div>"#
106 )?;
107
108 writeln!(out, "<div class=\"lock-session\">")?;
109
110 for (thread_index, events) in events.into_iter() {
111 let start = events.iter().map(|e| e.timestamp).min().unwrap_or(0);
112
113 let end = events
114 .iter()
115 .flat_map(|ev| closes.get(&ev.id).copied())
116 .max()
117 .unwrap_or(0);
118
119 writeln!(
120 out,
121 r#"<div data-toggle="event-{lock}-{thread_index}-details" data-start="{start}" data-end="{end}" class="timeline">"#
122 )?;
123
124 writeln!(
125 out,
126 r#"<div class="timeline-heading"><span>{thread_index}</span></div>"#
127 )?;
128
129 writeln!(out, r#"<div class="timeline-data">"#)?;
130
131 let mut details = Vec::new();
132
133 for ev in events {
134 let open = ev.timestamp;
135 let id = ev.id;
136
137 let Some(close) = closes.get(&ev.id).copied() else {
138 return Ok(());
139 };
140
141 writeln! {
142 details,
143 r#"
144 <tr data-entry data-entry-start="{open}" data-entry-close="{close}">
145 <td class="title" colspan="6">Event: {id}</td>
146 </tr>
147 "#
148 }?;
149
150 write_section(
151 &mut out,
152 ev,
153 (start, end),
154 close,
155 &children,
156 &closes,
157 &mut details,
158 )?;
159 }
160
161 writeln!(out, r#"<div class="timeline-target"></div>"#)?;
162 writeln!(out, "</div>")?;
163 writeln!(out, "</div>")?;
164
165 if !details.is_empty() {
166 writeln!(
167 out,
168 r#"<table id="event-{lock}-{thread_index}-details" class="details">"#
169 )?;
170
171 out.write_all(&details)?;
172 writeln!(out, "</table>")?;
173 }
174 }
175
176 writeln!(out, "</div>")?;
177 writeln!(out, "</div>")?;
178 }
179
180 writeln!(out, "</div>")?;
181 writeln!(
182 out,
183 r#"<script type="text/javascript" src="{script}"></script>"#
184 )?;
185 writeln!(out, "</body>")?;
186 writeln!(out, "</html>")?;
187 Ok(())
188}
189
190#[allow(clippy::too_many_arguments)]
191fn write_section(
192 out: &mut dyn io::Write,
193 ev: &Event,
194 span: (u64, u64),
195 close: u64,
196 children: &HashMap<EventId, Vec<&Event>>,
197 closes: &HashMap<EventId, u64>,
198 d: &mut Vec<u8>,
199) -> io::Result<()> {
200 let id = ev.id;
201 let title = ev.name.as_ref();
202 let open = ev.timestamp;
203
204 let (start, end) = span;
205
206 if start == end {
207 return Ok(());
208 }
209
210 let total = (end - start) as f32;
211
212 let left = (((open - start) as f32 / total) * 100.0).round() as u32;
213 let width = (((close - open) as f32 / total) * 100.0).round() as u32;
214
215 let s = Duration::from_nanos(open);
216 let e = Duration::from_nanos(close);
217 let duration = Duration::from_nanos(close - open);
218
219 let style = format!("width: {width}%; left: {left}%;");
220 let hover_title = format!("{title} ({s:?}-{e:?})");
221
222 writeln!(
223 out,
224 "<div id=\"event-{id}\" class=\"section {title}\" style=\"{style}\" title=\"{hover_title}\"></div>"
225 )?;
226
227 writeln! {
228 d,
229 r#"
230 <tr data-entry data-entry-start="{open}" data-entry-close="{close}">
231 <td class="title {title}">{title}</td>
232 <td>{s:?}</td>
233 <td>—</td>
234 <td>{e:?}</td>
235 <td>({duration:?})</td>
236 <td width="100%"></td>
237 </tr>
238 "#
239 }?;
240
241 if let Some(backtrace) = &ev.backtrace {
242 writeln!(
243 d,
244 r#"<tr><td>Backtrace:</td><td class="backtrace" colspan="5">{backtrace}</td></tr>"#
245 )?;
246 }
247
248 for ev in children.get(&ev.id).into_iter().flatten() {
249 let Some(child_close) = closes.get(&ev.id).copied() else {
250 continue;
251 };
252
253 write_section(out, ev, span, child_close, children, closes, d)?;
254 }
255
256 Ok(())
257}