`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
42 lines
2.0 KiB
Bash
Executable File
42 lines
2.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Launch benilla with the measurement instruments armed, so a play-test answers "where did the
|
|
# frame go?" instead of only "it felt slow".
|
|
#
|
|
# ./measure.sh # the standard set: per-system ms, the UI tick split, per handler
|
|
# ./measure.sh --stalls # the above plus the per-binding bill, which is noisier and ~2% of a
|
|
# # frame; only worth it when a handler's own time is already named
|
|
#
|
|
# Everything ./run.sh does, it does: it builds, it logs to logs/latest.log and logs/errors.log.
|
|
# The instruments print to stderr, which run.sh already captures, so the numbers land in the log
|
|
# beside the errors.
|
|
#
|
|
# Why a script and not a line of environment variables: the set is long, and a run with one of
|
|
# them missing looks exactly like a run that found nothing. This prints what it armed.
|
|
set -euo pipefail
|
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# Per-system milliseconds once a second, and the stall watchdog that names a system still running
|
|
# after WOW_STALL_MS. This is the one that says whether a frame went to the UI or the world.
|
|
export WOW_SYSTEM_TIMING=1
|
|
# The UI tick, split into its eleven engine phases, with `C_Timer` fires credited to the line that
|
|
# armed them.
|
|
export WOW_UI_TICK=5
|
|
# Per handler: self and total time, calls, and the worst single call — the column that names a
|
|
# handler stalling one frame in five hundred, which no mean can show.
|
|
export WOW_UI_HANDLERS=5
|
|
# The frame's own cost, split main thread against GPU, so a dip says which side it is on.
|
|
export WOW_FLASH_TRACE=1
|
|
export WOW_GPU_MS=1
|
|
|
|
if [ "${1:-}" = "--stalls" ]; then
|
|
# Per Lua binding. It wraps the bindings, not the globals — wrapping the globals is what
|
|
# unloaded a third of the interface once (`setfenv` is level-relative, see
|
|
# `benilla-ui/src/script/verb_prof.rs`).
|
|
export WOW_UI_VERBS=5
|
|
shift
|
|
fi
|
|
|
|
echo "measure: armed —"
|
|
env | grep -E '^WOW_(SYSTEM_TIMING|UI_TICK|UI_HANDLERS|UI_VERBS|FLASH_TRACE|GPU_MS)=' | sort | sed 's/^/ /'
|
|
exec "$root/run.sh" "$@"
|