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::surface_requirements::{SurfaceRequirement, SurfaceRequirementSet};
7use std::cell::{Cell, RefCell};
8
9use cranpose_core::NodeId;
10use cranpose_ui_graphics::Rect;
11
12const TOP_ISOLATED_LAYER_LIMIT: usize = 8;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct LayerSurfaceReasons {
16    pub explicit_offscreen: bool,
17    pub effect: bool,
18    pub backdrop: bool,
19    pub group_opacity: bool,
20    pub blend_mode: bool,
21    pub immediate_shadow: bool,
22    pub text_local_surface: bool,
23    pub motion_stable_capture: bool,
24    pub mixed_direct_content: bool,
25    pub non_translation_transform: bool,
26    pub pixel_stable_composite: bool,
27}
28
29impl LayerSurfaceReasons {
30    /// Returns true if any isolating requirement is set.
31    /// Delegates to `SurfaceRequirementSet::has_isolating_requirement` to
32    /// keep the semantics in sync (e.g. `mixed_direct_content` alone does
33    /// not count as isolating).
34    pub fn has_any(self) -> bool {
35        self.explicit_offscreen
36            || self.effect
37            || self.backdrop
38            || self.group_opacity
39            || self.blend_mode
40            || self.text_local_surface
41            || self.motion_stable_capture
42            || self.non_translation_transform
43    }
44
45    pub fn labels(self) -> impl Iterator<Item = &'static str> {
46        let mut labels = [None; 11];
47        let mut len = 0usize;
48
49        if self.explicit_offscreen {
50            labels[len] = Some("explicit_offscreen");
51            len += 1;
52        }
53        if self.effect {
54            labels[len] = Some("effect");
55            len += 1;
56        }
57        if self.backdrop {
58            labels[len] = Some("backdrop");
59            len += 1;
60        }
61        if self.group_opacity {
62            labels[len] = Some("group_opacity");
63            len += 1;
64        }
65        if self.blend_mode {
66            labels[len] = Some("blend_mode");
67            len += 1;
68        }
69        if self.immediate_shadow {
70            labels[len] = Some("immediate_shadow");
71            len += 1;
72        }
73        if self.text_local_surface {
74            labels[len] = Some("text_local_surface");
75            len += 1;
76        }
77        if self.motion_stable_capture {
78            labels[len] = Some("motion_stable_capture");
79            len += 1;
80        }
81        if self.mixed_direct_content {
82            labels[len] = Some("mixed_direct_content");
83            len += 1;
84        }
85        if self.non_translation_transform {
86            labels[len] = Some("non_translation_transform");
87            len += 1;
88        }
89        if self.pixel_stable_composite {
90            labels[len] = Some("pixel_stable_composite");
91            len += 1;
92        }
93
94        labels.into_iter().flatten().take(len)
95    }
96
97    pub fn display(self) -> String {
98        let mut joined = String::new();
99        for (index, label) in self.labels().enumerate() {
100            if index > 0 {
101                joined.push('+');
102            }
103            joined.push_str(label);
104        }
105        if joined.is_empty() {
106            joined.push_str("none");
107        }
108        joined
109    }
110
111    pub fn has_renderer_forced_surface(self) -> bool {
112        self.text_local_surface || self.non_translation_transform
113    }
114}
115
116impl From<SurfaceRequirementSet> for LayerSurfaceReasons {
117    fn from(requirements: SurfaceRequirementSet) -> Self {
118        Self {
119            explicit_offscreen: requirements.contains(SurfaceRequirement::ExplicitOffscreen),
120            effect: requirements.contains(SurfaceRequirement::RenderEffect),
121            backdrop: requirements.contains(SurfaceRequirement::Backdrop),
122            group_opacity: requirements.contains(SurfaceRequirement::GroupOpacity),
123            blend_mode: requirements.contains(SurfaceRequirement::BlendMode),
124            immediate_shadow: requirements.contains(SurfaceRequirement::ImmediateShadow),
125            text_local_surface: requirements.contains(SurfaceRequirement::TextMaterialMask),
126            motion_stable_capture: requirements.contains(SurfaceRequirement::MotionStableCapture),
127            mixed_direct_content: requirements.contains(SurfaceRequirement::MixedDirectContent),
128            non_translation_transform: requirements
129                .contains(SurfaceRequirement::NonTranslationTransform),
130            pixel_stable_composite: requirements.contains(SurfaceRequirement::PixelStableComposite),
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct IsolatedLayerStat {
137    pub node_id: Option<NodeId>,
138    pub logical_rect: Rect,
139    pub width: u32,
140    pub height: u32,
141    pub reasons: LayerSurfaceReasons,
142}
143
144impl IsolatedLayerStat {
145    fn pixel_area(self) -> u64 {
146        (self.width as u64) * (self.height as u64)
147    }
148}
149
150impl Default for IsolatedLayerStat {
151    fn default() -> Self {
152        Self {
153            node_id: None,
154            logical_rect: Rect {
155                x: 0.0,
156                y: 0.0,
157                width: 0.0,
158                height: 0.0,
159            },
160            width: 0,
161            height: 0,
162            reasons: LayerSurfaceReasons::default(),
163        }
164    }
165}
166
167#[derive(Clone, Copy, Debug, Default, PartialEq)]
168pub struct FrameStatsSnapshot {
169    pub submits: u32,
170    pub offscreen_acquires: u32,
171    pub offscreen_news: u32,
172    pub offscreen_total_bytes: u64,
173    pub upload_bytes: u64,
174    pub isolated_layer_renders: u32,
175    pub isolated_layer_pixels: u64,
176    pub layer_cache_hits: u32,
177    pub layer_cache_misses: u32,
178    pub layer_cache_evictions: u32,
179    pub layer_cache_hit_pixels: u64,
180    pub layer_cache_miss_pixels: u64,
181    pub blur_passes: u32,
182    pub composite_passes: u32,
183    pub effect_applies: u32,
184    pub shape_passes: u32,
185    pub image_passes: u32,
186    pub text_passes: u32,
187    pub offscreen_pool_size: u32,
188    pub offscreen_pool_bytes: u64,
189    pub text_pool_size: u32,
190    pub layer_cache_size: u32,
191    pub layer_cache_bytes: u64,
192    pub image_cache_size: u32,
193    pub text_cache_size: u32,
194    pub top_isolated_layers: [Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT],
195    pub top_isolated_layer_count: usize,
196}
197
198impl FrameStatsSnapshot {
199    pub fn top_isolated_layers(self) -> impl Iterator<Item = IsolatedLayerStat> {
200        self.top_isolated_layers
201            .into_iter()
202            .flatten()
203            .take(self.top_isolated_layer_count)
204    }
205
206    fn layer_cache_hit_rate(self) -> f64 {
207        let total = self.layer_cache_hits + self.layer_cache_misses;
208        if total > 0 {
209            (self.layer_cache_hits as f64 / total as f64) * 100.0
210        } else {
211            0.0
212        }
213    }
214
215    fn print(self, frame_count: u64) {
216        let mb = self.offscreen_total_bytes as f64 / (1024.0 * 1024.0);
217        let upload_mb = self.upload_bytes as f64 / (1024.0 * 1024.0);
218        let pool_mb = self.offscreen_pool_bytes as f64 / (1024.0 * 1024.0);
219        let layer_cache_hit_mpx = self.layer_cache_hit_pixels as f64 / 1_000_000.0;
220        let layer_cache_miss_mpx = self.layer_cache_miss_pixels as f64 / 1_000_000.0;
221        let layer_cache_mb = self.layer_cache_bytes as f64 / (1024.0 * 1024.0);
222        let isolated_layer_mpx = self.isolated_layer_pixels as f64 / 1_000_000.0;
223        eprintln!(
224            "[GPU f#{}] submits={} | offscreen: acq={} new={} {:.1}MB pool={}({:.1}MB) | \
225             uploads={:.2}MB | \
226             isolated_layers={} area={:.2}MP | \
227             layer_cache: hit={} miss={} {:.1}% evict={} hit_px={:.2}MP miss_px={:.2}MP size={}({:.1}MB) | \
228             blur={} composite={} effect={} | shape={} image={} text={} | \
229             caches: text_pool={} img={} txt={}",
230            frame_count,
231            self.submits,
232            self.offscreen_acquires,
233            self.offscreen_news,
234            mb,
235            self.offscreen_pool_size,
236            pool_mb,
237            upload_mb,
238            self.isolated_layer_renders,
239            isolated_layer_mpx,
240            self.layer_cache_hits,
241            self.layer_cache_misses,
242            self.layer_cache_hit_rate(),
243            self.layer_cache_evictions,
244            layer_cache_hit_mpx,
245            layer_cache_miss_mpx,
246            self.layer_cache_size,
247            layer_cache_mb,
248            self.blur_passes,
249            self.composite_passes,
250            self.effect_applies,
251            self.shape_passes,
252            self.image_passes,
253            self.text_passes,
254            self.text_pool_size,
255            self.image_cache_size,
256            self.text_cache_size,
257        );
258        for (index, layer) in self.top_isolated_layers().enumerate() {
259            eprintln!(
260                "  [isolated #{index}] node={:?} rect=({:.1},{:.1},{:.1},{:.1}) target={}x{} reasons={}",
261                layer.node_id,
262                layer.logical_rect.x,
263                layer.logical_rect.y,
264                layer.logical_rect.width,
265                layer.logical_rect.height,
266                layer.width,
267                layer.height,
268                layer.reasons.display(),
269            );
270        }
271    }
272}
273
274/// Per-frame debug counters for GPU work instrumentation.
275/// Uses `Cell` fields so counters can be bumped through shared references.
276#[derive(Default)]
277pub(crate) struct FrameStats {
278    pub submits: Cell<u32>,
279    pub offscreen_acquires: Cell<u32>,
280    pub offscreen_news: Cell<u32>,
281    pub offscreen_total_bytes: Cell<u64>,
282    pub upload_bytes: Cell<u64>,
283    pub isolated_layer_renders: Cell<u32>,
284    pub isolated_layer_pixels: Cell<u64>,
285    pub layer_cache_hits: Cell<u32>,
286    pub layer_cache_misses: Cell<u32>,
287    pub layer_cache_evictions: Cell<u32>,
288    pub layer_cache_hit_pixels: Cell<u64>,
289    pub layer_cache_miss_pixels: Cell<u64>,
290    pub blur_passes: Cell<u32>,
291    pub composite_passes: Cell<u32>,
292    pub effect_applies: Cell<u32>,
293    pub shape_passes: Cell<u32>,
294    pub image_passes: Cell<u32>,
295    pub text_passes: Cell<u32>,
296    // Pool/cache sizes snapshotted at end of frame
297    pub offscreen_pool_size: Cell<u32>,
298    pub offscreen_pool_bytes: Cell<u64>,
299    pub text_pool_size: Cell<u32>,
300    pub layer_cache_size: Cell<u32>,
301    pub layer_cache_bytes: Cell<u64>,
302    pub image_cache_size: Cell<u32>,
303    pub text_cache_size: Cell<u32>,
304    top_isolated_layers: RefCell<[Option<IsolatedLayerStat>; TOP_ISOLATED_LAYER_LIMIT]>,
305    top_isolated_layer_count: Cell<usize>,
306}
307
308impl FrameStats {
309    pub fn bump_submits(&self) {
310        self.submits.set(self.submits.get() + 1);
311    }
312
313    pub fn record_offscreen_acquire(&self, width: u32, height: u32, is_new: bool) {
314        self.offscreen_acquires
315            .set(self.offscreen_acquires.get() + 1);
316        if is_new {
317            self.offscreen_news.set(self.offscreen_news.get() + 1);
318        }
319        self.offscreen_total_bytes
320            .set(self.offscreen_total_bytes.get() + (width as u64) * (height as u64) * 4);
321    }
322
323    pub fn record_upload_bytes(&self, bytes: u64) {
324        self.upload_bytes
325            .set(self.upload_bytes.get().saturating_add(bytes));
326    }
327
328    pub fn record_isolated_layer_render(
329        &self,
330        width: u32,
331        height: u32,
332        node_id: Option<NodeId>,
333        logical_rect: Rect,
334        reasons: LayerSurfaceReasons,
335    ) {
336        self.isolated_layer_renders
337            .set(self.isolated_layer_renders.get().saturating_add(1));
338        self.isolated_layer_pixels.set(
339            self.isolated_layer_pixels
340                .get()
341                .saturating_add((width as u64) * (height as u64)),
342        );
343        self.record_top_isolated_layer(IsolatedLayerStat {
344            node_id,
345            logical_rect,
346            width,
347            height,
348            reasons,
349        });
350    }
351
352    pub fn record_layer_cache_hit(&self, width: u32, height: u32) {
353        self.layer_cache_hits
354            .set(self.layer_cache_hits.get().saturating_add(1));
355        self.layer_cache_hit_pixels.set(
356            self.layer_cache_hit_pixels
357                .get()
358                .saturating_add((width as u64) * (height as u64)),
359        );
360    }
361
362    pub fn record_layer_cache_miss(&self, width: u32, height: u32) {
363        self.layer_cache_misses
364            .set(self.layer_cache_misses.get().saturating_add(1));
365        self.layer_cache_miss_pixels.set(
366            self.layer_cache_miss_pixels
367                .get()
368                .saturating_add((width as u64) * (height as u64)),
369        );
370    }
371
372    pub fn record_layer_cache_eviction(&self) {
373        self.layer_cache_evictions
374            .set(self.layer_cache_evictions.get().saturating_add(1));
375    }
376
377    pub fn bump_shapes(&self) {
378        self.shape_passes.set(self.shape_passes.get() + 1);
379    }
380
381    pub fn bump_images(&self) {
382        self.image_passes.set(self.image_passes.get() + 1);
383    }
384
385    pub fn bump_text(&self) {
386        self.text_passes.set(self.text_passes.get() + 1);
387    }
388
389    pub fn snapshot(&self) -> FrameStatsSnapshot {
390        FrameStatsSnapshot {
391            submits: self.submits.get(),
392            offscreen_acquires: self.offscreen_acquires.get(),
393            offscreen_news: self.offscreen_news.get(),
394            offscreen_total_bytes: self.offscreen_total_bytes.get(),
395            upload_bytes: self.upload_bytes.get(),
396            isolated_layer_renders: self.isolated_layer_renders.get(),
397            isolated_layer_pixels: self.isolated_layer_pixels.get(),
398            layer_cache_hits: self.layer_cache_hits.get(),
399            layer_cache_misses: self.layer_cache_misses.get(),
400            layer_cache_evictions: self.layer_cache_evictions.get(),
401            layer_cache_hit_pixels: self.layer_cache_hit_pixels.get(),
402            layer_cache_miss_pixels: self.layer_cache_miss_pixels.get(),
403            blur_passes: self.blur_passes.get(),
404            composite_passes: self.composite_passes.get(),
405            effect_applies: self.effect_applies.get(),
406            shape_passes: self.shape_passes.get(),
407            image_passes: self.image_passes.get(),
408            text_passes: self.text_passes.get(),
409            offscreen_pool_size: self.offscreen_pool_size.get(),
410            offscreen_pool_bytes: self.offscreen_pool_bytes.get(),
411            text_pool_size: self.text_pool_size.get(),
412            layer_cache_size: self.layer_cache_size.get(),
413            layer_cache_bytes: self.layer_cache_bytes.get(),
414            image_cache_size: self.image_cache_size.get(),
415            text_cache_size: self.text_cache_size.get(),
416            top_isolated_layers: *self.top_isolated_layers.borrow(),
417            top_isolated_layer_count: self.top_isolated_layer_count.get(),
418        }
419    }
420
421    pub fn reset(&self) {
422        self.submits.set(0);
423        self.offscreen_acquires.set(0);
424        self.offscreen_news.set(0);
425        self.offscreen_total_bytes.set(0);
426        self.upload_bytes.set(0);
427        self.isolated_layer_renders.set(0);
428        self.isolated_layer_pixels.set(0);
429        self.layer_cache_hits.set(0);
430        self.layer_cache_misses.set(0);
431        self.layer_cache_evictions.set(0);
432        self.layer_cache_hit_pixels.set(0);
433        self.layer_cache_miss_pixels.set(0);
434        self.blur_passes.set(0);
435        self.composite_passes.set(0);
436        self.effect_applies.set(0);
437        self.shape_passes.set(0);
438        self.image_passes.set(0);
439        self.text_passes.set(0);
440        *self.top_isolated_layers.borrow_mut() = [None; TOP_ISOLATED_LAYER_LIMIT];
441        self.top_isolated_layer_count.set(0);
442    }
443
444    pub fn maybe_print_snapshot(
445        &self,
446        snapshot: FrameStatsSnapshot,
447        frame_count: &mut u64,
448        enabled: bool,
449    ) {
450        if !enabled {
451            return;
452        }
453        *frame_count += 1;
454        if (*frame_count).is_multiple_of(60) {
455            snapshot.print(*frame_count);
456        }
457    }
458
459    fn record_top_isolated_layer(&self, layer: IsolatedLayerStat) {
460        if !layer.reasons.has_any() {
461            return;
462        }
463
464        let mut top_layers = self.top_isolated_layers.borrow_mut();
465        let len = self.top_isolated_layer_count.get();
466        let insert_at = top_layers[..len]
467            .iter()
468            .enumerate()
469            .find_map(|(index, existing)| {
470                existing
471                    .filter(|existing| layer.pixel_area() > existing.pixel_area())
472                    .map(|_| index)
473            })
474            .unwrap_or(len);
475
476        if insert_at >= TOP_ISOLATED_LAYER_LIMIT {
477            return;
478        }
479
480        let new_len = if len < TOP_ISOLATED_LAYER_LIMIT {
481            len + 1
482        } else {
483            TOP_ISOLATED_LAYER_LIMIT
484        };
485
486        let mut index = new_len.saturating_sub(1);
487        while index > insert_at {
488            top_layers[index] = top_layers[index - 1];
489            index -= 1;
490        }
491        top_layers[insert_at] = Some(layer);
492        self.top_isolated_layer_count.set(new_len);
493    }
494}
495
496pub(crate) fn gpu_stats_enabled() -> bool {
497    use std::sync::OnceLock;
498    static ENABLED: OnceLock<bool> = OnceLock::new();
499    *ENABLED.get_or_init(|| {
500        std::env::var("CRANPOSE_GPU_STATS")
501            .map(|v| matches!(v.as_str(), "1" | "true" | "yes"))
502            .unwrap_or(false)
503    })
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn layer_cache_counters_accumulate_and_reset() {
512        let stats = FrameStats::default();
513        stats.record_upload_bytes(512);
514        stats.record_layer_cache_hit(10, 20);
515        stats.record_layer_cache_hit(3, 4);
516        stats.record_layer_cache_miss(5, 6);
517        stats.record_layer_cache_eviction();
518
519        assert_eq!(stats.layer_cache_hits.get(), 2);
520        assert_eq!(stats.layer_cache_misses.get(), 1);
521        assert_eq!(stats.layer_cache_evictions.get(), 1);
522        assert_eq!(stats.layer_cache_hit_pixels.get(), 212);
523        assert_eq!(stats.layer_cache_miss_pixels.get(), 30);
524
525        stats.record_isolated_layer_render(
526            7,
527            8,
528            Some(9),
529            Rect {
530                x: 2.0,
531                y: 3.0,
532                width: 4.0,
533                height: 5.0,
534            },
535            LayerSurfaceReasons {
536                text_local_surface: true,
537                ..LayerSurfaceReasons::default()
538            },
539        );
540        let snapshot = stats.snapshot();
541
542        assert_eq!(snapshot.isolated_layer_renders, 1);
543        assert_eq!(snapshot.isolated_layer_pixels, 56);
544        assert_eq!(snapshot.upload_bytes, 512);
545        assert_eq!(snapshot.layer_cache_hits, 2);
546        assert_eq!(snapshot.layer_cache_misses, 1);
547        let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
548        assert_eq!(top_layers.len(), 1);
549        assert_eq!(top_layers[0].node_id, Some(9));
550        assert_eq!(stats.layer_cache_hits.get(), 2);
551        assert_eq!(stats.layer_cache_misses.get(), 1);
552
553        stats.reset();
554
555        assert_eq!(stats.layer_cache_hits.get(), 0);
556        assert_eq!(stats.layer_cache_misses.get(), 0);
557        assert_eq!(stats.layer_cache_evictions.get(), 0);
558        assert_eq!(stats.layer_cache_hit_pixels.get(), 0);
559        assert_eq!(stats.layer_cache_miss_pixels.get(), 0);
560        assert_eq!(stats.upload_bytes.get(), 0);
561        assert_eq!(stats.isolated_layer_renders.get(), 0);
562        assert_eq!(stats.isolated_layer_pixels.get(), 0);
563        assert_eq!(stats.top_isolated_layer_count.get(), 0);
564    }
565
566    #[test]
567    fn maybe_print_snapshot_only_advances_frame_counter_when_enabled() {
568        let stats = FrameStats::default();
569        let snapshot = stats.snapshot();
570        let mut frame_count = 0;
571
572        stats.maybe_print_snapshot(snapshot, &mut frame_count, false);
573        assert_eq!(frame_count, 0);
574
575        stats.maybe_print_snapshot(snapshot, &mut frame_count, true);
576        assert_eq!(frame_count, 1);
577    }
578
579    #[test]
580    fn layer_surface_reasons_report_runtime_only_bits() {
581        let reasons = LayerSurfaceReasons {
582            immediate_shadow: true,
583            text_local_surface: true,
584            mixed_direct_content: true,
585            ..LayerSurfaceReasons::default()
586        };
587
588        assert!(reasons.has_any());
589        assert!(reasons.has_renderer_forced_surface());
590        assert_eq!(
591            reasons.labels().collect::<Vec<_>>(),
592            vec![
593                "immediate_shadow",
594                "text_local_surface",
595                "mixed_direct_content"
596            ]
597        );
598        assert_eq!(
599            reasons.display(),
600            "immediate_shadow+text_local_surface+mixed_direct_content"
601        );
602    }
603
604    #[test]
605    fn immediate_shadow_only_is_diagnostic_not_isolating() {
606        let reasons = LayerSurfaceReasons {
607            immediate_shadow: true,
608            ..LayerSurfaceReasons::default()
609        };
610
611        assert!(!reasons.has_any());
612        assert!(!reasons.has_renderer_forced_surface());
613        assert_eq!(
614            reasons.labels().collect::<Vec<_>>(),
615            vec!["immediate_shadow"]
616        );
617        assert_eq!(reasons.display(), "immediate_shadow");
618    }
619
620    #[test]
621    fn mixed_direct_content_only_is_diagnostic_not_isolating() {
622        let reasons = LayerSurfaceReasons {
623            mixed_direct_content: true,
624            ..LayerSurfaceReasons::default()
625        };
626
627        assert!(!reasons.has_any());
628        assert!(!reasons.has_renderer_forced_surface());
629        assert_eq!(
630            reasons.labels().collect::<Vec<_>>(),
631            vec!["mixed_direct_content"]
632        );
633        assert_eq!(reasons.display(), "mixed_direct_content");
634    }
635
636    #[test]
637    fn has_any_matches_has_isolating_requirement_for_each_requirement() {
638        let all_requirements = [
639            SurfaceRequirement::ExplicitOffscreen,
640            SurfaceRequirement::RenderEffect,
641            SurfaceRequirement::Backdrop,
642            SurfaceRequirement::GroupOpacity,
643            SurfaceRequirement::BlendMode,
644            SurfaceRequirement::ImmediateShadow,
645            SurfaceRequirement::TextMaterialMask,
646            SurfaceRequirement::MotionStableCapture,
647            SurfaceRequirement::NonTranslationTransform,
648            SurfaceRequirement::MixedDirectContent,
649            SurfaceRequirement::PixelStableComposite,
650        ];
651        for requirement in all_requirements {
652            let set = SurfaceRequirementSet::default().with(requirement);
653            let reasons = LayerSurfaceReasons::from(set);
654            assert_eq!(
655                reasons.has_any(),
656                set.has_isolating_requirement(),
657                "has_any vs has_isolating_requirement mismatch for {requirement:?}"
658            );
659        }
660    }
661
662    #[test]
663    fn top_isolated_layers_keep_largest_runtime_surfaces() {
664        let stats = FrameStats::default();
665        for index in 0..(TOP_ISOLATED_LAYER_LIMIT + 2) {
666            stats.record_isolated_layer_render(
667                16 + index as u32,
668                8 + index as u32,
669                Some(index),
670                Rect {
671                    x: index as f32,
672                    y: 0.0,
673                    width: 10.0,
674                    height: 10.0,
675                },
676                LayerSurfaceReasons {
677                    text_local_surface: true,
678                    ..LayerSurfaceReasons::default()
679                },
680            );
681        }
682
683        let snapshot = stats.snapshot();
684        let top_layers = snapshot.top_isolated_layers().collect::<Vec<_>>();
685        assert_eq!(top_layers.len(), TOP_ISOLATED_LAYER_LIMIT);
686        assert_eq!(top_layers[0].node_id, Some(TOP_ISOLATED_LAYER_LIMIT + 1));
687        assert_eq!(top_layers[1].node_id, Some(TOP_ISOLATED_LAYER_LIMIT));
688    }
689}