Skip to content

Lua Profiler

BlueShank edited this page Sep 1, 2026 · 1 revision

GMod's own vprof can tell you that gamemode hooks cost 8 ms, but not which hook, and profiling from Lua with debug.sethook breaks Starfall while detouring hook.Call costs more than it measures.
This does it from C++, above LuaJIT, so an armed profiler costs a couple of nanoseconds per call.

Arming it

It is off by default.
Arm it with CRASHCAPTURE_PROFILE=1 at launch, or from Lua at any time:

crashcapture.set("profile", true)

timer.Simple(30, function()
    for _, e in ipairs(crashcapture.profile(10)) do
        print(("%-40s %8.2f ms self  %6d calls"):format(e.name, e.self_ms, e.calls))
    end
    crashcapture.set("profile", false)
end)

crashcapture.profile([limit]) returns a table sorted by self time, highest first, capped at limit entries (default 20, max 64):

field meaning
name hook:Think, timer:MyAddon_Tick, timer.simple:x.lua:42, lua:sh_thing.lua:120, net:my_message, or <unattributed>
kind hook, timer, lua, net or other
source full path: timer creation site, or the Lua file for lua entries
calls times entered during the window
self_ms time in this entry, excluding nested Lua calls it made
total_ms time including everything it called
p50_ms median call
p99_ms 99th percentile call
max_ms the single worst call

The table itself also carries enabled, window_ms, completed, depth (Lua calls in flight right now), buckets / bucket_max, and dropped.

Window rotation

The profiler retires the current window and starts a fresh one every CRASHCAPTURE_PROFILE_WINDOW seconds (default 300), so it can be left armed indefinitely.
0 never rotates, use crashcapture.profile_reset() to zero the counters and start a fresh window yourself.

What it can and cannot see

  • Attribution is per hook name, not per addon.
    • hook:Think still aggregates every hook.Add("Think", ...) on the server.
    • It tells you which hook is expensive, not whose code.
  • timer.Simple callbacks on Windows are named by the closure (lua:...) rather than as timer.simple:...
    • Because MSVC inlines the callback runner into the simple-timer drain and removes the hook point that supplies the timer's creation site.

Clone this wiki locally