Skip to main content

blitz_dom/
debug.rs

1use blitz_traits::node_id::NodeId;
2use parley::layout::PositionedLayoutItem;
3
4use crate::BaseDocument;
5
6impl BaseDocument {
7    pub fn print_taffy_tree(&self) {
8        taffy::print_tree(self, taffy::NodeId::from(0usize));
9    }
10
11    pub fn debug_log_node(&self, node_id: NodeId) {
12        let node = &self.nodes[node_id];
13
14        #[cfg(feature = "tracing")]
15        {
16            tracing::info!("Layout: {:?}", node.final_layout());
17            tracing::info!("Style: {:?}", node.style());
18        }
19
20        println!("\nNode {} {}", node.id, node.node_debug_str());
21
22        println!("Attrs:");
23
24        for attr in node.attrs().into_iter().flatten() {
25            println!("    {}: {}", attr.name.local, attr.value);
26        }
27
28        if node.flags.is_inline_root() {
29            let inline_layout = &node
30                .data
31                .downcast_element()
32                .unwrap()
33                .inline_layout_data
34                .as_ref()
35                .unwrap();
36
37            println!(
38                "Size: {}x{}",
39                inline_layout.layout.width(),
40                inline_layout.layout.height()
41            );
42            println!("Text content: {:?}", inline_layout.text);
43            println!("Inline Boxes:");
44            for ibox in inline_layout.layout.inline_boxes() {
45                print!("(id: {}) ", ibox.id);
46            }
47            println!();
48            println!("Lines:");
49            for (i, line) in inline_layout.layout.lines().enumerate() {
50                let metrics = line.metrics();
51                let x = metrics.inline_min_coord;
52                let y = metrics.block_min_coord;
53                let w = metrics.inline_max_coord - metrics.inline_min_coord;
54                let h = metrics.block_max_coord - metrics.block_min_coord;
55                println!("Line {i}: x:{x} y:{y} width:{w} height:{h}");
56                for item in line.items() {
57                    print!("  ");
58                    match item {
59                        PositionedLayoutItem::GlyphRun(run) => {
60                            print!(
61                                "RUN (x: {}, w: {}) ",
62                                run.offset().round(),
63                                run.run().advance()
64                            )
65                        }
66                        PositionedLayoutItem::InlineBox(ibox) => print!(
67                            "BOX {:?} (id: {} x: {} y: {} w: {}, h: {})",
68                            ibox.kind,
69                            ibox.id,
70                            ibox.x.round(),
71                            ibox.y.round(),
72                            ibox.width.round(),
73                            ibox.height.round()
74                        ),
75                    }
76                    println!();
77                }
78            }
79        }
80
81        let layout = node.final_layout();
82        println!("Layout:");
83        println!(
84            "  x: {x} y: {y} w: {width} h: {height} content_w: {content_width} content_h: {content_height}",
85            x = layout.location.x,
86            y = layout.location.y,
87            width = layout.size.width,
88            height = layout.size.height,
89            content_width = layout.content_size.width,
90            content_height = layout.content_size.height,
91        );
92        println!(
93            "  border: l:{l} r:{r} t:{t} b:{b}",
94            l = layout.border.left,
95            r = layout.border.right,
96            t = layout.border.top,
97            b = layout.border.bottom,
98        );
99        println!(
100            "  padding: l:{l} r:{r} t:{t} b:{b}",
101            l = layout.padding.left,
102            r = layout.padding.right,
103            t = layout.padding.top,
104            b = layout.padding.bottom,
105        );
106        println!(
107            "  margin: l:{l} r:{r} t:{t} b:{b}",
108            l = layout.margin.left,
109            r = layout.margin.right,
110            t = layout.margin.top,
111            b = layout.margin.bottom,
112        );
113        println!("Parent: {:?}", node.parent);
114
115        let children: Vec<_> = node
116            .children
117            .iter()
118            .map(|id| &self.nodes[*id])
119            .map(|node| (node.id, node.order(), node.node_debug_str()))
120            .collect();
121        println!("Children: {children:?}");
122
123        println!("Layout Parent: {:?}", node.layout_parent.get());
124
125        let layout_children: Option<Vec<_>> = node.layout_children.borrow().as_ref().map(|lc| {
126            lc.iter()
127                .map(|id| &self.nodes[*id])
128                .map(|node| (node.id, node.order(), node.node_debug_str()))
129                .collect()
130        });
131        if let Some(layout_children) = layout_children {
132            println!("Layout Children: {layout_children:?}");
133        }
134
135        let paint_children: Option<Vec<_>> = node.paint_children.borrow().as_ref().map(|lc| {
136            lc.iter()
137                .map(|id| &self.nodes[*id])
138                .map(|node| (node.id, node.order(), node.node_debug_str()))
139                .collect()
140        });
141        if let Some(paint_children) = paint_children {
142            println!("Paint Children: {paint_children:?}");
143        }
144        // taffy::print_tree(&self.dom, node_id.into());
145    }
146}
147
148/// Report why the frame loop will not settle.
149///
150/// `is_animating()` is a single bool built from six independent sources, and
151/// when it is stuck true the app renders continuously and burns CPU on a page
152/// that looks idle. One bool cannot say which source is responsible, and the
153/// sources have very different meanings: a running CSS animation is correct
154/// and expected, a `<canvas>` is permanent by design, and a set of animations
155/// belonging to elements no longer in the document is a leak.
156///
157/// `BLITZ_ANIMATION_DEBUG=1` prints the breakdown, rate-limited to once a
158/// second so it can be left on while watching a real page.
159pub(crate) fn animation_reasons_enabled() -> bool {
160    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
161    *ENABLED.get_or_init(|| {
162        matches!(
163            std::env::var("BLITZ_ANIMATION_DEBUG").ok().as_deref(),
164            Some("1") | Some("true")
165        )
166    })
167}
168
169#[allow(clippy::too_many_arguments)]
170pub(crate) fn report_animation_reasons(
171    doc_id: usize,
172    canvas: bool,
173    css_animations: bool,
174    subdoc: bool,
175    custom_widget: bool,
176    scroll: bool,
177    scrollbars: bool,
178    nodes: Option<&str>,
179) {
180    use std::sync::Mutex;
181    use std::time::{Duration, Instant};
182
183    static LAST: Mutex<Option<Instant>> = Mutex::new(None);
184    let mut last = LAST.lock().unwrap();
185    let now = Instant::now();
186    if last.is_some_and(|t| now.duration_since(t) < Duration::from_secs(1)) {
187        return;
188    }
189    *last = Some(now);
190    drop(last);
191
192    let mut reasons: Vec<&str> = Vec::new();
193    for (flag, name) in [
194        (canvas, "canvas"),
195        (css_animations, "css-animations"),
196        (subdoc, "subdocument"),
197        (custom_widget, "custom-widget"),
198        (scroll, "scroll-animation"),
199        (scrollbars, "scrollbar-fade"),
200    ] {
201        if flag {
202            reasons.push(name);
203        }
204    }
205
206    eprintln!(
207        "[animating] doc={doc_id} {}{}",
208        reasons.join(","),
209        nodes.map(|n| format!("  {n}")).unwrap_or_default()
210    );
211}