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 shape_clip: bool,
23 pub immediate_shadow: bool,
24 pub text_local_surface: bool,
25 pub motion_stable_capture: bool,
26 pub mixed_direct_content: bool,
27 pub non_translation_transform: bool,
28 pub pixel_stable_composite: bool,
29}
30
31impl LayerSurfaceReasons {
32 pub fn has_any(self) -> bool {
37 self.explicit_offscreen
38 || self.effect
39 || self.backdrop
40 || self.group_opacity
41 || self.blend_mode
42 || self.shape_clip
43 || self.text_local_surface
44 || self.motion_stable_capture
45 || self.non_translation_transform
46 }
47
48 pub fn labels(self) -> impl Iterator<Item = &'static str> {
49 let mut labels = [None; 12];
50 let mut len = 0usize;
51
52 if self.explicit_offscreen {
53 labels[len] = Some("explicit_offscreen");
54 len += 1;
55 }
56 if self.effect {
57 labels[len] = Some("effect");
58 len += 1;
59 }
60 if self.backdrop {
61 labels[len] = Some("backdrop");
62 len += 1;
63 }
64 if self.group_opacity {
65 labels[len] = Some("group_opacity");
66 len += 1;
67 }
68 if self.blend_mode {
69 labels[len] = Some("blend_mode");
70 len += 1;
71 }
72 if self.shape_clip {
73 labels[len] = Some("shape_clip");
74 len += 1;
75 }
76 if self.immediate_shadow {
77 labels[len] = Some("immediate_shadow");
78 len += 1;
79 }
80 if self.text_local_surface {
81 labels[len] = Some("text_local_surface");
82 len += 1;
83 }
84 if self.motion_stable_capture {
85 labels[len] = Some("motion_stable_capture");
86 len += 1;
87 }
88 if self.mixed_direct_content {
89 labels[len] = Some("mixed_direct_content");
90 len += 1;
91 }
92 if self.non_translation_transform {
93 labels[len] = Some("non_translation_transform");
94 len += 1;
95 }
96 if self.pixel_stable_composite {
97 labels[len] = Some("pixel_stable_composite");
98 len += 1;
99 }
100
101 labels.into_iter().flatten().take(len)
102 }
103
104 pub fn display(self) -> String {
105 let mut joined = String::new();
106 for (index, label) in self.labels().enumerate() {
107 if index > 0 {
108 joined.push('+');
109 }
110 joined.push_str(label);
111 }
112 if joined.is_empty() {
113 joined.push_str("none");
114 }
115 joined
116 }
117
118 pub fn has_renderer_forced_surface(self) -> bool {
119 self.text_local_surface || self.non_translation_transform
120 }
121}
122
123impl From<SurfaceRequirementSet> for LayerSurfaceReasons {
124 fn from(requirements: SurfaceRequirementSet) -> Self {
125 Self {
126 explicit_offscreen: requirements.contains(SurfaceRequirement::ExplicitOffscreen),
127 effect: requirements.contains(SurfaceRequirement::RenderEffect),
128 backdrop: requirements.contains(SurfaceRequirement::Backdrop),
129 group_opacity: requirements.contains(SurfaceRequirement::GroupOpacity),
130 blend_mode: requirements.contains(SurfaceRequirement::BlendMode),
131 shape_clip: requirements.contains(SurfaceRequirement::ShapeClip),
132 immediate_shadow: requirements.contains(SurfaceRequirement::ImmediateShadow),
133 text_local_surface: requirements.contains(SurfaceRequirement::TextMaterialMask),
134 motion_stable_capture: requirements.contains(SurfaceRequirement::MotionStableCapture),
135 mixed_direct_content: requirements.contains(SurfaceRequirement::MixedDirectContent),
136 non_translation_transform: requirements
137 .contains(SurfaceRequirement::NonTranslationTransform),
138 pixel_stable_composite: requirements.contains(SurfaceRequirement::PixelStableComposite),
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct IsolatedLayerStat {
145 pub node_id: Option<NodeId>,
146 pub logical_rect: Rect,
147 pub width: u32,
148 pub height: u32,
149 pub reasons: LayerSurfaceReasons,
150}
151
152impl IsolatedLayerStat {
153 fn pixel_area(self) -> u64 {
154 (self.width as u64) * (self.height as u64)
155 }
156}
157
158impl Default for IsolatedLayerStat {
159 fn default() -> Self {
160 Self {
161 node_id: None,
162 logical_rect: Rect {
163 x: 0.0,
164 y: 0.0,
165 width: 0.0,
166 height: 0.0,
167 },
168 width: 0,
169 height: 0,
170 reasons: LayerSurfaceReasons::default(),
171 }
172 }
173}
174
175#[derive(Clone, Copy, Debug, Default, PartialEq)]
176pub struct FrameStatsSnapshot {
177 pub submits: u32,
178 pub encoder_count: u32,
179 pub submit_count: u32,
180 pub pass_count: u32,
181 pub offscreen_acquires: u32,
182 pub offscreen_news: u32,
183 pub offscreen_total_bytes: u64,
184 pub transient_texture_bytes: u64,
185 pub retained_texture_bytes: u64,
186 pub upload_bytes: u64,
187 pub isolated_layer_renders: u32,
188 pub isolated_layer_pixels: u64,
189 pub layer_cache_hits: u32,
190 pub layer_cache_misses: u32,
191 pub layer_cache_evictions: u32,
192 pub layer_cache_hit_pixels: u64,
193 pub layer_cache_miss_pixels: u64,
194 pub shadow_shape_cache_hits: u32,
195 pub shadow_shape_cache_misses: u32,
196 pub shadow_shape_cache_hit_pixels: u64,
197 pub shadow_shape_cache_miss_pixels: u64,
198 pub shadow_text_blur_fallbacks: u32,
199 pub blur_passes: u32,
200 pub composite_passes: u32,
201 pub effect_applies: u32,
202 pub shape_passes: u32,
203 pub image_passes: u32,
204 pub text_passes: u32,
205 pub text_image_cache_hits: u32,
206 pub text_image_cache_misses: u32,
207 pub text_image_cache_hit_pixels: u64,
208 pub text_image_cache_miss_pixels: u64,
209 pub text_image_raster_bytes: u64,
210 pub text_glyph_atlas_hits: u32,
211 pub text_glyph_atlas_misses: u32,
212 pub text_glyph_atlas_miss_pixels: u64,
213 pub offscreen_pool_size: u32,
214 pub offscreen_pool_bytes: u64,
215 pub text_pool_size: u32,
216 pub layer_cache_size: u32,
217 pub layer_cache_bytes: u64,
218 pub image_cache_size: u32,
219 pub text_cache_size: u32,
220 pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
221 pub top_isolated_layer_count: usize,
222}
223
224impl FrameStatsSnapshot {
225 pub(crate) fn with_command_stats_added(mut self, stats: FrameCommandStats) -> Self {
226 self.submits = self.submits.saturating_add(stats.submit_count);
227 self.encoder_count = self.encoder_count.saturating_add(stats.encoder_count);
228 self.submit_count = self.submit_count.saturating_add(stats.submit_count);
229 self.pass_count = self.pass_count.saturating_add(stats.pass_count);
230 self.transient_texture_bytes = self
231 .transient_texture_bytes
232 .saturating_add(stats.transient_texture_bytes);
233 self.retained_texture_bytes = self
234 .retained_texture_bytes
235 .max(stats.retained_texture_bytes);
236 self.upload_bytes = self.upload_bytes.saturating_add(stats.upload_bytes);
237 self
238 }
239
240 pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
241 self.top_isolated_layers
242 .into_iter()
243 .flatten()
244 .take(self.top_isolated_layer_count)
245 }
246
247 fn layer_cache_hit_rate(self) -> f64 {
248 let total = self.layer_cache_hits + self.layer_cache_misses;
249 if total > 0 {
250 (self.layer_cache_hits as f64 / total as f64) * 100.0
251 } else {
252 0.0
253 }
254 }
255
256 fn print(self, frame_count: u64) {
257 let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
258 let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
259 let retained_mb = self.retained_texture_bytes as f64 / (1024.0 * 1024.0);
260 let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
261 let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
262 let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
263 let shadow_cache_hit_mpx = self.shadow_shape_cache_hit_pixels as f64 / 1_000_000.0;
264 let shadow_cache_miss_mpx = self.shadow_shape_cache_miss_pixels as f64 / 1_000_000.0;
265 let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
266 let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
267 eprintln!(
268 "[GPU f#{}] encoders={} submits={} passes={} | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) retained={:.1}MB | \
269 uploads={:.2}MB | \
270 isolated_layers={} area={:.2}MP | \
271 layer_cache: hit={} miss={} {:.1}% evict={} hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) | \
272 shadow_cache: shape_hit={} shape_miss={} hit_px={:.2}MP miss_px={:.2}MP text_blur_fallback={} | \
273 blur={} composite={} effect={} | shape={} image={} text={} | \
274 text_img_cache: hit={} miss={} hit_px={:.2}MP miss_px={:.2}MP raster={:.2}MB | \
275 text_glyph_atlas: hit={} miss={} miss_px={:.2}MP | \
276 caches: text_pool={} img={} txt={}",
277 frame_count,
278 self.encoder_count,
279 self.submit_count,
280 self.pass_count,
281 self.offscreen_acquires,
282 self.offscreen_news,
283 mb,
284 self.offscreen_pool_size,
285 pool_mb,
286 retained_mb,
287 upload_mb,
288 self.isolated_layer_renders,
289 isolated_layer_mpx,
290 self.layer_cache_hits,
291 self.layer_cache_misses,
292 self.layer_cache_hit_rate(),
293 self.layer_cache_evictions,
294 layer_cache_hit_mpx,
295 layer_cache_miss_mpx,
296 self.layer_cache_size,
297 layer_cache_mb,
298 self.shadow_shape_cache_hits,
299 self.shadow_shape_cache_misses,
300 shadow_cache_hit_mpx,
301 shadow_cache_miss_mpx,
302 self.shadow_text_blur_fallbacks,
303 self.blur_passes,
304 self.composite_passes,
305 self.effect_applies,
306 self.shape_passes,
307 self.image_passes,
308 self.text_passes,
309 self.text_image_cache_hits,
310 self.text_image_cache_misses,
311 self.text_image_cache_hit_pixels as f64 / 1_000_000.0,
312 self.text_image_cache_miss_pixels as f64 / 1_000_000.0,
313 self.text_image_raster_bytes as f64 / (1024.0 * 1024.0),
314 self.text_glyph_atlas_hits,
315 self.text_glyph_atlas_misses,
316 self.text_glyph_atlas_miss_pixels as f64 / 1_000_000.0,
317 self.text_pool_size,
318 self.image_cache_size,
319 self.text_cache_size,
320 );
321 for (index, layer) in self.top_isolated_layers().enumerate() {
322 eprintln!(
323 " [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{} reasons={}",
324 layer.node_id,
325 layer.logical_rect.x,
326 layer.logical_rect.y,
327 layer.logical_rect.width,
328 layer.logical_rect.height,
329 layer.width,
330 layer.height,
331 layer.reasons.display(),
332 );
333 }
334 }
335}
336
337#[derive(Default)]
340pub(crate) struct FrameStats {
341 pub submits: Cell<u32>,
342 pub command_encoder_count: Cell<u32>,
343 pub command_submit_count: Cell<u32>,
344 pub command_pass_count: Cell<u32>,
345 pub command_transient_texture_bytes: Cell<u64>,
346 pub command_retained_texture_bytes: Cell<u64>,
347 pub command_upload_bytes: Cell<u64>,
348 pub offscreen_acquires: Cell<u32>,
349 pub offscreen_news: Cell<u32>,
350 pub offscreen_total_bytes: Cell<u64>,
351 pub upload_bytes: Cell<u64>,
352 pub isolated_layer_renders: Cell<u32>,
353 pub isolated_layer_pixels: Cell<u64>,
354 pub layer_cache_hits: Cell<u32>,
355 pub layer_cache_misses: Cell<u32>,
356 pub layer_cache_evictions: Cell<u32>,
357 pub layer_cache_hit_pixels: Cell<u64>,
358 pub layer_cache_miss_pixels: Cell<u64>,
359 pub shadow_shape_cache_hits: Cell<u32>,
360 pub shadow_shape_cache_misses: Cell<u32>,
361 pub shadow_shape_cache_hit_pixels: Cell<u64>,
362 pub shadow_shape_cache_miss_pixels: Cell<u64>,
363 pub shadow_text_blur_fallbacks: Cell<u32>,
364 pub blur_passes: Cell<u32>,
365 pub composite_passes: Cell<u32>,
366 pub effect_applies: Cell<u32>,
367 pub shape_passes: Cell<u32>,
368 pub image_passes: Cell<u32>,
369 pub text_passes: Cell<u32>,
370 pub text_image_cache_hits: Cell<u32>,
371 pub text_image_cache_misses: Cell<u32>,
372 pub text_image_cache_hit_pixels: Cell<u64>,
373 pub text_image_cache_miss_pixels: Cell<u64>,
374 pub text_image_raster_bytes: Cell<u64>,
375 pub text_glyph_atlas_hits: Cell<u32>,
376 pub text_glyph_atlas_misses: Cell<u32>,
377 pub text_glyph_atlas_miss_pixels: Cell<u64>,
378 pub offscreen_pool_size: Cell<u32>,
380 pub offscreen_pool_bytes: Cell<u64>,
381 pub text_pool_size: Cell<u32>,
382 pub layer_cache_size: Cell<u32>,
383 pub layer_cache_bytes: Cell<u64>,
384 pub image_cache_size: Cell<u32>,
385 pub text_cache_size: Cell<u32>,
386 top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
387 top_isolated_layer_count: Cell<usize>,
388 shadow_shape_cache_miss_log_count: Cell<u32>,
389}
390
391impl FrameStats {
392 pub fn record_command_stats(&self, stats: FrameCommandStats) {
393 self.submits
394 .set(self.submits.get().saturating_add(stats.submit_count));
395 self.command_encoder_count.set(
396 self.command_encoder_count
397 .get()
398 .saturating_add(stats.encoder_count),
399 );
400 self.command_submit_count.set(
401 self.command_submit_count
402 .get()
403 .saturating_add(stats.submit_count),
404 );
405 self.command_pass_count.set(
406 self.command_pass_count
407 .get()
408 .saturating_add(stats.pass_count),
409 );
410 self.command_transient_texture_bytes.set(
411 self.command_transient_texture_bytes
412 .get()
413 .saturating_add(stats.transient_texture_bytes),
414 );
415 self.command_retained_texture_bytes.set(
416 self.command_retained_texture_bytes
417 .get()
418 .max(stats.retained_texture_bytes),
419 );
420 self.command_upload_bytes.set(
421 self.command_upload_bytes
422 .get()
423 .saturating_add(stats.upload_bytes),
424 );
425 }
426
427 pub fn record_offscreen_acquire(&self, width: u32, height: u32, is_new: bool) {
428 self.offscreen_acquires
429 .set(self.offscreen_acquires.get() + 1);
430 if is_new {
431 self.offscreen_news.set(self.offscreen_news.get() + 1);
432 }
433 self.offscreen_total_bytes
434 .set(self.offscreen_total_bytes.get() + (width as u64) * (height as u64) * 4);
435 }
436
437 pub fn record_upload_bytes(&self, bytes: u64) {
438 self.upload_bytes
439 .set(self.upload_bytes.get().saturating_add(bytes));
440 }
441
442 pub fn record_isolated_layer_render(
443 &self,
444 width: u32,
445 height: u32,
446 node_id: Option<NodeId>,
447 logical_rect: Rect,
448 reasons: LayerSurfaceReasons,
449 ) {
450 self.isolated_layer_renders
451 .set(self.isolated_layer_renders.get().saturating_add(1));
452 self.isolated_layer_pixels.set(
453 self.isolated_layer_pixels
454 .get()
455 .saturating_add((width as u64) * (height as u64)),
456 );
457 self.record_top_isolated_layer(IsolatedLayerStat {
458 node_id,
459 logical_rect,
460 width,
461 height,
462 reasons,
463 });
464 }
465
466 pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
467 self.layer_cache_hits
468 .set(self.layer_cache_hits.get().saturating_add(1));
469 self.layer_cache_hit_pixels.set(
470 self.layer_cache_hit_pixels
471 .get()
472 .saturating_add((width as u64) * (height as u64)),
473 );
474 }
475
476 pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
477 self.layer_cache_misses
478 .set(self.layer_cache_misses.get().saturating_add(1));
479 self.layer_cache_miss_pixels.set(
480 self.layer_cache_miss_pixels
481 .get()
482 .saturating_add((width as u64) * (height as u64)),
483 );
484 }
485
486 pub fn record_layer_cache_eviction(&self) {
487 self.layer_cache_evictions
488 .set(self.layer_cache_evictions.get().saturating_add(1));
489 }
490
491 pub fn record_shadow_shape_cache_hit(&self, width: u32, height: u32) {
492 self.shadow_shape_cache_hits
493 .set(self.shadow_shape_cache_hits.get().saturating_add(1));
494 self.shadow_shape_cache_hit_pixels.set(
495 self.shadow_shape_cache_hit_pixels
496 .get()
497 .saturating_add((width as u64) * (height as u64)),
498 );
499 }
500
501 pub fn record_shadow_shape_cache_miss(&self, width: u32, height: u32) {
502 self.shadow_shape_cache_misses
503 .set(self.shadow_shape_cache_misses.get().saturating_add(1));
504 self.shadow_shape_cache_miss_pixels.set(
505 self.shadow_shape_cache_miss_pixels
506 .get()
507 .saturating_add((width as u64) * (height as u64)),
508 );
509 }
510
511 #[allow(clippy::too_many_arguments)]
512 pub fn maybe_print_shadow_shape_cache_miss(
513 &self,
514 width: u32,
515 height: u32,
516 content_hash: u64,
517 blur_radius: f32,
518 viewport_offset: [f32; 2],
519 shape_count: usize,
520 clip: Option<Rect>,
521 ) {
522 if !shadow_cache_diagnostics_enabled() {
523 return;
524 }
525
526 let count = self.shadow_shape_cache_miss_log_count.get();
527 if count >= 16 {
528 return;
529 }
530 self.shadow_shape_cache_miss_log_count.set(count + 1);
531
532 let clip_text = clip
533 .map(|clip| {
534 format!(
535 "({:.1},{:.1},{:.1},{:.1})",
536 clip.x, clip.y, clip.width, clip.height
537 )
538 })
539 .unwrap_or_else(|| "none".to_string());
540 eprintln!(
541 "[shadow-cache-miss #{count}] size={}x{} content_hash={content_hash} blur={:.2} viewport_offset=({:.1},{:.1}) shapes={} clip={}",
542 width,
543 height,
544 blur_radius,
545 viewport_offset[0],
546 viewport_offset[1],
547 shape_count,
548 clip_text,
549 );
550 }
551
552 pub fn record_shadow_text_blur_fallback(&self) {
553 self.shadow_text_blur_fallbacks
554 .set(self.shadow_text_blur_fallbacks.get().saturating_add(1));
555 }
556
557 pub fn bump_shapes(&self) {
558 self.shape_passes.set(self.shape_passes.get() + 1);
559 }
560
561 pub fn bump_images(&self) {
562 self.image_passes.set(self.image_passes.get() + 1);
563 }
564
565 pub fn bump_text(&self) {
566 self.text_passes.set(self.text_passes.get() + 1);
567 }
568
569 pub fn record_text_image_cache_hit(&self, width: u32, height: u32) {
570 self.text_image_cache_hits
571 .set(self.text_image_cache_hits.get().saturating_add(1));
572 self.text_image_cache_hit_pixels.set(
573 self.text_image_cache_hit_pixels
574 .get()
575 .saturating_add((width as u64) * (height as u64)),
576 );
577 }
578
579 pub fn record_text_image_cache_miss(&self, width: u32, height: u32) {
580 let pixels = (width as u64) * (height as u64);
581 self.text_image_cache_misses
582 .set(self.text_image_cache_misses.get().saturating_add(1));
583 self.text_image_cache_miss_pixels.set(
584 self.text_image_cache_miss_pixels
585 .get()
586 .saturating_add(pixels),
587 );
588 self.text_image_raster_bytes.set(
589 self.text_image_raster_bytes
590 .get()
591 .saturating_add(pixels * 4),
592 );
593 }
594
595 pub fn record_text_glyph_atlas_hit(&self) {
596 self.text_glyph_atlas_hits
597 .set(self.text_glyph_atlas_hits.get().saturating_add(1));
598 }
599
600 pub fn record_text_glyph_atlas_miss(&self, width: u32, height: u32) {
601 self.text_glyph_atlas_misses
602 .set(self.text_glyph_atlas_misses.get().saturating_add(1));
603 self.text_glyph_atlas_miss_pixels.set(
604 self.text_glyph_atlas_miss_pixels
605 .get()
606 .saturating_add((width as u64) * (height as u64)),
607 );
608 }
609
610 pub fn snapshot(&self) -> FrameStatsSnapshot {
611 let retained_texture_bytes = self
612 .offscreen_pool_bytes
613 .get()
614 .saturating_add(self.layer_cache_bytes.get());
615 FrameStatsSnapshot {
616 submits: self.submits.get(),
617 encoder_count: self.command_encoder_count.get(),
618 submit_count: self.command_submit_count.get(),
619 pass_count: self.command_pass_count.get(),
620 offscreen_acquires: self.offscreen_acquires.get(),
621 offscreen_news: self.offscreen_news.get(),
622 offscreen_total_bytes: self.offscreen_total_bytes.get(),
623 transient_texture_bytes: self
624 .offscreen_total_bytes
625 .get()
626 .saturating_add(self.command_transient_texture_bytes.get()),
627 retained_texture_bytes: retained_texture_bytes
628 .saturating_add(self.command_retained_texture_bytes.get()),
629 upload_bytes: self
630 .upload_bytes
631 .get()
632 .saturating_add(self.command_upload_bytes.get()),
633 isolated_layer_renders: self.isolated_layer_renders.get(),
634 isolated_layer_pixels: self.isolated_layer_pixels.get(),
635 layer_cache_hits: self.layer_cache_hits.get(),
636 layer_cache_misses: self.layer_cache_misses.get(),
637 layer_cache_evictions: self.layer_cache_evictions.get(),
638 layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
639 layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
640 shadow_shape_cache_hits: self.shadow_shape_cache_hits.get(),
641 shadow_shape_cache_misses: self.shadow_shape_cache_misses.get(),
642 shadow_shape_cache_hit_pixels: self.shadow_shape_cache_hit_pixels.get(),
643 shadow_shape_cache_miss_pixels: self.shadow_shape_cache_miss_pixels.get(),
644 shadow_text_blur_fallbacks: self.shadow_text_blur_fallbacks.get(),
645 blur_passes: self.blur_passes.get(),
646 composite_passes: self.composite_passes.get(),
647 effect_applies: self.effect_applies.get(),
648 shape_passes: self.shape_passes.get(),
649 image_passes: self.image_passes.get(),
650 text_passes: self.text_passes.get(),
651 text_image_cache_hits: self.text_image_cache_hits.get(),
652 text_image_cache_misses: self.text_image_cache_misses.get(),
653 text_image_cache_hit_pixels: self.text_image_cache_hit_pixels.get(),
654 text_image_cache_miss_pixels: self.text_image_cache_miss_pixels.get(),
655 text_image_raster_bytes: self.text_image_raster_bytes.get(),
656 text_glyph_atlas_hits: self.text_glyph_atlas_hits.get(),
657 text_glyph_atlas_misses: self.text_glyph_atlas_misses.get(),
658 text_glyph_atlas_miss_pixels: self.text_glyph_atlas_miss_pixels.get(),
659 offscreen_pool_size: self.offscreen_pool_size.get(),
660 offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
661 text_pool_size: self.text_pool_size.get(),
662 layer_cache_size: self.layer_cache_size.get(),
663 layer_cache_bytes: self.layer_cache_bytes.get(),
664 image_cache_size: self.image_cache_size.get(),
665 text_cache_size: self.text_cache_size.get(),
666 top_isolated_layers: *self.top_isolated_layers.borrow(),
667 top_isolated_layer_count: self.top_isolated_layer_count.get(),
668 }
669 }
670
671 pub fn reset(&self) {
672 self.submits.set(0);
673 self.command_encoder_count.set(0);
674 self.command_submit_count.set(0);
675 self.command_pass_count.set(0);
676 self.command_transient_texture_bytes.set(0);
677 self.command_retained_texture_bytes.set(0);
678 self.command_upload_bytes.set(0);
679 self.offscreen_acquires.set(0);
680 self.offscreen_news.set(0);
681 self.offscreen_total_bytes.set(0);
682 self.upload_bytes.set(0);
683 self.isolated_layer_renders.set(0);
684 self.isolated_layer_pixels.set(0);
685 self.layer_cache_hits.set(0);
686 self.layer_cache_misses.set(0);
687 self.layer_cache_evictions.set(0);
688 self.layer_cache_hit_pixels.set(0);
689 self.layer_cache_miss_pixels.set(0);
690 self.shadow_shape_cache_hits.set(0);
691 self.shadow_shape_cache_misses.set(0);
692 self.shadow_shape_cache_hit_pixels.set(0);
693 self.shadow_shape_cache_miss_pixels.set(0);
694 self.shadow_text_blur_fallbacks.set(0);
695 self.blur_passes.set(0);
696 self.composite_passes.set(0);
697 self.effect_applies.set(0);
698 self.shape_passes.set(0);
699 self.image_passes.set(0);
700 self.text_passes.set(0);
701 self.text_image_cache_hits.set(0);
702 self.text_image_cache_misses.set(0);
703 self.text_image_cache_hit_pixels.set(0);
704 self.text_image_cache_miss_pixels.set(0);
705 self.text_image_raster_bytes.set(0);
706 self.text_glyph_atlas_hits.set(0);
707 self.text_glyph_atlas_misses.set(0);
708 self.text_glyph_atlas_miss_pixels.set(0);
709 *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
710 self.top_isolated_layer_count.set(0);
711 self.shadow_shape_cache_miss_log_count.set(0);
712 }
713
714 pub fn maybe_print_snapshot(
715 &self,
716 snapshot: FrameStatsSnapshot,
717 frame_count: &mut u64,
718 enabled: bool,
719 ) {
720 if !enabled {
721 return;
722 }
723 *frame_count += 1;
724 if (*frame_count).is_multiple_of(60) {
725 snapshot.print(*frame_count);
726 }
727 }
728
729 fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
730 if !layer.reasons.has_any() {
731 return;
732 }
733
734 let mut top_layers = self.top_isolated_layers.borrow_mut();
735 let len = self.top_isolated_layer_count.get();
736 let insert_at = top_layers[..len]
737 .iter()
738 .enumerate()
739 .find_map(|(index, existing)| {
740 existing
741 .filter(|existing| layer.pixel_area() > existing.pixel_area())
742 .map(|_| index)
743 })
744 .unwrap_or(len);
745
746 if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
747 return;
748 }
749
750 let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
751 len + 1
752 } else {
753 TOP_ISOLATED_LAYER_LIMIT
754 };
755
756 let mut index = new_len.saturating_sub(1);
757 while index > insert_at {
758 top_layers[index] = top_layers[index - 1];
759 index -= 1;
760 }
761 top_layers[insert_at] = Some(layer);
762 self.top_isolated_layer_count.set(new_len);
763 }
764}
765
766pub(crate) fn gpu_stats_enabled() -> bool {
767 std::env::var("CRANPOSE_GPU_STATS")
768 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
769 .unwrap_or(false)
770}
771
772fn shadow_cache_diagnostics_enabled() -> bool {
773 std::env::var("CRANPOSE_GPU_SHADOW_CACHE_DIAG")
774 .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
775 .unwrap_or(false)
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781
782 #[test]
783 fn layer_cache_counters_accumulate_and_reset() {
784 let stats = FrameStats::default();
785 stats.record_upload_bytes(512);
786 stats.record_command_stats(FrameCommandStats {
787 encoder_count: 1,
788 submit_count: 1,
789 pass_count: 2,
790 transient_texture_bytes: 256,
791 retained_texture_bytes: 128,
792 upload_bytes: 64,
793 });
794 stats.bump_shapes();
795 stats.blur_passes.set(1);
796 stats.offscreen_total_bytes.set(1024);
797 stats.offscreen_pool_bytes.set(2048);
798 stats.record_layer_cache_hit(10, 20);
799 stats.record_layer_cache_hit(3, 4);
800 stats.record_layer_cache_miss(5, 6);
801 stats.record_layer_cache_eviction();
802 stats.record_shadow_shape_cache_hit(8, 9);
803 stats.record_shadow_shape_cache_miss(10, 11);
804 stats.record_shadow_text_blur_fallback();
805 stats.record_text_image_cache_hit(13, 17);
806 stats.record_text_image_cache_miss(19, 23);
807
808 assert_eq!(stats.layer_cache_hits.get(), 2);
809 assert_eq!(stats.layer_cache_misses.get(), 1);
810 assert_eq!(stats.layer_cache_evictions.get(), 1);
811 assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
812 assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
813 assert_eq!(stats.shadow_shape_cache_hits.get(), 1);
814 assert_eq!(stats.shadow_shape_cache_misses.get(), 1);
815 assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 72);
816 assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 110);
817 assert_eq!(stats.shadow_text_blur_fallbacks.get(), 1);
818
819 stats.record_isolated_layer_render(
820 7,
821 8,
822 Some(9),
823 Rect {
824 x: 2.0,
825 y: 3.0,
826 width: 4.0,
827 height: 5.0,
828 },
829 LayerSurfaceReasons {
830 text_local_surface: true,
831 ..LayerSurfaceReasons::default()
832 },
833 );
834 let snapshot = stats.snapshot();
835
836 assert_eq!(snapshot.isolated_layer_renders, 1);
837 assert_eq!(snapshot.isolated_layer_pixels, 56);
838 assert_eq!(snapshot.upload_bytes, 576);
839 assert_eq!(snapshot.encoder_count, 1);
840 assert_eq!(snapshot.submit_count, 1);
841 assert_eq!(snapshot.pass_count, 2);
842 assert_eq!(snapshot.transient_texture_bytes, 1280);
843 assert_eq!(snapshot.retained_texture_bytes, 2176);
844 assert_eq!(snapshot.layer_cache_hits, 2);
845 assert_eq!(snapshot.layer_cache_misses, 1);
846 assert_eq!(snapshot.shadow_shape_cache_hits, 1);
847 assert_eq!(snapshot.shadow_shape_cache_misses, 1);
848 assert_eq!(snapshot.shadow_shape_cache_hit_pixels, 72);
849 assert_eq!(snapshot.shadow_shape_cache_miss_pixels, 110);
850 assert_eq!(snapshot.shadow_text_blur_fallbacks, 1);
851 assert_eq!(snapshot.text_image_cache_hits, 1);
852 assert_eq!(snapshot.text_image_cache_misses, 1);
853 assert_eq!(snapshot.text_image_cache_hit_pixels, 221);
854 assert_eq!(snapshot.text_image_cache_miss_pixels, 437);
855 assert_eq!(snapshot.text_image_raster_bytes, 1748);
856 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
857 assert_eq!(top_layers.len(), 1);
858 assert_eq!(top_layers[0].node_id, Some(9));
859 assert_eq!(stats.layer_cache_hits.get(), 2);
860 assert_eq!(stats.layer_cache_misses.get(), 1);
861
862 stats.reset();
863
864 assert_eq!(stats.layer_cache_hits.get(), 0);
865 assert_eq!(stats.layer_cache_misses.get(), 0);
866 assert_eq!(stats.layer_cache_evictions.get(), 0);
867 assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
868 assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
869 assert_eq!(stats.shadow_shape_cache_hits.get(), 0);
870 assert_eq!(stats.shadow_shape_cache_misses.get(), 0);
871 assert_eq!(stats.shadow_shape_cache_hit_pixels.get(), 0);
872 assert_eq!(stats.shadow_shape_cache_miss_pixels.get(), 0);
873 assert_eq!(stats.shadow_text_blur_fallbacks.get(), 0);
874 assert_eq!(stats.text_image_cache_hits.get(), 0);
875 assert_eq!(stats.text_image_cache_misses.get(), 0);
876 assert_eq!(stats.text_image_cache_hit_pixels.get(), 0);
877 assert_eq!(stats.text_image_cache_miss_pixels.get(), 0);
878 assert_eq!(stats.text_image_raster_bytes.get(), 0);
879 assert_eq!(stats.upload_bytes.get(), 0);
880 assert_eq!(stats.isolated_layer_renders.get(), 0);
881 assert_eq!(stats.isolated_layer_pixels.get(), 0);
882 assert_eq!(stats.top_isolated_layer_count.get(), 0);
883 }
884
885 #[test]
886 fn command_stats_accumulate_and_reset() {
887 let stats = FrameStats::default();
888
889 stats.record_command_stats(FrameCommandStats {
890 encoder_count: 2,
891 submit_count: 2,
892 pass_count: 5,
893 transient_texture_bytes: 1024,
894 retained_texture_bytes: 2048,
895 upload_bytes: 512,
896 });
897 stats.bump_shapes();
898
899 let snapshot = stats.snapshot();
900 assert_eq!(snapshot.submits, 2);
901 assert_eq!(snapshot.encoder_count, 2);
902 assert_eq!(snapshot.submit_count, 2);
903 assert_eq!(snapshot.pass_count, 5);
904 assert_eq!(snapshot.transient_texture_bytes, 1024);
905 assert_eq!(snapshot.retained_texture_bytes, 2048);
906 assert_eq!(snapshot.upload_bytes, 512);
907
908 stats.reset();
909 let reset = stats.snapshot();
910 assert_eq!(reset.submits, 0);
911 assert_eq!(reset.encoder_count, 0);
912 assert_eq!(reset.submit_count, 0);
913 assert_eq!(reset.pass_count, 0);
914 assert_eq!(reset.transient_texture_bytes, 0);
915 assert_eq!(reset.retained_texture_bytes, 0);
916 assert_eq!(reset.upload_bytes, 0);
917 }
918
919 #[test]
920 fn snapshot_adds_explicit_readback_command_stats() {
921 let stats = FrameStats::default();
922 stats.record_command_stats(FrameCommandStats {
923 encoder_count: 1,
924 submit_count: 1,
925 pass_count: 2,
926 transient_texture_bytes: 128,
927 retained_texture_bytes: 512,
928 upload_bytes: 64,
929 });
930 let snapshot = stats
931 .snapshot()
932 .with_command_stats_added(FrameCommandStats {
933 encoder_count: 1,
934 submit_count: 1,
935 pass_count: 1,
936 transient_texture_bytes: 0,
937 retained_texture_bytes: 0,
938 upload_bytes: 0,
939 });
940
941 assert_eq!(snapshot.submits, 2);
942 assert_eq!(snapshot.encoder_count, 2);
943 assert_eq!(snapshot.submit_count, 2);
944 assert_eq!(snapshot.pass_count, 3);
945 assert_eq!(snapshot.transient_texture_bytes, 128);
946 assert_eq!(snapshot.retained_texture_bytes, 512);
947 assert_eq!(snapshot.upload_bytes, 64);
948 }
949
950 #[test]
951 fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
952 let stats = FrameStats::default();
953 let snapshot = stats.snapshot();
954 let mut frame_count = 0;
955
956 stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
957 assert_eq!(frame_count, 0);
958
959 stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
960 assert_eq!(frame_count, 1);
961 }
962
963 #[test]
964 fn layer_surface_reasons_report_runtime_only_bits() {
965 let reasons = LayerSurfaceReasons {
966 immediate_shadow: true,
967 text_local_surface: true,
968 mixed_direct_content: true,
969 ..LayerSurfaceReasons::default()
970 };
971
972 assert!(reasons.has_any());
973 assert!(reasons.has_renderer_forced_surface());
974 assert_eq!(
975 reasons.labels().collect::<Vec<_>>(),
976 vec![
977 "immediate_shadow",
978 "text_local_surface",
979 "mixed_direct_content"
980 ]
981 );
982 assert_eq!(
983 reasons.display(),
984 "immediate_shadow+text_local_surface+mixed_direct_content"
985 );
986 }
987
988 #[test]
989 fn immediate_shadow_only_is_diagnostic_not_isolating() {
990 let reasons = LayerSurfaceReasons {
991 immediate_shadow: true,
992 ..LayerSurfaceReasons::default()
993 };
994
995 assert!(!reasons.has_any());
996 assert!(!reasons.has_renderer_forced_surface());
997 assert_eq!(
998 reasons.labels().collect::<Vec<_>>(),
999 vec!["immediate_shadow"]
1000 );
1001 assert_eq!(reasons.display(), "immediate_shadow");
1002 }
1003
1004 #[test]
1005 fn mixed_direct_content_only_is_diagnostic_not_isolating() {
1006 let reasons = LayerSurfaceReasons {
1007 mixed_direct_content: true,
1008 ..LayerSurfaceReasons::default()
1009 };
1010
1011 assert!(!reasons.has_any());
1012 assert!(!reasons.has_renderer_forced_surface());
1013 assert_eq!(
1014 reasons.labels().collect::<Vec<_>>(),
1015 vec!["mixed_direct_content"]
1016 );
1017 assert_eq!(reasons.display(), "mixed_direct_content");
1018 }
1019
1020 #[test]
1021 fn has_any_matches_has_isolating_requirement_for_each_requirement() {
1022 let all_requirements = [
1023 SurfaceRequirement::ExplicitOffscreen,
1024 SurfaceRequirement::RenderEffect,
1025 SurfaceRequirement::Backdrop,
1026 SurfaceRequirement::GroupOpacity,
1027 SurfaceRequirement::BlendMode,
1028 SurfaceRequirement::ShapeClip,
1029 SurfaceRequirement::ImmediateShadow,
1030 SurfaceRequirement::TextMaterialMask,
1031 SurfaceRequirement::MotionStableCapture,
1032 SurfaceRequirement::NonTranslationTransform,
1033 SurfaceRequirement::MixedDirectContent,
1034 SurfaceRequirement::PixelStableComposite,
1035 ];
1036 for requirement in all_requirements {
1037 let set = SurfaceRequirementSet::default().with(requirement);
1038 let reasons = LayerSurfaceReasons::from(set);
1039 assert_eq!(
1040 reasons.has_any(),
1041 set.has_isolating_requirement(),
1042 "has_any vs has_isolating_requirement mismatch for {requirement:?}"
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn top_isolated_layers_keep_largest_runtime_surfaces() {
1049 let stats = FrameStats::default();
1050 for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
1051 stats.record_isolated_layer_render(
1052 16 + index as u32,
1053 8 + index as u32,
1054 Some(index),
1055 Rect {
1056 x: index as f32,
1057 y: 0.0,
1058 width: 10.0,
1059 height: 10.0,
1060 },
1061 LayerSurfaceReasons {
1062 text_local_surface: true,
1063 ..LayerSurfaceReasons::default()
1064 },
1065 );
1066 }
1067
1068 let snapshot = stats.snapshot();
1069 let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
1070 assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
1071 assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
1072 assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
1073 }
1074}