diff --git a/crates/benilla-app/src/ui_script/resolve_bench.rs b/crates/benilla-app/src/ui_script/resolve_bench.rs index 5dda4de7..38a609ca 100644 --- a/crates/benilla-app/src/ui_script/resolve_bench.rs +++ b/crates/benilla-app/src/ui_script/resolve_bench.rs @@ -383,6 +383,82 @@ fn a_region_moving_every_frame_costs_no_graph_derivation_on_the_shipped_ui() { ); } +/// Scrolling a panel costs zero derivations: the offset reaches the layout only as the scroll +/// child's anchor, which `write_anchor` names, so the two setters owe no conservative touch of +/// their own. +/// +/// This is the regression test for the largest single derivation source yet measured on a live +/// session — `SetVerticalScroll`/`SetHorizontalScroll` called `Model::touch_layout` on the line +/// before `reanchor_child`, which gave up naming the whole graph and then swallowed the precise +/// touch that followed. 472 of 806 derivations in grom's 2026-10-21 17:08 run, and the reason a +/// scrolling panel dragged `frame:GetLeft` from ~1 us a call to 94-376 us. +/// +/// Both axes, because they were byte-clones of each other and a fix to one is easy to forget. +#[test] +fn scrolling_a_panel_costs_no_graph_derivation_on_the_shipped_ui() { + benilla_formats::wow_data_or_skip!(); + let mut s = settled_default_ui(); + s.run( + r#" + BenchScroll = CreateFrame("ScrollFrame", "BenchScroll", UIParent) + BenchScroll:SetPoint("CENTER", 0, 0) + BenchScroll:SetWidth(300); BenchScroll:SetHeight(200) + BenchScrollChild = CreateFrame("Frame", "BenchScrollChild", BenchScroll) + BenchScrollChild:SetWidth(300); BenchScrollChild:SetHeight(900) + BenchScroll:SetScrollChild(BenchScrollChild) + -- A row anchored inside the child, so the scroll really does move something that resolves. + BenchScrollRow = BenchScrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal") + BenchScrollRow:SetPoint("TOPLEFT", BenchScrollChild, "TOPLEFT", 4, -4) + BenchScrollRow:SetText("row") + bench_scroll_n = 0 + function bench_scroll_frame() + bench_scroll_n = bench_scroll_n + 1 + BenchScroll:SetVerticalScroll(bench_scroll_n * 7) + BenchScroll:SetHorizontalScroll(bench_scroll_n * 2) + -- The read stock FrameXML does dozens of times a frame, and what forces the resolve. + local _ = BenchScrollRow:GetLeft() + end + "#, + ) + .unwrap(); + + // Positive control: the births above move the roster, so settling them must derive. Without + // this the assertion below passes just as well on a counter that never moves. + let born_at = s.layout_derivations(); + for _ in 0..6 { + s.run("bench_scroll_frame()").unwrap(); + app_frame_ticked(&mut s, 1.0 / 144.0); + } + assert!( + s.layout_derivations() > born_at, + "creating the ScrollFrame, its child and a FontString must derive the graph — a birth \ + moves the roster. Reading zero here means `layout_derivations` never moves and the \ + assertion below is vacuous." + ); + + let derives_before = s.layout_derivations(); + let walks_before = s.layout_gate_walks(); + for _ in 0..10 { + s.run("bench_scroll_frame()").unwrap(); + app_frame_ticked(&mut s, 1.0 / 144.0); + } + let derives = s.layout_derivations() - derives_before; + let walks = s.layout_gate_walks() - walks_before; + assert!( + walks >= 10, + "the frames must still be reaching the gate at all — {walks} walks over 10 frames means \ + this test stopped exercising the path it is guarding" + ); + assert_eq!( + derives, 0, + "10 frames of scrolling derived the layout graph {derives} times, i.e. about one per \ + scroll. A scroll offset moves exactly one thing — the scroll child's anchor offsets, \ + same point and same target — so `write_anchor` names the child and nothing else needs \ + saying. A derivation here means a setter has gone back to the conservative \ + `Model::touch_layout`, which also erases the precise touch that follows it." + ); +} + /// A tooltip content change solves a tooltip-sized scope on the full shipped UI, gated on counts, /// not milliseconds, which machine state skews. #[test] diff --git a/crates/benilla-ui/src/script/scrollframe.rs b/crates/benilla-ui/src/script/scrollframe.rs index 6a408dae..e6183efb 100644 --- a/crates/benilla-ui/src/script/scrollframe.rs +++ b/crates/benilla-ui/src/script/scrollframe.rs @@ -152,6 +152,26 @@ fn subtree_font_strings(model: &Model, f: FrameHandle) -> Vec mlua::Result<()> { let h = frame_handle_of(lua, this)?; let mut model = lua.app_data_mut::().expect("model app_data"); @@ -250,7 +270,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { if !changed { return Ok(()); } - lua.app_data_mut::().expect("model").touch_layout(); + // No `touch_layout` here: see [`reanchor_child`], which names the one node that moved. reanchor_child(lua, &this)?; fire_scroll(lua, &this, "OnVerticalScroll", px) })?, @@ -283,7 +303,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { if !changed { return Ok(()); } - lua.app_data_mut::().expect("model").touch_layout(); + // No `touch_layout` here either: see [`reanchor_child`]. reanchor_child(lua, &this)?; fire_scroll(lua, &this, "OnHorizontalScroll", px) })?,