1use crate::frame_graph::FrameCommandStats;
7use crate::surface_requirements::{SurfaceRequirement, SurfaceRequirementSet};
8use std::cell::{Cell, RefCell};
9
10use cranpose_core::NodeId;
11use cranpose_ui_graphics::Rect;
12
13const TOP_ISOLATED_LAYER_LIMIT: usize = 8;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct LayerSurfaceReasons {
17 pub explicit_offscreen: bool,
18 pub effect: bool,
19 pub backdrop: bool,
20 pub group_opacity: bool,
21 pub blend_mode: bool,
22 pub immediate_shadow: bool,
23 pub text_local_surface: bool,
24 pub motion_stable_capture: bool,
25 pub mixed_direct_content: bool,
26 pub non_translation_transform: bool,
27 pub pixel_stable_composite: bool,
28}
29
30impl LayerSurfaceReasons {
31 pub fn has_any(self) -> bool {
36 self.explicit_offscreen
37 || self.effect
38 || self.backdrop
39 || self.group_opacity
40 || self.blend_mode
41 || self.text_local_surface
42 || self.motion_stable_capture
43 || self.non_translation_transform
44 }
45
46 pub fn labels(self) -> impl Iterator<Item = &'static str> {
47 let mut labels = [None; 11];
48 let mut len = 0usize;
49
50 if self.explicit_offscreen {
51 labels[len] = Some("explicit_offscreen");
52 len += 1;
53 }
54 if self.effect {
55 labels[len] = Some("effect");
56 len += 1;
57 }
58 if self.backdrop {
59 labels[len] = Some("backdrop");
60 len += 1;
61 }
62 if self.group_opacity {
63 labels[len] = Some("group_opacity");
64 len += 1;
65 }
66 if self.blend_mode {
67 labels[len] = Some("blend_mode");
68 len += 1;
69 }
70 if self.immediate_shadow {
71 labels[len] = Some("immediate_shadow");
72 len += 1;
73 }
74 if self.text_local_surface {
75 labels[len] = Some("text_local_surface");
76 len += 1;
77 }
78 if self.motion_stable_capture {
79 labels[len] = Some("motion_stable_capture");
80 len += 1;
81 }
82 if self.mixed_direct_content {
83 labels[len] = Some("mixed_direct_content");
84 len += 1;
85 }
86 if self.non_translation_transform {
87 labels[len] = Some("non_translation_transform");
88 len += 1;
89 }
90 if self.pixel_stable_composite {
91 labels[len] = Some("pixel_stable_composite");
92 len += 1;
93 }
94
95 labels.into_iter().flatten().take(len)
96 }
97
98 pub fn display(self) -> String {
99 let mut joined = String::new();
100 for (index, label) in self.labels().enumerate() {
101 if index > 0 {
102 joined.push('+');
103 }
104 joined.push_str(label);
105 }
106 if joined.is_empty() {
107 joined.push_str("none");
108 }
109 joined
110 }
111
112 pub fn has_renderer_forced_surface(self) -> bool {
113 self.text_local_surface || self.non_translation_transform
114 }
115}
116
117impl From<SurfaceRequirementSet> for LayerSurfaceReasons {
118 fn from(requirements: SurfaceRequirementSet) -> Self {
119 Self {
120 explicit_offscreen: requirements.contains(SurfaceRequirement::ExplicitOffscreen),
121 effect: requirements.contains(SurfaceRequirement::RenderEffect),
122 backdrop: requirements.contains(SurfaceRequirement::Backdrop),
123 group_opacity: requirements.contains(SurfaceRequirement::GroupOpacity),
124 blend_mode: requirements.contains(SurfaceRequirement::BlendMode),
125 immediate_shadow: requirements.contains(SurfaceRequirement::ImmediateShadow),
126 text_local_surface: requirements.contains(SurfaceRequirement::TextMaterialMask),
127 motion_stable_capture: requirements.contains(SurfaceRequirement::MotionStableCapture),
128 mixed_direct_content: requirements.contains(SurfaceRequirement::MixedDirectContent),
129 non_translation_transform: requirements
130 .contains(SurfaceRequirement::NonTranslationTransform),
131 pixel_stable_composite: requirements.contains(SurfaceRequirement::PixelStableComposite),
132 }
133 }
134}
135
136#[derive(Clone, Copy, Debug, PartialEq)]
137pub struct IsolatedLayerStat {
138 pub node_id: Option<NodeId>,
139 pub logical_rect: Rect,
140 pub width: u32,
141 pub height: u32,
142 pub reasons: LayerSurfaceReasons,
143}
144
145impl IsolatedLayerStat {
146 fn pixel_area(self) -> u64 {
147 (self.width as u64) * (self.height as u64)
148 }
149}
150
151impl Default for IsolatedLayerStat {
152 fn default() -> Self {
153 Self {
154 node_id: None,
155 logical_rect: Rect {
156 x: 0.0,
157 y: 0.0,
158 width: 0.0,
159 height: 0.0,
160 },
161 width: 0,
162 height: 0,
163 reasons: LayerSurfaceReasons::default(),
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Default, PartialEq)]
169pub struct FrameStatsSnapshot {
170 pub submits: u32,
171 pub encoder_count: u32,
172 pub submit_count: u32,
173 pub pass_count: u32,
174 pub offscreen_acquires: u32,
175 pub offscreen_news: u32,
176 pub offscreen_total_bytes: u64,
177 pub transient_texture_bytes: u64,
178 pub retained_texture_bytes: u64,
179 pub upload_bytes: u64,
180 pub isolated_layer_renders: u32,
181 pub isolated_layer_pixels: u64,
182 pub layer_cache_hits: u32,
183 pub layer_cache_misses: u32,
184 pub layer_cache_evictions: u32,
185 pub layer_cache_hit_pixels: u64,
186 pub layer_cache_miss_pixels: u64,
187 pub blur_passes: u32,
188 pub composite_passes: u32,
189 pub effect_applies: u32,
190 pub shape_passes: u32,
191 pub image_passes: u32,
192 pub text_passes: u32,
193 pub offscreen_pool_size: u32,
194 pub offscreen_pool_bytes: u64,
195 pub text_pool_size: u32,
196 pub layer_cache_size: u32,
197 pub layer_cache_bytes: u64,
198 pub image_cache_size: u32,
199 pub text_cache_size: u32,
200 pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
201 pub top_isolated_layer_count: usize,
202}
203
204impl FrameStatsSnapshot {
205 pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
206 self.submits = self.submits.saturating_add(stats.submit_count);
207 self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
208 self.submit_count = self.submit_count.saturating_add(stats.submit_count);
209 self.pass_count = self.pass_count.saturating_add(stats.pass_count);
210 self.transient_texture_bytes = self
211 .transient_texture_bytes
212 .saturating_add(stats.transient_texture_bytes);
213 self.retained_texture_bytes = self
214 .retained_texture_bytes
215 .max(stats.retained_texture_bytes);
216 self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
217 self
218 }
219
220 pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
221 self.top_isolated_layers
222 .into_iter()
223 .flatten()
224 .take(self.top_isolated_layer_count)
225 }
226
227 fn layer_cache_hit_rate(self) -> f64 {
228 let total = self.layer_cache_hits + self.layer_cache_misses;
229 if total > 0 {
230 (self.layer_cache_hits as f64 / total as f64) * 100.0
231 } else {
232 0.0
233 }
234 }
235
236 fn print(self, frame_count: u64) {
237 let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
238 let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
239 let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
240 let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
241 let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
242 let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
243 let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
244 let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
245 eprintln!(
246 "[GPU f#{}] encoders={} submits={} passes={} | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) retained={:.1}MB | \
247 uploads={:.2}MB | \
248 isolated_layers={} area={:.2}MP | \
249 layer_cache: hit={} miss={} {:.1}% evict={} hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) | \
250 blur={} composite={} effect={} | shape={} image={} text={} | \
251 caches: text_pool={} img={} txt={}",
252 frame_count,
253 self.encoder_count,
254 self.submit_count,
255 self.pass_count,
256 self.offscreen_acquires,
257 self.offscreen_news,
258 mb,
259 self.offscreen_pool_size,
260 pool_mb,
261 retained_mb,
262 upload_mb,
263 self.isolated_layer_renders,
264 isolated_layer_mpx,
265 self.layer_cache_hits,
266 self.layer_cache_misses,
267 self.layer_cache_hit_rate(),
268 self.layer_cache_evictions,
269 layer_cache_hit_mpx,
270 layer_cache_miss_mpx,
271 self.layer_cache_size,
272 layer_cache_mb,
273 self.blur_passes,
274 self.composite_passes,
275 self.effect_applies,
276 self.shape_passes,
277 self.image_passes,
278 self.text_passes,
279 self.text_pool_size,
280 self.image_cache_size,
281 self.text_cache_size,
282 );
283 for (index, layer) in self.top_isolated_layers().enumerate() {
284 eprintln!(
285 " [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{} reasons={}",
286 layer.node_id,
287 layer.logical_rect.x,
288 layer.logical_rect.y,
289 layer.logical_rect.width,
290 layer.logical_rect.height,
291 layer.width,
292 layer.height,
293 layer.reasons.display(),
294 );
295 }
296 }
297}
298
299#[derive(Default)]
302pub(crate) struct FrameStats {
303 pub submits: Cell<u32>,
304 pub command_encoder_count: Cell<u32>,
305 pub command_submit_count: Cell<u32>,
306 pub command_pass_count: Cell<u32>,
307 pub command_transient_texture_bytes: Cell<u64>,
308 pub command_retained_texture_bytes: Cell<u64>,
309 pub command_upload_bytes: Cell<u64>,
310 pub offscreen_acquires: Cell<u32>,
311 pub offscreen_news: Cell<u32>,
312 pub offscreen_total_bytes: Cell<u64>,
313 pub upload_bytes: Cell<u64>,
314 pub isolated_layer_renders: Cell<u32>,
315 pub isolated_layer_pixels: Cell<u64>,
316 pub layer_cache_hits: Cell<u32>,
317 pub layer_cache_misses: Cell<u32>,
318 pub layer_cache_evictions: Cell<u32>,
319 pub layer_cache_hit_pixels: Cell<u64>,
320 pub layer_cache_miss_pixels: Cell<u64>,
321 pub blur_passes: Cell<u32>,
322 pub composite_passes: Cell<u32>,
323 pub effect_applies: Cell<u32>,
324 pub shape_passes: Cell<u32>,
325 pub image_passes: Cell<u32>,
326 pub text_passes: Cell<u32>,
327 pub offscreen_pool_size: Cell<u32>,
329 pub offscreen_pool_bytes: Cell<u64>,
330 pub text_pool_size: Cell<u32>,
331 pub layer_cache_size: Cell<u32>,
332 pub layer_cache_bytes: Cell<u64>,
333 pub image_cache_size: Cell<u32>,
334 pub text_cache_size: Cell<u32>,
335 top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
336 top_isolated_layer_count: Cell<usize>,
337}
338
339impl FrameStats {
340 pub fn record_command_stats(&self, stats: FrameCommandStats) {
341 self.submits
342 .set(self.submits.get().saturating_add(stats.submit_count));
343 self.command_encoder_count.set(
344 self.command_encoder_count
345 .get()
346 .saturating_add(stats.encoder_count),
347 );
348 self.command_submit_count.set(
349 self.command_submit_count
350 .get()
351 .saturating_add(stats.submit_count),
352 );
353 self.command_pass_count.set(
354 self.command_pass_count
355 .get()
356 .saturating_add(stats.pass_count),
357 );
358 self.command_transient_texture_bytes.set(
359 self.command_transient_texture_bytes
360 .get()
361 .saturating_add(stats.transient_texture_bytes),
362 );
363 self.command_retained_texture_bytes.set(
364 self.command_retained_texture_bytes
365 .get()
366 .max(stats.retained_texture_bytes),
367 );
368 self.command_upload_bytes.set(
369 self.command_upload_bytes
370 .get()
371 .saturating_add(stats.upload_bytes),
372 );
373 }
374
375 pub fn record_offscreen_acquire(&self, width: u32, height: u32, is_new: bool) {
376 self.offscreen_acquires
377 .set(self.offscreen_acquires.get() + 1);
378 if is_new {
379 self.offscreen_news.set(self.offscreen_news.get() + 1);
380 }
381 self.offscreen_total_bytes
382 .set(self.offscreen_total_bytes.get() + (width as u64) * (height as u64) * 4);
383 }
384
385 pub fn record_upload_bytes(&self, bytes: u64) {
386 self.upload_bytes
387 .set(self.upload_bytes.get().saturating_add(bytes));
388 }
389
390 pub fn record_isolated_layer_render(
391 &self,
392 width: u32,
393 height: u32,
394 node_id: Option<NodeId>,
395 logical_rect: Rect,
396 reasons: LayerSurfaceReasons,
397 ) {
398 self.isolated_layer_renders
399 .set(self.isolated_layer_renders.get().saturating_add(1));
400 self.isolated_layer_pixels.set(
401 self.isolated_layer_pixels
402 .get()
403 .saturating_add((width as u64) * (height as u64)),
404 );
405 self.record_top_isolated_layer(IsolatedLayerStat {
406 node_id,
407 logical_rect,
408 width,
409 height,
410 reasons,
411 });
412 }
413
414 pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
415 self.layer_cache_hits
416 .set(self.layer_cache_hits.get().saturating_add(1));
417 self.layer_cache_hit_pixels.set(
418 self.layer_cache_hit_pixels
419 .get()
420 .saturating_add((width as u64) * (height as u64)),
421 );
422 }
423
424 pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
425 self.layer_cache_misses
426 .set(self.layer_cache_misses.get().saturating_add(1));
427 self.layer_cache_miss_pixels.set(
428 self.layer_cache_miss_pixels
429 .get()
430 .saturating_add((width as u64) * (height as u64)),
431 );
432 }
433
434 pub fn record_layer_cache_eviction(&self) {
435 self.layer_cache_evictions
436 .set(self.layer_cache_evictions.get().saturating_add(1));
437 }
438
439 pub fn bump_shapes(&self) {
440 self.shape_passes.set(self.shape_passes.get() + 1);
441 }
442
443 pub fn bump_images(&self) {
444 self.image_passes.set(self.image_passes.get() + 1);
445 }
446
447 pub fn bump_text(&self) {
448 self.text_passes.set(self.text_passes.get() + 1);
449 }
450
451 pub fn snapshot(&self) -> FrameStatsSnapshot {
452 let pass_count = self
453 .blur_passes
454 .get()
455 .saturating_add(self.composite_passes.get())
456 .saturating_add(self.effect_applies.get())
457 .saturating_add(self.shape_passes.get())
458 .saturating_add(self.image_passes.get())
459 .saturating_add(self.text_passes.get());
460 let retained_texture_bytes = self
461 .offscreen_pool_bytes
462 .get()
463 .saturating_add(self.layer_cache_bytes.get());
464 FrameStatsSnapshot {
465 submits: self.submits.get(),
466 encoder_count: self.command_encoder_count.get(),
467 submit_count: self.command_submit_count.get(),
468 pass_count: self.command_pass_count.get().saturating_add(pass_count),
469 offscreen_acquires: self.offscreen_acquires.get(),
470 offscreen_news: self.offscreen_news.get(),
471 offscreen_total_bytes: self.offscreen_total_bytes.get(),
472 transient_texture_bytes: self
473 .offscreen_total_bytes
474 .get()
475 .saturating_add(self.command_transient_texture_bytes.get()),
476 retained_texture_bytes: retained_texture_bytes
477 .saturating_add(self.command_retained_texture_bytes.get()),
478 upload_bytes: self
479 .upload_bytes
480 .get()
481 .saturating_add(self.command_upload_bytes.get()),
482 isolated_layer_renders: self.isolated_layer_renders.get(),
483 isolated_layer_pixels: self.isolated_layer_pixels.get(),
484 layer_cache_hits: self.layer_cache_hits.get(),
485 layer_cache_misses: self.layer_cache_misses.get(),
486 layer_cache_evictions: self.layer_cache_evictions.get(),
487 layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
488 layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
489 blur_passes: self.blur_passes.get(),
490 composite_passes: self.composite_passes.get(),
491 effect_applies: self.effect_applies.get(),
492 shape_passes: self.shape_passes.get(),
493 image_passes: self.image_passes.get(),
494 text_passes: self.text_passes.get(),
495 offscreen_pool_size: self.offscreen_pool_size.get(),
496 offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
497 text_pool_size: self.text_pool_size.get(),
498 layer_cache_size: self.layer_cache_size.get(),
499 layer_cache_bytes: self.layer_cache_bytes.get(),
500 image_cache_size: self.image_cache_size.get(),
501 text_cache_size: self.text_cache_size.get(),
502 top_isolated_layers: *self.top_isolated_layers.borrow(),
503 top_isolated_layer_count: self.top_isolated_layer_count.get(),
504 }
505 }
506
507 pub fn reset(&self) {
508 self.submits.set(0);
509 self.command_encoder_count.set(0);
510 self.command_submit_count.set(0);
511 self.command_pass_count.set(0);
512 self.command_transient_texture_bytes.set(0);
513 self.command_retained_texture_bytes.set(0);
514 self.command_upload_bytes.set(0);
515 self.offscreen_acquires.set(0);
516 self.offscreen_news.set(0);
517 self.offscreen_total_bytes.set(0);
518 self.upload_bytes.set(0);
519 self.isolated_layer_renders.set(0);
520 self.isolated_layer_pixels.set(0);
521 self.layer_cache_hits.set(0);
522 self.layer_cache_misses.set(0);
523 self.layer_cache_evictions.set(0);
524 self.layer_cache_hit_pixels.set(0);
525 self.layer_cache_miss_pixels.set(0);
526 self.blur_passes.set(0);
527 self.composite_passes.set(0);
528 self.effect_applies.set(0);
529 self.shape_passes.set(0);
530 self.image_passes.set(0);
531 self.text_passes.set(0);
532 *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
533 self.top_isolated_layer_count.set(0);
534 }
535
536 pub fn maybe_print_snapshot(
537 &self,
538 snapshot: FrameStatsSnapshot,
539 frame_count: &mut u64,
540 enabled: bool,
541 ) {
542 if !enabled {
543 return;
544 }
545 *frame_count += 1;
546 if (*frame_count).is_multiple_of(60) {
547 snapshot.print(*frame_count);
548 }
549 }
550
551 fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
552 if !layer.reasons.has_any() {
553 return;
554 }
555
556 let mut top_layers = self.top_isolated_layers.borrow_mut();
557 let len = self.top_isolated_layer_count.get();
558 let insert_at = top_layers[..len]
559 .iter()
560 .enumerate()
561 .find_map(|(index, existing)| {
562 existing
563 .filter(|existing| layer.pixel_area() > existing.pixel_area())
564 .map(|_| index)
565 })
566 .unwrap_or(len);
567
568 if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
569 return;
570 }
571
572 let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
573 len + 1
574 } else {
575 TOP_ISOLATED_LAYER_LIMIT
576 };
577
578 let mut index = new_len.saturating_sub(1);
579 while index > insert_at {
580 top_layers[index] = top_layers[index - 1];
581 index -= 1;
582 }
583 top_layers[insert_at] = Some(layer);
584 self.top_isolated_layer_count.set(new_len);
585 }
586}
587
588pub(crate) fn gpu_stats_enabled() -> bool {
589 std::env::var("CRANPOSE_GPU_STATS")
590 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
591 .unwrap_or(false)
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn layer_cache_counters_accumulate_and_reset() {
600 let stats = FrameStats::default();
601 stats.record_upload_bytes(512);
602 stats.record_command_stats(FrameCommandStats {
603 encoder_count: 1,
604 submit_count: 1,
605 pass_count: 0,
606 transient_texture_bytes: 256,
607 retained_texture_bytes: 128,
608 upload_bytes: 64,
609 });
610 stats.bump_shapes();
611 stats.blur_passes.set(1);
612 stats.offscreen_total_bytes.set(1024);
613 stats.offscreen_pool_bytes.set(2048);
614 stats.record_layer_cache_hit(10, 20);
615 stats.record_layer_cache_hit(3, 4);
616 stats.record_layer_cache_miss(5, 6);
617 stats.record_layer_cache_eviction();
618
619 assert_eq!(stats.layer_cache_hits.get(), 2);
620 assert_eq!(stats.layer_cache_misses.get(), 1);
621 assert_eq!(stats.layer_cache_evictions.get(), 1);
622 assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
623 assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
624
625 stats.record_isolated_layer_render(
626 7,
627 8,
628 Some(9),
629 Rect {
630 x: 2.0,
631 y: 3.0,
632 width: 4.0,
633 height: 5.0,
634 },
635 LayerSurfaceReasons {
636 text_local_surface: true,
637 ..LayerSurfaceReasons::default()
638 },
639 );
640 let snapshot = stats.snapshot();
641
642 assert_eq!(snapshot.isolated_layer_renders, 1);
643 assert_eq!(snapshot.isolated_layer_pixels, 56);
644 assert_eq!(snapshot.upload_bytes, 576);
645 assert_eq!(snapshot.encoder_count, 1);
646 assert_eq!(snapshot.submit_count, 1);
647 assert_eq!(snapshot.pass_count, 2);
648 assert_eq!(snapshot.transient_texture_bytes, 1280);
649 assert_eq!(snapshot.retained_texture_bytes, 2176);
650 assert_eq!(snapshot.layer_cache_hits, 2);
651 assert_eq!(snapshot.layer_cache_misses, 1);
652 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
653 assert_eq!(top_layers.len(), 1);
654 assert_eq!(top_layers[0].node_id, Some(9));
655 assert_eq!(stats.layer_cache_hits.get(), 2);
656 assert_eq!(stats.layer_cache_misses.get(), 1);
657
658 stats.reset();
659
660 assert_eq!(stats.layer_cache_hits.get(), 0);
661 assert_eq!(stats.layer_cache_misses.get(), 0);
662 assert_eq!(stats.layer_cache_evictions.get(), 0);
663 assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
664 assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
665 assert_eq!(stats.upload_bytes.get(), 0);
666 assert_eq!(stats.isolated_layer_renders.get(), 0);
667 assert_eq!(stats.isolated_layer_pixels.get(), 0);
668 assert_eq!(stats.top_isolated_layer_count.get(), 0);
669 }
670
671 #[test]
672 fn command_stats_accumulate_and_reset() {
673 let stats = FrameStats::default();
674
675 stats.record_command_stats(FrameCommandStats {
676 encoder_count: 2,
677 submit_count: 2,
678 pass_count: 5,
679 transient_texture_bytes: 1024,
680 retained_texture_bytes: 2048,
681 upload_bytes: 512,
682 });
683 stats.bump_shapes();
684
685 let snapshot = stats.snapshot();
686 assert_eq!(snapshot.submits, 2);
687 assert_eq!(snapshot.encoder_count, 2);
688 assert_eq!(snapshot.submit_count, 2);
689 assert_eq!(snapshot.pass_count, 6);
690 assert_eq!(snapshot.transient_texture_bytes, 1024);
691 assert_eq!(snapshot.retained_texture_bytes, 2048);
692 assert_eq!(snapshot.upload_bytes, 512);
693
694 stats.reset();
695 let reset = stats.snapshot();
696 assert_eq!(reset.submits, 0);
697 assert_eq!(reset.encoder_count, 0);
698 assert_eq!(reset.submit_count, 0);
699 assert_eq!(reset.pass_count, 0);
700 assert_eq!(reset.transient_texture_bytes, 0);
701 assert_eq!(reset.retained_texture_bytes, 0);
702 assert_eq!(reset.upload_bytes, 0);
703 }
704
705 #[test]
706 fn snapshot_adds_explicit_readback_command_stats() {
707 let stats = FrameStats::default();
708 stats.record_command_stats(FrameCommandStats {
709 encoder_count: 1,
710 submit_count: 1,
711 pass_count: 2,
712 transient_texture_bytes: 128,
713 retained_texture_bytes: 512,
714 upload_bytes: 64,
715 });
716 let snapshot = stats
717 .snapshot()
718 .with_command_stats_added(FrameCommandStats {
719 encoder_count: 1,
720 submit_count: 1,
721 pass_count: 1,
722 transient_texture_bytes: 0,
723 retained_texture_bytes: 0,
724 upload_bytes: 0,
725 });
726
727 assert_eq!(snapshot.submits, 2);
728 assert_eq!(snapshot.encoder_count, 2);
729 assert_eq!(snapshot.submit_count, 2);
730 assert_eq!(snapshot.pass_count, 3);
731 assert_eq!(snapshot.transient_texture_bytes, 128);
732 assert_eq!(snapshot.retained_texture_bytes, 512);
733 assert_eq!(snapshot.upload_bytes, 64);
734 }
735
736 #[test]
737 fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
738 let stats = FrameStats::default();
739 let snapshot = stats.snapshot();
740 let mut frame_count = 0;
741
742 stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
743 assert_eq!(frame_count, 0);
744
745 stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
746 assert_eq!(frame_count, 1);
747 }
748
749 #[test]
750 fn layer_surface_reasons_report_runtime_only_bits() {
751 let reasons = LayerSurfaceReasons {
752 immediate_shadow: true,
753 text_local_surface: true,
754 mixed_direct_content: true,
755 ..LayerSurfaceReasons::default()
756 };
757
758 assert!(reasons.has_any());
759 assert!(reasons.has_renderer_forced_surface());
760 assert_eq!(
761 reasons.labels().collect::<Vec<_>>(),
762 vec![
763 "immediate_shadow",
764 "text_local_surface",
765 "mixed_direct_content"
766 ]
767 );
768 assert_eq!(
769 reasons.display(),
770 "immediate_shadow+text_local_surface+mixed_direct_content"
771 );
772 }
773
774 #[test]
775 fn immediate_shadow_only_is_diagnostic_not_isolating() {
776 let reasons = LayerSurfaceReasons {
777 immediate_shadow: true,
778 ..LayerSurfaceReasons::default()
779 };
780
781 assert!(!reasons.has_any());
782 assert!(!reasons.has_renderer_forced_surface());
783 assert_eq!(
784 reasons.labels().collect::<Vec<_>>(),
785 vec!["immediate_shadow"]
786 );
787 assert_eq!(reasons.display(), "immediate_shadow");
788 }
789
790 #[test]
791 fn mixed_direct_content_only_is_diagnostic_not_isolating() {
792 let reasons = LayerSurfaceReasons {
793 mixed_direct_content: true,
794 ..LayerSurfaceReasons::default()
795 };
796
797 assert!(!reasons.has_any());
798 assert!(!reasons.has_renderer_forced_surface());
799 assert_eq!(
800 reasons.labels().collect::<Vec<_>>(),
801 vec!["mixed_direct_content"]
802 );
803 assert_eq!(reasons.display(), "mixed_direct_content");
804 }
805
806 #[test]
807 fn has_any_matches_has_isolating_requirement_for_each_requirement() {
808 let all_requirements = [
809 SurfaceRequirement::ExplicitOffscreen,
810 SurfaceRequirement::RenderEffect,
811 SurfaceRequirement::Backdrop,
812 SurfaceRequirement::GroupOpacity,
813 SurfaceRequirement::BlendMode,
814 SurfaceRequirement::ImmediateShadow,
815 SurfaceRequirement::TextMaterialMask,
816 SurfaceRequirement::MotionStableCapture,
817 SurfaceRequirement::NonTranslationTransform,
818 SurfaceRequirement::MixedDirectContent,
819 SurfaceRequirement::PixelStableComposite,
820 ];
821 for requirement in all_requirements {
822 let set = SurfaceRequirementSet::default().with(requirement);
823 let reasons = LayerSurfaceReasons::from(set);
824 assert_eq!(
825 reasons.has_any(),
826 set.has_isolating_requirement(),
827 "has_any vs has_isolating_requirement mismatch for {requirement:?}"
828 );
829 }
830 }
831
832 #[test]
833 fn top_isolated_layers_keep_largest_runtime_surfaces() {
834 let stats = FrameStats::default();
835 for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
836 stats.record_isolated_layer_render(
837 16 + index as u32,
838 8 + index as u32,
839 Some(index),
840 Rect {
841 x: index as f32,
842 y: 0.0,
843 width: 10.0,
844 height: 10.0,
845 },
846 LayerSurfaceReasons {
847 text_local_surface: true,
848 ..LayerSurfaceReasons::default()
849 },
850 );
851 }
852
853 let snapshot = stats.snapshot();
854 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
855 assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
856 assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
857 assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
858 }
859}