hafen.client: Profiling
hafen.client():profiling() is the read surface over the client’s frame profiler, the same per-frame CPU and GPU trees the client’s own profile windows draw. It says where a frame went, in your addon or across the whole client. Unprotected.
local profiling = hafen.client():profiling()
local frame = profiling:frame()
local function round(milliseconds) return math.floor(milliseconds * 100 + 0.5) / 100 end -- %.2f prints the raw double here
hafen.log():write(string.format("%d fps, %s ms (ui %s, addons %s)",
frame.fps, round(frame.ms), round(frame.ui), round(frame.addons)))
Read
| Rule |
Detail |
| Read only while open |
A profiling addon reads and draws nothing until opened: a profiler that runs while you are not looking measures itself. Arm on a hotkey, read on the frames you watch, stop when the window closes. history() held as snapshots turns the graph into a timeline to scrub frame by frame, where a three-frame stutter is visible. |
| The handle |
A stateless proxy that never goes stale, a handle in the API’s one shape: a misspelt counter raises naming the ones there are. tostring(profiling) is Profiling. |
| The reads are snapshots |
Plain tables of frozen numbers: walking hundreds of samples is that many table lookups, not bridge calls. |
| Two kinds of verb |
Frame sampling (frame(), history(), addons(), widgets(), passes(), gl(), overhead()) exists only while the switch is on. The counters are pull-only numbers the client keeps anyway, answering armed or not at no cost. scope() and measure() always run your code and record only while armed. |
| Units |
Every duration is milliseconds, except the byte counts in memory(). |
| An absent key means “not measured”, never zero |
Everywhere on this surface. No connection, no net() keys. No world, no scene keys in render(). No scene key on a frame, since the 3D scene has no timing boundary of its own. Check with if frame.gpuMs then …, not > 0. |
frame()
| Key |
Type |
Description |
frameno |
number |
The client’s frame counter. |
t |
number |
Frame timestamp, seconds since client start. |
fps |
number |
Frames per second, as the client’s stats HUD computes it. hafen.client():fps() reads it armed or not. |
ms |
number |
This frame’s total UI-thread time. |
msAvg / msMin / msMax / msP95 |
number |
Frame time over the whole history ring. |
idle |
number |
Share of the last second spent waiting, 0.0..1.0. |
latency |
number |
UI-thread to GPU-fence lag. |
gpuMs |
number |
GPU time. Absent until the first fence lands. |
gpuFrameno |
number |
Which frame gpuMs belongs to. Absent with it. |
phases |
table |
The UI thread’s phase breakdown: dwait, stick, utick, sessions, draw, aux, wait, the client’s own names, so the numbers line up with its profile window. |
render |
table |
The render thread’s phases: tick, draw, swap, finish. Lags by about one frame: that profile closes a frame on the next frame’s fence. |
ui |
number |
Widget-tree cost this frame. |
addons |
number |
Lua time charged to addons this frame: the accounting the addon CPU watchdog uses, read rather than re-measured. |
GPU time arrives late. GL timestamps come back through fences several frames after their frame, so the frame that just finished has none yet. frame() reports the newest frame whose GPU time has landed and names it in gpuFrameno, trailing frameno.
history(n)
The ring holds several hundred frames, a few seconds at a normal framerate. A larger n is clamped, not an error. Each entry carries frameno, t, ms, addons and phases. gpuMs is there only if that frame’s GPU time resolved, so a frame graph skips the unresolved ones rather than drawing a dip to zero.
local function draw_bar(index, cpu_ms, gpu_ms)
-- implementation here
end
local frames = hafen.client():profiling():history(120) -- the last ~2 seconds, oldest first
for index, frame in ipairs(frames) do
draw_bar(index, frame.ms, frame.gpuMs) -- frame.gpuMs may be nil
end
When profiling is off
| Rule |
Detail |
Empty tables, never nil |
frame(), history(), addons(), widgets(), passes(), gl() and overhead() return {}, so for _, frame in ipairs(profiling:history(60)) do … end does nothing while off. |
| The first sample |
On the second frame after arming, since arming is next-frame: a freshly armed profiler answers empty for one frame. |
reset() |
Empties the ring and every per-addon, per-scope and per-widget figure, as arming does. The counters are unaffected. textcache()’s tallies are left alone, since reset() owns the frame ring, not a cache’s bookkeeping. |
See Also
- Counters — the ones that answer whether profiling is armed or not.
- Attribution — who spent the frame: addons, widgets, passes, GL, and the overhead.
hafen.client():options() — the switch that arms all of this.
- Drawing — the cache
textcache() reports on.