Creating or freeing a frame or region re-derives only what it touches (#915)
Any write that changed the layout graph's roster (a frame or region created, freed, re-owned or re-pointed) made the next resolve derive the whole graph: about 1.35 ms for one `CreateTexture` or `CreateFrame` on the stock UI, against 0.05 ms for a no-op. A write now names the nodes it touched, and the resolve re-derives just those in the cached graph: a joining node is added with its edges, a leaving one is removed and its dependents seeded, a staying one has its edges and inputs re-synced. The full derivation stays the fallback. A region's owner and an art change are now layout inputs that name their region. Verify now also compares the graph itself. A randomized test checks rects and graph against a scratch solve after every batch of creates, frees, re-owns and re-anchors. `CreateTexture` now costs 0.14 ms, `CreateFrame` 0.15 ms, a backdrop rebuild 0.22 ms. Fixes #903 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
55729baff9
commit
fa5568dcdc
@@ -73,6 +73,19 @@ fn app_frame_ticked(s: &mut UiScript, dt: f32) {
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
/// A forced rebuild counts a derivation: the positive control behind every zero asserted on
|
||||
/// `layout_derivations`, now that births derive nothing.
|
||||
fn derivations_move(s: &mut UiScript) {
|
||||
let before = s.layout_derivations();
|
||||
s.force_full_layout_resolve();
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.layout_derivations() > before,
|
||||
"a forced rebuild must count a derivation — reading zero means `layout_derivations` never \
|
||||
moves and every zero asserted on it is vacuous"
|
||||
);
|
||||
}
|
||||
|
||||
/// A tooltip whose lines change width every call, as in a hover sweep across a bag grid.
|
||||
fn install_changing_tooltip(s: &UiScript, owner: &str, func: &str) {
|
||||
s.run(&format!(
|
||||
@@ -267,8 +280,7 @@ fn a_region_moving_every_frame_costs_one_gate_walk_on_the_shipped_ui() {
|
||||
|
||||
/// Those walks cost zero derivations of the layout graph: a moving region names its node, so the
|
||||
/// resolve re-hashes one node, not the roster; a derivation per frame means a write site fell back
|
||||
/// to the conservative `touch_layout`. The VM ticks, so the shipped handlers are held to it too,
|
||||
/// and a birth must derive, which proves the counter moves.
|
||||
/// to the conservative `touch_layout`. The VM ticks, so the shipped handlers are held to it too.
|
||||
#[test]
|
||||
fn a_region_moving_every_frame_costs_no_graph_derivation_on_the_shipped_ui() {
|
||||
benilla_formats::wow_data_or_skip!();
|
||||
@@ -290,18 +302,18 @@ fn a_region_moving_every_frame_costs_no_graph_derivation_on_the_shipped_ui() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Positive control: a birth moves the roster, so it must derive.
|
||||
// Births join the cached graph without a derivation.
|
||||
let born_at = s.layout_derivations();
|
||||
for _ in 0..6 {
|
||||
s.run("bench_spark2_frame()").unwrap();
|
||||
app_frame_ticked(&mut s, 1.0 / 144.0);
|
||||
}
|
||||
assert!(
|
||||
s.layout_derivations() > born_at,
|
||||
"creating a frame and a texture must derive the graph — a birth moves the roster, which \
|
||||
is the one thing the per-node ledger cannot describe. Reading zero here means \
|
||||
`layout_derivations` never moves and the assertion below is vacuous."
|
||||
assert_eq!(
|
||||
s.layout_derivations() - born_at,
|
||||
0,
|
||||
"creating a frame and a texture derived the layout graph: a birth names its nodes, which join the cached graph"
|
||||
);
|
||||
derivations_move(&mut s);
|
||||
|
||||
let derives_before = s.layout_derivations();
|
||||
let walks_before = s.layout_gate_walks();
|
||||
@@ -435,17 +447,18 @@ fn a_tooltip_line_flipping_wrapped_to_plain_costs_no_graph_derivation() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Positive control: the owner's birth and the line pool's growth move the roster and derive.
|
||||
// Births join the cached graph without a derivation.
|
||||
let born_at = s.layout_derivations();
|
||||
for _ in 0..8 {
|
||||
s.run("flip_change()").unwrap();
|
||||
app_frame(&mut s);
|
||||
}
|
||||
assert!(
|
||||
s.layout_derivations() > born_at,
|
||||
"creating the owner and growing the tooltip's line pool must derive the graph — reading \
|
||||
zero here means `layout_derivations` never moves and the assertion below is vacuous."
|
||||
assert_eq!(
|
||||
s.layout_derivations() - born_at,
|
||||
0,
|
||||
"creating the owner and growing the line pool derived the layout graph: a birth names its nodes, which join the cached graph"
|
||||
);
|
||||
derivations_move(&mut s);
|
||||
|
||||
let derives_before = s.layout_derivations();
|
||||
for _ in 0..10 {
|
||||
@@ -493,17 +506,18 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Positive control: twelve births and the line pool's growth move the roster and derive.
|
||||
// Births join the cached graph without a derivation.
|
||||
let born_at = s.layout_derivations();
|
||||
for _ in 0..40 {
|
||||
s.run("sweep_change()").unwrap();
|
||||
app_frame(&mut s);
|
||||
}
|
||||
assert!(
|
||||
s.layout_derivations() > born_at,
|
||||
"creating twelve owners and growing the line pool must derive the graph — reading zero \
|
||||
here means `layout_derivations` never moves and the assertion below proves nothing."
|
||||
assert_eq!(
|
||||
s.layout_derivations() - born_at,
|
||||
0,
|
||||
"creating twelve owners and growing the line pool derived the layout graph: a birth names its nodes, which join the cached graph"
|
||||
);
|
||||
derivations_move(&mut s);
|
||||
|
||||
// Two laps of the twelve owners: every step re-hovers a button the graph already knows.
|
||||
let derives_before = s.layout_derivations();
|
||||
@@ -515,9 +529,9 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
|
||||
assert_eq!(
|
||||
derives, 0,
|
||||
"24 hovers across 12 owners derived the layout graph {derives} times — one per slot \
|
||||
crossed. `SetOwner` re-points ONE node's anchor and must patch that node's edges \
|
||||
(`Model::touch_layout_retarget_frame`); the conservative touch re-derives the whole \
|
||||
roster on every slot the cursor passes over."
|
||||
crossed. `SetOwner` re-points ONE node's anchor and must name that node \
|
||||
(`Model::touch_layout_frame`), whose edges re-derive in the cached graph; the \
|
||||
conservative touch re-derives the whole roster on every slot the cursor passes over."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -556,16 +570,18 @@ fn an_action_bar_hover_sweep_costs_no_graph_derivation() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Positive control: the births and the line pool's growth are structural.
|
||||
// Births join the cached graph without a derivation.
|
||||
let born_at = s.layout_derivations();
|
||||
for _ in 0..40 {
|
||||
s.run("bar_hover()").unwrap();
|
||||
app_frame(&mut s);
|
||||
}
|
||||
assert!(
|
||||
s.layout_derivations() > born_at,
|
||||
"creating twelve buttons must derive the graph — zero here makes the assertion vacuous."
|
||||
assert_eq!(
|
||||
s.layout_derivations() - born_at,
|
||||
0,
|
||||
"creating twelve buttons derived the layout graph: a birth names its nodes, which join the cached graph"
|
||||
);
|
||||
derivations_move(&mut s);
|
||||
|
||||
let derives_before = s.layout_derivations();
|
||||
for _ in 0..24 {
|
||||
@@ -623,16 +639,18 @@ fn a_bag_addon_hover_sweep_costs_no_graph_derivation() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Positive control: the births and the line pool's growth are structural.
|
||||
// Births join the cached graph without a derivation.
|
||||
let born_at = s.layout_derivations();
|
||||
for _ in 0..40 {
|
||||
s.run("bag_hover()").unwrap();
|
||||
app_frame(&mut s);
|
||||
}
|
||||
assert!(
|
||||
s.layout_derivations() > born_at,
|
||||
"creating twelve slots must derive the graph — zero here makes the assertion vacuous."
|
||||
assert_eq!(
|
||||
s.layout_derivations() - born_at,
|
||||
0,
|
||||
"creating twelve slots derived the layout graph: a birth names its nodes, which join the cached graph"
|
||||
);
|
||||
derivations_move(&mut s);
|
||||
|
||||
let derives_before = s.layout_derivations();
|
||||
for _ in 0..24 {
|
||||
@@ -647,3 +665,64 @@ fn a_bag_addon_hover_sweep_costs_no_graph_derivation() {
|
||||
EMPTY set and names its node like any other."
|
||||
);
|
||||
}
|
||||
|
||||
/// What one structural change costs on the full shipped UI, per run, resolve and extract: a no-op,
|
||||
/// a `SetWidth`, a `CreateTexture`, a `CreateFrame` and a backdrop rebuild, each as the mean of
|
||||
/// 200, with the graph derivations it paid beside it.
|
||||
#[test]
|
||||
#[ignore = "bench, run explicitly"]
|
||||
fn structural_change_costs() {
|
||||
let mut s = settled_default_ui();
|
||||
s.run(
|
||||
r#"
|
||||
BenchHost = CreateFrame("Frame", "BenchHost", UIParent)
|
||||
BenchHost:SetPoint("CENTER", 0, 0); BenchHost:SetWidth(200); BenchHost:SetHeight(100)
|
||||
bench_n = 0
|
||||
bench_spec = { bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16,
|
||||
edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for _ in 0..4 {
|
||||
app_frame(&mut s);
|
||||
}
|
||||
let cases: [(&str, &str); 5] = [
|
||||
("no-op", "bench_n = bench_n + 1"),
|
||||
(
|
||||
"SetWidth",
|
||||
"bench_n = bench_n + 1; BenchHost:SetWidth(200 + math.mod(bench_n, 2))",
|
||||
),
|
||||
(
|
||||
"CreateTexture",
|
||||
"bench_n = bench_n + 1; local t = BenchHost:CreateTexture(nil, 'ARTWORK'); \
|
||||
t:SetWidth(4); t:SetHeight(4); t:SetPoint('TOPLEFT', BenchHost, 'TOPLEFT', \
|
||||
math.mod(bench_n, 50), 0)",
|
||||
),
|
||||
(
|
||||
"CreateFrame",
|
||||
"bench_n = bench_n + 1; local f = CreateFrame('Frame', nil, BenchHost); \
|
||||
f:SetWidth(4); f:SetHeight(4); f:SetPoint('TOPLEFT', BenchHost, 'TOPLEFT', \
|
||||
math.mod(bench_n, 50), 0)",
|
||||
),
|
||||
(
|
||||
"SetBackdrop",
|
||||
"bench_n = bench_n + 1; BenchHost:SetBackdrop(bench_spec)",
|
||||
),
|
||||
];
|
||||
const N: u32 = 200;
|
||||
for (name, lua) in cases {
|
||||
let d0 = s.layout_derivations();
|
||||
let t = Instant::now();
|
||||
for _ in 0..N {
|
||||
s.run(lua).unwrap();
|
||||
app_frame(&mut s);
|
||||
std::hint::black_box(s.extract());
|
||||
}
|
||||
println!(
|
||||
"STRUCT {name:<14} {:.3} ms/iter derivations/iter={:.2}",
|
||||
ms(t) / f64::from(N),
|
||||
(s.layout_derivations() - d0) as f64 / f64::from(N),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user