Files
benilla-octo/crates/benilla-ui/src/script/surface.rs
T
gromandClaude Opus 5 c5911ccd6f verbs: an instrument that charges a load's time to the binding it went into
`WOW_UI_HANDLERS` names the handler and `WOW_UI_TICK` names the tick phase, and
between them they left the question this answers. A reload of the player's
interface spends **4.47 s inside handler bodies**, and single calls in there cost
hundreds of milliseconds — `pfBag:OnHide` 394 ms in ONE call — which is not Lua
arithmetic. A handler's self time includes the engine work it provokes, so that
time is ours: it is inside the verbs the handler called, and nothing charged it to
them.

`WOW_UI_VERBS=1` wraps every global and every widget method at the end of
`UiScript::new` — before FrameXML or any addon runs, so everything it finds is
ours — and times each call. Off, nothing is wrapped and nothing is paid.

It works offline, which matters more than the live case: the addon harness's
`--together` mode loads the player's whole folder into one VM in load order, which
is the closest thing to a login burst that needs no server. The first run over his
91 addons, three passes:

```
[ui-verbs] 519 verbs, 17,966,414 calls, 9,148 ms in all
    4384     6393    685.8 us  pcall            (addon code, nested)
    1498 16071556      0.1 us  type             (5.4M calls a load, Ace's)
    1241     1950    636.2 us  frame:GetWidth   ← 650 calls a load at 0.6 ms each
     548    33093     16.6 us  CreateFrame
     219    37524      5.8 us  fontstring:SetPoint
      95       99    960.3 us  frame:GetHeight
      67      123    545.9 us  frame:GetLeft
      17        3   5628.6 us  frame:GetCenter
```

A geometry read costing 636 µs is the whole finding, and the fix is the next
commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-18 18:51:45 -06:00

163 lines
5.7 KiB
Rust

//! benilla's widget method surface asked of a running VM, shared by the `dump_widget_methods`
//! example and the widget-surface gate (`script::tests::widget_surface`). Each row is a live
//! instance asked `type(obj.Name) == "function"`, as an addon asks it; only the candidate names
//! come from elsewhere, the keys of the VM's method tables ([`METHOD_TABLES`]), since a Rust
//! `__index` dispatcher cannot be enumerated from Lua.
use std::collections::BTreeSet;
use mlua::{Table, Value};
use super::UiScript;
/// The VM's named-registry method tables: every widget method benilla registers is a key in one,
/// and the per-instance probe decides which a class answers.
pub(super) const METHOD_TABLES: &[&str] = &[
"__benilla_frame_methods",
"__benilla_region_methods",
"__benilla_texture_methods",
"__benilla_fontstring_methods",
"__benilla_title_methods",
"__benilla_font_methods",
"__benilla_button_methods",
"__benilla_checkbutton_methods",
"__benilla_lootbutton_methods",
"__benilla_editbox_methods",
"__benilla_statusbar_methods",
"__benilla_slider_methods",
"__benilla_scrollframe_methods",
"__benilla_simplehtml_methods",
"__benilla_colorselect_methods",
"__benilla_model_methods",
"__benilla_playermodel_methods",
"__benilla_dressupmodel_methods",
"__benilla_tabardmodel_methods",
"__benilla_plain_messageframe_methods",
"__benilla_scrollingmessageframe_methods",
"__benilla_minimap_methods",
"__benilla_tooltip_methods",
];
/// Every class benilla can hand an addon, with a Lua expression making one, published as
/// `DW_<Class>`. `Frame` must come first: the region and font rows are made by it.
/// `TaxiRouteFrame` is left out, being a plain `Frame` to Lua.
const CLASSES: &[(&str, &str)] = &[
("Frame", r#"CreateFrame("Frame", "DWFrameN", UIParent)"#),
(
"WorldFrame",
r#"CreateFrame("WorldFrame", "DWWorldN", UIParent)"#,
),
("Button", r#"CreateFrame("Button", "DWButtonN", UIParent)"#),
(
"LootButton",
r#"CreateFrame("LootButton", "DWLootN", UIParent)"#,
),
(
"CheckButton",
r#"CreateFrame("CheckButton", "DWCheckN", UIParent)"#,
),
("EditBox", r#"CreateFrame("EditBox", "DWEditN", UIParent)"#),
(
"StatusBar",
r#"CreateFrame("StatusBar", "DWStatusN", UIParent)"#,
),
("Slider", r#"CreateFrame("Slider", "DWSliderN", UIParent)"#),
(
"ScrollFrame",
r#"CreateFrame("ScrollFrame", "DWScrollN", UIParent)"#,
),
("Model", r#"CreateFrame("Model", "DWModelN", UIParent)"#),
(
"PlayerModel",
r#"CreateFrame("PlayerModel", "DWPlayerModelN", UIParent)"#,
),
(
"DressUpModel",
r#"CreateFrame("DressUpModel", "DWDressN", UIParent)"#,
),
(
"TabardModel",
r#"CreateFrame("TabardModel", "DWTabardN", UIParent)"#,
),
(
"MessageFrame",
r#"CreateFrame("MessageFrame", "DWMessageN", UIParent)"#,
),
(
"ScrollingMessageFrame",
r#"CreateFrame("ScrollingMessageFrame", "DWScrollMsgN", UIParent)"#,
),
(
"ColorSelect",
r#"CreateFrame("ColorSelect", "DWColorN", UIParent)"#,
),
(
"SimpleHTML",
r#"CreateFrame("SimpleHTML", "DWHtmlN", UIParent)"#,
),
(
"MovieFrame",
r#"CreateFrame("MovieFrame", "DWMovieN", UIParent)"#,
),
(
"GameTooltip",
r#"CreateFrame("GameTooltip", "DWTooltipN", UIParent)"#,
),
(
"Minimap",
r#"CreateFrame("Minimap", "DWMinimapN", UIParent)"#,
),
("Texture", r#"DW_Frame:CreateTexture("DWTexN")"#),
("FontString", r#"DW_Frame:CreateFontString("DWFSN")"#),
("TitleRegion", r#"DW_Frame:CreateTitleRegion()"#),
("Font", r#"CreateFont("DWFontN")"#),
];
/// Every `(class, method)` pair this VM answers, one live instance per class. A class that cannot
/// be made reports `(class, "!NOT-INSTANTIABLE")` rather than dropping out.
pub fn widget_method_census(script: &UiScript) -> mlua::Result<Vec<(String, String)>> {
let mut candidates: BTreeSet<String> = BTreeSet::new();
for key in METHOD_TABLES {
let table: Table = script.lua().named_registry_value(key).map_err(|e| {
mlua::Error::runtime(format!(
"candidate source '{key}' is not a table in this VM ({e}) — a renamed or retired \
method table would silently shrink this census; fix the list, do not ignore it"
))
})?;
for pair in table.pairs::<Value, Value>() {
let (k, _) = pair?;
if let Value::String(s) = k {
candidates.insert(s.to_str()?.to_string());
}
}
}
// Hand the names to the VM once; the per-class probe is then one chunk, not one per name.
let names = script.lua().create_table()?;
for (i, n) in candidates.iter().enumerate() {
names.set(i + 1, n.as_str())?;
}
script.lua().globals().set("DW_NAMES", names)?;
let mut rows: Vec<(String, String)> = Vec::new();
for (class, expr) in CLASSES {
if let Err(e) = script.run(&format!("DW_{class} = {expr}")) {
eprintln!("{class}: could not be instantiated: {e}");
rows.push(((*class).to_string(), "!NOT-INSTANTIABLE".to_string()));
continue;
}
let found: Vec<String> = script.eval(&format!(
"local o = DW_{class} \
local out = {{}} \
for i = 1, table.getn(DW_NAMES) do \
local n = DW_NAMES[i] \
if type(o[n]) == 'function' then table.insert(out, n) end \
end \
return out"
))?;
for name in found {
rows.push(((*class).to_string(), name));
}
}
rows.sort();
Ok(rows)
}