shuyi_ui/perf.rs
1//! Frame times and spans as data (ADR-0046), in debug builds: what the
2//! platform's `App` measures of each frame and keeps in a [`PerfRing`] of
3//! the last [`PERF_FRAMES`] [`FrameRecord`]s, for tests, tools and agents.
4//!
5//! Only a backend has a clock. It hands one over ([`PerfClock`]), the
6//! platform's `App` moves it into the game's context, and the context reads
7//! it: around the update and the drawing, and around each span a game
8//! marks. With debug tools off ([`Ctx::debug_tools`]) nothing is measured
9//! and the clock is never read.
10//!
11//! A game marks a span of its own around any part of an update:
12//!
13//! ```
14//! use shuyi_ui::{Ctx, GameData};
15//!
16//! fn plan_turn<G: GameData>(ctx: &mut Ctx<G>) -> u32 {
17//! ctx.span("ai", |_ctx| 2 + 2)
18//! }
19//! ```
20//!
21//! [`Ctx::span`] calls the closure and gives back what it returns; in a
22//! measured frame it also keeps the span's start, length and depth in the
23//! frame's record. A time never reaches an update: nothing reads a record
24//! to decide anything, so a run plays the same with the clock on and off.
25//!
26//! The heap is counted too, where the backend counts it (a debug build of
27//! `shuyi-app` or of a game's tool program): each record's
28//! [`heap`](FrameRecord::heap) holds the live bytes, their peak and the
29//! allocations in that frame ([`HeapCounts`]), as the backend's
30//! [`MemoryReading`] gave them at the frame's end. The counter counts the
31//! whole process, every thread's allocations.
32//!
33//! Where the backend's counter has memory tags ([`MemoryScopes`]), it
34//! also counts what each part of a frame allocates under a tag of its own
35//! ([`TagClass`]): the frame's phases, each span a game marks, and each
36//! asset kind the backend sees decoded. Entering a tag changes nothing a
37//! game reads. A [`HeapSnapshot`] is what holds the heap at a frame, by
38//! tag, and a [`HeapDiff`] what grew and shrank between two
39//! ([`HeapSnapshot::diff`]); both are lines of a trace ([`PerfLine`]).
40//!
41//! [`Ctx::debug_tools`]: crate::Ctx::debug_tools
42//! [`Ctx::span`]: crate::Ctx::span
43
44use crate::prelude::*;
45use alloc::borrow::Cow;
46use alloc::collections::VecDeque;
47use alloc::collections::vec_deque;
48
49use serde::{Deserialize, Serialize};
50
51/// How many frames' records a [`PerfRing`] keeps by default: ten seconds
52/// at 60 a second.
53pub const PERF_FRAMES: usize = 600;
54
55/// How many spans a frame's record keeps: those past it are counted in
56/// [`FrameRecord::spans_dropped`].
57pub const MAX_SPANS: usize = 64;
58
59/// A clock in whole microseconds, from a backend
60/// (`shuyi_platform::Platform::clock`) or a test.
61#[derive(Debug, Clone, Copy)]
62#[non_exhaustive]
63pub enum PerfClock {
64 /// The device's clock: microseconds from any fixed start, never going
65 /// back.
66 Real(fn() -> u64),
67 /// A clock that moves by fixed steps on each read, so a test's records
68 /// are the same on every run ([`FakeClock`]).
69 Fake(FakeClock),
70}
71
72impl PerfClock {
73 /// A [`FakeClock`] at 0.
74 pub fn fake() -> Self {
75 Self::Fake(FakeClock::new())
76 }
77
78 /// The time now, in microseconds.
79 pub fn read(&mut self) -> u64 {
80 match self {
81 Self::Real(now) => now(),
82 Self::Fake(fake) => fake.read(),
83 }
84 }
85}
86
87/// A clock for tests. It starts at 0, and its read number *n* (from 0)
88/// adds 100 × (*n* mod 7 + 1) microseconds and gives the new time: 100,
89/// 300, 600, 1000, …
90#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
91pub struct FakeClock {
92 now: u64,
93 reads: u64,
94}
95
96impl FakeClock {
97 /// A clock at 0, never read.
98 pub const fn new() -> Self {
99 Self { now: 0, reads: 0 }
100 }
101
102 /// Moves on and gives the new time.
103 pub fn read(&mut self) -> u64 {
104 self.now += 100 * (self.reads % 7 + 1);
105 self.reads += 1;
106 self.now
107 }
108
109 /// How many times it has been read.
110 pub fn reads(&self) -> u64 {
111 self.reads
112 }
113}
114
115/// How long the parts of one frame took, in whole microseconds. Build it
116/// with [`FrameTimes::new`] (ADR-0039); a backend may add a part.
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
118#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119#[serde(deny_unknown_fields)]
120#[non_exhaustive]
121pub struct FrameTimes {
122 /// `Game::frame` from its start to the drawing: the input, the
123 /// modules' `before_update` and the screens' update.
124 pub update_us: u32,
125 /// The drawing: the screens and the modules drawing the frame
126 /// ([`Game::redraw`](crate::Game::redraw)).
127 pub draw_us: u32,
128 /// The platform's `present`: in `shuyi-app`, painting the frame into
129 /// the window.
130 pub paint_us: u32,
131 /// From the end of the last frame to the start of this one, outside
132 /// `App`: the backend's buffer swap and its wait for vsync or the frame
133 /// cap. 0 on the first frame measured.
134 pub present_us: u32,
135 /// [`present_us`](Self::present_us) plus this frame from its start to
136 /// its end: never less than the four above together. The rest is
137 /// `App`'s own (the system's events, the audio, the record).
138 pub frame_us: u32,
139}
140
141impl FrameTimes {
142 /// Every part 0.
143 pub const fn new() -> Self {
144 Self {
145 update_us: 0,
146 draw_us: 0,
147 paint_us: 0,
148 present_us: 0,
149 frame_us: 0,
150 }
151 }
152}
153
154/// A span a game marked ([`Ctx::span`](crate::Ctx::span)), as it ended.
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
157#[serde(deny_unknown_fields)]
158#[non_exhaustive]
159pub struct SpanEvent {
160 /// What the game called it.
161 pub name: Cow<'static, str>,
162 /// When it started, in microseconds from the frame's start.
163 pub start_us: u32,
164 /// How long it took, in microseconds.
165 pub us: u32,
166 /// How many spans it was inside: 0 for one inside no other.
167 pub depth: u8,
168}
169
170impl SpanEvent {
171 /// A span called `name` of `us` microseconds, `start_us` into the
172 /// frame, inside `depth` others.
173 pub fn new(name: impl Into<Cow<'static, str>>, start_us: u32, us: u32, depth: u8) -> Self {
174 Self {
175 name: name.into(),
176 start_us,
177 us,
178 depth,
179 }
180 }
181}
182
183/// The heap's counts at a frame's end, in bytes asked for (never the
184/// allocator's own overhead). Build it with [`HeapCounts::new`]
185/// (ADR-0039).
186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
187#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
188#[serde(deny_unknown_fields)]
189#[non_exhaustive]
190pub struct HeapCounts {
191 /// The bytes allocated and not yet freed.
192 pub live_bytes: u64,
193 /// The most bytes live at once since the program started.
194 pub peak_bytes: u64,
195 /// The calls that allocated or reallocated since the last reading: one
196 /// reading a frame, so the frame's.
197 pub allocs: u32,
198}
199
200impl HeapCounts {
201 /// `live_bytes` live, `peak_bytes` at the most, and `allocs` calls
202 /// since the last reading.
203 pub const fn new(live_bytes: u64, peak_bytes: u64, allocs: u32) -> Self {
204 Self {
205 live_bytes,
206 peak_bytes,
207 allocs,
208 }
209 }
210}
211
212/// What a backend reads of the device's memory at a frame's end
213/// (`shuyi_platform::Platform::memory`). Build it with
214/// [`MemoryReading::new`] (ADR-0039).
215#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
217#[serde(deny_unknown_fields)]
218#[non_exhaustive]
219pub struct MemoryReading {
220 /// The heap's counts.
221 pub heap: HeapCounts,
222}
223
224impl MemoryReading {
225 /// A reading of the heap's counts `heap`.
226 pub const fn new(heap: HeapCounts) -> Self {
227 Self { heap }
228 }
229}
230
231/// What a memory tag names (ADR-0046 §6): the heap counter counts each
232/// block under the innermost tag entered when it was allocated. Its number
233/// is the class a backend's counter keeps with each name
234/// ([`MemoryScopes::tag`]); `Other` is 0, the class of the counter's own
235/// `other` and `overflow`.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
237#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
238#[serde(rename_all = "kebab-case")]
239#[repr(u8)]
240#[non_exhaustive]
241pub enum TagClass {
242 /// A phase of the frame: `load`, `update`, `draw`, `paint`, `perf`,
243 /// and `app` for the rest of `App`'s frame.
244 Phase = 1,
245 /// A span the game marked ([`Ctx::span`](crate::Ctx::span)).
246 Span = 2,
247 /// An asset kind being decoded: `image`, `palette`, …
248 Asset = 3,
249 /// None of those: `other`, what is allocated outside any tag, and
250 /// `overflow`, the names past the counter's table.
251 Other = 0,
252}
253
254impl TagClass {
255 /// The class numbered `n`, if there is one.
256 pub const fn from_u8(n: u8) -> Option<Self> {
257 match n {
258 0 => Some(Self::Other),
259 1 => Some(Self::Phase),
260 2 => Some(Self::Span),
261 3 => Some(Self::Asset),
262 _ => None,
263 }
264 }
265}
266
267/// How a backend lets the engine tag what it allocates (ADR-0046 §6):
268/// three plain functions over its heap counter's tags
269/// (`shuyi_platform::Platform::memory_scopes`), so neither this crate nor
270/// the platform depends on the counter. Build it with
271/// [`MemoryScopes::new`] (ADR-0039).
272///
273/// With scopes in its context ([`Ctx::set_memory_scopes`]), the game's
274/// frame enters `update` and `draw`, each [`Ctx::span`] its own name, and
275/// the platform's `App` its own phases. Entering a tag changes nothing a
276/// game reads.
277///
278/// [`Ctx::set_memory_scopes`]: crate::Ctx::set_memory_scopes
279/// [`Ctx::span`]: crate::Ctx::span
280#[derive(Debug, Clone, Copy)]
281#[non_exhaustive]
282pub struct MemoryScopes {
283 /// The tag of a name of a class ([`TagClass`] as a number), the same
284 /// each time it is asked for.
285 pub tag: fn(u8, &'static str) -> u16,
286 /// Makes a tag the current one, and gives back the one before.
287 pub enter: fn(u16) -> u16,
288 /// Puts back the tag `enter` gave.
289 pub leave: fn(u16),
290}
291
292impl MemoryScopes {
293 /// Scopes that find a tag with `tag`, and `enter` and `leave` it.
294 pub const fn new(
295 tag: fn(u8, &'static str) -> u16,
296 enter: fn(u16) -> u16,
297 leave: fn(u16),
298 ) -> Self {
299 Self { tag, enter, leave }
300 }
301
302 /// Enters the tag of `class`'s `name`, and gives back the tag before,
303 /// for [`leave`](Self::leave).
304 pub fn enter(&self, class: TagClass, name: &'static str) -> u16 {
305 (self.enter)((self.tag)(class as u8, name))
306 }
307
308 /// Puts back `before`, the tag [`enter`](Self::enter) gave.
309 pub fn leave(&self, before: u16) {
310 (self.leave)(before);
311 }
312}
313
314/// Leaves a tag [`Ctx`](crate::Ctx) entered: the tag before it, with the
315/// scopes that entered it.
316pub(crate) fn leave_tag(entered: Option<(MemoryScopes, u16)>) {
317 if let Some((scopes, before)) = entered {
318 scopes.leave(before);
319 }
320}
321
322/// `bytes` for people: under 1,024 as `1023 B`, then in `KB`, `MB` and
323/// `GB` with one decimal, rounded down (`1.0 KB` is 1,024 bytes, `1.0 MB`
324/// 1,048,576), in integers.
325pub fn bytes_text(bytes: u64) -> String {
326 const UNITS: [(u64, &str); 3] = [(1 << 30, "GB"), (1 << 20, "MB"), (1 << 10, "KB")];
327 let Some(&(unit, name)) = UNITS.iter().find(|&&(unit, _)| bytes >= unit) else {
328 return format!("{bytes} B");
329 };
330 let tenths = u128::from(bytes) * 10 / u128::from(unit);
331 format!("{}.{} {name}", tenths / 10, tenths % 10)
332}
333
334/// What one frame measured. Build it with [`FrameRecord::new`] (ADR-0039).
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
337#[serde(deny_unknown_fields)]
338#[non_exhaustive]
339pub struct FrameRecord {
340 /// The frame (`App::frame_number`: frames count from 1).
341 pub frame: u32,
342 /// How long its parts took.
343 pub times: FrameTimes,
344 /// The spans that ended in it, in the order they ended (an inner one
345 /// before its outer one), at most [`MAX_SPANS`].
346 pub spans: Vec<SpanEvent>,
347 /// The spans past [`MAX_SPANS`], counted and not kept.
348 pub spans_dropped: u32,
349 /// The heap's counts at the frame's end, where the backend counts it
350 /// (a debug build of `shuyi-app` or a tool program); `None` where it
351 /// doesn't, and left out of the JSON then.
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub heap: Option<HeapCounts>,
354}
355
356impl FrameRecord {
357 /// The record of `frame`, with nothing measured yet.
358 pub fn new(frame: u32) -> Self {
359 Self {
360 frame,
361 times: FrameTimes::new(),
362 spans: Vec::new(),
363 spans_dropped: 0,
364 heap: None,
365 }
366 }
367}
368
369/// The last frames' records, oldest first, and how many older ones were
370/// dropped: [`LogRing`](crate::log::LogRing)'s twin. Each record pushed
371/// gets a sequence number, from 0 and never reused, so a reader takes the
372/// records [`since`](Self::since) the last it took.
373#[derive(Debug, Clone, PartialEq, Eq)]
374pub struct PerfRing {
375 records: VecDeque<FrameRecord>,
376 capacity: usize,
377 dropped: u64,
378 /// The sequence number of the next record pushed.
379 next: u64,
380}
381
382impl PerfRing {
383 /// An empty ring that keeps `capacity` records.
384 pub fn new(capacity: usize) -> Self {
385 Self {
386 records: VecDeque::new(),
387 capacity,
388 dropped: 0,
389 next: 0,
390 }
391 }
392
393 /// Keeps `record`, dropping (and counting) the oldest when full.
394 pub fn push(&mut self, record: FrameRecord) {
395 self.next += 1;
396 if self.capacity == 0 {
397 self.dropped += 1;
398 return;
399 }
400 if self.records.len() == self.capacity {
401 self.records.pop_front();
402 self.dropped += 1;
403 }
404 self.records.push_back(record);
405 }
406
407 /// The kept records, oldest first.
408 pub fn records(&self) -> vec_deque::Iter<'_, FrameRecord> {
409 self.records.iter()
410 }
411
412 /// How many records were dropped to keep the newest.
413 pub fn dropped(&self) -> u64 {
414 self.dropped
415 }
416
417 /// The sequence number the next record pushed gets.
418 pub fn next_seq(&self) -> u64 {
419 self.next
420 }
421
422 /// The kept records whose sequence number is `seq` or more, oldest
423 /// first: those after the last a reader took, if it kept
424 /// [`next_seq`](Self::next_seq) then.
425 pub fn since(&self, seq: u64) -> vec_deque::Iter<'_, FrameRecord> {
426 let skip = crate::log::skip_before(seq, self.next, self.records.len());
427 self.records.range(skip..)
428 }
429}
430
431/// One memory tag's counts in a [`HeapSnapshot`], in bytes asked for.
432/// Build it with [`TagRow::new`] (ADR-0039).
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435#[serde(deny_unknown_fields)]
436#[non_exhaustive]
437pub struct TagRow {
438 /// What the tag names: a phase, a span, an asset kind, or `other`.
439 pub class: TagClass,
440 /// Its name: `update`, the span's (`ai`), the asset kind's (`image`),
441 /// `other` or `overflow`.
442 pub name: Cow<'static, str>,
443 /// The bytes live under it: allocated, or last grown, under it and not
444 /// yet freed.
445 pub live_bytes: u64,
446 /// The blocks live under it.
447 pub live_blocks: u32,
448 /// The bytes ever allocated under it: a block grown adds its new size.
449 pub allocated_bytes: u64,
450 /// The most bytes live under it at once.
451 pub peak_bytes: u64,
452}
453
454impl TagRow {
455 /// `class`'s `name` with `live_bytes` live in `live_blocks` blocks, as
456 /// if just allocated: its bytes ever allocated and its peak are its
457 /// live bytes (assign them otherwise).
458 pub fn new(
459 class: TagClass,
460 name: impl Into<Cow<'static, str>>,
461 live_bytes: u64,
462 live_blocks: u32,
463 ) -> Self {
464 Self {
465 class,
466 name: name.into(),
467 live_bytes,
468 live_blocks,
469 allocated_bytes: live_bytes,
470 peak_bytes: live_bytes,
471 }
472 }
473}
474
475/// What holds the heap at a frame (ADR-0046 §6): its live bytes, and each
476/// memory tag's counts. Build it with [`HeapSnapshot::new`] (ADR-0039),
477/// which puts the rows in order; compare two with [`diff`](Self::diff).
478#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
479#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
480#[serde(deny_unknown_fields)]
481#[non_exhaustive]
482pub struct HeapSnapshot {
483 /// The frame it was taken after (frames count from 1; 0 is before the
484 /// first).
485 pub frame: u32,
486 /// The bytes allocated and not yet freed.
487 pub live_bytes: u64,
488 /// The most bytes live at once since the program started.
489 pub peak_bytes: u64,
490 /// Each tag with bytes live or ever allocated under it: the most live
491 /// bytes first, then by class, then by name.
492 pub tags: Vec<TagRow>,
493 /// How many names had no tag of their own, past the counter's table,
494 /// and count under `overflow`.
495 pub overflowed: u32,
496}
497
498impl HeapSnapshot {
499 /// A snapshot of frame 0 with `live_bytes` live, `peak_bytes` at the
500 /// most, and the rows `tags`, put in order: the most live bytes first,
501 /// then by class, then by name. None overflowed.
502 pub fn new(live_bytes: u64, peak_bytes: u64, mut tags: Vec<TagRow>) -> Self {
503 tags.sort_by(|a, b| {
504 b.live_bytes
505 .cmp(&a.live_bytes)
506 .then_with(|| (a.class as u8).cmp(&(b.class as u8)))
507 .then_with(|| a.name.cmp(&b.name))
508 });
509 Self {
510 frame: 0,
511 live_bytes,
512 peak_bytes,
513 tags,
514 overflowed: 0,
515 }
516 }
517
518 /// What changed from this snapshot to `later`: the tags whose live
519 /// bytes or blocks changed, a tag missing from one counting as 0
520 /// there.
521 pub fn diff(&self, later: &HeapSnapshot) -> HeapDiff {
522 let mut tags: Vec<TagDelta> = Vec::new();
523 for row in &self.tags {
524 let after = later.row(row.class, &row.name);
525 let (bytes, blocks) = after.map_or((0, 0), |a| (a.live_bytes, a.live_blocks));
526 tags.push(TagDelta::new(
527 row.class,
528 row.name.clone(),
529 change(row.live_bytes, bytes),
530 i64::from(blocks) - i64::from(row.live_blocks),
531 ));
532 }
533 for row in &later.tags {
534 if self.row(row.class, &row.name).is_none() {
535 tags.push(TagDelta::new(
536 row.class,
537 row.name.clone(),
538 change(0, row.live_bytes),
539 i64::from(row.live_blocks),
540 ));
541 }
542 }
543 tags.retain(|t| t.live_bytes != 0 || t.live_blocks != 0);
544 tags.sort_by(|a, b| {
545 b.live_bytes
546 .unsigned_abs()
547 .cmp(&a.live_bytes.unsigned_abs())
548 .then_with(|| (a.class as u8).cmp(&(b.class as u8)))
549 .then_with(|| a.name.cmp(&b.name))
550 });
551 HeapDiff {
552 from: self.frame,
553 to: later.frame,
554 live_bytes: change(self.live_bytes, later.live_bytes),
555 tags,
556 }
557 }
558
559 /// The row of `class`'s `name`, if it has one.
560 fn row(&self, class: TagClass, name: &str) -> Option<&TagRow> {
561 self.tags
562 .iter()
563 .find(|r| r.class == class && r.name == name)
564 }
565}
566
567/// `to` less `from`, at most [`i64`]'s range.
568fn change(from: u64, to: u64) -> i64 {
569 let change = i128::from(to) - i128::from(from);
570 let fits = change.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
571 i64::try_from(fits).unwrap_or_default()
572}
573
574/// How one memory tag changed between two snapshots. Build it with
575/// [`TagDelta::new`] (ADR-0039).
576#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
577#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
578#[serde(deny_unknown_fields)]
579#[non_exhaustive]
580pub struct TagDelta {
581 /// What the tag names.
582 pub class: TagClass,
583 /// Its name.
584 pub name: Cow<'static, str>,
585 /// How many more bytes are live under it: less than 0 for fewer.
586 pub live_bytes: i64,
587 /// How many more blocks are live under it: less than 0 for fewer.
588 pub live_blocks: i64,
589}
590
591impl TagDelta {
592 /// `class`'s `name`, with `live_bytes` more bytes live in
593 /// `live_blocks` more blocks.
594 pub fn new(
595 class: TagClass,
596 name: impl Into<Cow<'static, str>>,
597 live_bytes: i64,
598 live_blocks: i64,
599 ) -> Self {
600 Self {
601 class,
602 name: name.into(),
603 live_bytes,
604 live_blocks,
605 }
606 }
607}
608
609/// What grew and shrank in the heap between two snapshots
610/// ([`HeapSnapshot::diff`]). Build an empty one with [`HeapDiff::new`]
611/// (ADR-0039).
612#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
613#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
614#[serde(deny_unknown_fields)]
615#[non_exhaustive]
616pub struct HeapDiff {
617 /// The earlier snapshot's frame.
618 pub from: u32,
619 /// The later snapshot's frame.
620 pub to: u32,
621 /// How many more bytes are live: less than 0 for fewer.
622 pub live_bytes: i64,
623 /// The tags whose live bytes or blocks changed: the largest change in
624 /// bytes first (more or fewer), then by class, then by name.
625 pub tags: Vec<TagDelta>,
626}
627
628impl HeapDiff {
629 /// Nothing changed from frame `from` to frame `to`.
630 pub fn new(from: u32, to: u32) -> Self {
631 Self {
632 from,
633 to,
634 live_bytes: 0,
635 tags: Vec::new(),
636 }
637 }
638}
639
640/// One line of a perf trace, as agents read it (ADR-0046 §7): a JSON
641/// object tagged by `kind`, one a line (`serde_json::to_string(&line)` and
642/// a newline).
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
644#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
645#[serde(tag = "kind", rename_all = "kebab-case")]
646#[non_exhaustive]
647pub enum PerfLine {
648 /// What one frame measured: `{"kind":"frame","frame":12,…}`.
649 Frame(FrameRecord),
650 /// What held the heap at a frame:
651 /// `{"kind":"snapshot","frame":600,"live_bytes":…,"tags":[…],…}`.
652 Snapshot(HeapSnapshot),
653 /// What changed between two snapshots:
654 /// `{"kind":"diff","from":1,"to":600,"live_bytes":98304,"tags":[…]}`.
655 Diff(HeapDiff),
656}
657
658/// How a set of times spread, in whole microseconds: the median, the 99th
659/// percentile (nearest rank), the worst and the mean rounded down.
660#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
662#[serde(deny_unknown_fields)]
663pub struct Spread {
664 /// The median: half the times are this or less.
665 pub p50: u32,
666 /// 99 in a hundred times are this or less.
667 pub p99: u32,
668 /// The longest.
669 pub worst: u32,
670 /// The mean, rounded down.
671 pub mean: u32,
672}
673
674/// How `values` spread, or `None` for no values. The percentiles are
675/// nearest-rank: the value at rank ⌈p × n / 100⌉ of the n values sorted,
676/// all in integers.
677pub fn spread(values: impl IntoIterator<Item = u32>) -> Option<Spread> {
678 let mut sorted: Vec<u32> = values.into_iter().collect();
679 if sorted.is_empty() {
680 return None;
681 }
682 sorted.sort_unstable();
683 let n = sorted.len();
684 let rank = |p: usize| (p * n).div_ceil(100).clamp(1, n) - 1;
685 let sum: u64 = sorted.iter().map(|&v| u64::from(v)).sum();
686 let count = u64::try_from(n).unwrap_or(u64::MAX);
687 Some(Spread {
688 p50: sorted[rank(50)],
689 p99: sorted[rank(99)],
690 worst: sorted[n - 1],
691 mean: u32::try_from(sum / count).unwrap_or(u32::MAX),
692 })
693}
694
695/// The microseconds from `from` to `to`: none if `to` is earlier, and at
696/// most `u32::MAX`.
697pub(crate) fn micros(from: u64, to: u64) -> u32 {
698 u32::try_from(to.saturating_sub(from)).unwrap_or(u32::MAX)
699}
700
701/// What the context measures of a frame: its clock, and while a frame is
702/// measured, its start, the spans open and those ended, and the update's
703/// and the drawing's times.
704#[derive(Debug, Default)]
705pub(crate) struct PerfState {
706 clock: Option<PerfClock>,
707 /// The backend's memory tags, if it gave them.
708 pub(crate) scopes: Option<MemoryScopes>,
709 /// The frame's start, while one is measured.
710 start: Option<u64>,
711 /// How many spans are open.
712 depth: u8,
713 record: Option<FrameRecord>,
714}
715
716impl PerfState {
717 pub(crate) fn clock(&self) -> Option<PerfClock> {
718 self.clock
719 }
720
721 /// Takes `clock` in place of any; `None` stops measuring, and a frame
722 /// being measured ends with nothing.
723 pub(crate) fn set_clock(&mut self, clock: Option<PerfClock>) {
724 self.clock = clock;
725 if clock.is_none() {
726 self.start = None;
727 self.record = None;
728 }
729 }
730
731 /// The time, if there is a clock.
732 pub(crate) fn now(&mut self) -> Option<u64> {
733 self.clock.as_mut().map(PerfClock::read)
734 }
735
736 /// The time, in a measured frame only.
737 pub(crate) fn now_in_frame(&mut self) -> Option<u64> {
738 self.start?;
739 self.now()
740 }
741
742 /// Starts measuring a frame at the time now, if there is a clock.
743 pub(crate) fn begin_frame(&mut self) -> Option<u64> {
744 let start = self.now()?;
745 self.start = Some(start);
746 self.depth = 0;
747 self.record = Some(FrameRecord::new(0));
748 Some(start)
749 }
750
751 /// Ends the frame being measured: its update's and drawing's times and
752 /// its spans, as a record of frame 0 for `App` to finish.
753 pub(crate) fn end_frame(&mut self) -> Option<FrameRecord> {
754 self.start = None;
755 self.depth = 0;
756 self.record.take()
757 }
758
759 /// Keeps the update's and the drawing's times: the update from `start`
760 /// to `drawing`, the drawing from there to `drawn`.
761 pub(crate) fn set_frame_times(
762 &mut self,
763 start: Option<u64>,
764 drawing: Option<u64>,
765 drawn: Option<u64>,
766 ) {
767 if let (Some(record), Some(start), Some(drawing), Some(drawn)) =
768 (&mut self.record, start, drawing, drawn)
769 {
770 record.times.update_us = micros(start, drawing);
771 record.times.draw_us = micros(drawing, drawn);
772 }
773 }
774
775 /// Opens a span, in a measured frame: its start and its depth.
776 pub(crate) fn open_span(&mut self) -> Option<(u64, u8)> {
777 let start = self.now_in_frame()?;
778 let depth = self.depth;
779 self.depth = depth.saturating_add(1);
780 Some((start, depth))
781 }
782
783 /// Closes the span `open_span` opened, keeping it if the frame is still
784 /// measured.
785 pub(crate) fn close_span(&mut self, name: &'static str, (start, depth): (u64, u8)) {
786 self.depth = depth;
787 let Some(end) = self.now_in_frame() else {
788 return;
789 };
790 let (Some(frame_start), Some(record)) = (self.start, &mut self.record) else {
791 return;
792 };
793 if record.spans.len() < MAX_SPANS {
794 let at = micros(frame_start, start);
795 record
796 .spans
797 .push(SpanEvent::new(name, at, micros(start, end), depth));
798 } else {
799 record.spans_dropped = record.spans_dropped.saturating_add(1);
800 }
801 }
802}
803
804#[cfg(test)]
805mod tests;