`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
163 lines
5.7 KiB
Rust
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)
|
|
}
|