perf(statusbar): cache layout/scale derived data behind epochs (major)

Status bar rescanned the whole object map twice per frame (layout_names +
scale_picker_list), deep-cloned an already-cached Arc, and built two
undo/redo label vecs consumed ~never. Cache layout names (epoch key) and
scale picker (epoch + unit-factor + imperial-family + current-scale key)
as Arc behind choke-point invalidation (layout/scale impl methods,
undo/redo swaps, file-open, doc-reset); borrow across the widget boundary
(owned-Arc params; E0515 forbids &'a slices over call-site temporaries);
build undo/redo labels only when their dropdown is open.

Tests:
- cached_layout_names_returns_same_arc_until_bump: hit-path Arc identity
- cached_layout_names_invalidated_by_add_layout: bump yields new Arc + names
- cached_scale_picker_returns_same_arc_until_bump: hit-path Arc identity
- cached_scale_picker_invalidated_by_add_scale: bump yields new Arc + scale
- cached_scale_picker_invalidated_by_current_scale_change: key completeness

Bench (cargo bench ui_statusbar_derived_data, 3k entities): cached hit below
timer granularity vs ~1us uncached scans, PASS (50us threshold).
This commit is contained in:
Karim Jerbi committed 2026-09-21 22:31:16 +01:00
1 parent baba5bccd3
commit dcc05f310d
16 files changed
+327 -43

No files matched your search

+4 -3
View File
@@ -17,11 +17,12 @@ pub fn menu_entries(
is_model: bool,
current_scale_name: &str,
viewport_scale: Option<f64>,
file_scales: Vec<(String, f32, f64)>,
file_scales: &[(String, f32, f64)],
) -> Vec<Entry<'static>> {
let mut entries: Vec<Entry<'static>> = file_scales
.into_iter()
.iter()
.map(|(label, _anno_scale, vp_scale)| {
let vp_scale = *vp_scale;
let active = if is_model {
label.eq_ignore_ascii_case(current_scale_name)
} else {
@@ -38,7 +39,7 @@ pub fn menu_entries(
} else {
Message::SetViewportScale(label.clone())
};
Entry::close(scale_row(label, active, msg))
Entry::close(scale_row(label.clone(), active, msg))
})
.collect();
+4 -4
View File
@@ -14,7 +14,7 @@ use crate::ui::statusbar::status_menu::Entry;
/// - `types`: entity-type names present in the current layout.
/// - `excluded`: types currently filtered out (unchecked).
pub fn menu_entries(
types: Vec<String>,
types: &[String],
excluded: &HashSet<String>,
) -> Vec<Entry<'static>> {
// "Select All / Clear All" header, mirroring the OSNAP popup: Select All
@@ -51,10 +51,10 @@ pub fn menu_entries(
vec![Entry::stay(empty_row())]
} else {
types
.into_iter()
.iter()
.map(|name| {
let included = !excluded.contains(&name);
Entry::stay(type_row(name, included))
let included = !excluded.contains(name);
Entry::stay(type_row(name.clone(), included))
})
.collect()
};
+1
View File
@@ -25,6 +25,7 @@ mod draw_panel;
mod modify_panel;
mod color_dropdown;
use widgets::{StyleContext, *};
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
mod collapse;
use collapse::{CollapsePanels, Panel};
pub use collapse::CollapseMode;
+2 -2
View File
@@ -301,8 +301,8 @@ pub(super) const TOP_HIST_GAP: f32 = 4.0;
// ── Dropdown / combo ID constants ─────────────────────────────────────────
pub(super) const UNDO_HISTORY_ID: &str = "UNDO_HISTORY";
pub(super) const REDO_HISTORY_ID: &str = "REDO_HISTORY";
pub(crate) const UNDO_HISTORY_ID: &str = "UNDO_HISTORY";
pub(crate) const REDO_HISTORY_ID: &str = "REDO_HISTORY";
pub(super) const LAYER_COMBO_ID: &str = "LAYER_COMBO";
/// Dropdown id for the tab-bar panel-density selector.
pub(super) const COLLAPSE_MODE_ID: &str = "COLLAPSE_MODE";
+28 -20
View File
@@ -38,13 +38,16 @@ const ST_ANNO_AUTO_ADD: &[u8] = include_bytes!("../../../assets/icons/add_scale.
const ST_VP_SCALE_SYNC: &[u8] = include_bytes!("../../../assets/icons/sync.svg");
pub struct StatusMenuData<'a> {
pub layout_names: Vec<String>,
/// Cached layout names (shared `Arc`, moved in — never deep-cloned).
pub layout_names: Arc<Vec<String>>,
pub polar_custom_input: &'a str,
pub scale_is_model: bool,
pub current_scale_name: String,
pub scale_list: Vec<(String, f32, f64)>,
/// Cached scale picker list (shared `Arc`, moved in).
pub scale_list: Arc<Vec<(String, f32, f64)>>,
pub has_selection: bool,
pub selection_types: Vec<String>,
/// Entity-type names in the current layout (shared `Arc`, moved in).
pub selection_types: Arc<Vec<String>>,
pub selection_filter: &'a rustc_hash::FxHashSet<String>,
pub tooltip_hidden: bool,
}
@@ -72,10 +75,9 @@ impl StatusBar {
otrack: bool,
isometric_drafting: bool,
iso_plane: crate::app::settings::IsoPlane,
layouts: Vec<String>,
layouts: Arc<Vec<String>>,
block_tabs: Vec<String>,
reorderable_layouts: Vec<String>,
current_layout: String,
current_layout: &'a str,
active_block: Option<String>,
// Start/welcome view has no drawing to own layouts.
is_start: bool,
@@ -172,7 +174,7 @@ impl StatusBar {
t!("Model and layout list"),
tooltip_hidden,
),
statusbar_menu::layout_entries(&layout_names, &current_layout),
statusbar_menu::layout_entries(&layout_names, current_layout),
200.0,
)
};
@@ -231,7 +233,7 @@ impl StatusBar {
scale_is_model,
&current_scale_name,
viewport_scale,
scale_list,
&scale_list,
),
if scale_is_model { 150.0 } else { 120.0 },
)
@@ -378,7 +380,7 @@ impl StatusBar {
if vis(StatusPill::Space) {
pills.push(
tip(
space_mode_btn(&current_layout, in_mspace),
space_mode_btn(current_layout, in_mspace),
t!("PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space"),
)
.into(),
@@ -501,7 +503,7 @@ impl StatusBar {
tooltip_hidden,
),
crate::ui::popup::selection_filter_popup::menu_entries(
selection_types,
&selection_types,
selection_filter,
),
180.0,
@@ -571,16 +573,22 @@ impl StatusBar {
let mut left: Vec<Element<'_, Message>> = Vec::new();
left.push(menu_btn);
if show_layout_tabs {
let reorderable_layouts: Arc<[String]> = reorderable_layouts.into();
for name in layouts {
let is_active = active_block.is_none() && name == current_layout;
// Paper layouts only: `layouts[0]` is always "Model". Derived once
// from the shared `Arc` and cloned (cheap refcount bump) per tab.
let reorderable_layouts: Arc<[String]> =
Arc::from(layouts.get(1..).unwrap_or(&[]));
// Shared once per frame: block tabs carry no reorder set, so reuse
// one empty Arc (refcount bump) instead of allocating per tab.
let empty_reorderable: Arc<[String]> = Arc::from([] as [String; 0]);
for name in layouts.iter() {
let is_active = active_block.is_none() && name.as_str() == current_layout;
let renaming = rename_state
.filter(|(orig, _)| *orig == name)
.filter(|(orig, _)| orig.as_str() == name.as_str())
.map(|(_, edit)| edit.as_str());
let switch_msg = Message::LayoutSwitch(name.clone());
let switch_msg = Message::LayoutSwitch(name.to_owned());
left.push(
space_tab(
name,
name.to_owned(),
is_active,
renaming,
!is_start,
@@ -591,16 +599,16 @@ impl StatusBar {
.into(),
);
}
for name in block_tabs {
for name in block_tabs.iter() {
let is_active = active_block.as_deref() == Some(name.as_str());
let switch_msg = Message::BlockEditSwitch(name.clone());
let switch_msg = Message::BlockEditSwitch(name.to_owned());
left.push(
space_tab(
name,
name.to_owned(),
is_active,
None,
!is_start,
Arc::from(Vec::<String>::new()),
empty_reorderable.clone(),
switch_msg,
"SB_BLOCK_TAB",
)