//! One place to ask what the machine is holding. //! //! Every storage primitive already knows its own occupancy, its high-water mark //! and how many times it refused. What it cannot do by itself is be *found*: a //! caller that wants the whole picture has to name each one, read five //! accessors off it, and know whether the numbers are bytes or items. That //! knowledge spreads — into the shell's report line, into a debug overlay, into //! whatever records baselines — and every copy of it is a place a budget can be //! added without being reported, which is the one failure a budget report must //! not have. //! //! So the shape here is deliberately small: a [`BudgetReport`] is plain data, a //! [`Budgeted`] thing hands over one or more of them, and [`visit_all`] walks a //! borrowed list of them. A consumer sees reports and never sees an arena, a //! pool, or a draw list. //! //! # The registry does not own anything //! //! There is no registration step and nothing to keep in sync. A report is built //! on demand by reading the live primitive, so there is no second copy of the //! truth that can go stale, and no way to hold a report *and* the thing it //! describes and have them disagree about anything except time. //! //! The "registry" is therefore a borrowed slice at the call site: //! //! ``` //! use microverse_core::budget::{self, BudgetReport}; //! use microverse_core::{Arena, DrawList}; //! //! let level = Arena::with_capacity("level", 4096); //! let scratch = Arena::with_capacity("frame", 1024); //! let draws = DrawList::with_capacity("draws", 512); //! //! let mut lines = 0; //! budget::visit_all(&[&level, &scratch, &draws], &mut |b: BudgetReport| { //! assert!(b.used <= b.cap); //! lines += 1; //! }); //! assert_eq!(lines, 3); //! ``` //! //! That array is a stack literal sized by the compiler, so adding a budget is //! one word at one site and there is no registry capacity to pick — which //! matters, because a capacity picked before anything measures one is exactly //! the guess `BUDGETS.md` exists to replace. //! //! # Why a visitor and not an iterator //! //! Three shapes were available and two of them cost something the crate is not //! willing to pay. //! //! Returning `Vec` allocates, which is denied here and would put //! the report out of reach of anything on the frame path. //! //! Returning `impl Iterator` does not allocate, but it is //! not object-safe, so `&dyn Budgeted` would be impossible and the borrowed //! slice above could not exist. Aggregates make it worse: a [`Graph`] holds two //! budgets, and a world will hold an aggregate of aggregates, so every level of //! nesting would chain another iterator type through the signature. //! //! A visitor composes across that tree for free — an aggregate calls its //! children's `budgets` with the same closure — stays object-safe, and //! allocates nothing. The cost is that a caller cannot lazily take the first //! two and stop, which nothing wants to do with a budget report. //! //! [`Graph`]: crate::transform::Graph //! //! # Order is stable //! //! Reports arrive in the order the implementations visit them, which is source //! order and therefore fixed. That is a requirement rather than an //! accident: a baseline that is recorded and diffed //! (headway:gamedev/ugly-elephant-slide) compares runs line by line, and a set //! that reordered itself between runs would read as every budget changing at //! once. use core::fmt; /// What a budget is counted in. /// /// The unit is carried rather than inferred from the name because a consumer /// formatting `84/4096` has no other way to know whether to write `B` or /// `items`, and guessing from a name like `"level"` is the kind of thing that /// works until somebody adds a pool called `"bytes"`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum BudgetKind { /// Slots, nodes, commands — anything counted one at a time. Items, /// Bytes of a byte buffer, as an [`Arena`] counts them. /// /// [`Arena`]: crate::arena::Arena Bytes, } impl BudgetKind { /// The unit, short enough to sit after a number in a report line. pub const fn unit(self) -> &'static str { match self { BudgetKind::Items => "items", BudgetKind::Bytes => "B", } } } /// What one bounded resource is holding, and what it has held. /// /// Plain `Copy` data with public fields. It is a *reading*, not a handle: it /// borrows nothing, so it can be stored, compared against a previous run, or /// sent somewhere, and it says nothing about the primitive it came from beyond /// these six numbers. /// /// The counts are `u32` rather than `usize` so that a report means the same /// thing on every target and can be compared against one recorded elsewhere. /// See [`BudgetReport::from_usize_counts`] for what happens to a count that /// does not fit. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct BudgetReport { /// What the resource calls itself — the name it was constructed with. pub name: &'static str, /// Occupancy right now. pub used: u32, /// The fixed capacity it was built with. pub cap: u32, /// The high-water mark since the last `reset_stats`, which is the number a /// budget should be set from. pub peak: u32, /// How many operations were refused for want of capacity since the last /// `reset_stats`. pub overflows: u32, /// Whether the counts are items or bytes. pub kind: BudgetKind, } impl BudgetReport { /// Build a report from counts a primitive keeps as `usize`. /// /// Counts that exceed [`u32::MAX`] **saturate**, and that choice is the /// interesting part. Wrapping would turn a 5 GiB arena into a plausible /// small number and quietly make the report a lie; saturating produces /// `4294967295`, which is not a number any budget in this engine has and /// therefore reads as "go and look" rather than as data. Neither case is /// expected — the engine measures its budgets in tens of kilobytes — so the /// question is only which failure is legible, and an absurd number is. pub const fn from_usize_counts( name: &'static str, used: usize, cap: usize, peak: usize, overflows: u32, kind: BudgetKind, ) -> Self { Self { name, used: saturate(used), cap: saturate(cap), peak: saturate(peak), overflows, kind, } } /// Occupancy as a fraction of capacity, for a bar an overlay can draw. /// /// A zero-capacity resource reads as `1.0`. It holds nothing and never can, /// so every operation on it overflows — "full" is the honest reading, and /// it is the one that keeps `0 / 0` from putting a `NaN` into a layout. pub fn fraction(&self) -> f32 { if self.cap == 0 { return 1.0; } self.used as f32 / self.cap as f32 } /// The high-water mark as a fraction of capacity. See [`Self::fraction`] /// for the zero-capacity case, which is the same. pub fn peak_fraction(&self) -> f32 { if self.cap == 0 { return 1.0; } self.peak as f32 / self.cap as f32 } /// Whether anything was refused for want of capacity. /// /// The question a report line is usually being scanned for, and a name for /// it here keeps `> 0` from being written at every call site. pub const fn overflowed(&self) -> bool { self.overflows > 0 } } impl fmt::Display for BudgetReport { /// One line, in the order a reader asks the questions: what is it, how full /// is it, how full has it been, did it refuse anything. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{} {}/{} {} peak {} overflows {}", self.name, self.used, self.cap, self.kind.unit(), self.peak, self.overflows, ) } } /// Something that can say what it is holding. /// /// Implemented by each storage primitive for itself, and by anything that owns /// several so that a consumer never has to know the difference. A [`Graph`] /// reports two budgets, a [`Pool`] reports one, and both are one call. /// /// [`Graph`]: crate::transform::Graph /// [`Pool`]: crate::pool::Pool /// /// # Implementing it /// /// Visit every budget you own, in a fixed order, and visit nothing else. An /// aggregate forwards the same closure to its children: /// /// ``` /// use microverse_core::budget::{BudgetReport, Budgeted}; /// use microverse_core::{Arena, DrawList}; /// /// struct Machine { /// scratch: Arena, /// draws: DrawList, /// } /// /// impl Budgeted for Machine { /// fn budgets(&self, visit: &mut dyn FnMut(BudgetReport)) { /// self.scratch.budgets(visit); /// self.draws.budgets(visit); /// } /// } /// /// let m = Machine { /// scratch: Arena::with_capacity("frame", 64), /// draws: DrawList::with_capacity("draws", 8), /// }; /// /// let mut names = ["", ""]; /// let mut n = 0; /// m.budgets(&mut |b| { /// names[n] = b.name; /// n += 1; /// }); /// assert_eq!(names, ["frame", "draws"]); /// ``` /// /// The closure is `&mut dyn FnMut` rather than a generic parameter so that the /// trait stays object-safe. That is what makes `&dyn Budgeted` — and therefore /// [`visit_all`] — possible, and it costs one indirect call per budget on a /// path that runs once a second at most. pub trait Budgeted { /// Hand every budget this owns to `visit`, in a stable order. fn budgets(&self, visit: &mut dyn FnMut(BudgetReport)); } /// Walk several budgeted things as though they were one. /// /// The whole registry: a borrowed slice, read in order, owning nothing. The /// shell builds the slice at the one site that knows what exists, and every /// consumer downstream of this call sees only reports. /// /// ``` /// use microverse_core::budget; /// use microverse_core::{Arena, DrawList}; /// /// let level = Arena::with_capacity("level", 128); /// let draws = DrawList::with_capacity("draws", 4); /// /// let mut worst = 0.0f32; /// budget::visit_all(&[&level, &draws], &mut |b| worst = worst.max(b.fraction())); /// ``` pub fn visit_all(sources: &[&dyn Budgeted], visit: &mut dyn FnMut(BudgetReport)) { for source in sources { source.budgets(visit); } } /// A `usize` count as a `u32`, saturating rather than wrapping. See /// [`BudgetReport::from_usize_counts`] for why. const fn saturate(n: usize) -> u32 { if n > u32::MAX as usize { u32::MAX } else { n as u32 } } #[cfg(test)] // Test code allocates freely. The rule is about the frame path, and a test that // had to assemble its expectations in a fixed-capacity buffer would be testing // the buffer. What allocation the frame path actually does is measured in // `tests/no_alloc.rs`, not guessed at from a lint. #[allow( clippy::disallowed_methods, clippy::disallowed_macros, reason = "test code is not the frame path" )] mod tests { use super::*; fn report(used: u32, cap: u32) -> BudgetReport { BudgetReport { name: "b", used, cap, peak: used, overflows: 0, kind: BudgetKind::Items } } #[test] fn a_report_reads_as_one_line_with_its_unit() { let bytes = BudgetReport { name: "level", used: 84, cap: 4096, peak: 84, overflows: 0, kind: BudgetKind::Bytes, }; assert_eq!(bytes.to_string(), "level 84/4096 B peak 84 overflows 0"); let items = BudgetReport { name: "draws", used: 441, cap: 4096, peak: 441, overflows: 2, kind: BudgetKind::Items, }; assert_eq!(items.to_string(), "draws 441/4096 items peak 441 overflows 2"); } #[test] fn occupancy_is_a_fraction_and_an_empty_budget_is_not_full() { assert_eq!(report(0, 4).fraction(), 0.0); assert_eq!(report(2, 4).fraction(), 0.5); assert_eq!(report(4, 4).fraction(), 1.0); } /// A zero-capacity budget is a real, tested configuration — a draw list /// that may submit nothing — and the arithmetic must not hand a `NaN` to /// whatever is laying out a bar. #[test] fn a_budget_with_no_capacity_reads_as_full_rather_than_as_nan() { let empty = report(0, 0); assert_eq!(empty.fraction(), 1.0); assert_eq!(empty.peak_fraction(), 1.0); assert!(!empty.fraction().is_nan()); } /// The failure that matters is a count that *wraps* into a plausible small /// number. Saturation is chosen so an impossible count reads as impossible. #[test] fn a_count_too_large_for_the_report_saturates_instead_of_wrapping() { let huge = u32::MAX as usize + 1; let b = BudgetReport::from_usize_counts("vast", huge, huge, huge, 0, BudgetKind::Bytes); assert_eq!(b.used, u32::MAX, "a wrap here would have read as 0"); assert_eq!(b.cap, u32::MAX); assert_eq!(b.peak, u32::MAX); } #[test] fn overflowed_is_the_question_a_report_line_is_scanned_for() { assert!(!report(1, 4).overflowed()); let mut refused = report(4, 4); refused.overflows = 1; assert!(refused.overflowed()); } }