Skip to main content

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;