lua: a frame that never leaves the VM fails its call, not the client
The load has had an instruction bound since `3b6975fd` — "so a hung load reports instead of freezing the loading screen" — and the end of the load disarmed it, "so no handler is killed for being slow". Right about slow, and it leaves a handler that never returns at all able to freeze the client until the player kills it, which is what happened to him this morning. So the bound does not go at the end of the load, it changes shape. Wall time rather than instructions, because the number has to be obviously beyond anything legitimate and a count is not: a frame still inside Lua after fifteen seconds is not slow, it is stuck. `tick` restamps every frame, so no frame is charged the one before it and a handler reached later in the frame — a click, a fired event — measures from that mark, which can only make the bound tighter. The raise names the chunk and the line, so his log says which addon rather than that something somewhere looped. It shares the one count hook, which costs 5-13% of pure Lua execution time (measured: 30M instructions, 235-260 ms bare against 267-274 ms hooked, which also puts his machine at ~125M VM instructions a second — what sizes the load's 200M at about 1.6 s). Lua is a small part of a play frame, so that is a fraction of a percent of frame time, and the load was already paying it. `WOW_LUA_RUNAWAY=<secs>` moves the bound, `0` disarms it. Two tests: a loop with no exit fails its own call, names its chunk and leaves the VM usable; and a frame is not charged the wall time that passed before it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -388,6 +388,32 @@ fn report_load(world: &World, start: std::time::Instant, covering: bool, root: &
|
||||
);
|
||||
}
|
||||
|
||||
/// How long one frame may stay inside Lua before the call is failed as non-terminating.
|
||||
///
|
||||
/// A `/reload` is the longest legitimate stretch of Lua in a session and it runs under the load's
|
||||
/// own instruction bound, not this one; during play nothing comes close, and the player's own
|
||||
/// heaviest frames are milliseconds. At ~125M VM instructions a second (measured on his machine)
|
||||
/// this is some two billion instructions, so a loop reaches it in seconds and a handler never
|
||||
/// does. `WOW_LUA_RUNAWAY=<secs>` moves it, `0` disarms it.
|
||||
const RUNAWAY_FRAME_SECS: f32 = 15.0;
|
||||
|
||||
/// Arm [`UiScript::set_runaway_limit`] for the session, unless the player turned it off. The
|
||||
/// client froze on his machine twice in one morning with nothing in the log — a frozen frame
|
||||
/// writes no line, so the only way to learn where it stopped is to make the VM refuse to stay
|
||||
/// there.
|
||||
fn arm_runaway_limit(script: &UiScript) {
|
||||
let secs = std::env::var("WOW_LUA_RUNAWAY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.unwrap_or(RUNAWAY_FRAME_SECS);
|
||||
if secs <= 0.0 {
|
||||
script.clear_instruction_budget();
|
||||
info!("ui_script: runaway-frame bound disarmed (WOW_LUA_RUNAWAY=0)");
|
||||
return;
|
||||
}
|
||||
script.set_runaway_limit(std::time::Duration::from_secs_f32(secs));
|
||||
}
|
||||
|
||||
/// Seat the VM's screen size and text measurer for the load, under the current window and
|
||||
/// `uiScale`: the load-edge half of [`super::extract::tick_script`]'s per-frame pair. A fresh VM
|
||||
/// answers 1024×768 until then, and a value computed once at load keeps it: stock
|
||||
@@ -567,8 +593,9 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
|
||||
// and never again: `[0xce8820]` has one writer, and a `/reloadui` does not re-run it, where
|
||||
// this edge re-reads it on every reload.
|
||||
world.insert_resource(crate::combat_text::read_damage_text_font(&script));
|
||||
// The load is over: disarm its instruction bound, so no handler is killed for being slow.
|
||||
script.clear_instruction_budget();
|
||||
// The load is over: its instruction bound goes, so no handler is killed for being slow, and
|
||||
// a wall-clock bound takes over, so one that never returns at all is killed anyway.
|
||||
arm_runaway_limit(&script);
|
||||
world.insert_non_send_resource(script);
|
||||
world.insert_resource(AddOnIdentity(identity));
|
||||
// Arm the world latch, the reference's `0x490168 call 0x4908c0`: a reload creates no entity
|
||||
|
||||
Reference in New Issue
Block a user