Coalesce resize layout diagnostics
This commit is contained in:
1 parent
480f0bc99f
commit
82fa6c1123
7 files changed
+316
-28
No files matched your search
@@ -10,8 +10,12 @@
|
||||
//! added together. Use an uninstrumented build under `perf` for final CPU
|
||||
//! totals; counting every primitive and distinct widget deliberately perturbs
|
||||
//! the instrumented run.
|
||||
//!
|
||||
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
|
||||
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||
//! selection is a set and survives [`take`] until cleared.
|
||||
|
||||
use crate::WidgetId;
|
||||
use crate::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -119,6 +123,7 @@ pub struct Report {
|
||||
distinct_text_widgets: usize,
|
||||
hot_widgets: Vec<Callsite>,
|
||||
hot_text: Vec<Callsite>,
|
||||
traces: Vec<TraceEvent>,
|
||||
}
|
||||
|
||||
impl Default for Report {
|
||||
@@ -130,6 +135,7 @@ impl Default for Report {
|
||||
distinct_text_widgets: 0,
|
||||
hot_widgets: Vec::new(),
|
||||
hot_text: Vec::new(),
|
||||
traces: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +166,11 @@ impl Report {
|
||||
&self.hot_text
|
||||
}
|
||||
|
||||
/// Ordered layout events for widgets selected with [`trace_widget`].
|
||||
pub fn traces(&self) -> &[TraceEvent] {
|
||||
&self.traces
|
||||
}
|
||||
|
||||
/// Formats nonzero totals divided by `frames`.
|
||||
pub fn per_frame(&self, frames: usize) -> String {
|
||||
let divisor = frames.max(1) as f64;
|
||||
@@ -211,6 +222,12 @@ impl Report {
|
||||
);
|
||||
}
|
||||
}
|
||||
if !self.traces.is_empty() {
|
||||
let _ = writeln!(out, " targeted layout trace:");
|
||||
for event in &self.traces {
|
||||
let _ = writeln!(out, " {event:?}");
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -223,6 +240,58 @@ pub struct Callsite {
|
||||
pub distinct_widths: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ReuseOutcome {
|
||||
Exact,
|
||||
Moved,
|
||||
Dirty,
|
||||
WrongParent,
|
||||
Unslotted,
|
||||
OwnResize,
|
||||
DescendantResize,
|
||||
}
|
||||
|
||||
/// One targeted layout event. Events are retained in execution order, making
|
||||
/// repeated constraint paths visible without logging every widget globally.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum TraceEvent {
|
||||
DrawRequest {
|
||||
id: WidgetId,
|
||||
parent: Option<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: Vec2,
|
||||
slotted: bool,
|
||||
},
|
||||
Reuse {
|
||||
id: WidgetId,
|
||||
outcome: ReuseOutcome,
|
||||
},
|
||||
SizeReported {
|
||||
id: WidgetId,
|
||||
size: Size,
|
||||
},
|
||||
Placed {
|
||||
id: WidgetId,
|
||||
parent: WidgetId,
|
||||
region: UiRegion,
|
||||
},
|
||||
SizeRead {
|
||||
id: WidgetId,
|
||||
reader: WidgetId,
|
||||
size: Size,
|
||||
},
|
||||
HintRead {
|
||||
id: WidgetId,
|
||||
reader: WidgetId,
|
||||
axis: Axis,
|
||||
hint: Option<Len>,
|
||||
},
|
||||
TextRendered {
|
||||
id: WidgetId,
|
||||
width: Option<f32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Calls {
|
||||
label: String,
|
||||
@@ -235,6 +304,7 @@ struct Current {
|
||||
report: Report,
|
||||
widgets: HashMap<WidgetId, Calls>,
|
||||
text_widgets: HashMap<WidgetId, Calls>,
|
||||
traced: HashSet<WidgetId>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -255,6 +325,79 @@ pub(crate) fn draw_widget(id: WidgetId, label: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds a widget to the targeted trace set. Selection survives [`take`]
|
||||
/// until explicitly removed or cleared.
|
||||
pub fn trace_widget(id: impl Into<WidgetId>) {
|
||||
CURRENT.with_borrow_mut(|current| {
|
||||
current.traced.insert(id.into());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn untrace_widget(id: impl Into<WidgetId>) {
|
||||
CURRENT.with_borrow_mut(|current| {
|
||||
current.traced.remove(&id.into());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_traced_widgets() {
|
||||
CURRENT.with_borrow_mut(|current| current.traced.clear());
|
||||
}
|
||||
|
||||
fn trace(id: WidgetId, event: TraceEvent) {
|
||||
CURRENT.with_borrow_mut(|current| {
|
||||
if current.traced.contains(&id) {
|
||||
current.report.traces.push(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn draw_request(
|
||||
id: WidgetId,
|
||||
parent: Option<WidgetId>,
|
||||
region: UiRegion,
|
||||
pixel_size: Vec2,
|
||||
slotted: bool,
|
||||
) {
|
||||
trace(
|
||||
id,
|
||||
TraceEvent::DrawRequest {
|
||||
id,
|
||||
parent,
|
||||
region,
|
||||
pixel_size,
|
||||
slotted,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
||||
trace(id, TraceEvent::Reuse { id, outcome });
|
||||
}
|
||||
|
||||
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||
trace(id, TraceEvent::SizeReported { id, size });
|
||||
}
|
||||
|
||||
pub(crate) fn placed(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
||||
trace(id, TraceEvent::Placed { id, parent, region });
|
||||
}
|
||||
|
||||
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
||||
trace(id, TraceEvent::SizeRead { id, reader, size });
|
||||
}
|
||||
|
||||
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<Len>) {
|
||||
trace(
|
||||
id,
|
||||
TraceEvent::HintRead {
|
||||
id,
|
||||
reader,
|
||||
axis,
|
||||
hint,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
||||
CURRENT.with_borrow_mut(|current| {
|
||||
let calls = current.text_widgets.entry(id).or_default();
|
||||
@@ -263,6 +406,12 @@ pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
||||
}
|
||||
calls.count += 1;
|
||||
calls.widths.insert(width.map(f32::to_bits));
|
||||
if current.traced.contains(&id) {
|
||||
current
|
||||
.report
|
||||
.traces
|
||||
.push(TraceEvent::TextRendered { id, width });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Axis {
|
||||
X,
|
||||
Y,
|
||||
|
||||
@@ -111,6 +111,8 @@ impl<'a> Painter<'a> {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::PlaceCalls);
|
||||
let region = region.within(&self.region);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::placed(id.id(), self.id, region);
|
||||
self.widget_at(id, region, true)
|
||||
}
|
||||
|
||||
@@ -150,6 +152,8 @@ impl<'a> Painter<'a> {
|
||||
.widgets()
|
||||
.get_dyn(id.id())
|
||||
.and_then(|widget| widget.size_hint(axis));
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::hint_read(id.id(), self.id, axis, hint);
|
||||
match hint {
|
||||
Some(hint) => {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -262,7 +266,10 @@ 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);
|
||||
{
|
||||
diag::bump(Counter::SizeReads);
|
||||
diag::size_read(self.child.id(), self.painter.id, self.size);
|
||||
}
|
||||
self.painter.depend_on_size(self.child);
|
||||
self.size
|
||||
}
|
||||
|
||||
+61
-24
@@ -1,5 +1,5 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||
use crate::{
|
||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
||||
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
||||
@@ -37,8 +37,11 @@ impl UiRenderState {
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
self.output_size = size.into();
|
||||
self.resized = true;
|
||||
let size = size.into();
|
||||
if size != self.output_size {
|
||||
self.output_size = size;
|
||||
self.resized = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_size(&self) -> Vec2 {
|
||||
@@ -77,11 +80,17 @@ impl UiRenderState {
|
||||
{
|
||||
#[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);
|
||||
let dependents: Vec<_> = self
|
||||
.active
|
||||
.iter()
|
||||
.filter_map(|(&id, active)| active.reads_output.then_some(id))
|
||||
.collect();
|
||||
for id in dependents {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ResizeDependents);
|
||||
rsc.widgets_mut().needs_redraw.insert(id);
|
||||
if let Some(top) = self.mark_readers(id, rsc) {
|
||||
rsc.widgets_mut().needs_redraw.insert(top);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +136,10 @@ impl UiRenderState {
|
||||
rsc: &mut dyn UiRsc,
|
||||
) -> Size {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::DrawRequests);
|
||||
{
|
||||
diag::bump(Counter::DrawRequests);
|
||||
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
|
||||
}
|
||||
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) {
|
||||
@@ -175,6 +187,8 @@ impl UiRenderState {
|
||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||
let size = widget.draw(&mut painter);
|
||||
drop(widget);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::size_reported(id, size);
|
||||
|
||||
let Painter {
|
||||
state: _,
|
||||
@@ -271,7 +285,10 @@ impl UiRenderState {
|
||||
diag::bump(Counter::ReuseAttempts);
|
||||
if rsc.widgets().needs_redraw.contains(&id) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseDirty);
|
||||
{
|
||||
diag::bump(Counter::ReuseDirty);
|
||||
diag::reuse(id, ReuseOutcome::Dirty);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let active = self.active.get(&id)?;
|
||||
@@ -279,7 +296,10 @@ impl UiRenderState {
|
||||
// longer sits in, and its slot names the wrong parent.
|
||||
if active.parent_move != parent_move {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseWrongParent);
|
||||
{
|
||||
diag::bump(Counter::ReuseWrongParent);
|
||||
diag::reuse(id, ReuseOutcome::WrongParent);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let (size, old_region, slot, old_px) =
|
||||
@@ -294,7 +314,10 @@ impl UiRenderState {
|
||||
}
|
||||
if !changed.iter().any(|&c| c) && old_region == region {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseExact);
|
||||
{
|
||||
diag::bump(Counter::ReuseExact);
|
||||
diag::reuse(id, ReuseOutcome::Exact);
|
||||
}
|
||||
return Some(size);
|
||||
}
|
||||
// Only a placed widget can be given a different box without drawing
|
||||
@@ -302,7 +325,10 @@ impl UiRenderState {
|
||||
// entry says where all of it went.
|
||||
if slot == parent_move {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseUnslotted);
|
||||
{
|
||||
diag::bump(Counter::ReuseUnslotted);
|
||||
diag::reuse(id, ReuseOutcome::Unslotted);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if changed.iter().any(|&c| c) {
|
||||
@@ -316,12 +342,18 @@ impl UiRenderState {
|
||||
// and has to lay out around what it comes to.
|
||||
if redraws {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseOwnResize);
|
||||
{
|
||||
diag::bump(Counter::ReuseOwnResize);
|
||||
diag::reuse(id, ReuseOutcome::OwnResize);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if self.redraws_under(id, changed, rsc) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseDescendantResize);
|
||||
{
|
||||
diag::bump(Counter::ReuseDescendantResize);
|
||||
diag::reuse(id, ReuseOutcome::DescendantResize);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -330,7 +362,10 @@ impl UiRenderState {
|
||||
active.region = region;
|
||||
active.px = px;
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReuseMoved);
|
||||
{
|
||||
diag::bump(Counter::ReuseMoved);
|
||||
diag::reuse(id, ReuseOutcome::Moved);
|
||||
}
|
||||
Some(size)
|
||||
}
|
||||
|
||||
@@ -446,14 +481,16 @@ impl UiRenderState {
|
||||
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.
|
||||
while let Some(id) = rsc
|
||||
.widgets()
|
||||
.needs_redraw
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by_key(|&id| self.depth(id))
|
||||
{
|
||||
// not matter. Resize dirtiness already marks whole reader chains, so
|
||||
// choosing their shallowest roots coalesces descendants that share a
|
||||
// reader and gives each changing box its final constraints first.
|
||||
while let Some(id) = {
|
||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
||||
match self.resized {
|
||||
true => dirty.min_by_key(|&id| self.depth(id)),
|
||||
false => dirty.max_by_key(|&id| self.depth(id)),
|
||||
}
|
||||
} {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::QueuePops);
|
||||
self.redraw(id, rsc);
|
||||
|
||||
Reference in new issue
Block a user