1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::hash::Hash;
4use std::{path::Path, time::Duration};
5use svg::node::element as Svg;
6use svg::node::element::path::Data;
7use svg::Document;
8
9use crate::event::{ActorId, EventKind, EventStore};
10
11const APPROX_FONT_HEIGHT: f64 = 15.0;
12
13#[derive(Clone, Debug)]
16struct ScriptComment {
17 contents: String,
18}
19
20impl ScriptComment {
21 fn new(contents: impl AsRef<str>) -> Self {
22 Self {
23 contents: contents.as_ref().to_owned(),
24 }
25 }
26}
27
28impl svg::node::NodeDefaultHash for ScriptComment {
29 fn default_hash(&self, state: &mut std::collections::hash_map::DefaultHasher) {
30 self.contents.hash(state);
31 }
32}
33
34impl std::fmt::Display for ScriptComment {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "<script>")
37 .and_then(|_| write!(f, "<![CDATA[{}]]>", self.contents))
38 .and_then(|_| write!(f, "</script>"))
39 }
40}
41
42impl svg::Node for ScriptComment {
43 fn get_name(&self) -> &'static str {
44 "script"
45 }
46}
47
48#[derive(Deserialize, Serialize)]
49struct RenderOpts {
50 us_per_line: u64,
51 sublines: u32,
52 us_per_pixel: u32,
53 pixels_per_actor: f64,
54 actor_margin: f64,
55 actor_name_padding: f64,
56 top_margin: f64,
57 side_margin: f64,
58 heading: String,
59}
60
61impl Default for RenderOpts {
62 fn default() -> Self {
63 Self {
64 us_per_line: Duration::from_secs(1).as_micros() as u64,
65 sublines: 10,
66 us_per_pixel: 10000,
67 pixels_per_actor: 20.0,
68 actor_margin: 0.5,
69 actor_name_padding: 5.0,
70 top_margin: 20.0,
71 side_margin: 20.0,
72 heading: "".into(),
73 }
74 }
75}
76
77#[derive(Deserialize, Default)]
78pub struct RendererBuilder {
79 opts: RenderOpts,
80}
81
82impl RendererBuilder {
83 pub fn build(self) -> Renderer {
84 Renderer { opts: self.opts }
85 }
86
87 pub fn heading(mut self, heading: impl AsRef<str>) -> Self {
88 self.opts.heading = heading.as_ref().into();
89 self
90 }
91}
92
93#[derive(Deserialize, Serialize)]
94pub struct Renderer {
95 opts: RenderOpts,
96}
97
98impl Renderer {
99 fn us_to_pixel(&self, us: i64) -> f64 {
100 us as f64 / self.opts.us_per_pixel as f64
101 }
102
103 fn render_line_time(&self, us: i64) -> String {
104 let seconds = us as f64 / 1_000_000.0;
106 let fac = us as f64 % 1_000_000.0;
107 format!("{seconds}.{fac}")
108 }
109
110 fn calculate_heading_height(&self) -> f64 {
111 let heading_start = self.opts.top_margin + APPROX_FONT_HEIGHT;
112 let lines = self.opts.heading.lines().count() as f64;
113 let heading_end = heading_start + lines * APPROX_FONT_HEIGHT +
114 2.0 * APPROX_FONT_HEIGHT;
116 heading_end
117 }
118
119 fn render_heading(&self, mut output: Document) -> Result<Document> {
120 let mut current_y = self.opts.top_margin + APPROX_FONT_HEIGHT;
121 for line in self.opts.heading.lines() {
122 let text = Svg::Text::new(line)
123 .set("class", "heading")
124 .set("x", self.opts.side_margin)
125 .set("y", current_y);
126 current_y += APPROX_FONT_HEIGHT;
127 output = output.add(text);
128 }
129
130 Ok(output)
131 }
132
133 fn render_actor(
134 &self,
135 mut output: Svg::Group,
136 y: f64,
137 box_width: f64,
138 first_event_pixel: f64,
139 events: &EventStore,
140 actor: ActorId,
141 ) -> Result<Svg::Group> {
142 let mut g = Svg::Group::new().set("class", "actor");
143
144 let tooltip_prefix = events.get_actor(&actor).tooltip.clone();
145
146 let mut actor_start: Option<i64> = None;
147 for (i, event) in events
148 .events_for(&actor)
149 .with_context(|| "Failed to get actor events")?
150 .enumerate()
151 {
152 let (start, duration) = match event.kind {
153 EventKind::Span(start, duration) => (start, duration),
154 _ => unimplemented!(),
156 };
157
158 if i == 0 {
160 actor_start = Some(start);
161 }
162
163 let width = match duration {
164 Some(duration) => self.us_to_pixel(duration as i64),
165 None => (first_event_pixel + box_width) - self.us_to_pixel(start),
166 };
167
168 let mut state = Svg::Rectangle::new()
169 .set("class", "span")
170 .set("width", width)
171 .set(
172 "height",
173 self.opts.pixels_per_actor - 2.0 * self.opts.actor_margin,
174 )
175 .set("x", self.us_to_pixel(start))
176 .set("y", y + self.opts.actor_margin);
177
178 let attrs = state.get_attributes_mut();
179 for (key, value) in event.fields.clone().into_iter() {
180 let current = attrs.entry(key.clone()).or_insert("".into()).clone();
181 attrs.insert(key, format!("{value} {current}").into());
182 }
183
184 if let Some(tip) = event
185 .tooltip
186 .as_ref()
187 .map(|tip| tooltip_prefix.clone().unwrap_or_default() + tip)
188 {
189 let tooltip = Svg::Title::new(tip);
190 state = state.add(tooltip);
191 }
192
193 g = g.add(state);
194 }
195
196 if let Some(start) = actor_start {
197 let actor_name = events.get_actor(&actor);
198
199 let (class, padding) =
200 if self.us_to_pixel(start) < (first_event_pixel + box_width) / 2.0 {
201 ("left", self.opts.actor_name_padding)
202 } else {
203 ("right", -self.opts.actor_name_padding)
204 };
205
206 let text = Svg::Text::new(actor_name.identity.clone())
207 .set("class", class)
208 .set("x", self.us_to_pixel(start) + padding)
209 .set("y", y + self.opts.pixels_per_actor * 0.8);
212
213 g = g.add(text);
214 }
215
216 output = output.add(g);
217 Ok(output)
218 }
219
220 fn render_lines(
221 &self,
222 mut g: Svg::Group,
223 first_event_time: i64,
224 last_event_time: i64,
225 box_height: f64,
226 ) -> Result<Svg::Group> {
227 let first_bar = first_event_time
228 - (first_event_time % self.opts.us_per_line as i64)
229 - self.opts.us_per_line as i64;
230 let last_bar = last_event_time + (last_event_time % self.opts.us_per_line as i64);
231
232 let step = self.opts.us_per_line as usize / self.opts.sublines as usize;
233 for x in (first_bar..=last_bar).step_by(step) {
234 if x < first_event_time || x > last_event_time {
235 continue;
236 }
237
238 let scaled_x = self.us_to_pixel(x);
239
240 let data = Data::new()
241 .move_to((scaled_x, 0))
242 .line_by((0, box_height))
243 .close();
244
245 let mut path = Svg::Path::new().set("d", data);
246
247 if x.unsigned_abs() % self.opts.us_per_line == 0 {
248 let text = Svg::Text::new(self.render_line_time(x))
249 .set("class", "label")
250 .set("x", scaled_x)
251 .set("y", -5);
252 g = g.add(text);
253 } else {
254 path = path.set("class", "subline");
255 }
256
257 g = g.add(path);
258 }
259
260 Ok(g)
261 }
262
263 fn render_css(&self, document: Document) -> Result<Document> {
264 let defs = Svg::Definitions::new().add(Svg::Style::new(include_str!("assets/style.css")));
265 Ok(document.add(defs))
266 }
267
268 pub fn render_script(&self, document: Document) -> Result<Document> {
269 let script = include_str!("assets/script.js")
270 .replace("__LEFT_OFFSET__", &self.opts.side_margin.to_string())
271 .replace("__US_PER_PIXEL__", &self.opts.us_per_pixel.to_string())
272 .replace(
273 "__HEADING_HEIGHT__",
274 &self.calculate_heading_height().to_string(),
275 );
276 Ok(document.add(ScriptComment::new(script)))
277 }
278
279 pub fn render(&self, path: impl AsRef<Path>, events: EventStore) -> Result<()> {
280 let first_event_time = events
282 .all_events()
283 .min_by_key(|e| e.start_time())
284 .map(|e| {
285 if e.start_time() > 0 {
286 0
287 } else {
288 e.start_time()
289 }
290 })
291 .unwrap_or(0);
292
293 let last_event_time = events
294 .all_events()
295 .filter_map(|e| e.end_time())
296 .max()
297 .unwrap_or(0);
298
299 let mut actors = events
301 .actors()
302 .filter_map(|actor| events.events_for(&actor).ok()?.next().map(|e| (actor, e)))
303 .collect::<Vec<_>>();
304
305 actors.sort_by_key(|(_, event)| event.start_time());
306
307 let heading_height = self.calculate_heading_height();
308
309 let box_width = self.us_to_pixel(last_event_time - first_event_time);
311 let box_height = actors.len() as f64 * self.opts.pixels_per_actor;
312
313 let mut document = Document::new()
314 .set("width", box_width + 2.0 * self.opts.side_margin)
315 .set("height", box_height + heading_height + self.opts.top_margin);
316
317 let serialized = svg::node::Comment::new(serde_json::to_string(&(self, &events))?);
318 document = document.add(serialized);
319
320 document = self.render_script(document)?;
321 document = self.render_css(document)?;
322 document = self.render_heading(document)?;
323
324 let start_x = self.opts.side_margin
325 + if first_event_time < 0 {
326 -self.us_to_pixel(first_event_time)
327 } else {
328 0.0
329 };
330
331 let mut g = Svg::Group::new().set(
332 "transform",
333 format!("translate({start_x}, {heading_height})"),
334 );
335 g = self.render_lines(g, first_event_time, last_event_time, box_height)?;
336
337 let mut y = 0.0;
338 for (actor, _) in actors.into_iter() {
339 g = self
340 .render_actor(
341 g,
342 y,
343 box_width,
344 self.us_to_pixel(first_event_time),
345 &events,
346 actor,
347 )
348 .with_context(|| "Failed to render actor events")?;
349
350 y += self.opts.pixels_per_actor;
351 }
352
353 document = document
354 .add(g)
355 .add(
356 Svg::Rectangle::new()
357 .set("id", "indicator")
358 .set("width", 1.0)
359 .set("height", box_height),
360 )
361 .add(Svg::Text::new("").set("id", "indicator-text"));
362
363 svg::save(path, &document).with_context(|| "Failed to save svg")
364 }
365}
366
367impl Default for Renderer {
368 fn default() -> Self {
369 Self {
370 opts: RenderOpts::default(),
371 }
372 }
373}