1use 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
22pub const VIEWPORT: Size = Size::new(1280.0, 800.0);
24pub const SCALE: f32 = 2.0;
26const FRAME: f64 = 1.0 / 120.0;
28const SETTLE: u32 = 16;
30const WARM: u32 = 32;
32pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Class {
41 Static,
43 Virtualized,
45 Animated,
47 Picture,
49 App,
51}
52
53pub type Step = Box<dyn FnMut(&mut Ui, u32) -> bool>;
56
57pub struct Setup {
59 pub root: AnyView,
61 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#[derive(Clone, Copy, Debug)]
73pub struct Ceiling {
74 pub interaction: &'static str,
76 pub counter: &'static str,
78 pub per_round: u64,
80}
81
82pub const fn ceiling(interaction: &'static str, counter: &'static str, per_round: u64) -> Ceiling {
84 Ceiling { interaction, counter, per_round }
85}
86
87#[derive(Clone, Copy, Debug)]
89pub struct PerfScene {
90 pub name: &'static str,
92 pub covers: &'static [&'static str],
94 pub class: Class,
96 pub interactions: &'static [&'static str],
98 pub make: fn(&str) -> Setup,
100 pub ceilings: &'static [Ceiling],
102}
103
104pub type Counters = BTreeMap<&'static str, u64>;
106
107pub const REPORTED_ONLY: &[&str] = &["render_allocations"];
109
110#[derive(Clone, Debug)]
112pub struct Outcome {
113 pub name: String,
115 pub counters: Counters,
117 pub frames: Vec<FrameTime>,
119}
120
121#[derive(Clone, Copy, Debug, Default)]
123pub struct FrameTime {
124 pub ui: Duration,
126 pub build: Duration,
128 pub layout: Duration,
130 pub place: Duration,
132 pub paint: Duration,
134 pub render: Duration,
136 pub gpu: Option<Duration>,
138}
139
140pub 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 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 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 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#[derive(Clone, Debug)]
265pub struct Visit {
266 pub name: String,
268 pub worst: Duration,
270 pub frames: u32,
272 pub rasterized: u64,
274}
275
276impl Bench {
277 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 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 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
386pub 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
394pub 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
419pub 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
427pub fn nothing() -> Step {
429 Box::new(|_, _| false)
430}
431
432pub 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}