chartr_core/
render.rs

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// The built in Svg::Script type does escaping that breaks non trivial scripts
14// so make our own that just renders it plainly
15#[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        // TODO: we probably shouldn't hard code this as seconds
105        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            // Skip a couple of "lines" after the text of the heading
115            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                //TODO: handle instants
155                _ => unimplemented!(),
156            };
157
158            // Only draw the actor label at the start of the first span
159            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                // Assume the font is probably about 80% of the line
210                // height.
211                .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        // First, determine how many lines we need
281        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        // Gather the relevant actors for height calculation and such
300        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        // TODO: consider heading width may be greater than box width
310        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}