perf(ribbon): stop building and laying out 5 panel renderings per frame

Each ribbon panel was rendered five ways every frame so CollapsePanels could
roll back to any density at layout time: full, compact, button, tight and a
full-size flyout that duplicated the full rendering.
The layout pass ran four full measure-layouts per panel even in forced modes,
and the degradation decision was an untested u8 inline in Ribbon::view.

Now the choice is a pure, unit-tested decide_levels() whose levels are a typed
Level enum (Full/Compact/Collapsed/Tight). Panel carries its four renderings
as elements: [Element; 4] indexed by Level, with a slot(i, level) helper for
the tree stride. Forced modes (Full/Compact/Collapsed) measure nothing at all
and derive the row height from the placed panel. The open flyout draws the
panel's own `full` rendering inside a self-drawn box (identical weakest-bg +
1px neutral-border to the container it replaced), so no duplicate
render_group is built per panel per frame. Level, Panel, Widths, CollapsePanels
and decide_levels are pub(crate); CollapseMode stays pub (re-exported).

Tests (collapse.rs tests module, added RED first):
- forced_modes_pin_every_panel
- auto_huge_width_keeps_everything_full
- auto_degrades_from_the_right_one_panel_at_a_time
- auto_stops_degrading_once_the_row_fits
- auto_escalates_a_single_panel_past_collapsed_to_tight
- auto_cascade_drops_every_collapsed_panel_to_tight
- auto_runs_the_compact_phase_across_the_row_first
- no_panels_stays_empty
- single_panel_walks_the_whole_ladder
- zero_widths_never_overflow
- endless_row_degrades_everything_to_tight
- unbounded_row_keeps_everything_full
- nan_budget_degrades_both_phases_but_no_tight_cascade
- exact_fit_at_full_stays_full
- matches_the_pre_refactor_algorithm: byte-equivalence sweep against the
  verbatim pre-refactor u8 algorithm (280 width/budget combos incl. the
  degenerate 0/0/inf/NaN inputs), proving the decision logic is unchanged.

Measured with the committed #[ignore]d benchmark
(`cargo test --lib --release -- --ignored --nocapture bench_ribbon_view_construction`)
run as one instrument across pre-change / this change: Ribbon::view()
construction ~676 -> ~425 us/frame (~37% of view construction saved); forced
modes run 1 layout call per panel per frame instead of 5.

Verification: cargo build clean; cargo test --lib = 382 passed (367 baseline
+ 15 new ribbon tests) / 2 known pre-existing failures unchanged
(save_then_open_round_trips, tilted_ucs_places_planar_entities_with_the_plane_normal).
This commit is contained in:
Karim Jerbi committed 2026-08-16 10:23:44 +01:00
1 parent 6df23ad8c2
commit 32c3844526
2 files changed
+572 -192

No files matched your search

+77 -30
View File
@@ -586,7 +586,7 @@ impl Ribbon {
let ts = self.toggle_state(show_block_palette);
Panel {
id: g.title.to_string(),
full: render_group(
elements: [render_group(
false,
g,
&self.active_tool,
@@ -600,7 +600,7 @@ impl Ribbon {
self.active_lineweight,
&style_ctx,
),
compact: render_group(
render_group(
true,
g,
&self.active_tool,
@@ -614,7 +614,7 @@ impl Ribbon {
self.active_lineweight,
&style_ctx,
),
button: collapse_button(
collapse_button(
g,
self.last_panel_tool.get(g.title).copied(),
&self.active_tool,
@@ -629,7 +629,7 @@ impl Ribbon {
&style_ctx,
false,
),
tight: collapse_button(
collapse_button(
g,
self.last_panel_tool.get(g.title).copied(),
&self.active_tool,
@@ -644,32 +644,7 @@ impl Ribbon {
&style_ctx,
true,
),
flyout: container(render_group(
false,
g,
&self.active_tool,
&self.open_dropdown,
&self.last_cmd,
ts,
&self.layer_infos,
&self.active_layer,
self.active_color,
&self.active_linetype,
self.active_lineweight,
&style_ctx,
))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weakest.color,
)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
})
.into(),
],
}
})
.collect();
@@ -1578,4 +1553,76 @@ mod tests {
let off = ribbon.toggle_state(false);
assert!(!is_active_tool("BLOCKPALETTE", &None, &off));
}
/// Reproducible element-construction benchmark for the ribbon view. Run with:
/// `cargo test --lib --release -- --ignored --nocapture bench_ribbon_view_construction`
///
/// Times `Ribbon::view()` element construction — the dominant cost in the
/// cheap (non-render) part of a ribbon frame, and the thing Mission #4
/// reworked. `#[ignore]`d so normal runs stay silent; state is populated so
/// Auto-mode panels actually have four densities to build.
#[test]
#[ignore]
fn bench_ribbon_view_construction() {
use std::time::Instant;
use super::*;
let mut ribbon = Ribbon::new();
ribbon.set_styles(
vec![
"Standard".to_string(),
"Title".to_string(),
"Annotative".to_string(),
],
"Standard",
vec!["Standard".to_string()],
"Standard",
vec!["Standard".to_string()],
"Standard",
vec!["Standard".to_string()],
"Standard",
);
ribbon.set_layers(
vec![
LayerInfo {
name: "0".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
LayerInfo {
name: "DRAWING".to_string(),
color: Color::TRANSPARENT,
visible: true,
frozen: false,
locked: false,
},
],
"0",
);
ribbon.set_available_linetypes(vec![
LinetypeItem {
name: "Continuous".to_string(),
art: String::new(),
},
LinetypeItem {
name: "DASHED".to_string(),
art: String::new(),
},
]);
let n = 200u32;
// Warm-up for allocator/tree-slot settling before timing begins.
let _ = ribbon.view(false, false, 0, 0, false);
let start = Instant::now();
for _ in 0..n {
let _ = ribbon.view(false, false, 0, 0, false);
}
let per_frame = start.elapsed() / n;
println!(
"bench_ribbon_view_construction: {per_frame:?} per `Ribbon::view()` \
element build (n = {n}, release measurement recommended)"
);
}
}