forked from 1b-agent/opencadstudio-upstream
perf(ribbon): build only selected density after measuring
This commit is contained in:
1 parent
140303217f
commit
f0479a5fc6
6 files changed
+260
-63
No files matched your search
@@ -215,6 +215,10 @@ strip = true
|
||||
name = "performance_benchmarks"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "ribbon_layout_benchmark"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "command_benchmarks"
|
||||
harness = false
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//! End-to-end ribbon view construction and layout benchmark.
|
||||
//!
|
||||
//! Unlike the broader performance bench's widget-construction-only metric, this
|
||||
//! carries Iced's widget-tree cache across frames and runs layout at a fixed size.
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
use OpenCADStudio::ui::ribbon::{LayerInfo, Ribbon};
|
||||
use iced::{Color, Size};
|
||||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||||
|
||||
fn main() {
|
||||
let mut ribbon = Ribbon::new();
|
||||
ribbon.set_styles(
|
||||
vec!["Standard".into(), "Title".into(), "Annotative".into()],
|
||||
"Standard",
|
||||
vec!["Standard".into()],
|
||||
"Standard",
|
||||
vec!["Standard".into()],
|
||||
"Standard",
|
||||
vec!["Standard".into()],
|
||||
"Standard",
|
||||
);
|
||||
ribbon.set_layers(
|
||||
["0", "DRAWING", "DIMENSIONS", "ANNOTATIONS"]
|
||||
.into_iter()
|
||||
.map(|name| LayerInfo {
|
||||
name: name.into(),
|
||||
color: Color::TRANSPARENT,
|
||||
visible: true,
|
||||
frozen: false,
|
||||
locked: false,
|
||||
vp_frozen: None,
|
||||
})
|
||||
.collect(),
|
||||
"0",
|
||||
);
|
||||
|
||||
let mut renderer = iced_test::futures::futures::executor::block_on(
|
||||
<iced::Renderer as iced_test::core::renderer::Headless>::new(
|
||||
iced_test::core::renderer::Settings::default(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("headless renderer");
|
||||
let mut cache = Cache::default();
|
||||
let bounds = Size::new(1600.0, 180.0);
|
||||
|
||||
let frame = |cache, renderer: &mut iced::Renderer| {
|
||||
let ui = UserInterface::build(
|
||||
ribbon.view(false, false, 0, 0, false, None),
|
||||
bounds,
|
||||
cache,
|
||||
renderer,
|
||||
);
|
||||
black_box(ui.into_cache())
|
||||
};
|
||||
|
||||
for _ in 0..10 {
|
||||
cache = frame(cache, &mut renderer);
|
||||
}
|
||||
|
||||
let iterations = 100;
|
||||
let runs = 7;
|
||||
let mut samples = Vec::with_capacity(runs);
|
||||
for _ in 0..runs {
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
cache = frame(cache, &mut renderer);
|
||||
}
|
||||
samples.push(start.elapsed().as_secs_f64() * 1_000_000.0 / iterations as f64);
|
||||
}
|
||||
samples.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"ribbon view + Iced layout (cached steady state): median {:.1} µs/frame; samples {:?}",
|
||||
samples[runs / 2],
|
||||
samples
|
||||
);
|
||||
}
|
||||
+141
-10
@@ -24,10 +24,25 @@ use crate::app::Message;
|
||||
/// One panel in its four density renderings. `tight` is the collapsed button
|
||||
/// with a small representative icon instead of the large one — the last step
|
||||
/// before the buttons are squeezed together. An open flyout draws the panel's
|
||||
/// own `full` rendering; see `FlyoutOverlay`. `elements` is indexed by `Level`.
|
||||
/// own `full` rendering; see `FlyoutOverlay`. Elements are built lazily and
|
||||
/// indexed by `Level`.
|
||||
pub(crate) struct Panel<'a> {
|
||||
pub(crate) id: String,
|
||||
pub(crate) elements: [Element<'a, Message>; 4],
|
||||
elements: [Option<Element<'a, Message>>; SLOTS],
|
||||
build: Box<dyn Fn(Level) -> Element<'a, Message> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a> Panel<'a> {
|
||||
pub(crate) fn deferred(
|
||||
id: impl Into<String>,
|
||||
build: impl Fn(Level) -> Element<'a, Message> + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
elements: std::array::from_fn(|_| None),
|
||||
build: Box::new(build),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Number of tree slots per panel: [full, compact, button, tight].
|
||||
@@ -236,11 +251,24 @@ impl<'a> CollapsePanels<'a> {
|
||||
/// The shown element of panel `i` at `level` — an index into its `elements`
|
||||
/// array, which the tree slots mirror (`slot(i, level)`).
|
||||
fn shown(&self, i: usize, level: Level) -> &Element<'a, Message> {
|
||||
&self.panels[i].elements[level as usize]
|
||||
self.panels[i].elements[level as usize]
|
||||
.as_ref()
|
||||
.expect("selected ribbon density is built during layout")
|
||||
}
|
||||
|
||||
fn shown_mut(&mut self, i: usize, level: Level) -> &mut Element<'a, Message> {
|
||||
&mut self.panels[i].elements[level as usize]
|
||||
self.panels[i].elements[level as usize]
|
||||
.as_mut()
|
||||
.expect("selected ribbon density is built during layout")
|
||||
}
|
||||
|
||||
fn ensure_element(&mut self, tree: &mut widget::Tree, i: usize, level: Level) {
|
||||
let index = level as usize;
|
||||
if self.panels[i].elements[index].is_none() {
|
||||
let mut element = (self.panels[i].build)(level);
|
||||
tree.children[slot(i, level)].diff(element.as_widget_mut());
|
||||
self.panels[i].elements[index] = Some(element);
|
||||
}
|
||||
}
|
||||
|
||||
fn levels_snapshot(&self, n: usize) -> Vec<Level> {
|
||||
@@ -260,11 +288,16 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||
let mut refs = Vec::with_capacity(self.panels.len() * SLOTS);
|
||||
for p in &mut self.panels {
|
||||
refs.extend(p.elements.iter_mut());
|
||||
let child_count = self.panels.len() * SLOTS;
|
||||
tree.children.truncate(child_count);
|
||||
tree.children.resize_with(child_count, widget::Tree::empty);
|
||||
for (panel_index, panel) in self.panels.iter_mut().enumerate() {
|
||||
for level in [Level::Full, Level::Compact, Level::Collapsed, Level::Tight] {
|
||||
if let Some(element) = panel.elements[level as usize].as_mut() {
|
||||
tree.children[slot(panel_index, level)].diff(element.as_widget_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
tree.diff_children(&mut refs);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
@@ -294,11 +327,17 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
heights.clone_from(&cache.heights);
|
||||
} else {
|
||||
for i in 0..n {
|
||||
self.ensure_element(tree, i, Level::Full);
|
||||
let f = self.panels[i].elements[Level::Full as usize]
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[slot(i, Level::Full)], renderer, &natural)
|
||||
.size();
|
||||
self.ensure_element(tree, i, Level::Compact);
|
||||
let c = self.panels[i].elements[Level::Compact as usize]
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Compact)],
|
||||
@@ -306,7 +345,10 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
self.ensure_element(tree, i, Level::Collapsed);
|
||||
let b = self.panels[i].elements[Level::Collapsed as usize]
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Collapsed)],
|
||||
@@ -314,7 +356,10 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
self.ensure_element(tree, i, Level::Tight);
|
||||
let t = self.panels[i].elements[Level::Tight as usize]
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Tight)],
|
||||
@@ -342,6 +387,18 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
decide_levels(self.mode, &widths, max_w)
|
||||
};
|
||||
|
||||
// Measurements let Auto choose a density without building all variants
|
||||
// again. On a cache hit only the selected element is built. If a
|
||||
// collapsed panel is open, its Full element is also needed by the flyout.
|
||||
for (i, &level) in levels.iter().enumerate() {
|
||||
self.ensure_element(tree, i, level);
|
||||
if matches!(level, Level::Collapsed | Level::Tight)
|
||||
&& self.open.as_deref() == Some(self.panels[i].id.as_str())
|
||||
{
|
||||
self.ensure_element(tree, i, Level::Full);
|
||||
}
|
||||
}
|
||||
|
||||
// The row is "tight" once any panel has dropped to its small icon — the
|
||||
// last, most cramped state. The tab bar hides its mode selector then.
|
||||
if let Some(out) = &self.tight_out {
|
||||
@@ -572,7 +629,9 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
// never shows slot 0 while the panel is button-form, so it
|
||||
// is free to double as the overlay content).
|
||||
return Some(overlay::Element::new(Box::new(FlyoutOverlay {
|
||||
content: &mut self.panels[p].elements[Level::Full as usize],
|
||||
content: self.panels[p].elements[Level::Full as usize]
|
||||
.as_mut()
|
||||
.expect("open collapsed flyout has full panel content"),
|
||||
tree: &mut tree.children[slot(p, Level::Full)],
|
||||
anchor,
|
||||
})));
|
||||
@@ -595,7 +654,9 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
let (chunk, rest) = tree_rest.split_at_mut(SLOTS);
|
||||
tree_rest = rest;
|
||||
let level = levels.get(i).copied().unwrap_or(Level::Full);
|
||||
let child = &mut panel.elements[level as usize];
|
||||
let Some(child) = panel.elements[level as usize].as_mut() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(o) = child.as_widget_mut().overlay(
|
||||
&mut chunk[level as usize],
|
||||
child_layout,
|
||||
@@ -753,6 +814,76 @@ mod tests {
|
||||
assert!(!cache.matches(17, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_builds_all_densities_once_then_only_the_selected_density() {
|
||||
use iced_test::runtime::user_interface::{Cache, UserInterface};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
fn view(counter: Arc<AtomicUsize>, open: Option<String>) -> Element<'static, Message> {
|
||||
let panel = Panel::deferred("test", move |level| {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
let (width, height) = match level {
|
||||
Level::Full => (200.0, 80.0),
|
||||
Level::Compact => (150.0, 70.0),
|
||||
Level::Collapsed => (100.0, 50.0),
|
||||
Level::Tight => (50.0, 30.0),
|
||||
};
|
||||
iced::widget::container(iced::widget::text("panel"))
|
||||
.width(Length::Fixed(width))
|
||||
.height(Length::Fixed(height))
|
||||
.into()
|
||||
});
|
||||
CollapsePanels::new(vec![panel], open, 80.0)
|
||||
.measure_key(17)
|
||||
.into()
|
||||
}
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let mut renderer = iced_test::futures::futures::executor::block_on(
|
||||
<Renderer as iced_test::core::renderer::Headless>::new(
|
||||
iced_test::core::renderer::Settings::default(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("headless renderer");
|
||||
|
||||
let ui = UserInterface::build(
|
||||
view(count.clone(), None),
|
||||
Size::new(1000.0, 100.0),
|
||||
Cache::default(),
|
||||
&mut renderer,
|
||||
);
|
||||
assert_eq!(count.load(Ordering::Relaxed), 4);
|
||||
|
||||
let ui = UserInterface::build(
|
||||
view(count.clone(), None),
|
||||
Size::new(1000.0, 100.0),
|
||||
ui.into_cache(),
|
||||
&mut renderer,
|
||||
);
|
||||
assert_eq!(count.load(Ordering::Relaxed), 5, "only Full is rebuilt");
|
||||
|
||||
let ui = UserInterface::build(
|
||||
view(count.clone(), None),
|
||||
Size::new(170.0, 100.0),
|
||||
ui.into_cache(),
|
||||
&mut renderer,
|
||||
);
|
||||
assert_eq!(count.load(Ordering::Relaxed), 6, "resize builds Compact");
|
||||
|
||||
let _ui = UserInterface::build(
|
||||
view(count.clone(), Some("test".to_string())),
|
||||
Size::new(20.0, 100.0),
|
||||
ui.into_cache(),
|
||||
&mut renderer,
|
||||
);
|
||||
assert_eq!(
|
||||
count.load(Ordering::Relaxed),
|
||||
8,
|
||||
"tight selection builds Tight and Full for the open flyout"
|
||||
);
|
||||
}
|
||||
|
||||
fn w(full: f32, compact: f32, button: f32, tight: f32) -> Widths {
|
||||
Widths {
|
||||
full,
|
||||
|
||||
+24
-40
@@ -632,16 +632,13 @@ impl Ribbon {
|
||||
&'a self,
|
||||
g: &'a RibbonGroup,
|
||||
ts: widgets::ToggleState,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
lead: &dyn Fn(bool) -> Vec<Element<'a, Message>>,
|
||||
mode: CollapseMode,
|
||||
flyout_open: bool,
|
||||
style_ctx: StyleContext<'a>,
|
||||
) -> Panel<'a> {
|
||||
let group = |compact: bool| {
|
||||
let group = move |compact: bool| {
|
||||
render_group(
|
||||
compact,
|
||||
g,
|
||||
lead(compact),
|
||||
Vec::new(),
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
@@ -651,10 +648,10 @@ impl Ribbon {
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
style_ctx,
|
||||
&style_ctx,
|
||||
)
|
||||
};
|
||||
let button = |tight: bool| {
|
||||
let button = move |tight: bool| {
|
||||
collapse_button(
|
||||
g,
|
||||
self.last_panel_tool.get(g.title).copied(),
|
||||
@@ -667,26 +664,16 @@ impl Ribbon {
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
style_ctx,
|
||||
&style_ctx,
|
||||
tight,
|
||||
)
|
||||
};
|
||||
let empty = || text("").into();
|
||||
let elements = match mode {
|
||||
CollapseMode::Auto => [group(false), group(true), button(false), button(true)],
|
||||
CollapseMode::Full => [group(false), empty(), empty(), empty()],
|
||||
CollapseMode::Compact => [empty(), group(true), empty(), empty()],
|
||||
CollapseMode::Collapsed => [
|
||||
if flyout_open { group(false) } else { empty() },
|
||||
empty(),
|
||||
button(false),
|
||||
empty(),
|
||||
],
|
||||
};
|
||||
Panel {
|
||||
id: g.title.to_string(),
|
||||
elements,
|
||||
}
|
||||
Panel::deferred(g.title, move |level| match level {
|
||||
collapse::Level::Full => group(false),
|
||||
collapse::Level::Compact => group(true),
|
||||
collapse::Level::Collapsed => button(false),
|
||||
collapse::Level::Tight => button(true),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_dropdown(&mut self, id: &str) {
|
||||
@@ -981,13 +968,17 @@ impl Ribbon {
|
||||
// shrink to compact icon columns, then it collapses to a ▾ flyout
|
||||
// button. See `CollapsePanels`.
|
||||
let ts = self.toggle_state(show_block_palette);
|
||||
let measure_key = panel_measure_key(
|
||||
&groups,
|
||||
&self.last_cmd,
|
||||
&self.last_panel_tool,
|
||||
&style_ctx,
|
||||
visibility.as_ref(),
|
||||
);
|
||||
let measure_key = if self.collapse_mode == CollapseMode::Auto {
|
||||
panel_measure_key(
|
||||
&groups,
|
||||
&self.last_cmd,
|
||||
&self.last_panel_tool,
|
||||
&style_ctx,
|
||||
visibility.as_ref(),
|
||||
)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let panels: Vec<Panel<'_>> = groups
|
||||
.into_iter()
|
||||
.map(|g| {
|
||||
@@ -996,14 +987,7 @@ impl Ribbon {
|
||||
{
|
||||
visibility_panel::panel(visibility.clone())
|
||||
} else {
|
||||
self.panel(
|
||||
g,
|
||||
ts,
|
||||
&style_ctx,
|
||||
&|_| Vec::new(),
|
||||
self.collapse_mode,
|
||||
self.collapsed_open.as_deref() == Some(g.title),
|
||||
)
|
||||
self.panel(g, ts, style_ctx)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
use iced::widget::{button, column, container, pick_list, row, text, tooltip};
|
||||
use iced::{Element, Fill, Theme};
|
||||
|
||||
use super::collapse::Panel;
|
||||
use super::widgets::{make_icon, tool_btn_style, LARGE_ICON, LARGE_W, SMALL_ICON, TOOL_BAR_H};
|
||||
use super::collapse::{Level, Panel};
|
||||
use super::widgets::{LARGE_ICON, LARGE_W, SMALL_ICON, TOOL_BAR_H, make_icon, tool_btn_style};
|
||||
use crate::app::Message;
|
||||
use crate::modules::IconKind;
|
||||
use crate::ui::window::block_visibility::{Action, RibbonContext};
|
||||
use crate::ui::ROW_H;
|
||||
use crate::ui::window::block_visibility::{Action, RibbonContext};
|
||||
|
||||
fn tool(
|
||||
id: &'static str,
|
||||
@@ -152,14 +152,11 @@ fn collapsed(tight: bool) -> Element<'static, Message> {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(super) fn panel(context: Option<RibbonContext>) -> Panel<'static> {
|
||||
Panel {
|
||||
id: "Visibility".into(),
|
||||
elements: [
|
||||
view(context.clone(), false),
|
||||
view(context, true),
|
||||
collapsed(false),
|
||||
collapsed(true),
|
||||
],
|
||||
}
|
||||
pub(super) fn panel<'a>(context: Option<RibbonContext>) -> Panel<'a> {
|
||||
Panel::deferred("Visibility", move |level| match level {
|
||||
Level::Full => view(context.clone(), false),
|
||||
Level::Compact => view(context.clone(), true),
|
||||
Level::Collapsed => collapsed(false),
|
||||
Level::Tight => collapsed(true),
|
||||
})
|
||||
}
|
||||
@@ -321,6 +321,7 @@ pub(super) const PROP_LW_ID: &str = "PROP_LW";
|
||||
|
||||
// ── Style context (passed from Ribbon to render_large) ────────────────────
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct StyleContext<'a> {
|
||||
pub text_style_names: &'a [String],
|
||||
pub active_text_style: &'a str,
|
||||
|
||||
Reference in new issue
Block a user