1use std::collections::{BTreeMap, HashMap, HashSet};
6
7use crate::analysis::{self, Burn, InputPath, Phases, Verdict};
8use crate::{Frame, NameId, Origin, Placement, Span, Timeline, Trace};
9
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
11pub enum Selection {
12 #[default]
13 Whole,
14 Frames { first: u64, last: u64 },
16 Time { from_us: u64, to_us: u64 },
18}
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct Filter {
22 pub name: Option<String>,
23 pub origins: Option<Vec<String>>,
24 pub hide_names: Vec<String>,
25 pub min_us: u64,
26 pub verdicts: Option<Vec<String>>,
27 pub hide_unchanged: bool,
28 pub span_ids: Option<Vec<u32>>,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum Operation {
34 Summary,
35 Frames,
36 Frame(u64),
37 Spans,
38 Span(u32),
39 Counters,
40 Marks,
41}
42
43impl Operation {
44 pub fn name(self) -> &'static str {
45 match self {
46 Self::Summary => "summary",
47 Self::Frames => "frames",
48 Self::Frame(_) => "frame",
49 Self::Spans => "spans",
50 Self::Span(_) => "span",
51 Self::Counters => "counters",
52 Self::Marks => "marks",
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct Query {
59 pub operation: Operation,
60 pub selection: Selection,
61 pub filter: Filter,
62 pub sort: String,
63 pub descending: bool,
64 pub limit: Option<usize>,
66}
67
68impl Query {
69 pub fn new(operation: Operation) -> Self {
70 Self {
71 operation,
72 selection: Selection::Whole,
73 filter: Filter::default(),
74 sort: match operation {
75 Operation::Frames => "work",
76 Operation::Spans => "self",
77 Operation::Marks => "start",
78 _ => "name",
79 }
80 .into(),
81 descending: matches!(operation, Operation::Frames | Operation::Spans),
82 limit: matches!(
83 operation,
84 Operation::Frames | Operation::Spans | Operation::Counters | Operation::Marks
85 )
86 .then_some(20),
87 }
88 }
89
90 pub fn set_sort(&mut self, sort: impl Into<String>) {
93 self.sort = sort.into();
94 self.descending = matches!(
95 self.sort.as_str(),
96 "work" | "self" | "total" | "worst" | "calls"
97 );
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub struct Scope {
103 pub requested: Selection,
104 pub indices: Vec<usize>,
106 pub intervals: Vec<(u64, u64)>,
109}
110
111impl Scope {
112 pub fn contains(&self, t: u64) -> bool {
113 let i = self.intervals.partition_point(|(a, _)| *a <= t);
114 i > 0 && t < self.intervals[i - 1].1
115 }
116 pub fn duration_us(&self) -> u64 {
117 self.intervals
118 .iter()
119 .fold(0u64, |s, (a, b)| s.saturating_add(b.saturating_sub(*a)))
120 }
121}
122
123#[derive(Clone, Debug, Default, PartialEq)]
124pub struct Distribution {
125 pub samples: usize,
126 pub avg_us: Option<f64>,
127 pub p50_us: Option<u64>,
128 pub p95_us: Option<u64>,
129 pub p99_us: Option<u64>,
130 pub max_us: Option<u64>,
131}
132
133impl Distribution {
134 fn of(values: &[u64]) -> Self {
135 if values.is_empty() {
136 return Self::default();
137 }
138 let mut sorted = values.to_vec();
139 sorted.sort_unstable();
140 let percentile = |q: f64| sorted[((sorted.len() - 1) as f64 * q).round() as usize];
142 let max = *sorted.last().unwrap();
143 Self {
144 samples: values.len(),
145 avg_us: Some(values.iter().map(|&v| v as f64).sum::<f64>() / values.len() as f64),
146 p50_us: Some(percentile(0.50)),
147 p95_us: Some(percentile(0.95)),
148 p99_us: Some(percentile(0.99)),
149 max_us: Some(max),
150 }
151 }
152}
153
154#[derive(Clone, Debug, PartialEq)]
155pub struct Summary {
156 pub frames: usize,
157 pub complete_frames: usize,
158 pub duration_us: u64,
159 pub budget_us: Option<u64>,
160 pub work: Distribution,
161 pub pacing: Distribution,
162 pub phases: Vec<(&'static str, Distribution)>,
163 pub over_budget: Option<usize>,
164 pub unchanged: usize,
165}
166
167impl Summary {
168 pub fn over_budget_percent(&self) -> Option<f64> {
170 self.over_budget
171 .filter(|_| self.complete_frames > 0)
172 .map(|n| 100.0 * n as f64 / self.complete_frames as f64)
173 }
174}
175
176#[derive(Clone, Debug, PartialEq)]
177pub struct FrameDetail {
178 pub index: usize,
179 pub frame: Frame,
180 pub phases: Phases,
181 pub verdict: Option<Verdict>,
182 pub path: Option<InputPath>,
183 pub fresh: Vec<Placement>,
184 pub notes: Vec<String>,
185}
186
187#[derive(Clone, Debug, PartialEq)]
188pub struct SpanDetail {
189 pub span: Span,
190 pub name: NameId,
191 pub self_us: u64,
192 pub frame: Option<u64>,
193 pub depth: u32,
194 pub placement: Option<Placement>,
195 pub children: Vec<u32>,
196}
197
198#[derive(Clone, Debug, PartialEq)]
199pub struct SpanGroup {
200 pub name: NameId,
201 pub origin: Origin,
202 pub calls: u64,
203 pub total_us: u64,
204 pub self_us: u64,
205 pub worst_us: u64,
206 pub cpu_us: Option<u64>,
207 pub example_id: u32,
208}
209
210#[derive(Clone, Debug, PartialEq)]
211pub struct Counter {
212 pub name: NameId,
213 pub avg: f64,
214 pub max: i64,
215}
216
217#[derive(Clone, Debug, PartialEq)]
218pub struct Gauge {
219 pub name: NameId,
220 pub last: f64,
221 pub least: f64,
222 pub most: f64,
223}
224
225#[derive(Clone, Debug, PartialEq)]
226pub enum Data {
227 Summary(Summary),
228 Frames(Vec<FrameDetail>),
229 Frame(Box<FrameDetail>),
230 Spans(Vec<SpanGroup>),
231 Span(SpanDetail),
232 Counters(Vec<Counter>, Vec<Gauge>),
233 Marks(Vec<crate::Mark>),
234}
235
236#[derive(Clone, Debug, PartialEq)]
237pub struct ResultSet {
238 pub query: Query,
239 pub scope: Scope,
240 pub matched: usize,
241 pub returned: usize,
242 pub data: Data,
243}
244
245#[derive(Clone, Debug, PartialEq)]
246pub struct Readings {
247 pub summary: Summary,
248 pub spans: Vec<SpanGroup>,
249 pub origins: Vec<(Origin, u64, u64)>,
250 pub components: Vec<analysis::PlacementStat>,
251 pub counters: Vec<Counter>,
252 pub gauges: Vec<Gauge>,
253 pub burns: Vec<Burn>,
255 pub gaps: analysis::Gaps,
256}
257
258#[derive(Clone, Debug, PartialEq)]
259pub struct Delta {
260 pub metric: String,
261 pub unit: &'static str,
262 pub before: Option<f64>,
263 pub after: Option<f64>,
264 pub delta: Option<f64>,
265 pub percent: Option<f64>,
266}
267
268#[derive(Clone, Debug, PartialEq)]
269pub struct Comparison {
270 pub before: ResultSet,
271 pub after: ResultSet,
272 pub rows: Vec<Delta>,
273 pub matched: usize,
274 pub limit: Option<usize>,
275}
276
277pub fn compare(
280 before: &Explorer<'_>,
281 bq: &Query,
282 after: &Explorer<'_>,
283 aq: &Query,
284 limit: Option<usize>,
285) -> Result<Comparison, String> {
286 if bq.operation != Operation::Summary || aq.operation != Operation::Summary {
287 return Err("comparison inputs must be summary queries".into());
288 }
289 fn metrics(ex: &Explorer<'_>, r: &ResultSet) -> Vec<(String, &'static str, Option<f64>)> {
290 let Data::Summary(s) = &r.data else {
291 unreachable!()
292 };
293 let mut v = vec![
294 ("duration".into(), "ms", Some(s.duration_us as f64 / 1000.0)),
295 ("frames".into(), "count", Some(s.frames as f64)),
296 (
297 "complete_frames".into(),
298 "count",
299 Some(s.complete_frames as f64),
300 ),
301 ("unchanged".into(), "count", Some(s.unchanged as f64)),
302 ("over_budget_percent".into(), "%", s.over_budget_percent()),
303 (
304 "over_budget".into(),
305 "count",
306 s.over_budget.map(|n| n as f64),
307 ),
308 ];
309 for (n, d) in [("work", &s.work), ("pacing", &s.pacing)]
310 .into_iter()
311 .chain(s.phases.iter().map(|(n, d)| (*n, d)))
312 {
313 v.push((format!("{n}.avg"), "ms", d.avg_us.map(|t| t / 1000.0)));
314 for (field, value) in [
315 ("p50", d.p50_us),
316 ("p95", d.p95_us),
317 ("p99", d.p99_us),
318 ("max", d.max_us),
319 ] {
320 v.push((
321 format!("{n}.{field}"),
322 "ms",
323 value.map(|t| t as f64 / 1000.0),
324 ));
325 }
326 }
327 let frames = s.frames as f64;
328 let mut groups = ex.span_groups(&r.scope, &r.query.filter);
329 groups.sort_by(|a, b| {
330 ex.trace
331 .name(a.name)
332 .cmp(ex.trace.name(b.name))
333 .then_with(|| origin_name(ex.trace, a.origin).cmp(origin_name(ex.trace, b.origin)))
334 });
335 for g in groups {
336 let name = ex.trace.name(g.name);
338 let origin = origin_name(ex.trace, g.origin);
339 let n = format!("span[{name:?},{origin:?}]");
340 for (field, unit, value) in [
341 ("calls", "count", Some(g.calls as f64)),
342 ("total", "ms", Some(g.total_us as f64 / 1000.0)),
343 ("self", "ms", Some(g.self_us as f64 / 1000.0)),
344 ("worst", "ms", Some(g.worst_us as f64 / 1000.0)),
345 (
346 "calls_per_frame",
347 "count/frame",
348 (frames > 0.0).then_some(g.calls as f64 / frames),
349 ),
350 (
351 "total_per_frame",
352 "ms/frame",
353 (frames > 0.0).then_some(g.total_us as f64 / 1000.0 / frames),
354 ),
355 (
356 "self_per_frame",
357 "ms/frame",
358 (frames > 0.0).then_some(g.self_us as f64 / 1000.0 / frames),
359 ),
360 ] {
361 v.push((format!("{n}.{field}"), unit, value));
362 }
363 }
364 let (cs, gs) = ex.counters(&r.scope);
365 for c in cs {
366 v.push((
367 format!("counter[{}].avg", ex.trace.name(c.name)),
368 "count/frame",
369 Some(c.avg),
370 ));
371 }
372 for g in gs {
373 v.push((
374 format!("gauge[{}].last", ex.trace.name(g.name)),
375 "value",
376 Some(g.last),
377 ));
378 }
379 v
380 }
381 let b = before.execute(bq)?;
382 let a = after.execute(aq)?;
383 let bm = metrics(before, &b);
384 let am = metrics(after, &a);
385 let mut order: Vec<_> = bm.iter().map(|(n, _, _)| n.clone()).collect();
386 let seen: HashSet<_> = order.iter().cloned().collect();
387 order.extend(
388 am.iter()
389 .filter(|(n, _, _)| !seen.contains(n))
390 .map(|(n, _, _)| n.clone()),
391 );
392 let bm: HashMap<_, _> = bm.into_iter().map(|(n, u, v)| (n, (u, v))).collect();
393 let am: HashMap<_, _> = am.into_iter().map(|(n, u, v)| (n, (u, v))).collect();
394 let matched = order.len();
395 order.truncate(limit.unwrap_or(usize::MAX));
396 let rows = order
397 .into_iter()
398 .map(|metric| {
399 let bv = bm.get(&metric);
400 let av = am.get(&metric);
401 let unit = bv.or(av).unwrap().0;
402 let before = bv.and_then(|x| x.1);
403 let after = av.and_then(|x| x.1);
404 let delta = before.zip(after).map(|(b, a)| a - b);
405 let percent = delta
406 .zip(before)
407 .and_then(|(d, b)| (b != 0.0).then_some(d / b.abs() * 100.0));
408 Delta {
409 metric,
410 unit,
411 before,
412 after,
413 delta,
414 percent,
415 }
416 })
417 .collect();
418 Ok(Comparison {
419 before: b,
420 after: a,
421 rows,
422 matched,
423 limit,
424 })
425}
426
427pub fn budget_us(trace: &Trace) -> Option<u64> {
428 trace
429 .session
430 .as_ref()
431 .and_then(|s| {
432 (s.budget_ms.is_finite() && s.budget_ms > 0.0)
433 .then_some((s.budget_ms as f64 * 1000.0) as u64)
434 })
435 .filter(|b| *b > 0)
436}
437
438pub fn origin_name(trace: &Trace, origin: Origin) -> &str {
439 match origin {
440 Origin::Kui => "kui",
441 Origin::Host => "host",
442 Origin::Extension(n) => trace.name(n),
443 }
444}
445
446pub fn verdict_kind(v: &Verdict) -> &'static str {
447 match v {
448 Verdict::Late { .. } => "late",
449 Verdict::Blocked { .. } => "blocked",
450 Verdict::Slow { .. } => "slow",
451 Verdict::Waited { .. } => "waited",
452 Verdict::Wasted => "wasted",
453 Verdict::Burn { .. } => "burn",
454 Verdict::Fine { .. } => "fine",
455 }
456}
457
458pub fn placement_of<'t>(tl: &'t Timeline, trace: &Trace, s: &Span) -> Option<&'t Placement> {
459 let ns = trace.name(s.name).strip_prefix("fill ")?;
460 let at = tl.placements.partition_point(|p| p.t < s.end());
461 tl.placements
462 .get(at)
463 .filter(|p| p.t <= s.end().saturating_add(50))
464 .filter(|p| {
465 trace
466 .name(p.component)
467 .strip_prefix(ns)
468 .is_some_and(|s| s.starts_with('/'))
469 })
470}
471
472pub struct Explorer<'a> {
475 pub trace: &'a Trace,
476 pub timeline: &'a Timeline,
477 index: std::borrow::Cow<'a, Index>,
478}
479
480#[derive(Clone)]
483pub struct Index {
484 budget: Option<u64>,
485 burns: Vec<Burn>,
486 spans: HashMap<u32, usize>,
487 frames: HashMap<u64, usize>,
488 children: Vec<(u32, u32)>,
489 self_times: Vec<u64>,
490 depths: Vec<u32>,
491}
492
493impl std::ops::Deref for Explorer<'_> {
494 type Target = Index;
495 fn deref(&self) -> &Index {
496 &self.index
497 }
498}
499
500impl<'a> Explorer<'a> {
501 pub fn new(trace: &'a Trace, timeline: &'a Timeline) -> Self {
502 let spans: HashMap<u32, usize> = timeline
503 .spans
504 .iter()
505 .enumerate()
506 .map(|(i, s)| (s.id, i))
507 .collect();
508 let mut children = vec![0u64; timeline.spans.len()];
509 for s in &timeline.spans {
510 if s.id != s.parent
511 && let Some(&p) = spans.get(&s.parent)
512 {
513 let parent = &timeline.spans[p];
514 if s.track == parent.track {
515 let length = s
516 .end()
517 .min(parent.end())
518 .saturating_sub(s.start.max(parent.start));
519 children[p] = children[p].saturating_add(length);
520 }
521 }
522 }
523 let self_times = timeline
524 .spans
525 .iter()
526 .enumerate()
527 .map(|(i, s)| s.dur.saturating_sub(children[i]))
528 .collect();
529 let mut depths = vec![None; timeline.spans.len()];
531 let mut visited = vec![0usize; timeline.spans.len()];
532 let mut path = Vec::new();
533 for i in 0..timeline.spans.len() {
534 if depths[i].is_some() {
535 continue;
536 }
537 path.clear();
538 let mut at = i;
539 let depth = loop {
540 if let Some(d) = depths[at] {
541 break d + 1;
542 }
543 if visited[at] == i + 1 {
544 break 0u32;
545 }
546 visited[at] = i + 1;
547 path.push(at);
548 let s = &timeline.spans[at];
549 match spans.get(&s.parent) {
550 Some(&p) if p != at && timeline.spans[p].track == s.track => at = p,
551 _ => break 0,
552 }
553 };
554 let mut d = depth;
555 for &p in path.iter().rev() {
556 depths[p] = Some(d);
557 d = d.saturating_add(1);
558 }
559 }
560 let budget = budget_us(trace);
561 let frames = timeline
562 .frames
563 .iter()
564 .enumerate()
565 .map(|(i, f)| (f.frame, i))
566 .collect();
567 let mut children: Vec<_> = timeline
568 .spans
569 .iter()
570 .filter(|s| s.parent != 0 && s.parent != s.id)
571 .map(|s| (s.parent, s.id))
572 .collect();
573 children.sort_unstable();
574 Self {
575 trace,
576 timeline,
577 index: std::borrow::Cow::Owned(Index {
578 budget,
579 burns: analysis::burns(timeline, trace),
580 spans,
581 frames,
582 children,
583 self_times,
584 depths: depths.into_iter().map(|d| d.unwrap_or(0)).collect(),
585 }),
586 }
587 }
588
589 pub fn with_index(trace: &'a Trace, timeline: &'a Timeline, index: &'a Index) -> Self {
590 Self {
591 trace,
592 timeline,
593 index: std::borrow::Cow::Borrowed(index),
594 }
595 }
596
597 pub fn into_index(self) -> Index {
598 self.index.into_owned()
599 }
600
601 pub fn frame_index(&self, id: u64) -> Option<usize> {
602 self.index.frames.get(&id).copied()
603 }
604
605 pub fn frame_detail(&self, index: usize) -> FrameDetail {
606 let tl = self.timeline;
607 let f = &tl.frames[index];
608 let end = f.end_or_begin().saturating_add(1);
609 FrameDetail {
610 index,
611 frame: f.clone(),
612 phases: analysis::phases(tl, index, self.trace),
613 verdict: self
614 .budget
615 .filter(|_| frame_work_us(f).is_some())
616 .map(|b| analysis::verdict(tl, index, b, &self.burns, self.trace)),
617 path: analysis::input_path(tl, index, self.trace),
618 fresh: tl
619 .placements_in(f.begin, end)
620 .iter()
621 .filter(|p| self.trace.name(p.answer) != analysis::REPLAYED)
622 .copied()
623 .collect(),
624 notes: tl
625 .notes_in(f.gap_from, end)
626 .iter()
627 .map(|n| n.1.clone())
628 .collect(),
629 }
630 }
631
632 pub fn span_detail(&self, id: u32) -> Option<SpanDetail> {
633 let &i = self.spans.get(&id)?;
634 let s = &self.timeline.spans[i];
635 let placement = placement_of(self.timeline, self.trace, s).copied();
636 Some(SpanDetail {
637 span: s.clone(),
638 name: placement.map_or(s.name, |p| p.component),
639 self_us: self.self_times[i],
640 frame: self
641 .frame_for_time(s.start)
642 .map(|i| self.timeline.frames[i].frame),
643 depth: self.depths[i],
644 placement,
645 children: {
646 let a = self
647 .index
648 .children
649 .partition_point(|(parent, _)| *parent < id);
650 let b = self
651 .index
652 .children
653 .partition_point(|(parent, _)| *parent <= id);
654 self.index.children[a..b]
655 .iter()
656 .map(|(_, child)| *child)
657 .collect()
658 },
659 })
660 }
661
662 pub fn frame_for_time(&self, t: u64) -> Option<usize> {
663 frame_for_time(self.timeline, t)
664 }
665
666 fn effective_end(&self, i: usize) -> u64 {
667 let f = &self.timeline.frames[i];
668 if f.end.is_none() {
669 frame_end(f).max(
670 self.timeline
671 .frames
672 .get(i + 1)
673 .map_or(self.timeline.end, |f| f.begin)
674 .saturating_add(1),
675 )
676 } else {
677 frame_end(f)
678 }
679 }
680
681 pub fn resolve(&self, selection: &Selection, filter: &Filter) -> Result<Scope, String> {
682 match selection {
683 Selection::Frames { first, last } if first > last => {
684 return Err("frame range must be increasing".into());
685 }
686 Selection::Time { from_us, to_us } if from_us >= to_us => {
687 return Err("time range must have positive length".into());
688 }
689 _ => {}
690 }
691 let mut indices = Vec::new();
692 let mut intervals: Vec<(u64, u64)> = Vec::new();
693 for (i, f) in self.timeline.frames.iter().enumerate() {
694 let selected = match selection {
695 Selection::Whole => true,
696 Selection::Frames { first, last } => f.frame >= *first && f.frame <= *last,
697 Selection::Time { from_us, to_us } => {
698 f.gap_from < *to_us && self.effective_end(i) > *from_us
699 }
700 };
701 if !selected || !self.keeps_frame(i, filter) {
702 continue;
703 }
704 indices.push(i);
705 let end = self.effective_end(i);
706 if let Some((_, b)) = intervals.last_mut()
707 && f.gap_from <= *b
708 {
709 *b = (*b).max(end);
710 } else {
711 intervals.push((f.gap_from, end));
712 }
713 }
714 if matches!(selection, Selection::Whole)
716 && filter.verdicts.is_none()
717 && !filter.hide_unchanged
718 {
719 intervals =
720 if self.timeline.end >= self.timeline.start && (!self.trace.records.is_empty()) {
721 vec![(self.timeline.start, self.timeline.end.saturating_add(1))]
722 } else {
723 Vec::new()
724 };
725 }
726 Ok(Scope {
727 requested: selection.clone(),
728 indices,
729 intervals,
730 })
731 }
732
733 pub fn keeps_frame(&self, i: usize, filter: &Filter) -> bool {
734 let f = &self.timeline.frames[i];
735 let verdict = filter
736 .verdicts
737 .as_ref()
738 .and_then(|_| self.budget.filter(|_| frame_work_us(f).is_some()))
739 .map(|b| analysis::verdict(self.timeline, i, b, &self.burns, self.trace));
740 matches_frame(f.unchanged, verdict.as_ref().map(verdict_kind), filter)
741 }
742
743 pub fn keeps_span(&self, s: &Span, filter: &Filter) -> bool {
744 matches_span(self.trace, self.timeline, s, filter)
745 }
746
747 pub fn selected_spans<'b>(
748 &'b self,
749 scope: &'b Scope,
750 filter: &'b Filter,
751 ) -> impl Iterator<Item = (usize, &'a Span)> + 'b {
752 let ids = filter
753 .span_ids
754 .as_ref()
755 .map(|v| v.iter().copied().collect::<HashSet<_>>());
756 let whole_population = scope.requested == Selection::Whole
759 && filter.verdicts.is_none()
760 && !filter.hide_unchanged;
761 self.timeline
762 .spans
763 .iter()
764 .enumerate()
765 .filter(move |(_, s)| {
766 scope.contains(s.start)
767 && (whole_population
768 || self
769 .frame_for_time(s.start)
770 .is_some_and(|i| scope.indices.binary_search(&i).is_ok()))
771 && self.keeps_span(s, filter)
772 && ids.as_ref().is_none_or(|ids| ids.contains(&s.id))
773 })
774 }
775
776 pub fn summary(&self, scope: &Scope) -> Summary {
777 let frames: Vec<_> = scope
778 .indices
779 .iter()
780 .map(|&i| &self.timeline.frames[i])
781 .collect();
782 let works: Vec<_> = frames.iter().filter_map(|f| frame_work_us(f)).collect();
783 let pacing: Vec<_> = scope
784 .indices
785 .windows(2)
786 .filter(|w| w[1] == w[0] + 1)
787 .map(|w| {
788 self.timeline.frames[w[1]]
789 .begin
790 .saturating_sub(self.timeline.frames[w[0]].begin)
791 })
792 .collect();
793 let phases: Vec<_> = scope
794 .indices
795 .iter()
796 .filter(|&&i| self.timeline.frames[i].end.is_some())
797 .map(|&i| {
798 (
799 analysis::phases(self.timeline, i, self.trace),
800 self.timeline.frames[i].present.is_some(),
801 )
802 })
803 .collect();
804 let phases = Phases::default()
805 .named()
806 .into_iter()
807 .enumerate()
808 .map(|(j, (name, _))| {
809 let values: Vec<_> = phases
810 .iter()
811 .filter(|(_, present)| j < 5 || *present)
812 .map(|(p, _)| p.named()[j].1)
813 .collect();
814 (name, Distribution::of(&values))
815 })
816 .collect();
817 Summary {
818 frames: frames.len(),
819 complete_frames: works.len(),
820 duration_us: scope.duration_us(),
821 budget_us: self.budget,
822 work: Distribution::of(&works),
823 pacing: Distribution::of(&pacing),
824 phases,
825 over_budget: self
826 .budget
827 .filter(|_| !works.is_empty())
828 .map(|b| works.iter().filter(|&&w| w > b).count()),
829 unchanged: frames.iter().filter(|f| f.unchanged == Some(true)).count(),
830 }
831 }
832
833 pub fn span_groups(&self, scope: &Scope, filter: &Filter) -> Vec<SpanGroup> {
834 let mut groups: HashMap<(NameId, Origin), SpanGroup> = HashMap::new();
835 for (i, s) in self.selected_spans(scope, filter) {
836 let name = placement_of(self.timeline, self.trace, s).map_or(s.name, |p| p.component);
837 let g = groups.entry((name, s.origin)).or_insert(SpanGroup {
838 name,
839 origin: s.origin,
840 calls: 0,
841 total_us: 0,
842 self_us: 0,
843 worst_us: 0,
844 cpu_us: Some(0),
845 example_id: s.id,
846 });
847 g.calls += 1;
848 g.total_us = g.total_us.saturating_add(s.dur);
849 g.self_us = g.self_us.saturating_add(self.self_times[i]);
850 if s.dur > g.worst_us || (s.dur == g.worst_us && s.id < g.example_id) {
851 g.example_id = s.id;
852 }
853 g.worst_us = g.worst_us.max(s.dur);
854 g.cpu_us = g.cpu_us.zip(s.cpu).map(|(a, b)| a.saturating_add(b));
855 }
856 groups.into_values().collect()
857 }
858
859 pub fn origins(&self, scope: &Scope, filter: &Filter) -> Vec<(Origin, u64, u64)> {
860 let mut groups: HashMap<Origin, (u64, u64)> = HashMap::new();
861 for (i, s) in self.selected_spans(scope, filter) {
862 let g = groups.entry(s.origin).or_default();
863 g.0 = g.0.saturating_add(self.self_times[i]);
864 g.1 += 1;
865 }
866 let mut out: Vec<_> = groups.into_iter().map(|(o, (us, n))| (o, us, n)).collect();
867 out.sort_by(|a, b| {
868 b.1.cmp(&a.1)
869 .then_with(|| origin_name(self.trace, a.0).cmp(origin_name(self.trace, b.0)))
870 });
871 out
872 }
873
874 pub fn counters(&self, scope: &Scope) -> (Vec<Counter>, Vec<Gauge>) {
875 let mut counters: BTreeMap<NameId, (f64, i64)> = BTreeMap::new();
876 for &i in &scope.indices {
877 for &(n, v) in &self.timeline.frames[i].counters {
878 let g = counters.entry(n).or_insert((0.0, i64::MIN));
879 g.0 += v as f64;
880 g.1 = g.1.max(v);
881 }
882 }
883 let counters = counters
884 .into_iter()
885 .map(|(name, (sum, max))| Counter {
886 name,
887 avg: sum / scope.indices.len().max(1) as f64,
888 max,
889 })
890 .collect();
891 let mut gauges: BTreeMap<NameId, Gauge> = BTreeMap::new();
892 for g in self
893 .timeline
894 .gauges
895 .iter()
896 .filter(|g| scope.contains(g.t) && g.value.is_finite())
897 {
898 let v = gauges.entry(g.name).or_insert(Gauge {
899 name: g.name,
900 last: g.value,
901 least: g.value,
902 most: g.value,
903 });
904 v.last = g.value;
905 v.least = v.least.min(g.value);
906 v.most = v.most.max(g.value);
907 }
908 (counters, gauges.into_values().collect())
909 }
910
911 pub fn readings(&self, scope: &Scope, filter: &Filter) -> Readings {
914 let mut spans = self.span_groups(scope, filter);
915 spans.sort_by(|a, b| {
916 b.self_us
917 .cmp(&a.self_us)
918 .then_with(|| self.trace.name(a.name).cmp(self.trace.name(b.name)))
919 .then_with(|| {
920 origin_name(self.trace, a.origin).cmp(origin_name(self.trace, b.origin))
921 })
922 .then(a.name.cmp(&b.name))
923 .then(a.origin.cmp(&b.origin))
924 });
925 let mut selected = Timeline::default();
926 selected.placements = self
927 .timeline
928 .placements
929 .iter()
930 .filter(|p| scope.contains(p.t))
931 .copied()
932 .collect();
933 let (counters, gauges) = self.counters(scope);
934 let indices: HashSet<_> = scope.indices.iter().copied().collect();
935 let mut burns = Vec::new();
936 for b in &self.burns {
937 let mut first = None;
938 for i in b.first..=b.last {
939 if indices.contains(&i) {
940 first.get_or_insert(i);
941 } else if let Some(start) = first.take() {
942 burns.push(Burn {
943 first: start,
944 last: i - 1,
945 });
946 }
947 }
948 if let Some(first) = first {
949 burns.push(Burn {
950 first,
951 last: b.last,
952 });
953 }
954 }
955 Readings {
956 summary: self.summary(scope),
957 spans,
958 origins: self.origins(scope, filter),
959 components: analysis::placements(&selected, self.trace),
960 counters,
961 gauges,
962 burns,
963 gaps: analysis::gaps_in(self.timeline, self.trace, scope.indices.iter().copied()),
964 }
965 }
966
967 pub fn execute(&self, query: &Query) -> Result<ResultSet, String> {
968 let mut selection = query.selection.clone();
969 let mut filter = query.filter.clone();
970 match query.operation {
971 Operation::Frame(id) => {
972 selection = Selection::Frames {
973 first: id,
974 last: id,
975 };
976 filter = Filter::default();
977 }
978 Operation::Span(id) => {
979 if let Some(s) = self.span_detail(id)
980 && let Some(id) = s.frame
981 {
982 selection = Selection::Frames {
983 first: id,
984 last: id,
985 };
986 }
987 filter = Filter::default();
988 }
989 _ => {}
990 }
991 let scope = self.resolve(&selection, &filter)?;
992 let limit = query.limit.unwrap_or(usize::MAX);
993 let flip = |o: std::cmp::Ordering| if query.descending { o.reverse() } else { o };
994 let (data, matched, returned) = match query.operation {
995 Operation::Summary => (Data::Summary(self.summary(&scope)), 1, 1),
996 Operation::Frame(id) => {
997 let i = self
998 .frame_index(id)
999 .ok_or_else(|| format!("no frame {id} in this recording"))?;
1000 (Data::Frame(Box::new(self.frame_detail(i))), 1, 1)
1001 }
1002 Operation::Span(id) => (
1003 Data::Span(
1004 self.span_detail(id)
1005 .ok_or_else(|| format!("no span {id} in this recording"))?,
1006 ),
1007 1,
1008 1,
1009 ),
1010 Operation::Frames => {
1011 let mut indices = scope.indices.clone();
1012 if !["frame", "start", "work"].contains(&query.sort.as_str()) {
1013 return Err("frames --sort needs frame, start or work".into());
1014 }
1015 indices.sort_by(|&a, &b| {
1016 let a = &self.timeline.frames[a];
1017 let b = &self.timeline.frames[b];
1018 match query.sort.as_str() {
1019 "frame" => flip(a.frame.cmp(&b.frame)),
1020 "start" => flip(a.begin.cmp(&b.begin)),
1021 _ => match (frame_work_us(a), frame_work_us(b)) {
1022 (Some(a), Some(b)) => flip(a.cmp(&b)),
1023 (None, Some(_)) => std::cmp::Ordering::Greater,
1024 (Some(_), None) => std::cmp::Ordering::Less,
1025 (None, None) => std::cmp::Ordering::Equal,
1026 },
1027 }
1028 .then(a.frame.cmp(&b.frame))
1029 });
1030 let matched = indices.len();
1031 indices.truncate(limit);
1032 let returned = indices.len();
1033 (
1034 Data::Frames(indices.into_iter().map(|i| self.frame_detail(i)).collect()),
1035 matched,
1036 returned,
1037 )
1038 }
1039 Operation::Spans => {
1040 let mut groups = self.span_groups(&scope, &filter);
1041 if !["self", "total", "calls", "worst", "name"].contains(&query.sort.as_str()) {
1042 return Err("spans --sort needs self, total, calls, worst or name".into());
1043 }
1044 groups.sort_by(|a, b| {
1045 flip(match query.sort.as_str() {
1046 "total" => a.total_us.cmp(&b.total_us),
1047 "calls" => a.calls.cmp(&b.calls),
1048 "worst" => a.worst_us.cmp(&b.worst_us),
1049 "name" => self.trace.name(a.name).cmp(self.trace.name(b.name)),
1050 _ => a.self_us.cmp(&b.self_us),
1051 })
1052 .then_with(|| self.trace.name(a.name).cmp(self.trace.name(b.name)))
1053 .then_with(|| {
1054 origin_name(self.trace, a.origin).cmp(origin_name(self.trace, b.origin))
1055 })
1056 .then(a.name.cmp(&b.name))
1057 .then(a.origin.cmp(&b.origin))
1058 });
1059 let matched = groups.len();
1060 groups.truncate(limit);
1061 let returned = groups.len();
1062 (Data::Spans(groups), matched, returned)
1063 }
1064 Operation::Counters => {
1065 if query.sort != "name" {
1066 return Err("counters --sort needs name".into());
1067 }
1068 let (mut counters, mut gauges) = self.counters(&scope);
1069 let mut names: Vec<_> = counters
1071 .iter()
1072 .map(|c| (self.trace.name(c.name), 0, c.name))
1073 .chain(gauges.iter().map(|g| (self.trace.name(g.name), 1, g.name)))
1074 .collect();
1075 names.sort_by(|a, b| flip(a.0.cmp(b.0)).then(a.1.cmp(&b.1)));
1076 let matched = names.len();
1077 names.truncate(limit);
1078 let returned = names.len();
1079 counters.retain(|c| names.iter().any(|n| n.1 == 0 && n.2 == c.name));
1080 gauges.retain(|g| names.iter().any(|n| n.1 == 1 && n.2 == g.name));
1081 counters.sort_by(|a, b| flip(self.trace.name(a.name).cmp(self.trace.name(b.name))));
1082 gauges.sort_by(|a, b| flip(self.trace.name(a.name).cmp(self.trace.name(b.name))));
1083 (Data::Counters(counters, gauges), matched, returned)
1084 }
1085 Operation::Marks => {
1086 if !["name", "start"].contains(&query.sort.as_str()) {
1087 return Err("marks --sort needs name or start".into());
1088 }
1089 let mut marks: Vec<_> = self
1090 .timeline
1091 .marks
1092 .iter()
1093 .filter(|m| scope.contains(m.t))
1094 .copied()
1095 .collect();
1096 marks.sort_by(|a, b| {
1097 flip(if query.sort == "start" {
1098 a.t.cmp(&b.t)
1099 } else {
1100 self.trace.name(a.name).cmp(self.trace.name(b.name))
1101 })
1102 .then(a.t.cmp(&b.t))
1103 .then(a.track.0.cmp(&b.track.0))
1104 });
1105 let matched = marks.len();
1106 marks.truncate(limit);
1107 let returned = marks.len();
1108 (Data::Marks(marks), matched, returned)
1109 }
1110 };
1111 let mut query = query.clone();
1112 query.selection = selection;
1113 query.filter = filter;
1114 Ok(ResultSet {
1115 query,
1116 scope,
1117 matched,
1118 returned,
1119 data,
1120 })
1121 }
1122}
1123
1124pub fn frame_work_us(frame: &Frame) -> Option<u64> {
1128 let end = frame.end?;
1129 let present = frame.present?;
1130 Some(
1131 end.saturating_sub(frame.begin)
1132 .saturating_add(present.render),
1133 )
1134}
1135
1136pub fn frame_for_time(timeline: &Timeline, t: u64) -> Option<usize> {
1141 let i = timeline.frames.partition_point(|f| {
1142 f.end
1143 .map_or(f.begin, |end| end.max(f.begin.saturating_add(1)))
1144 <= t
1145 });
1146 if i == timeline.frames.len()
1147 && let Some(f) = timeline.frames.last()
1148 && f.end.is_none()
1149 && t >= f.gap_from
1150 && t <= timeline.end
1151 {
1152 return Some(i - 1);
1153 }
1154 timeline
1155 .frames
1156 .get(i)
1157 .filter(|f| f.gap_from <= t)
1158 .map(|_| i)
1159}
1160
1161fn frame_end(f: &Frame) -> u64 {
1162 f.present
1163 .map_or(f.end_or_begin(), |p| p.t.max(f.end_or_begin()))
1164 .max(f.begin.saturating_add(1))
1165}
1166
1167pub fn milliseconds(ms: f64) -> Result<u64, String> {
1168 if !ms.is_finite() || ms < 0.0 || ms * 1000.0 >= u64::MAX as f64 {
1169 return Err(
1170 "time must be finite, nonnegative milliseconds within the recording clock".into(),
1171 );
1172 }
1173 Ok((ms * 1000.0).round() as u64)
1174}
1175
1176pub fn frame_range(s: &str) -> Result<Selection, String> {
1177 let (a, b) = s.split_once(':').ok_or("frame range needs FIRST:LAST")?;
1178 let first = a.parse().map_err(|_| "invalid first frame number")?;
1179 let last = b.parse().map_err(|_| "invalid last frame number")?;
1180 if first > last {
1181 return Err("frame range must be increasing".into());
1182 }
1183 Ok(Selection::Frames { first, last })
1184}
1185
1186pub fn time_range(s: &str) -> Result<Selection, String> {
1187 fn endpoint(s: &str) -> Result<u64, String> {
1188 let (n, ms) = if let Some(n) = s.strip_suffix("us") {
1189 (n, 0.001)
1190 } else if let Some(n) = s.strip_suffix("ms") {
1191 (n, 1.0)
1192 } else if let Some(n) = s.strip_suffix('s') {
1193 (n, 1000.0)
1194 } else {
1195 return Err("time endpoints need us, ms or s units".into());
1196 };
1197 let n = n.parse::<f64>().map_err(|_| format!("invalid time {s}"))?;
1198 milliseconds(n * ms)
1199 }
1200 let (a, b) = s
1201 .split_once(':')
1202 .ok_or("time range needs FROM:TO with units, as 1s:1500ms")?;
1203 let from_us = endpoint(a)?;
1204 let to_us = endpoint(b)?;
1205 if from_us >= to_us {
1206 return Err("time range must have positive length".into());
1207 }
1208 Ok(Selection::Time { from_us, to_us })
1209}
1210
1211pub fn matches_span(trace: &Trace, timeline: &Timeline, s: &Span, filter: &Filter) -> bool {
1213 s.dur >= filter.min_us
1214 && filter
1215 .origins
1216 .as_ref()
1217 .is_none_or(|os| os.iter().any(|o| o == origin_name(trace, s.origin)))
1218 && !filter.hide_names.iter().any(|n| n == trace.name(s.name))
1219 && filter.name.as_ref().is_none_or(|n| {
1220 let n = n.to_lowercase();
1221 trace.name(s.name).to_lowercase().contains(&n)
1222 || placement_of(timeline, trace, s)
1223 .is_some_and(|p| trace.name(p.component).to_lowercase().contains(&n))
1224 })
1225}
1226
1227pub fn matches_frame(unchanged: Option<bool>, verdict: Option<&str>, filter: &Filter) -> bool {
1228 !(filter.hide_unchanged && unchanged == Some(true))
1229 && filter
1230 .verdicts
1231 .as_ref()
1232 .is_none_or(|vs| verdict.is_some_and(|v| vs.iter().any(|k| k == v)))
1233}
1234
1235#[cfg(test)]
1236mod tests;