Skip to main content

kui_trace/
analysis.rs

1//! What a timeline says, read the same way by every reader (ADR 0058,
2//! decisions 9 and 10): a frame's phases, the path from an input to the
3//! screen, a verdict on why it lagged, the runs of frames with no input,
4//! and the recording grouped by span, by origin and by what its gaps held.
5
6use std::collections::HashMap;
7
8use crate::model::{Frame, Input, Timeline};
9use crate::record::{LoopEvent, NameId, Origin, Span, Track};
10
11/// The core's own spans (origin [`Origin::Kui`]) and the drivers'.
12pub const BUILD: &str = "build";
13pub const LAYOUT: &str = "layout";
14pub const EMIT: &str = "emit";
15pub const DEVTOOLS: &str = "devtools";
16/// A driver's call into the app's event handler (origin [`Origin::Host`]).
17pub const HANDLER: &str = "handler";
18/// An extension's fill is `fill <namespace>`.
19pub const FILL: &str = "fill ";
20/// A placement's answer when its last fill was pushed again.
21pub const REPLAYED: &str = "replayed";
22
23/// The frame-cause kinds that are input: a run of frames with none of
24/// them is a burn.
25pub const INPUT_CAUSES: [&str; 12] = [
26    "pointerMove",
27    "pointerLeave",
28    "button",
29    "wheel",
30    "key",
31    "modifiers",
32    "text",
33    "preedit",
34    "access",
35    "fileDrag",
36    "files",
37    "menu",
38];
39
40/// A burn: at least this many frames…
41pub const BURN_FRAMES: usize = 30;
42/// …each within this of the one before (µs), with no input among them.
43pub const BURN_GAP_US: u64 = 100_000;
44
45/// Something that turns a name id into its string: a [`crate::Trace`]
46/// or a writer's [`crate::Names`].
47pub trait NameOf {
48    fn name_of(&self, id: NameId) -> &str;
49}
50
51impl NameOf for crate::model::Trace {
52    fn name_of(&self, id: NameId) -> &str {
53        self.name(id)
54    }
55}
56
57impl NameOf for crate::model::Names {
58    fn name_of(&self, id: NameId) -> &str {
59        self.text(id)
60    }
61}
62
63/// A frame's time by phase, µs.
64#[derive(Clone, Copy, Debug, Default, PartialEq)]
65pub struct Phases {
66    /// The app's handlers in the frame's gap.
67    pub handler: u64,
68    pub build: u64,
69    pub layout: u64,
70    pub emit: u64,
71    pub devtools: u64,
72    /// The driver's.
73    pub render: u64,
74    pub wait: u64,
75}
76
77impl Phases {
78    /// `(name, µs)`, in the order a frame runs them.
79    pub fn named(&self) -> [(&'static str, u64); 7] {
80        [
81            (HANDLER, self.handler),
82            (BUILD, self.build),
83            (DEVTOOLS, self.devtools),
84            (LAYOUT, self.layout),
85            (EMIT, self.emit),
86            ("render", self.render),
87            ("vsync wait", self.wait),
88        ]
89    }
90
91    /// Everything but vsync's wait.
92    pub fn work(&self) -> u64 {
93        self.handler + self.build + self.devtools + self.layout + self.emit + self.render
94    }
95}
96
97/// Frame `i`'s phases: the core's spans inside it, the handlers in its
98/// gap, the driver's present.
99pub fn phases(tl: &Timeline, i: usize, names: &impl NameOf) -> Phases {
100    let f = &tl.frames[i];
101    let mut p = Phases::default();
102    for s in tl.spans_in(f.gap_from, f.end_or_begin().max(f.begin + 1)) {
103        if s.track != Track::MAIN {
104            continue;
105        }
106        let n = names.name_of(s.name);
107        match (s.origin, n) {
108            (Origin::Kui, BUILD) => p.build += s.dur,
109            (Origin::Kui, LAYOUT) => p.layout += s.dur,
110            (Origin::Kui, EMIT) => p.emit += s.dur,
111            (Origin::Kui, DEVTOOLS) => p.devtools += s.dur,
112            (_, HANDLER) if s.start < f.begin => p.handler += s.dur,
113            _ => {}
114        }
115    }
116    if let Some(pr) = f.present {
117        p.render = pr.render;
118        p.wait = pr.wait;
119    }
120    p
121}
122
123/// The path from the first input frame `i` answers to the driver's
124/// present record, in timeline µs. This does not measure physical display.
125#[derive(Clone, Copy, Debug, PartialEq)]
126pub struct InputPath {
127    pub input: Input,
128    /// The first handler span after the input, before the frame.
129    pub handler: Option<(u64, u64)>,
130    pub begin: u64,
131    pub end: Option<u64>,
132    pub present: Option<u64>,
133}
134
135impl InputPath {
136    pub fn endpoint(&self) -> &'static str {
137        if self.present.is_some() {
138            "present_call"
139        } else if self.end.is_some() {
140            "core_end"
141        } else {
142            "frame_begin"
143        }
144    }
145
146    /// Each leg: `(what, µs)` — waiting for a handler, in it, waiting for
147    /// the frame, the frame, then the driver's present record.
148    pub fn legs(&self) -> Vec<(&'static str, u64)> {
149        let mut v = Vec::new();
150        let mut at = self.input.t;
151        if let Some((a, b)) = self.handler {
152            v.push(("to handler", a.saturating_sub(at)));
153            v.push(("handler", b.saturating_sub(a)));
154            at = b;
155        }
156        v.push(("to frame", self.begin.saturating_sub(at)));
157        if let Some(e) = self.end {
158            v.push(("frame", e.saturating_sub(self.begin)));
159            if let Some(p) = self.present {
160                v.push(("to present call", p.saturating_sub(e)));
161            }
162        }
163        v
164    }
165
166    /// From input to the driver's present record, or the last known core endpoint.
167    pub fn total(&self) -> u64 {
168        self.present
169            .or(self.end)
170            .unwrap_or(self.begin)
171            .saturating_sub(self.input.t)
172    }
173}
174
175/// Frame `i`'s input path, when an input arrived in its gap.
176pub fn input_path(tl: &Timeline, i: usize, names: &impl NameOf) -> Option<InputPath> {
177    let f = &tl.frames[i];
178    let input = *tl
179        .inputs_in(f.gap_from, f.begin.max(f.gap_from + 1))
180        .first()?;
181    let handler = tl
182        .spans_in(input.t, f.begin)
183        .iter()
184        .find(|s| names.name_of(s.name) == HANDLER)
185        .map(|s| (s.start, s.end()));
186    Some(InputPath {
187        input,
188        handler,
189        begin: f.begin,
190        end: f.end,
191        present: f.present.map(|p| p.t),
192    })
193}
194
195/// A run of frames with no input: indices into the timeline's frames.
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub struct Burn {
198    pub first: usize,
199    pub last: usize,
200}
201
202impl Burn {
203    pub fn len(&self) -> usize {
204        self.last - self.first + 1
205    }
206
207    pub fn is_empty(&self) -> bool {
208        false
209    }
210}
211
212fn has_input(tl: &Timeline, f: &Frame, names: &impl NameOf) -> bool {
213    f.causes
214        .iter()
215        .any(|c| INPUT_CAUSES.contains(&names.name_of(*c)))
216        || !tl
217            .inputs_in(f.gap_from, f.begin.max(f.gap_from + 1))
218            .is_empty()
219}
220
221/// Every burn: [`BURN_FRAMES`] frames or more, each beginning within
222/// [`BURN_GAP_US`] of the one before, none answering input.
223pub fn burns(tl: &Timeline, names: &impl NameOf) -> Vec<Burn> {
224    let mut out = Vec::new();
225    let mut run: Option<usize> = None;
226    let close = |run: Option<usize>, last: usize, out: &mut Vec<Burn>| {
227        if let Some(first) = run
228            && last + 1 - first >= BURN_FRAMES
229        {
230            out.push(Burn { first, last });
231        }
232    };
233    for (i, f) in tl.frames.iter().enumerate() {
234        let quiet = !has_input(tl, f, names);
235        let near = i > 0 && f.begin.saturating_sub(tl.frames[i - 1].begin) <= BURN_GAP_US;
236        match (run, quiet) {
237            (Some(_), true) if near => {}
238            (_, true) => {
239                if let Some(first) = run {
240                    close(Some(first), i - 1, &mut out);
241                }
242                run = Some(i);
243            }
244            (_, false) => {
245                if run.is_some() {
246                    close(run, i - 1, &mut out);
247                }
248                run = None;
249            }
250        }
251    }
252    if run.is_some() && !tl.frames.is_empty() {
253        close(run, tl.frames.len() - 1, &mut out);
254    }
255    out
256}
257
258/// Why a frame lagged, the first that holds (ADR 0058, decision 9).
259#[derive(Clone, Debug, PartialEq)]
260pub enum Verdict {
261    /// An input waited this long for its frame; the loop slept through
262    /// it, or was busy.
263    Late { wait: u64, slept: bool },
264    /// A gap this long with no sleep recorded: the thread was busy
265    /// outside kui.
266    Blocked { busy: u64 },
267    /// Over the budget: the slowest phase, and the span with the most
268    /// self time in the frame.
269    Slow {
270        work: u64,
271        phase: &'static str,
272        phase_us: u64,
273        span: Option<(NameId, u64)>,
274    },
275    /// The frame's wall time far over its CPU time.
276    Waited { wall: u64, cpu: u64 },
277    /// It drew what the frame before drew.
278    Wasted,
279    /// Frame `k` (from 1) of a run of `of` with no input.
280    Burn { k: usize, of: usize },
281    /// Within the budget: its work as a share of it.
282    Fine { share: f32 },
283}
284
285/// Frame `i`'s verdict against `budget` µs; `burns` from [`burns`].
286pub fn verdict(
287    tl: &Timeline,
288    i: usize,
289    budget: u64,
290    burns: &[Burn],
291    names: &impl NameOf,
292) -> Verdict {
293    let f = &tl.frames[i];
294    let budget = budget.max(1);
295    if let Some(path) = input_path(tl, i, names) {
296        let wait = f.begin.saturating_sub(path.input.t);
297        if wait > budget {
298            let slept = tl
299                .loops_in(path.input.t, f.begin)
300                .iter()
301                .any(|(_, e)| matches!(e, LoopEvent::Sleep { .. }));
302            return Verdict::Late { wait, slept };
303        }
304    }
305    if tl.has_loop() && i > 0 {
306        let gap = f.begin.saturating_sub(f.gap_from);
307        let slept = tl
308            .loops_in(f.gap_from, f.begin)
309            .iter()
310            .any(|(_, e)| matches!(e, LoopEvent::Sleep { .. }));
311        if !slept && gap > budget * 2 {
312            let handled: u64 = tl
313                .spans_in(f.gap_from, f.begin)
314                .iter()
315                .filter(|s| s.parent == 0 && s.track == Track::MAIN)
316                .map(|s| s.dur)
317                .sum();
318            return Verdict::Blocked {
319                busy: gap.saturating_sub(handled),
320            };
321        }
322    }
323    let p = phases(tl, i, names);
324    let work = f.work_us();
325    if work > budget {
326        let (phase, phase_us) = p.named()[1..6]
327            .iter()
328            .copied()
329            .max_by_key(|(_, us)| *us)
330            .unwrap_or(("", 0));
331        let span = tl
332            .spans_in(f.begin, f.end_or_begin().max(f.begin + 1))
333            .iter()
334            .filter(|s| s.track == Track::MAIN)
335            .map(|s| (s.name, tl.self_us(s)))
336            .max_by_key(|(_, us)| *us);
337        return Verdict::Slow {
338            work,
339            phase,
340            phase_us,
341            span,
342        };
343    }
344    let (wall, cpu) = tl
345        .spans_in(f.begin, f.end_or_begin().max(f.begin + 1))
346        .iter()
347        .filter(|s| s.parent == 0 && s.track == Track::MAIN)
348        .fold((0u64, None::<u64>), |(w, c), s| {
349            (
350                w + s.dur,
351                match (c, s.cpu) {
352                    (None, Some(x)) => Some(x),
353                    (Some(a), Some(x)) => Some(a + x),
354                    (c, None) => c,
355                },
356            )
357        });
358    if let Some(cpu) = cpu
359        && wall > cpu + 2_000
360        && wall - cpu > wall / 3
361    {
362        return Verdict::Waited { wall, cpu };
363    }
364    if f.unchanged == Some(true) {
365        return Verdict::Wasted;
366    }
367    if let Some(b) = burns.iter().find(|b| (b.first..=b.last).contains(&i)) {
368        return Verdict::Burn {
369            k: i - b.first + 1,
370            of: b.len(),
371        };
372    }
373    Verdict::Fine {
374        share: work as f32 / budget as f32,
375    }
376}
377
378/// A span name's spread over frames: calls, total, self, worst, and CPU
379/// where read.
380#[derive(Clone, Debug, PartialEq)]
381pub struct SpanStat {
382    pub name: NameId,
383    pub origin: Origin,
384    pub calls: u64,
385    pub total: u64,
386    pub self_total: u64,
387    pub worst: u64,
388    pub cpu: Option<u64>,
389}
390
391/// Every span of the timeline, grouped by name and origin, the most total
392/// first.
393pub fn by_span(tl: &Timeline, spans: &[Span]) -> Vec<SpanStat> {
394    let mut map: HashMap<(NameId, Origin), SpanStat> = HashMap::new();
395    for s in spans {
396        let st = map.entry((s.name, s.origin)).or_insert(SpanStat {
397            name: s.name,
398            origin: s.origin,
399            calls: 0,
400            total: 0,
401            self_total: 0,
402            worst: 0,
403            cpu: None,
404        });
405        st.calls += 1;
406        st.total += s.dur;
407        st.self_total += tl.self_us(s);
408        st.worst = st.worst.max(s.dur);
409        if let Some(c) = s.cpu {
410            st.cpu = Some(st.cpu.unwrap_or(0) + c);
411        }
412    }
413    let mut v: Vec<SpanStat> = map.into_values().collect();
414    v.sort_by(|a, b| b.total.cmp(&a.total).then(a.name.cmp(&b.name)));
415    v
416}
417
418/// Self time by origin — the host, each extension, kui — the most first.
419pub fn by_origin(tl: &Timeline, spans: &[Span]) -> Vec<(Origin, u64, u64)> {
420    let mut map: HashMap<Origin, (u64, u64)> = HashMap::new();
421    for s in spans {
422        let e = map.entry(s.origin).or_default();
423        e.0 += tl.self_us(s);
424        e.1 += 1;
425    }
426    let mut v: Vec<(Origin, u64, u64)> = map.into_iter().map(|(o, (t, n))| (o, t, n)).collect();
427    v.sort_by_key(|o| std::cmp::Reverse(o.1));
428    v
429}
430
431/// What the gaps between frames held, summed over the timeline, µs.
432#[derive(Clone, Copy, Debug, Default, PartialEq)]
433pub struct Gaps {
434    /// The whole of every gap.
435    pub total: u64,
436    /// In the app's handlers.
437    pub handlers: u64,
438    /// The loop asleep.
439    pub asleep: u64,
440    /// The rest, where a driver recorded its loop: busy outside kui.
441    pub busy: u64,
442}
443
444pub fn gaps(tl: &Timeline, names: &impl NameOf) -> Gaps {
445    gaps_in(tl, names, 1..tl.frames.len())
446}
447
448/// Gaps preceding selected original frame indices. A sliced population keeps
449/// the original preceding gap, rather than treating its first frame as frame 0.
450pub fn gaps_in(
451    tl: &Timeline,
452    names: &impl NameOf,
453    indices: impl IntoIterator<Item = usize>,
454) -> Gaps {
455    let mut g = Gaps::default();
456    for i in indices {
457        if i == 0 {
458            continue;
459        }
460        let f = &tl.frames[i];
461        let (a, b) = (f.gap_from, f.begin);
462        g.total += b.saturating_sub(a);
463        g.handlers += tl
464            .spans_in(a, b)
465            .iter()
466            .filter(|s| names.name_of(s.name) == HANDLER)
467            .map(|s| s.dur.min(b.saturating_sub(s.start)))
468            .sum::<u64>();
469        let mut sleep_at: Option<u64> = None;
470        for (t, e) in tl.loops_in(a, b) {
471            match e {
472                LoopEvent::Sleep { .. } => sleep_at = Some(*t),
473                LoopEvent::Woke { .. } => {
474                    if let Some(s) = sleep_at.take() {
475                        g.asleep += t - s;
476                    }
477                }
478                LoopEvent::Os { .. } => {}
479            }
480        }
481        if let Some(s) = sleep_at {
482            g.asleep += b - s;
483        }
484    }
485    if tl.has_loop() {
486        g.busy = g.total.saturating_sub(g.handlers + g.asleep);
487    }
488    g
489}
490
491/// `(avg, p95, max)` of `v`; zeros for none.
492pub fn spread(v: &[u64]) -> (f64, u64, u64) {
493    if v.is_empty() {
494        return (0.0, 0, 0);
495    }
496    let mut s = v.to_vec();
497    s.sort_unstable();
498    let avg = s.iter().sum::<u64>() as f64 / s.len() as f64;
499    let p95 = s[((s.len() - 1) as f64 * 0.95).round() as usize];
500    (avg, p95, s[s.len() - 1])
501}
502
503/// Each counter's average a frame and its most, by name. A frame that
504/// recorded no value for a counter counts as 0 — the core writes a
505/// counter only where it moved — so a glyph rasterized once in a hundred
506/// frames averages 0.01, not 1.
507pub fn counters(tl: &Timeline) -> Vec<(NameId, f64, i64)> {
508    let mut map: HashMap<NameId, (i64, i64)> = HashMap::new();
509    for f in &tl.frames {
510        for (n, v) in &f.counters {
511            let e = map.entry(*n).or_insert((0, i64::MIN));
512            e.0 += v;
513            e.1 = e.1.max(*v);
514        }
515    }
516    let frames = tl.frames.len().max(1) as f64;
517    let mut out: Vec<(NameId, f64, i64)> = map
518        .into_iter()
519        .map(|(n, (sum, max))| (n, sum as f64 / frames, max))
520        .collect();
521    out.sort_by_key(|c| c.0);
522    out
523}
524
525/// Each gauge's last, least and most, by name.
526pub fn gauges(tl: &Timeline) -> Vec<(NameId, f64, f64, f64)> {
527    let mut map: HashMap<NameId, (f64, f64, f64)> = HashMap::new();
528    for g in &tl.gauges {
529        let e = map.entry(g.name).or_insert((g.value, g.value, g.value));
530        e.0 = g.value;
531        e.1 = e.1.min(g.value);
532        e.2 = e.2.max(g.value);
533    }
534    let mut out: Vec<(NameId, f64, f64, f64)> = map
535        .into_iter()
536        .map(|(n, (l, lo, hi))| (n, l, lo, hi))
537        .collect();
538    out.sort_by_key(|g| g.0);
539    out
540}
541
542/// A component kind's placements over the timeline: how many, how many
543/// replayed, and the rest by answer and reason, the most first.
544#[derive(Clone, Debug, PartialEq)]
545pub struct PlacementStat {
546    pub component: NameId,
547    pub placed: u64,
548    pub replayed: u64,
549    /// `(answer, why, count)` for each way it was drawn fresh.
550    pub fresh: Vec<(NameId, NameId, u64)>,
551}
552
553impl PlacementStat {
554    /// Drawn fresh: placed and not replayed.
555    pub fn drawn(&self) -> u64 {
556        self.placed - self.replayed
557    }
558}
559
560/// Every placement of the timeline, grouped by component kind, the most
561/// drawn fresh first.
562pub fn placements(tl: &Timeline, names: &impl NameOf) -> Vec<PlacementStat> {
563    /// Placed, replayed, and drawn fresh by `(answer, why)`.
564    type Tally = (u64, u64, HashMap<(NameId, NameId), u64>);
565    let mut map: HashMap<NameId, Tally> = HashMap::new();
566    for p in &tl.placements {
567        let e = map.entry(p.component).or_default();
568        e.0 += 1;
569        if names.name_of(p.answer) == REPLAYED {
570            e.1 += 1;
571        } else {
572            *e.2.entry((p.answer, p.why)).or_default() += 1;
573        }
574    }
575    let mut v: Vec<PlacementStat> = map
576        .into_iter()
577        .map(|(component, (placed, replayed, fresh))| {
578            let mut fresh: Vec<(NameId, NameId, u64)> =
579                fresh.into_iter().map(|((a, w), n)| (a, w, n)).collect();
580            fresh.sort_by(|a, b| b.2.cmp(&a.2).then((a.0, a.1).cmp(&(b.0, b.1))));
581            PlacementStat {
582                component,
583                placed,
584                replayed,
585                fresh,
586            }
587        })
588        .collect();
589    v.sort_by(|a, b| {
590        b.drawn()
591            .cmp(&a.drawn())
592            .then(a.component.cmp(&b.component))
593    });
594    v
595}