Skip to main content

cranpose_render_wgpu/
gpu_stats.rs

1//! Per-frame GPU render counters.
2//!
3//! Counters are always collected so tests and perf harnesses can assert them.
4//! Setting `CRANPOSE_GPU_STATS=1` prints a summary line every 60 frames to stderr.
5
6use 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    /// Returns true if any isolating requirement is set.
32    /// Delegates to `SurfaceRequirementSet::has_isolating_requirement` to
33    /// keep the semantics in sync (e.g. `mixed_direct_content` alone does
34    /// not count as isolating).
35    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/// Per-frame debug counters for GPU work instrumentation.
300/// Uses `Cell` fields so counters can be bumped through shared references.
301#[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    // Pool/cache sizes snapshotted at end of frame
328    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}