Skip to main content

ravenrs_perf/
lib.rs

1//! The performance gate of RavenRS.
2//!
3//! A [`PerfScene`] is a screen and the interactions to try on it. [`Bench::measure`] drives
4//! it with a real [`Ui`] and a real [`Renderer`] on an offscreen texture, without a window,
5//! and counts the work: builds, layouts, paints, shaping, allocations, draw calls and
6//! bytes sent to the GPU. Those counts are exact, so `gate` can compare them with a
7//! baseline on every run, where a time measured on a laptop would only be noise.
8//!
9//! Times are measured apart by `time`, before a release.
10
11use std::collections::BTreeMap;
12use std::time::{Duration, Instant};
13
14use ravenrs_core::{BuildCause, Event, Point, Rect, Role, Size};
15use ravenrs_render::{GpuContext, RenderStats, Renderer, wgpu};
16use ravenrs_style::Theme;
17use ravenrs_widgets::{AnyView, Ui, UiStats};
18
19pub mod capacity;
20pub mod scenes;
21
22/// The window the scenes are measured in, in logical pixels.
23pub const VIEWPORT: Size = Size::new(1280.0, 800.0);
24/// Physical pixels per logical pixel: the laptop's screen.
25pub const SCALE: f32 = 2.0;
26/// Time between frames on the 120 Hz screen.
27const FRAME: f64 = 1.0 / 120.0;
28/// Frames before anything is looked at, for layout feedback to settle.
29const SETTLE: u32 = 16;
30/// Rounds of the interaction before counting, for caches to fill.
31const WARM: u32 = 32;
32/// Rounds counted, which ceilings are multiplied by.
33pub const COUNTED_ROUNDS: u32 = 16;
34
35const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm;
36const TARGET: (u32, u32) = ((VIEWPORT.width * SCALE) as u32, (VIEWPORT.height * SCALE) as u32);
37
38/// What a component is, which says what may be asked of it.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Class {
41    /// Draws what it has; costs nothing once drawn.
42    Static,
43    /// Shows a window over much more data: the cost follows the window.
44    Virtualized,
45    /// Moves by itself.
46    Animated,
47    /// A picture made on the CPU, whose cost is paid when its data or size changes.
48    Picture,
49    /// A whole screen of a real application.
50    App,
51}
52
53/// One round of an interaction: `true` asks for a frame, as a platform does when
54/// [`Ui::handle_event`] says so.
55pub type Step = Box<dyn FnMut(&mut Ui, u32) -> bool>;
56
57/// A screen ready to be measured under one interaction.
58pub struct Setup {
59    /// What is on screen.
60    pub root: AnyView,
61    /// What the interaction does each round.
62    pub step: Step,
63}
64
65impl std::fmt::Debug for Setup {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("Setup").finish_non_exhaustive()
68    }
69}
70
71/// A limit on a counter, per round. A baseline can follow a slow decay; this cannot.
72#[derive(Clone, Copy, Debug)]
73pub struct Ceiling {
74    /// The interaction it applies to, or `"*"`.
75    pub interaction: &'static str,
76    /// The counter.
77    pub counter: &'static str,
78    /// The most it may reach per round counted.
79    pub per_round: u64,
80}
81
82/// A shorter way to write a [`Ceiling`].
83pub const fn ceiling(interaction: &'static str, counter: &'static str, per_round: u64) -> Ceiling {
84    Ceiling { interaction, counter, per_round }
85}
86
87/// A screen and the interactions to try on it.
88#[derive(Clone, Copy, Debug)]
89pub struct PerfScene {
90    /// Its name in reports and in the baseline.
91    pub name: &'static str,
92    /// The public components it answers for.
93    pub covers: &'static [&'static str],
94    /// What kind of thing it is.
95    pub class: Class,
96    /// The interactions to try.
97    pub interactions: &'static [&'static str],
98    /// Makes a fresh copy of the screen for one interaction.
99    pub make: fn(&str) -> Setup,
100    /// The limits that no baseline can move.
101    pub ceilings: &'static [Ceiling],
102}
103
104/// Counts of the work done during the rounds counted, by name.
105pub type Counters = BTreeMap<&'static str, u64>;
106
107/// Counters that vary with the driver, so they are shown but never compared.
108pub const REPORTED_ONLY: &[&str] = &["render_allocations"];
109
110/// What an interaction did.
111#[derive(Clone, Debug)]
112pub struct Outcome {
113    /// `scene/interaction`.
114    pub name: String,
115    /// The counts.
116    pub counters: Counters,
117    /// How long each frame took, when asked for.
118    pub frames: Vec<FrameTime>,
119}
120
121/// What one frame cost.
122#[derive(Clone, Copy, Debug, Default)]
123pub struct FrameTime {
124    /// Handling the event, building, laying out and painting.
125    pub ui: Duration,
126    /// Of that, the views running.
127    pub build: Duration,
128    /// Of that, solving the layout.
129    pub layout: Duration,
130    /// Of that, placing elements.
131    pub place: Duration,
132    /// Of that, painting.
133    pub paint: Duration,
134    /// Recording and submitting the GPU's work.
135    pub render: Duration,
136    /// The GPU's own time, from a frame or two before.
137    pub gpu: Option<Duration>,
138}
139
140/// Where the measurements run: the GPU, and what counts allocations.
141pub struct Bench {
142    gpu: GpuContext,
143    target: wgpu::TextureView,
144    allocations: fn() -> u64,
145}
146
147impl std::fmt::Debug for Bench {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("Bench").finish_non_exhaustive()
150    }
151}
152
153impl Bench {
154    /// Opens the GPU. `allocations` tells how many allocations happened so far; only the
155    /// binary can install the allocator that knows.
156    pub fn new(allocations: fn() -> u64) -> Result<Self, String> {
157        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
158        let gpu = pollster::block_on(GpuContext::new(&instance, None)).map_err(|e| e.to_string())?;
159        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
160            label: Some("perf target"),
161            size: wgpu::Extent3d { width: TARGET.0, height: TARGET.1, depth_or_array_layers: 1 },
162            mip_level_count: 1,
163            sample_count: 1,
164            dimension: wgpu::TextureDimension::D2,
165            format: FORMAT,
166            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
167            view_formats: &[],
168        });
169        let target = texture.create_view(&wgpu::TextureViewDescriptor::default());
170        Ok(Self { gpu, target, allocations })
171    }
172
173    /// The GPU being measured.
174    pub fn adapter(&self) -> String {
175        let info = self.gpu.adapter.get_info();
176        format!("{} ({:?}), {}x{} target", info.name, info.backend, TARGET.0, TARGET.1)
177    }
178
179    fn wait(&self) {
180        let _ = self.gpu.device.poll(wgpu::PollType::wait_indefinitely());
181    }
182
183    fn frame(&self, ui: &mut Ui, renderer: &mut Renderer) {
184        renderer.render(ui.update(), &self.target, TARGET, SCALE);
185        self.wait();
186    }
187
188    /// Drives one interaction of `scene` and counts what it does. With `rounds`, it
189    /// counts that many and also keeps their times, with the GPU's clock on.
190    pub fn measure(&self, scene: &PerfScene, interaction: &str, rounds: Option<u32>) -> Outcome {
191        let Setup { root, mut step } = (scene.make)(interaction);
192        let mut renderer = Renderer::new(&self.gpu.device, &self.gpu.queue, FORMAT);
193        let mut ui = Ui::new(root, Theme::raven_light());
194        ui.set_viewport(VIEWPORT, SCALE);
195        let timed = rounds.is_some();
196        if timed {
197            let started = Instant::now();
198            ui.set_phase_clock(Some(std::rc::Rc::new(move || started.elapsed().as_secs_f64())));
199            renderer.set_gpu_timing(true);
200        }
201        let mut now = 0.0;
202        for _ in 0..SETTLE {
203            now += FRAME;
204            ui.set_time(now);
205            self.frame(&mut ui, &mut renderer);
206        }
207        let mut round = 0;
208        for _ in 0..WARM {
209            now += FRAME;
210            ui.set_time(now);
211            let asked = step(&mut ui, round);
212            round += 1;
213            if asked || ui.needs_redraw() {
214                self.frame(&mut ui, &mut renderer);
215            }
216        }
217
218        let before = ui.stats();
219        let mut tally = Tally::default();
220        let mut frames = Vec::new();
221        let mut first_stats: Option<RenderStats> = None;
222        for _ in 0..rounds.unwrap_or(COUNTED_ROUNDS) {
223            now += FRAME;
224            ui.set_time(now);
225            let phases = ui.phase_times();
226            let (allocations, started) = ((self.allocations)(), Instant::now());
227            let asked = step(&mut ui, round);
228            round += 1;
229            let draw = asked || ui.needs_redraw();
230            if draw {
231                ui.update();
232            }
233            let spent = started.elapsed();
234            tally.ui_allocations += (self.allocations)() - allocations;
235            if !draw {
236                continue;
237            }
238            let (allocations, started) = ((self.allocations)(), Instant::now());
239            let stats = renderer.render(ui.scene(), &self.target, TARGET, SCALE);
240            let rendered = started.elapsed();
241            tally.render_allocations += (self.allocations)() - allocations;
242            self.wait();
243            tally.add(&stats, first_stats.get_or_insert(stats));
244            if timed {
245                let after = ui.phase_times();
246                frames.push(FrameTime {
247                    ui: spent,
248                    build: after.build - phases.build,
249                    layout: after.layout - phases.layout,
250                    place: after.place - phases.place,
251                    paint: after.paint - phases.paint,
252                    render: rendered,
253                    gpu: stats.gpu_time,
254                });
255            }
256        }
257        let counters = tally.into_counters(before, ui.stats(), &ui);
258        Outcome { name: format!("{}/{interaction}", scene.name), counters, frames }
259    }
260}
261
262/// One thing opened for the first or a later time: its worst frame, which is the one a
263/// person sees stutter. Warm frames say nothing about this.
264#[derive(Clone, Debug)]
265pub struct Visit {
266    /// What was opened.
267    pub name: String,
268    /// The worst frame's time on the CPU, from building to submitting.
269    pub worst: Duration,
270    /// Frames it took to settle.
271    pub frames: u32,
272    /// Glyphs rasterized on the way.
273    pub rasterized: u64,
274}
275
276impl Bench {
277    /// Puts `root` on screen for the first time, then runs each visit in turn, each
278    /// until the screen stops asking for frames.
279    pub fn tour(
280        &self,
281        root: AnyView,
282        prepare: impl FnOnce(&mut Ui),
283        visits: Vec<(String, Box<dyn FnOnce()>)>,
284    ) -> Vec<Visit> {
285        let mut renderer = Renderer::new(&self.gpu.device, &self.gpu.queue, FORMAT);
286        let mut ui = Ui::new(root, Theme::raven_light());
287        ui.set_viewport(VIEWPORT, SCALE);
288        let mut now = 0.0;
289        let mut out = vec![self.settle("first frame", &mut ui, &mut renderer, &mut now)];
290        prepare(&mut ui);
291        out.push(self.settle("signed in", &mut ui, &mut renderer, &mut now));
292        for (name, visit) in visits {
293            visit();
294            out.push(self.settle(&name, &mut ui, &mut renderer, &mut now));
295        }
296        // Each visit saw the running total; what it added is what it cost.
297        let mut seen = 0;
298        for visit in &mut out {
299            let total = visit.rasterized.max(seen);
300            visit.rasterized = total - seen;
301            seen = total;
302        }
303        out
304    }
305
306    fn settle(&self, name: &str, ui: &mut Ui, renderer: &mut Renderer, now: &mut f64) -> Visit {
307        let mut visit = Visit { name: name.to_owned(), worst: Duration::ZERO, frames: 0, rasterized: 0 };
308        // Long enough for transitions; what keeps moving forever stops being counted here.
309        for _ in 0..60 {
310            *now += FRAME;
311            ui.set_time(*now);
312            if !ui.needs_redraw() && visit.frames > 0 {
313                break;
314            }
315            let started = Instant::now();
316            let stats = renderer.render(ui.update(), &self.target, TARGET, SCALE);
317            visit.worst = visit.worst.max(started.elapsed());
318            visit.frames += 1;
319            self.wait();
320            visit.rasterized = stats.glyphs_rasterized;
321        }
322        visit
323    }
324}
325
326#[derive(Default)]
327struct Tally {
328    frames: u64,
329    ui_allocations: u64,
330    render_allocations: u64,
331    draw_calls: u64,
332    breaks_kind: u64,
333    breaks_image: u64,
334    breaks_clip: u64,
335    instances: u64,
336    uploaded_bytes: u64,
337    rasterized: u64,
338    resets: u64,
339}
340
341impl Tally {
342    fn add(&mut self, stats: &RenderStats, first: &RenderStats) {
343        self.frames += 1;
344        self.draw_calls += stats.draw_calls as u64;
345        self.breaks_kind += stats.breaks_kind as u64;
346        self.breaks_image += stats.breaks_image as u64;
347        self.breaks_clip += stats.breaks_clip as u64;
348        self.instances += (stats.quads + stats.glyphs + stats.images + stats.paths) as u64;
349        self.uploaded_bytes += stats.uploaded_bytes as u64;
350        self.rasterized = stats.glyphs_rasterized - first.glyphs_rasterized;
351        self.resets = stats.atlas_resets - first.atlas_resets;
352    }
353
354    fn into_counters(self, before: UiStats, after: UiStats, ui: &Ui) -> Counters {
355        Counters::from([
356            ("frames", self.frames),
357            ("builds", after.builds - before.builds),
358            ("layouts", after.layouts - before.layouts),
359            ("placements", after.placements - before.placements),
360            ("paints", after.paints - before.paints),
361            ("shapes", after.shapes - before.shapes),
362            ("ui_allocations", self.ui_allocations),
363            ("render_allocations", self.render_allocations),
364            ("draw_calls", self.draw_calls),
365            ("breaks_kind", self.breaks_kind),
366            ("breaks_image", self.breaks_image),
367            ("breaks_clip", self.breaks_clip),
368            ("instances", self.instances),
369            ("uploaded_bytes", self.uploaded_bytes),
370            ("glyphs_rasterized", self.rasterized),
371            ("atlas_resets", self.resets),
372            ("elements", ui.element_count() as u64),
373            ("primitives", ui.scene().len() as u64),
374            ("wakes_scheduled", u64::from(ui.next_wake().is_some())),
375            ("cause_data", after.causes.count(BuildCause::Data) - before.causes.count(BuildCause::Data)),
376            ("cause_hover", after.causes.count(BuildCause::Hover) - before.causes.count(BuildCause::Hover)),
377            ("cause_clock", after.causes.count(BuildCause::Clock) - before.causes.count(BuildCause::Clock)),
378            (
379                "cause_scroll",
380                after.causes.count(BuildCause::Scroll) - before.causes.count(BuildCause::Scroll),
381            ),
382        ])
383    }
384}
385
386/// Moves the pointer back and forth between two places, as if crossing two controls.
387pub fn hover_between(a: Point, b: Point) -> Step {
388    Box::new(move |ui, round| {
389        let position = if round % 2 == 0 { a } else { b };
390        ui.handle_event(&Event::PointerMove { position })
391    })
392}
393
394/// Moves the pointer back and forth between the first two buttons on screen, so
395/// every round really changes what is hovered, wherever the layout put them.
396pub fn hover_buttons() -> Step {
397    let mut places: Option<(Point, Point)> = None;
398    Box::new(move |ui, round| {
399        let (a, b) = *places.get_or_insert_with(|| {
400            let viewport = Rect::from_origin_size(Point::ZERO, VIEWPORT);
401            let found: Vec<Point> = ui
402                .access_tree()
403                .nodes
404                .iter()
405                .filter(|node| node.role == Role::Button && viewport.contains(node.bounds.center()))
406                .map(|node| node.bounds.center())
407                .take(2)
408                .collect();
409            match found.as_slice() {
410                [a, b] => (*a, *b),
411                _ => panic!("a hover scene needs two buttons on screen"),
412            }
413        });
414        let position = if round % 2 == 0 { a } else { b };
415        ui.handle_event(&Event::PointerMove { position })
416    })
417}
418
419/// Scrolls down and back up at `position`.
420pub fn scroll_at(position: Point) -> Step {
421    Box::new(move |ui, round| {
422        let delta = Point::new(0.0, if round % 2 == 0 { -40.0 } else { 40.0 });
423        ui.handle_event(&Event::Scroll { position, delta })
424    })
425}
426
427/// Nothing happens: what is left is what the screen does by itself.
428pub fn nothing() -> Step {
429    Box::new(|_, _| false)
430}
431
432/// The window changes size by a pixel, which redoes everything from scratch.
433pub fn resize() -> Step {
434    Box::new(|ui, round| {
435        let width = VIEWPORT.width - (round % 2) as f32;
436        ui.set_viewport(Size::new(width, VIEWPORT.height), SCALE);
437        true
438    })
439}