1use std::collections::HashMap;
7
8use crate::model::{Frame, Input, Timeline};
9use crate::record::{LoopEvent, NameId, Origin, Span, Track};
10
11pub const BUILD: &str = "build";
13pub const LAYOUT: &str = "layout";
14pub const EMIT: &str = "emit";
15pub const DEVTOOLS: &str = "devtools";
16pub const HANDLER: &str = "handler";
18pub const FILL: &str = "fill ";
20pub const REPLAYED: &str = "replayed";
22
23pub 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
40pub const BURN_FRAMES: usize = 30;
42pub const BURN_GAP_US: u64 = 100_000;
44
45pub 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
65pub struct Phases {
66 pub handler: u64,
68 pub build: u64,
69 pub layout: u64,
70 pub emit: u64,
71 pub devtools: u64,
72 pub render: u64,
74 pub wait: u64,
75}
76
77impl Phases {
78 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 pub fn work(&self) -> u64 {
93 self.handler + self.build + self.devtools + self.layout + self.emit + self.render
94 }
95}
96
97pub 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#[derive(Clone, Copy, Debug, PartialEq)]
126pub struct InputPath {
127 pub input: Input,
128 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 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 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
175pub 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#[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
221pub 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#[derive(Clone, Debug, PartialEq)]
260pub enum Verdict {
261 Late { wait: u64, slept: bool },
264 Blocked { busy: u64 },
267 Slow {
270 work: u64,
271 phase: &'static str,
272 phase_us: u64,
273 span: Option<(NameId, u64)>,
274 },
275 Waited { wall: u64, cpu: u64 },
277 Wasted,
279 Burn { k: usize, of: usize },
281 Fine { share: f32 },
283}
284
285pub 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#[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
391pub 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
418pub 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
433pub struct Gaps {
434 pub total: u64,
436 pub handlers: u64,
438 pub asleep: u64,
440 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
448pub 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
491pub 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
503pub 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
525pub 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#[derive(Clone, Debug, PartialEq)]
545pub struct PlacementStat {
546 pub component: NameId,
547 pub placed: u64,
548 pub replayed: u64,
549 pub fresh: Vec<(NameId, NameId, u64)>,
551}
552
553impl PlacementStat {
554 pub fn drawn(&self) -> u64 {
556 self.placed - self.replayed
557 }
558}
559
560pub fn placements(tl: &Timeline, names: &impl NameOf) -> Vec<PlacementStat> {
563 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}