Retain opt-in layout performance diagnostics

This commit is contained in:
iris-ai committed 2026-09-14 16:42:03 -04:00
1 parent 84f589e364
commit 480f0bc99f
8 files changed
+613 -22

No files matched your search

+78 -18
View File
@@ -1,3 +1,5 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
use crate::{
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
@@ -44,6 +46,10 @@ impl UiRenderState {
}
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::Updates);
#[cfg(feature = "layout-diagnostics")]
let _update = diag::timer(TimerKind::Update);
// safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not
if !rsc.widgets().waiting.is_empty() {
@@ -68,9 +74,15 @@ impl UiRenderState {
// A region is a fraction of the output plus an offset, resolved
// against the window in the shader, so a resize moves the whole
// drawing on its own. Only a widget that read pixels can be wrong.
for (&id, active) in &self.active {
if active.reads_output {
rsc.widgets_mut().needs_redraw.insert(id);
{
#[cfg(feature = "layout-diagnostics")]
let _marking = diag::timer(TimerKind::ResizeMarking);
for (&id, active) in &self.active {
if active.reads_output {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeDependents);
rsc.widgets_mut().needs_redraw.insert(id);
}
}
}
}
@@ -81,6 +93,8 @@ impl UiRenderState {
}
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
// free all resources & cache
if let Some(id) = root {
@@ -112,6 +126,8 @@ impl UiRenderState {
old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc,
) -> Size {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DrawRequests);
let mut old_children = old_children.unwrap_or_default();
if self.active.contains_key(&id) {
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
@@ -151,6 +167,11 @@ impl UiRenderState {
rsc,
};
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::WidgetDraws);
diag::draw_widget(id, painter.rsc.widgets().label(id));
}
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
let size = widget.draw(&mut painter);
drop(widget);
@@ -246,31 +267,42 @@ impl UiRenderState {
parent_move: MoveIdx,
rsc: &mut dyn UiRsc,
) -> Option<Size> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseDirty);
return None;
}
let active = self.active.get(&id)?;
// Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent.
if active.parent_move != parent_move {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseWrongParent);
return None;
}
let (size, old, slot, was) = (active.size, active.region, active.move_idx, active.px);
let (size, old_region, slot, old_px) =
(active.size, active.region, active.move_idx, active.px);
// In pixels, because `region` is a fraction of a slot's box and that
// box may be what changed -- an unchanged fraction of a box half the
// size is half the widget.
let px = self.px_of(parent_move, region);
let mut changed = [false; 2];
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
*c = px.axis(axis) != was.axis(axis);
*c = px.axis(axis) != old_px.axis(axis);
}
if !changed.iter().any(|&c| c) && old == region {
if !changed.iter().any(|&c| c) && old_region == region {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseExact);
return Some(size);
}
// Only a placed widget can be given a different box without drawing
// again: everything it drew is a fraction of its slot's box, so one
// entry says where all of it went.
if slot == parent_move {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseUnslotted);
return None;
}
if changed.iter().any(|&c| c) {
@@ -282,7 +314,14 @@ impl UiRenderState {
// Anything under it that has to be drawn again is drawn by drawing
// this, because whatever reads that widget's size sits in between
// and has to lay out around what it comes to.
if redraws || self.redraws_under(id, changed, rsc) {
if redraws {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseOwnResize);
return None;
}
if self.redraws_under(id, changed, rsc) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseDescendantResize);
return None;
}
}
@@ -290,6 +329,8 @@ impl UiRenderState {
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.px = px;
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseMoved);
Some(size)
}
@@ -303,11 +344,15 @@ impl UiRenderState {
/// change length has no descendant whose box did, and the walk stops
/// there -- an 80-wide child of a widened row is not asked at all.
fn redraws_under(&self, id: WidgetId, changed: [bool; 2], rsc: &dyn UiRsc) -> bool {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeChecks);
let Some(active) = self.active.get(&id) else {
return false;
};
let size_deps = &active.size_deps;
active.children.iter().any(|&child| {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeCheckChildren);
let Some(data) = self.active.get(&child) else {
return false;
};
@@ -397,6 +442,8 @@ impl UiRenderState {
}
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::IncrementalLayout);
// A reader's answer is only valid after every dirty size it reads has
// settled. Equal-depth widgets are independent, so their order does
// not matter.
@@ -407,6 +454,8 @@ impl UiRenderState {
.copied()
.max_by_key(|&id| self.depth(id))
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops);
self.redraw(id, rsc);
}
rsc.free();
@@ -416,6 +465,8 @@ impl UiRenderState {
let mut depth = 0;
let mut at = Some(id);
while let Some(id) = at {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DepthSteps);
at = self.active.get(&id).and_then(|active| active.parent);
depth += 1;
}
@@ -480,6 +531,8 @@ impl UiRenderState {
if (self.resized || box_changed)
&& let Some(top) = self.mark_readers(id, rsc)
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::EagerReaderRedraws);
self.redraw(top, rsc);
rsc.widgets_mut().needs_redraw.remove(&id);
return;
@@ -493,8 +546,10 @@ impl UiRenderState {
let Some(active) = self.remove(id, false, rsc) else {
return;
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let was = active.size;
let old_size = active.size;
let size = self.draw_inner(
active.layer,
id,
@@ -507,16 +562,21 @@ impl UiRenderState {
rsc,
);
if size != was
&& let Some(parent) = self.active.get(&id).and_then(|active| active.parent)
&& self
.active
.get(&parent)
.is_some_and(|active| active.size_deps.contains(&id))
{
// Propagate one dependency edge at a time. If drawing the reader
// does not change its own size, nothing above it can observe this.
rsc.widgets_mut().needs_redraw.insert(parent);
if size != old_size {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::SizeChanges);
if let Some(parent) = self.active.get(&id).and_then(|active| active.parent)
&& self
.active
.get(&parent)
.is_some_and(|active| active.size_deps.contains(&id))
{
// Propagate one dependency edge at a time. If drawing the reader
// does not change its own size, nothing above it can observe this.
rsc.widgets_mut().needs_redraw.insert(parent);
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReaderEdges);
}
}
}