unlock/
html.rs

1//! Module to format captured lock events as html.
2
3use 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
14/// Write events to the given path.
15pub 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    // Start of trace.
54    let mut start = u64::MAX;
55    // End of trace.
56    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('<', "&lt;").replace('>', "&gt");
102
103        writeln!(
104            out,
105            r#"<div class="title">{kind:?}&lt;{type_name}&gt; (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>&mdash;</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}