Skip to main content

kui_trace/
adapters.rs

1//! The recording written for the tools that already exist (ADR 0058,
2//! decision 12): Chrome's trace-event JSON (`chrome://tracing`,
3//! Perfetto's viewer), Perfetto's own protobuf, speedscope's and the
4//! Firefox Profiler's. An adapter maps what its format has and drops what
5//! it has not.
6//!
7//! Every adapter reads one `Model` made from the timeline, so they draw
8//! the same rows with the same names:
9//!
10//! - **lanes**: `frames` first, then `main`, `loop` (when the driver
11//!   recorded it) and each declared track. A track a span or mark names
12//!   but no record declared is `track N`.
13//! - **slices** (intervals): a frame is `frame N` on `frames`, from its
14//!   begin to its end (to the recording's end for one cut off, marked
15//!   `unfinished`), with its causes, holders, unchanged, nodes and its
16//!   present's render and wait as arguments; its present is `render` then
17//!   `vsync wait` on `frames` after it, ending where the driver reported
18//!   it and never past the next frame's begin. A span is its name on its
19//!   track, its category its origin (`kui`, `host`, an extension's
20//!   namespace), with its node, CPU and self time. The loop asleep is
21//!   `asleep` on `loop`, from its sleep to its waking. Slices on one lane
22//!   are made to nest: a child is cut to its parent's end, since no format
23//!   here draws two slices that cross on one row.
24//! - **instants**: inputs, events, wakes, requests and each component
25//!   drawn fresh (`<component> <answer>`, its reason and node as
26//!   arguments; a replayed one is not written) on `main`; the
27//!   loop's wakings and OS events on `loop`; marks on their track; notes
28//!   on `frames` (global in Chrome's).
29//! - **series**: each counter by name, a step over each frame it moved
30//!   in (its value from the frame's begin, 0 from its end); each gauge by
31//!   name, a point a reading.
32//!
33//! A verdict, a burn or a phase is not written: they are readings of the
34//! timeline, and every tool here reads its own.
35
36mod chrome;
37mod firefox;
38mod json;
39mod perfetto;
40mod speedscope;
41#[cfg(test)]
42mod tests;
43
44use std::cmp::Reverse;
45
46use crate::model::Trace;
47use crate::record::{LoopEvent, Origin, Record, Track};
48
49/// A format a recording is written in.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Format {
52    /// kui's own (`.kuitrace`).
53    Kuitrace,
54    /// Chrome's trace-event JSON (`.json`).
55    Chrome,
56    /// Perfetto's protobuf (`.pftrace`, `.perfetto-trace`).
57    Perfetto,
58    /// speedscope's (`.speedscope.json`).
59    Speedscope,
60    /// The Firefox Profiler's, as a Gecko profile (`.firefox.json`).
61    Firefox,
62}
63
64impl Format {
65    /// The format a path's name asks for; `.kuitrace` for one it does not
66    /// name.
67    pub fn from_path(path: &str) -> Format {
68        let p = path.to_ascii_lowercase();
69        if p.ends_with(".speedscope.json") {
70            Format::Speedscope
71        } else if p.ends_with(".firefox.json") {
72            Format::Firefox
73        } else if p.ends_with(".json") {
74            Format::Chrome
75        } else if p.ends_with(".pftrace") || p.ends_with(".perfetto-trace") {
76            Format::Perfetto
77        } else {
78            Format::Kuitrace
79        }
80    }
81
82    /// By the name `kui-trace convert --to` takes.
83    pub fn parse(s: &str) -> Option<Format> {
84        Some(match s {
85            "kuitrace" | "kui" => Format::Kuitrace,
86            "chrome" | "json" => Format::Chrome,
87            "perfetto" | "pftrace" => Format::Perfetto,
88            "speedscope" => Format::Speedscope,
89            "firefox" => Format::Firefox,
90            _ => return None,
91        })
92    }
93
94    /// The name [`Format::parse`] takes for it.
95    pub fn name(self) -> &'static str {
96        match self {
97            Format::Kuitrace => "kuitrace",
98            Format::Chrome => "chrome",
99            Format::Perfetto => "perfetto",
100            Format::Speedscope => "speedscope",
101            Format::Firefox => "firefox",
102        }
103    }
104}
105
106/// `trace` in `format`.
107pub fn write(trace: &Trace, format: Format) -> Vec<u8> {
108    match format {
109        Format::Kuitrace => trace.to_bytes(),
110        Format::Chrome => chrome(trace).into_bytes(),
111        Format::Perfetto => perfetto(trace),
112        Format::Speedscope => speedscope(trace).into_bytes(),
113        Format::Firefox => firefox(trace).into_bytes(),
114    }
115}
116
117/// Chrome's trace-event JSON: `X` slices, `i` instants and `C` counters,
118/// a thread a lane, times in µs. Loads in `chrome://tracing` and
119/// Perfetto's viewer.
120pub fn chrome(trace: &Trace) -> String {
121    chrome::write(&Model::of(trace))
122}
123
124/// Perfetto's protobuf trace: a track a lane and a counter track a
125/// series, slices as begin and end track events, times in ns.
126pub fn perfetto(trace: &Trace) -> Vec<u8> {
127    perfetto::write(&Model::of(trace))
128}
129
130/// speedscope's file format: an evented profile a lane with slices.
131pub fn speedscope(trace: &Trace) -> String {
132    speedscope::write(&Model::of(trace))
133}
134
135/// The Firefox Profiler's: a Gecko profile of markers, a thread a lane.
136pub fn firefox(trace: &Trace) -> String {
137    firefox::write(&Model::of(trace))
138}
139
140/// An argument's value.
141#[derive(Clone, Debug, PartialEq)]
142pub(crate) enum Val {
143    Str(String),
144    U(u64),
145    I(i64),
146    F(f64),
147    B(bool),
148}
149
150/// A row: the frames, or a track.
151#[derive(Clone, Debug, PartialEq)]
152pub(crate) struct Lane {
153    pub track: Option<Track>,
154    pub name: String,
155    /// The thread id Chrome's and Perfetto's give it: a track's id plus
156    /// one (`main` 1, `loop` 2, declared tracks 3 on), the frames one past
157    /// the last.
158    pub tid: u64,
159}
160
161/// An interval on a lane.
162#[derive(Clone, Debug, PartialEq)]
163pub(crate) struct Slice {
164    pub lane: usize,
165    pub start: u64,
166    pub end: u64,
167    /// What a timeline draws: `frame 12`.
168    pub label: String,
169    /// What a tool that groups by name groups it under: `frame`.
170    pub name: String,
171    pub cat: String,
172    pub args: Vec<(&'static str, Val)>,
173}
174
175/// A point on a lane.
176#[derive(Clone, Debug, PartialEq)]
177pub(crate) struct Instant {
178    pub lane: usize,
179    pub t: u64,
180    pub name: String,
181    pub cat: &'static str,
182    pub args: Vec<(&'static str, Val)>,
183    /// Across every lane, where the format has that.
184    pub global: bool,
185}
186
187/// A counter or a gauge over time: [`Val::I`] or [`Val::F`] points.
188#[derive(Clone, Debug, PartialEq)]
189pub(crate) struct Series {
190    pub name: String,
191    pub gauge: bool,
192    pub points: Vec<(u64, Val)>,
193}
194
195/// The timeline as rows of slices, instants and series, which every
196/// adapter draws.
197#[derive(Clone, Debug, Default, PartialEq)]
198pub(crate) struct Model {
199    pub app: String,
200    pub meta: Vec<(&'static str, Val)>,
201    /// The wall clock at 0, ms since the Unix epoch, where recorded.
202    pub wall_ms: u64,
203    pub lanes: Vec<Lane>,
204    /// By lane, then start, a parent before its children; nested.
205    pub slices: Vec<Slice>,
206    pub instants: Vec<Instant>,
207    pub series: Vec<Series>,
208    pub start: u64,
209    pub end: u64,
210}
211
212/// The frames' lane's index.
213pub(crate) const FRAMES: usize = 0;
214/// `main`'s.
215pub(crate) const MAIN: usize = 1;
216
217/// An origin as text: `kui`, `host`, or an extension's namespace.
218pub fn origin_label(trace: &Trace, o: Origin) -> String {
219    match o {
220        Origin::Kui => "kui".into(),
221        Origin::Host => "host".into(),
222        Origin::Extension(ns) => match trace.name(ns) {
223            "" => "extension".into(),
224            n => n.into(),
225        },
226    }
227}
228
229fn named(prefix: &str, name: &str) -> String {
230    if name.is_empty() {
231        prefix.to_string()
232    } else {
233        format!("{prefix} {name}")
234    }
235}
236
237/// A node key as text.
238pub(crate) fn hex(node: u64) -> String {
239    format!("{node:016x}")
240}
241
242/// One step of a lane's slices in order: `(at, opens, slice index)`.
243pub(crate) type Edge = (u64, bool, usize);
244
245impl Model {
246    pub fn of(trace: &Trace) -> Model {
247        let tl = trace.timeline();
248        let mut m = Model {
249            start: tl.start,
250            end: tl.end,
251            ..Default::default()
252        };
253        m.app = trace
254            .session
255            .as_ref()
256            .map(|s| s.app.clone())
257            .filter(|a| !a.is_empty())
258            .unwrap_or_else(|| "kui".into());
259        if let Some(s) = &trace.session {
260            m.wall_ms = s.wall_ms;
261            m.meta = vec![
262                ("kui", Val::Str(s.kui.clone())),
263                ("app", Val::Str(s.app.clone())),
264                ("platform", Val::Str(s.platform.clone())),
265                ("refresh_hz", Val::F(s.refresh_hz as f64)),
266                ("budget_ms", Val::F(s.budget_ms as f64)),
267                ("scale", Val::F(s.scale as f64)),
268                ("wall_ms", Val::U(s.wall_ms)),
269            ];
270        }
271
272        // The lanes: frames, main, loop when used, the declared tracks,
273        // then any named by a record but never declared.
274        let mut used: Vec<Track> = tl
275            .spans
276            .iter()
277            .map(|s| s.track)
278            .chain(tl.marks.iter().map(|k| k.track))
279            .collect();
280        if !tl.loops.is_empty() {
281            used.push(Track::LOOP);
282        }
283        let mut declared: Vec<Track> = trace.tracks.iter().map(|(t, _)| *t).collect();
284        declared.sort();
285        let mut order = vec![Track::MAIN];
286        if used.contains(&Track::LOOP) {
287            order.push(Track::LOOP);
288        }
289        for t in declared {
290            if !order.contains(&t) {
291                order.push(t);
292            }
293        }
294        used.sort();
295        used.dedup();
296        for t in used {
297            if !order.contains(&t) {
298                order.push(t);
299            }
300        }
301        m.lanes.push(Lane {
302            track: None,
303            name: "frames".into(),
304            tid: 0,
305        });
306        for t in &order {
307            let name = match trace.track_name(*t) {
308                "" => format!("track {}", t.0),
309                n => n.to_string(),
310            };
311            m.lanes.push(Lane {
312                track: Some(*t),
313                name,
314                tid: t.0 as u64 + 1,
315            });
316        }
317        m.lanes[FRAMES].tid = m.lanes.iter().map(|l| l.tid).max().unwrap_or(0) + 1;
318        let lane_of = |t: Track| -> usize { 1 + order.iter().position(|o| *o == t).unwrap_or(0) };
319
320        // Frames and their presents.
321        for (i, f) in tl.frames.iter().enumerate() {
322            let end = f.end.unwrap_or(tl.end.max(f.begin));
323            let mut args = vec![("frame", Val::U(f.frame))];
324            if !f.causes.is_empty() {
325                let causes: Vec<&str> = f.causes.iter().map(|c| trace.name(*c)).collect();
326                args.push(("causes", Val::Str(causes.join(", "))));
327            }
328            if !f.holders.is_empty() {
329                let holders: Vec<String> = f
330                    .holders
331                    .iter()
332                    .map(|h| {
333                        let mut s = named(trace.name(h.what), trace.name(h.name));
334                        if h.node != 0 {
335                            s.push_str(&format!(" node {}", hex(h.node)));
336                        }
337                        s
338                    })
339                    .collect();
340                args.push(("holders", Val::Str(holders.join("; "))));
341            }
342            if let Some(u) = f.unchanged {
343                args.push(("unchanged", Val::B(u)));
344            }
345            args.push(("nodes", Val::U(f.nodes as u64)));
346            if let Some(p) = f.present {
347                args.push(("render_us", Val::U(p.render)));
348                args.push(("wait_us", Val::U(p.wait)));
349            }
350            if f.end.is_none() {
351                args.push(("unfinished", Val::B(true)));
352            }
353            m.slices.push(Slice {
354                lane: FRAMES,
355                start: f.begin,
356                end,
357                label: format!("frame {}", f.frame),
358                name: "frame".into(),
359                cat: "frame".into(),
360                args,
361            });
362            if let Some(p) = f.present {
363                // The driver reports once render and the wait are done;
364                // both after the frame, neither into the next.
365                let limit = tl.frames.get(i + 1).map_or(u64::MAX, |n| n.begin.max(end));
366                let rs =
367                    p.t.saturating_sub(p.render.saturating_add(p.wait))
368                        .max(end)
369                        .min(limit);
370                let re = rs.saturating_add(p.render).min(limit);
371                let we = re.saturating_add(p.wait).min(limit);
372                for (name, a, b) in [("render", rs, re), ("vsync wait", re, we)] {
373                    if b > a {
374                        m.slices.push(Slice {
375                            lane: FRAMES,
376                            start: a,
377                            end: b,
378                            label: name.into(),
379                            name: name.into(),
380                            cat: "present".into(),
381                            args: vec![("frame", Val::U(f.frame))],
382                        });
383                    }
384                }
385            }
386        }
387
388        // Spans.
389        for s in &tl.spans {
390            let name = match trace.name(s.name) {
391                "" => "(unnamed)".to_string(),
392                n => n.to_string(),
393            };
394            let cat = origin_label(trace, s.origin);
395            let mut args = vec![("origin", Val::Str(cat.clone()))];
396            if s.node != 0 {
397                args.push(("node", Val::Str(hex(s.node))));
398            }
399            if let Some(c) = s.cpu {
400                args.push(("cpu_us", Val::U(c)));
401            }
402            args.push(("self_us", Val::U(tl.self_us(s))));
403            args.push(("id", Val::U(s.id as u64)));
404            if s.parent != 0 {
405                args.push(("parent", Val::U(s.parent as u64)));
406            }
407            if s.cut {
408                args.push(("cut", Val::B(true)));
409            }
410            m.slices.push(Slice {
411                lane: lane_of(s.track),
412                start: s.start,
413                end: s.end(),
414                label: name.clone(),
415                name,
416                cat,
417                args,
418            });
419        }
420
421        // The loop: asleep as slices, the rest as instants.
422        let lp = lane_of(Track::LOOP);
423        let mut asleep: Option<(u64, Option<u64>)> = None;
424        let close = |m: &mut Model, at: u64, by: Option<&str>, sleep: (u64, Option<u64>)| {
425            let mut args = Vec::new();
426            if let Some(u) = sleep.1 {
427                args.push(("until_us", Val::U(u)));
428            }
429            if let Some(by) = by.filter(|b| !b.is_empty()) {
430                args.push(("woke_by", Val::Str(by.to_string())));
431            }
432            m.slices.push(Slice {
433                lane: lp,
434                start: sleep.0,
435                end: at.max(sleep.0),
436                label: "asleep".into(),
437                name: "asleep".into(),
438                cat: "loop".into(),
439                args,
440            });
441        };
442        for (t, e) in &tl.loops {
443            match e {
444                LoopEvent::Sleep { until } => {
445                    if let Some(s) = asleep.take() {
446                        close(&mut m, *t, None, s);
447                    }
448                    asleep = Some((*t, *until));
449                }
450                LoopEvent::Woke { by } => {
451                    if let Some(s) = asleep.take() {
452                        close(&mut m, *t, Some(trace.name(*by)), s);
453                    }
454                    m.instants.push(Instant {
455                        lane: lp,
456                        t: *t,
457                        name: named("woke", trace.name(*by)),
458                        cat: "loop",
459                        args: Vec::new(),
460                        global: false,
461                    });
462                }
463                LoopEvent::Os { kind } => m.instants.push(Instant {
464                    lane: lp,
465                    t: *t,
466                    name: named("os", trace.name(*kind)),
467                    cat: "loop",
468                    args: Vec::new(),
469                    global: false,
470                }),
471            }
472        }
473        if let Some(s) = asleep {
474            close(&mut m, tl.end, None, s);
475        }
476
477        // Instants.
478        for i in &tl.inputs {
479            let mut args = vec![("kind", Val::Str(trace.name(i.kind).into()))];
480            if i.count > 1 {
481                args.push(("count", Val::U(i.count as u64)));
482                args.push(("until_us", Val::U(i.until)));
483            }
484            if let Some(o) = i.os_t {
485                args.push(("os_us", Val::U(o)));
486            }
487            m.instants.push(Instant {
488                lane: MAIN,
489                t: i.t,
490                name: named("input", trace.name(i.kind)),
491                cat: "input",
492                args,
493                global: false,
494            });
495        }
496        for e in &tl.events {
497            let mut args = vec![("kind", Val::Str(trace.name(e.kind).into()))];
498            if e.node != 0 {
499                args.push(("node", Val::Str(hex(e.node))));
500            }
501            m.instants.push(Instant {
502                lane: MAIN,
503                t: e.t,
504                name: named("event", trace.name(e.kind)),
505                cat: "event",
506                args,
507                global: false,
508            });
509        }
510        for (t, n) in &tl.wakes {
511            m.instants.push(Instant {
512                lane: MAIN,
513                t: *t,
514                name: named("wake", trace.name(*n)),
515                cat: "wake",
516                args: Vec::new(),
517                global: false,
518            });
519        }
520        for r in &tl.requests {
521            let mut args = Vec::new();
522            if !trace.name(r.at).is_empty() {
523                args.push(("at", Val::Str(trace.name(r.at).into())));
524            }
525            m.instants.push(Instant {
526                lane: MAIN,
527                t: r.t,
528                name: named("request", trace.name(r.why)),
529                cat: "request",
530                args,
531                global: false,
532            });
533        }
534        for k in &tl.marks {
535            m.instants.push(Instant {
536                lane: lane_of(k.track),
537                t: k.t,
538                name: match trace.name(k.name) {
539                    "" => "mark".into(),
540                    n => n.into(),
541                },
542                cat: "mark",
543                args: Vec::new(),
544                global: false,
545            });
546        }
547        for p in &tl.placements {
548            if trace.name(p.answer) == crate::analysis::REPLAYED {
549                continue;
550            }
551            let mut args = vec![("node", Val::Str(format!("{:016x}", p.node)))];
552            if !trace.name(p.why).is_empty() {
553                args.push(("why", Val::Str(trace.name(p.why).into())));
554            }
555            m.instants.push(Instant {
556                lane: MAIN,
557                t: p.t,
558                name: format!("{} {}", trace.name(p.component), trace.name(p.answer)),
559                cat: "placement",
560                args,
561                global: false,
562            });
563        }
564        for (t, text) in tl.notes.clone() {
565            m.instants.push(Instant {
566                lane: FRAMES,
567                t,
568                name: "note".into(),
569                cat: "note",
570                args: vec![("text", Val::Str(text))],
571                global: true,
572            });
573        }
574        m.instants.sort_by_key(|i| i.t);
575
576        // Series: a counter is a frame's total, written only for a frame
577        // it moved in, so it is drawn as a step over its frame — its value
578        // from the frame's begin, back to 0 at its end — or a viewer that
579        // holds a counter's last value (all of them) shows one busy frame
580        // as busy ever after.
581        let span_of: std::collections::HashMap<u64, (u64, u64)> = tl
582            .frames
583            .iter()
584            .map(|f| (f.frame, (f.begin, f.end_or_begin())))
585            .collect();
586        for r in &trace.records {
587            if let Record::Counter {
588                t,
589                frame,
590                name,
591                value,
592            } = r
593            {
594                let (begin, end) = span_of.get(frame).copied().unwrap_or((*t, *t));
595                let name = trace.name(*name);
596                m.point(name, false, begin, Val::I(*value));
597                m.point(name, false, end.max(begin + 1), Val::I(0));
598            }
599        }
600        for g in &tl.gauges {
601            m.point(trace.name(g.name), true, g.t, Val::F(g.value));
602        }
603        for s in &mut m.series {
604            s.points.sort_by_key(|p| p.0);
605        }
606
607        m.nest();
608        m
609    }
610
611    fn point(&mut self, name: &str, gauge: bool, t: u64, v: Val) {
612        let name = if name.is_empty() { "(unnamed)" } else { name };
613        let i = match self
614            .series
615            .iter()
616            .position(|s| s.name == name && s.gauge == gauge)
617        {
618            Some(i) => i,
619            None => {
620                self.series.push(Series {
621                    name: name.into(),
622                    gauge,
623                    points: Vec::new(),
624                });
625                self.series.len() - 1
626            }
627        };
628        self.series[i].points.push((t, v));
629    }
630
631    /// Sorts the slices by lane and start, a parent first, and cuts each to
632    /// the slice it starts inside, so a lane's slices nest.
633    fn nest(&mut self) {
634        let mut idx: Vec<usize> = (0..self.slices.len()).collect();
635        idx.sort_by_key(|&i| {
636            let s = &self.slices[i];
637            (s.lane, s.start, Reverse(s.end), i)
638        });
639        let mut sorted: Vec<Slice> = idx.into_iter().map(|i| self.slices[i].clone()).collect();
640        let mut stack: Vec<(usize, u64)> = Vec::new();
641        for s in &mut sorted {
642            while let Some(&(lane, end)) = stack.last() {
643                if lane != s.lane || end <= s.start {
644                    stack.pop();
645                } else {
646                    break;
647                }
648            }
649            if let Some(&(_, end)) = stack.last() {
650                s.end = s.end.min(end);
651            }
652            stack.push((s.lane, s.end));
653        }
654        // A cut can leave equal starts out of order by end.
655        sorted.sort_by_key(|s| (s.lane, s.start, Reverse(s.end)));
656        self.slices = sorted;
657    }
658
659    /// The slices of `lane`, as opens and closes in order: each close
660    /// before what opens at its time, a child's before its parent's.
661    pub fn edges(&self, lane: usize) -> Vec<Edge> {
662        let mut out = Vec::new();
663        let mut stack: Vec<usize> = Vec::new();
664        for (i, s) in self.slices.iter().enumerate() {
665            if s.lane != lane {
666                continue;
667            }
668            while let Some(&top) = stack.last() {
669                if self.slices[top].end <= s.start {
670                    out.push((self.slices[top].end, false, top));
671                    stack.pop();
672                } else {
673                    break;
674                }
675            }
676            out.push((s.start, true, i));
677            stack.push(i);
678        }
679        while let Some(top) = stack.pop() {
680            out.push((self.slices[top].end, false, top));
681        }
682        out
683    }
684
685    /// Whether `lane` has a slice.
686    pub fn has_slices(&self, lane: usize) -> bool {
687        self.slices.iter().any(|s| s.lane == lane)
688    }
689}