perf(ui): the handler profiler names the worst single call, not only the mean
`WOW_UI_HANDLERS` ranked handlers by self time over the window, which is the right ranking for what costs framerate and the wrong one for what costs a frame. A handler that stalls once every four seconds has a mean near zero over five hundred frames and never reaches the table at all. The player's session had a 15-28 ms tick in nearly every five-second window — one dropped frame every few seconds, which is the kind of thing a player reports as "it hitches" and no average will ever show. The table said `UIParent:OnUpdate 60.8 us/f` and nothing about it. So each slot keeps its costliest single call, the table gets a `worst` column, and the handlers whose worst call cost a frame are listed again underneath — but only the ones the table left out, so the block says something new or nothing. `tick_bench` prints the same ranking. It names the stall on the first run, offline, in this interface: ``` 15768.7 us once (self 17080.8 us over 240 calls) pfQuestMapTracker:OnUpdate 12339.4 us once (self 13042.0 us over 240 calls) AceEvent20Frame:OnUpdate 5380.9 us once (self 16166.0 us over 240 calls) pfQuest\quest.lua:348:OnUpdate ``` 92% of the tracker's whole cost over 240 frames is one call. Both of the top two are steady per-frame allocators, which made the collector the first suspect; the bench's own heap line refutes it — 222.8 MB live against 29.1 kB/frame of churn is ~131 s between 200%-pause cycles, not the ~4 s the stall recurs on. What the per-binding bill says is in there instead: one `pcall` of 7.5 ms, and `frame:GetLeft` at 92 us a call, which is a layout resolve against a dirty graph. `measure.sh` comes with it. The set of instruments worth arming for a play-test is long enough that a run with one of them missing is normal, and such a run looks exactly like a run that found nothing — the session this column came from was missing `WOW_SYSTEM_TIMING`, so it could say what the UI cost and not what the frame cost. The script arms the set and prints what it armed. The test asserts the property the column exists for and not a duration: a handler called ten times for 50k iterations each outranks one called once for 150k by self time, and the second still has the larger worst call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -35,12 +35,22 @@ fn env_period() -> Option<f32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// A single call this slow is a dropped frame at any framerate a player plays at, so it is worth
|
||||
/// naming on its own even when its mean is nothing.
|
||||
const STALL_US: f64 = 2000.0;
|
||||
|
||||
/// How many stalling handlers the report names.
|
||||
const STALL_ROWS: usize = 8;
|
||||
|
||||
/// One `(frame, script)` pair's accumulation over the current window.
|
||||
struct Slot {
|
||||
script: String,
|
||||
calls: u32,
|
||||
total_ns: u64,
|
||||
self_ns: u64,
|
||||
/// The costliest single call's self time. A handler that stalls a frame once every few
|
||||
/// seconds is invisible in a mean over 500 frames, and a stall is what a player feels.
|
||||
worst_ns: u64,
|
||||
}
|
||||
|
||||
/// The profiler's state, one per VM, keyed by frame id: names resolve at report time, and a frame
|
||||
@@ -115,12 +125,14 @@ impl Drop for Fire<'_> {
|
||||
slot.calls += 1;
|
||||
slot.total_ns += total;
|
||||
slot.self_ns += self_ns;
|
||||
slot.worst_ns = slot.worst_ns.max(self_ns);
|
||||
}
|
||||
None => slots.push(Slot {
|
||||
script: self.script.to_string(),
|
||||
calls: 1,
|
||||
total_ns: total,
|
||||
self_ns,
|
||||
worst_ns: self_ns,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -137,6 +149,8 @@ pub struct HandlerRow {
|
||||
pub self_us: f64,
|
||||
/// Microseconds in this handler, including the handlers it fired.
|
||||
pub total_us: f64,
|
||||
/// The costliest single call's self time, in microseconds.
|
||||
pub worst_us: f64,
|
||||
}
|
||||
|
||||
impl UiScript {
|
||||
@@ -162,33 +176,38 @@ impl UiScript {
|
||||
/// This VM's handler costs over the current window as totals, heaviest self time first.
|
||||
pub fn handler_profile(&self) -> Vec<HandlerRow> {
|
||||
// Snapshot, then release the borrow: labelling reaches into the model and back into Lua.
|
||||
let raw: Vec<(u32, String, u32, u64, u64)> = match self.lua.app_data_ref::<HandlerProf>() {
|
||||
Some(prof) => prof
|
||||
.rows
|
||||
.iter()
|
||||
.flat_map(|(&id, slots)| {
|
||||
slots.iter().map(move |slot| {
|
||||
(
|
||||
id,
|
||||
slot.script.clone(),
|
||||
slot.calls,
|
||||
slot.self_ns,
|
||||
slot.total_ns,
|
||||
)
|
||||
let raw: Vec<(u32, String, u32, u64, u64, u64)> =
|
||||
match self.lua.app_data_ref::<HandlerProf>() {
|
||||
Some(prof) => prof
|
||||
.rows
|
||||
.iter()
|
||||
.flat_map(|(&id, slots)| {
|
||||
slots.iter().map(move |slot| {
|
||||
(
|
||||
id,
|
||||
slot.script.clone(),
|
||||
slot.calls,
|
||||
slot.self_ns,
|
||||
slot.total_ns,
|
||||
slot.worst_ns,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
None => return Vec::new(),
|
||||
};
|
||||
.collect(),
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let mut rows: Vec<HandlerRow> = raw
|
||||
.into_iter()
|
||||
.map(|(id, script, calls, self_ns, total_ns)| HandlerRow {
|
||||
frame: self.handler_label(id, &script),
|
||||
script,
|
||||
calls,
|
||||
self_us: self_ns as f64 / 1000.0,
|
||||
total_us: total_ns as f64 / 1000.0,
|
||||
})
|
||||
.map(
|
||||
|(id, script, calls, self_ns, total_ns, worst_ns)| HandlerRow {
|
||||
frame: self.handler_label(id, &script),
|
||||
script,
|
||||
calls,
|
||||
self_us: self_ns as f64 / 1000.0,
|
||||
total_us: total_ns as f64 / 1000.0,
|
||||
worst_us: worst_ns as f64 / 1000.0,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
rows.sort_by(|a, b| {
|
||||
b.self_us
|
||||
@@ -287,13 +306,14 @@ fn print_report(rows: &[HandlerRow], window: f32, frames: u32, nested: u32) {
|
||||
total_self / per_frame,
|
||||
);
|
||||
eprintln!("[ui-handlers] by script: {}", rollup.join(" "));
|
||||
eprintln!("[ui-handlers] self/f total/f calls/f handler");
|
||||
eprintln!("[ui-handlers] self/f total/f calls/f worst handler");
|
||||
for row in rows.iter().take(REPORT_ROWS) {
|
||||
eprintln!(
|
||||
"[ui-handlers] {:8.1} {:8.1} {:8.2} {}:{}",
|
||||
"[ui-handlers] {:8.1} {:8.1} {:8.2} {:8.1} {}:{}",
|
||||
row.self_us / per_frame,
|
||||
row.total_us / per_frame,
|
||||
f64::from(row.calls) / per_frame,
|
||||
row.worst_us,
|
||||
row.frame,
|
||||
row.script,
|
||||
);
|
||||
@@ -301,10 +321,36 @@ fn print_report(rows: &[HandlerRow], window: f32, frames: u32, nested: u32) {
|
||||
if let Some(rest) = rows.len().checked_sub(REPORT_ROWS).filter(|n| *n > 0) {
|
||||
let tail: f64 = rows.iter().skip(REPORT_ROWS).map(|r| r.self_us).sum();
|
||||
eprintln!(
|
||||
"[ui-handlers] {:8.1} … {rest} more",
|
||||
"[ui-handlers] {:8.1} … {rest} more",
|
||||
tail / per_frame
|
||||
);
|
||||
}
|
||||
print_stalls(rows);
|
||||
}
|
||||
|
||||
/// The handlers whose worst single call cost a frame, which the table above cannot show: it ranks
|
||||
/// by the mean, and a handler that stalls once in five hundred frames has a mean near zero. Only
|
||||
/// the ones the table left out, so the block says something new or nothing.
|
||||
fn print_stalls(rows: &[HandlerRow]) {
|
||||
let mut worst: Vec<&HandlerRow> = rows
|
||||
.iter()
|
||||
.skip(REPORT_ROWS)
|
||||
.filter(|r| r.worst_us >= STALL_US)
|
||||
.collect();
|
||||
if worst.is_empty() {
|
||||
return;
|
||||
}
|
||||
worst.sort_by(|a, b| b.worst_us.total_cmp(&a.worst_us));
|
||||
eprintln!(
|
||||
"[ui-handlers] worst single call, among the {} not listed above:",
|
||||
rows.len() - REPORT_ROWS
|
||||
);
|
||||
for row in worst.iter().take(STALL_ROWS) {
|
||||
eprintln!(
|
||||
"[ui-handlers] {:8.1} us once (self {:.1} us over {} calls) {}:{}",
|
||||
row.worst_us, row.self_us, row.calls, row.frame, row.script,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the profiler's `app_data` slot, armed from the environment. Unconditional: mlua refuses
|
||||
|
||||
Reference in New Issue
Block a user