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

+28 -3
View File
@@ -1,3 +1,5 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
@@ -37,6 +39,8 @@ impl<'a> Painter<'a> {
/// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write(
self.layer,
PrimitiveInst {
@@ -104,6 +108,8 @@ impl<'a> Painter<'a> {
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceCalls);
let region = region.within(&self.region);
self.widget_at(id, region, true)
}
@@ -139,9 +145,24 @@ impl<'a> Painter<'a> {
/// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
self.depend_on_size(id);
Some(hint)
let hint = self
.rsc
.widgets()
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis));
match hint {
Some(hint) => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits);
self.depend_on_size(id);
Some(hint)
}
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintMisses);
None
}
}
}
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
@@ -156,6 +177,8 @@ impl<'a> Painter<'a> {
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
#[cfg(feature = "layout-diagnostics")]
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width)
}
@@ -238,6 +261,8 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
impl<W: ?Sized> DrawResult<'_, '_, W> {
pub fn size(self) -> Size {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::SizeReads);
self.painter.depend_on_size(self.child);
self.size
}
+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);
}
}
}