1mod 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Format {
52 Kuitrace,
54 Chrome,
56 Perfetto,
58 Speedscope,
60 Firefox,
62}
63
64impl Format {
65 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 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 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
106pub 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
117pub fn chrome(trace: &Trace) -> String {
121 chrome::write(&Model::of(trace))
122}
123
124pub fn perfetto(trace: &Trace) -> Vec<u8> {
127 perfetto::write(&Model::of(trace))
128}
129
130pub fn speedscope(trace: &Trace) -> String {
132 speedscope::write(&Model::of(trace))
133}
134
135pub fn firefox(trace: &Trace) -> String {
137 firefox::write(&Model::of(trace))
138}
139
140#[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#[derive(Clone, Debug, PartialEq)]
152pub(crate) struct Lane {
153 pub track: Option<Track>,
154 pub name: String,
155 pub tid: u64,
159}
160
161#[derive(Clone, Debug, PartialEq)]
163pub(crate) struct Slice {
164 pub lane: usize,
165 pub start: u64,
166 pub end: u64,
167 pub label: String,
169 pub name: String,
171 pub cat: String,
172 pub args: Vec<(&'static str, Val)>,
173}
174
175#[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 pub global: bool,
185}
186
187#[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#[derive(Clone, Debug, Default, PartialEq)]
198pub(crate) struct Model {
199 pub app: String,
200 pub meta: Vec<(&'static str, Val)>,
201 pub wall_ms: u64,
203 pub lanes: Vec<Lane>,
204 pub slices: Vec<Slice>,
206 pub instants: Vec<Instant>,
207 pub series: Vec<Series>,
208 pub start: u64,
209 pub end: u64,
210}
211
212pub(crate) const FRAMES: usize = 0;
214pub(crate) const MAIN: usize = 1;
216
217pub 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
237pub(crate) fn hex(node: u64) -> String {
239 format!("{node:016x}")
240}
241
242pub(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 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 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 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 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 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 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 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 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 sorted.sort_by_key(|s| (s.lane, s.start, Reverse(s.end)));
656 self.slices = sorted;
657 }
658
659 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 pub fn has_slices(&self, lane: usize) -> bool {
687 self.slices.iter().any(|s| s.lane == lane)
688 }
689}