Retain opt-in layout performance diagnostics
This commit is contained in:
1 parent
84f589e364
commit
480f0bc99f
8 files changed
+613
-22
No files matched your search
@@ -3,6 +3,9 @@ name = "iris"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
layout-diagnostics = ["iris-core/layout-diagnostics"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -3,6 +3,9 @@ name = "iris-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
layout-diagnostics = []
|
||||
|
||||
[dependencies]
|
||||
wgpu = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
//! Opt-in counters and coarse timers for explaining CPU layout cost.
|
||||
//!
|
||||
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
|
||||
//! instrumentation is compiled into Iris. The retained rig in
|
||||
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
|
||||
//!
|
||||
//! Timers are inclusive: `update total` contains `full layout` or
|
||||
//! `incremental layout`, and `text render` contains shaping and glyph
|
||||
//! placement. They locate cost within one instrumented run and must not be
|
||||
//! added together. Use an uninstrumented build under `perf` for final CPU
|
||||
//! totals; counting every primitive and distinct widget deliberately perturbs
|
||||
//! the instrumented run.
|
||||
|
||||
use crate::WidgetId;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Write,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Counter {
|
||||
Updates,
|
||||
ResizeDependents,
|
||||
DrawRequests,
|
||||
WidgetDraws,
|
||||
PlaceCalls,
|
||||
SizeReads,
|
||||
HintHits,
|
||||
HintMisses,
|
||||
ReuseAttempts,
|
||||
ReuseExact,
|
||||
ReuseMoved,
|
||||
ReuseDirty,
|
||||
ReuseWrongParent,
|
||||
ReuseUnslotted,
|
||||
ReuseOwnResize,
|
||||
ReuseDescendantResize,
|
||||
ResizeChecks,
|
||||
ResizeCheckChildren,
|
||||
QueuePops,
|
||||
DepthSteps,
|
||||
EagerReaderRedraws,
|
||||
LocalRedraws,
|
||||
SizeChanges,
|
||||
ReaderEdges,
|
||||
PrimitiveWrites,
|
||||
TextRenders,
|
||||
TextShapeHits,
|
||||
TextShapes,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
const COUNT: usize = Self::TextShapes as usize + 1;
|
||||
|
||||
const NAMES: [&'static str; Self::COUNT] = [
|
||||
"updates",
|
||||
"resize dependents",
|
||||
"draw requests",
|
||||
"widget draws",
|
||||
"place calls",
|
||||
"draw-result size reads",
|
||||
"hint hits",
|
||||
"hint misses",
|
||||
"reuse attempts",
|
||||
"reuse exact",
|
||||
"reuse moved",
|
||||
"reuse: dirty",
|
||||
"reuse: wrong parent",
|
||||
"reuse: unslotted",
|
||||
"reuse: own resize",
|
||||
"reuse: descendant resize",
|
||||
"resize checks",
|
||||
"resize children checked",
|
||||
"redraw queue pops",
|
||||
"depth steps",
|
||||
"eager reader redraws",
|
||||
"local redraws",
|
||||
"size changes",
|
||||
"reader edges",
|
||||
"primitive writes",
|
||||
"text renders",
|
||||
"text shape hits",
|
||||
"text shapes",
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum TimerKind {
|
||||
Update,
|
||||
FullLayout,
|
||||
ResizeMarking,
|
||||
IncrementalLayout,
|
||||
TextRender,
|
||||
TextShape,
|
||||
GlyphPlacement,
|
||||
}
|
||||
|
||||
impl TimerKind {
|
||||
const COUNT: usize = Self::GlyphPlacement as usize + 1;
|
||||
|
||||
const NAMES: [&'static str; Self::COUNT] = [
|
||||
"update total",
|
||||
"full layout",
|
||||
"resize marking",
|
||||
"incremental layout",
|
||||
"text render",
|
||||
"text shape",
|
||||
"glyph placement",
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Report {
|
||||
counters: [u64; Counter::COUNT],
|
||||
nanos: [u64; TimerKind::COUNT],
|
||||
distinct_widgets: usize,
|
||||
distinct_text_widgets: usize,
|
||||
hot_widgets: Vec<Callsite>,
|
||||
hot_text: Vec<Callsite>,
|
||||
}
|
||||
|
||||
impl Default for Report {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
counters: [0; Counter::COUNT],
|
||||
nanos: [0; TimerKind::COUNT],
|
||||
distinct_widgets: 0,
|
||||
distinct_text_widgets: 0,
|
||||
hot_widgets: Vec::new(),
|
||||
hot_text: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
||||
Counter::NAMES.into_iter().zip(self.counters)
|
||||
}
|
||||
|
||||
/// Inclusive elapsed time accumulated for each targeted operation.
|
||||
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
||||
TimerKind::NAMES.into_iter().zip(self.nanos)
|
||||
}
|
||||
|
||||
pub fn distinct_widgets(&self) -> usize {
|
||||
self.distinct_widgets
|
||||
}
|
||||
|
||||
pub fn distinct_text_widgets(&self) -> usize {
|
||||
self.distinct_text_widgets
|
||||
}
|
||||
|
||||
pub fn hot_widgets(&self) -> &[Callsite] {
|
||||
&self.hot_widgets
|
||||
}
|
||||
|
||||
pub fn hot_text(&self) -> &[Callsite] {
|
||||
&self.hot_text
|
||||
}
|
||||
|
||||
/// Formats nonzero totals divided by `frames`.
|
||||
pub fn per_frame(&self, frames: usize) -> String {
|
||||
let divisor = frames.max(1) as f64;
|
||||
let mut out = String::new();
|
||||
for (name, value) in self.counters() {
|
||||
if value != 0 {
|
||||
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
|
||||
}
|
||||
}
|
||||
if self.distinct_widgets != 0 {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {:<27} {:>12}",
|
||||
"distinct widgets", self.distinct_widgets
|
||||
);
|
||||
}
|
||||
if self.distinct_text_widgets != 0 {
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {:<27} {:>12}",
|
||||
"distinct text widgets", self.distinct_text_widgets
|
||||
);
|
||||
}
|
||||
for (name, nanos) in self.timings_ns() {
|
||||
if nanos != 0 {
|
||||
let ms = nanos as f64 / divisor / 1_000_000.0;
|
||||
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
|
||||
}
|
||||
}
|
||||
if !self.hot_widgets.is_empty() {
|
||||
let _ = writeln!(out, " hottest widget draws:");
|
||||
for callsite in &self.hot_widgets {
|
||||
let calls = callsite.calls as f64 / divisor;
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {calls:>9.2} {:?} {}",
|
||||
callsite.id, callsite.label
|
||||
);
|
||||
}
|
||||
}
|
||||
if !self.hot_text.is_empty() {
|
||||
let _ = writeln!(out, " hottest text renders:");
|
||||
for callsite in &self.hot_text {
|
||||
let calls = callsite.calls as f64 / divisor;
|
||||
let _ = writeln!(
|
||||
out,
|
||||
" {calls:>9.2} {:>3} widths {:?} {}",
|
||||
callsite.distinct_widths, callsite.id, callsite.label
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Callsite {
|
||||
pub id: WidgetId,
|
||||
pub label: String,
|
||||
pub calls: u64,
|
||||
pub distinct_widths: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Calls {
|
||||
label: String,
|
||||
count: u64,
|
||||
widths: HashSet<Option<u32>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Current {
|
||||
report: Report,
|
||||
widgets: HashMap<WidgetId, Calls>,
|
||||
text_widgets: HashMap<WidgetId, Calls>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
|
||||
}
|
||||
|
||||
pub(crate) fn bump(counter: Counter) {
|
||||
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
|
||||
}
|
||||
|
||||
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
|
||||
CURRENT.with_borrow_mut(|current| {
|
||||
let calls = current.widgets.entry(id).or_default();
|
||||
if calls.label.is_empty() {
|
||||
calls.label = label.to_owned();
|
||||
}
|
||||
calls.count += 1;
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
if calls.label.is_empty() {
|
||||
calls.label = label.to_owned();
|
||||
}
|
||||
calls.count += 1;
|
||||
calls.widths.insert(width.map(f32::to_bits));
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) struct Timer {
|
||||
kind: TimerKind,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
pub(crate) fn timer(kind: TimerKind) -> Timer {
|
||||
Timer {
|
||||
kind,
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Timer {
|
||||
fn drop(&mut self) {
|
||||
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
||||
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes all diagnostics accumulated on this thread and resets them.
|
||||
pub fn take() -> Report {
|
||||
CURRENT.with_borrow_mut(|current| {
|
||||
current.report.distinct_widgets = current.widgets.len();
|
||||
current.report.distinct_text_widgets = current.text_widgets.len();
|
||||
current.report.hot_widgets = hottest(¤t.widgets);
|
||||
current.report.hot_text = hottest(¤t.text_widgets);
|
||||
let report = std::mem::take(&mut current.report);
|
||||
current.widgets.clear();
|
||||
current.text_widgets.clear();
|
||||
report
|
||||
})
|
||||
}
|
||||
|
||||
fn hottest(calls: &HashMap<WidgetId, Calls>) -> Vec<Callsite> {
|
||||
let mut calls: Vec<_> = calls
|
||||
.iter()
|
||||
.map(|(&id, calls)| Callsite {
|
||||
id,
|
||||
label: calls.label.clone(),
|
||||
calls: calls.count,
|
||||
distinct_widths: calls.widths.len(),
|
||||
})
|
||||
.collect();
|
||||
calls.sort_by(|a, b| b.calls.cmp(&a.calls).then_with(|| a.label.cmp(&b.label)));
|
||||
calls.truncate(8);
|
||||
calls
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn taking_a_report_resets_its_counters() {
|
||||
let _ = take();
|
||||
bump(Counter::Updates);
|
||||
bump(Counter::Updates);
|
||||
|
||||
let report = take();
|
||||
assert_eq!(report.counters().next(), Some(("updates", 2)));
|
||||
assert!(take().counters().all(|(_, count)| count == 0));
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@
|
||||
#![feature(coerce_unsized)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
pub mod layout_diagnostics;
|
||||
|
||||
mod attr;
|
||||
mod event;
|
||||
mod num;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
||||
use crate::{
|
||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||
};
|
||||
@@ -138,8 +140,14 @@ impl TextBuffer {
|
||||
max_width: width,
|
||||
};
|
||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::TextShapeHits);
|
||||
return;
|
||||
}
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::TextShapes);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _shape = diag::timer(TimerKind::TextShape);
|
||||
let mut builder = data
|
||||
.layout_ctx
|
||||
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
|
||||
@@ -271,8 +279,16 @@ impl TextData {
|
||||
attrs: &TextAttrs,
|
||||
width: Option<f32>,
|
||||
) -> RenderedText {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::TextRenders);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _render = diag::timer(TimerKind::TextRender);
|
||||
buffer.shape(self, attrs, width);
|
||||
let glyphs = self.place(buffer);
|
||||
let glyphs = {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _place = diag::timer(TimerKind::GlyphPlacement);
|
||||
self.place(buffer)
|
||||
};
|
||||
RenderedText {
|
||||
glyphs,
|
||||
size: buffer.size(),
|
||||
|
||||
+28
-3
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! Retained CPU-layout diagnostics on one reproducible random tree.
|
||||
//!
|
||||
//! Counters and phase timers:
|
||||
//!
|
||||
//! cargo test --release --features layout-diagnostics \
|
||||
//! --test layout_diagnostics -- --ignored --nocapture
|
||||
//!
|
||||
//! Uninstrumented hardware totals for one phase:
|
||||
//!
|
||||
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
|
||||
//! -e cycles:u,instructions:u cargo test --release \
|
||||
//! --test layout_diagnostics -- --ignored --nocapture
|
||||
//!
|
||||
//! `IRIS_PHASE` is `cold`, `repaint`, `size`, `scroll`, `resize`, or `all`.
|
||||
//! `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load.
|
||||
|
||||
use iris::harness::Harness;
|
||||
use iris::prelude::*;
|
||||
use iris::random::{Edits, Tree, grow};
|
||||
use std::time::Instant;
|
||||
|
||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||
|
||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
harness.state.root = Some(root);
|
||||
harness.frame();
|
||||
println!(
|
||||
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
|
||||
tree.ids.len(),
|
||||
harness.render.active_widgets()
|
||||
);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
(harness, tree)
|
||||
}
|
||||
|
||||
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let frames = elapsed.len();
|
||||
println!(
|
||||
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, total {:.1} ms",
|
||||
elapsed[0],
|
||||
elapsed[frames / 2],
|
||||
elapsed.iter().sum::<f64>(),
|
||||
);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
let diagnostics = iris::core::layout_diagnostics::take();
|
||||
print!("{}", diagnostics.per_frame(frames));
|
||||
for callsite in diagnostics.hot_text().iter().take(3) {
|
||||
let mut ancestry = Vec::new();
|
||||
let mut id = Some(callsite.id);
|
||||
while let Some(widget) = id {
|
||||
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
|
||||
id = _harness
|
||||
.render
|
||||
.active
|
||||
.get(&widget)
|
||||
.and_then(|active| active.parent);
|
||||
}
|
||||
println!(" text ancestry: {}", ancestry.join(" < "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
label: &str,
|
||||
frames: usize,
|
||||
harness: &mut Harness,
|
||||
mut change: impl FnMut(&mut Harness, usize),
|
||||
) {
|
||||
let mut elapsed = Vec::with_capacity(frames);
|
||||
for frame in 0..frames {
|
||||
change(harness, frame);
|
||||
let start = Instant::now();
|
||||
harness.frame();
|
||||
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
|
||||
}
|
||||
report(label, elapsed, harness);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "measurement, not a check"]
|
||||
fn layout_cost() {
|
||||
let seed = env("IRIS_SEED", 1_u64);
|
||||
let depth = env("IRIS_DEPTH", 7_usize);
|
||||
let frames = env("IRIS_FRAMES", 100_usize);
|
||||
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
|
||||
let phase = env("IRIS_PHASE", String::from("all"));
|
||||
assert!(
|
||||
["all", "cold", "repaint", "size", "scroll", "resize"].contains(&phase.as_str()),
|
||||
"unknown IRIS_PHASE {phase:?}"
|
||||
);
|
||||
let selected = |name| phase == "all" || phase == name;
|
||||
|
||||
if selected("cold") {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
harness.state.root = Some(root);
|
||||
println!(
|
||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||
tree.ids.len()
|
||||
);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
run("cold", 1, &mut harness, |_, _| {});
|
||||
drop(tree);
|
||||
}
|
||||
|
||||
if selected("repaint") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
let leaf = tree.ids[0];
|
||||
run("repaint", frames, &mut harness, move |harness, _| {
|
||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
||||
});
|
||||
}
|
||||
|
||||
if selected("size") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
let sized = tree.sized[0];
|
||||
run("size", frames, &mut harness, move |harness, frame| {
|
||||
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0));
|
||||
});
|
||||
}
|
||||
|
||||
if selected("scroll") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
let scroll = tree.scrolls[0];
|
||||
run("scroll", frames, &mut harness, move |harness, frame| {
|
||||
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
||||
});
|
||||
}
|
||||
|
||||
if selected("resize") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
run("resize", frames, &mut harness, |harness, frame| {
|
||||
harness.resize((OUTPUT.0 - (frame % 2) as f32 * 8.0, OUTPUT.1));
|
||||
});
|
||||
drop(tree);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user