Harden the jit.profile sampler: callback recovery and timer-stop ordering - #663
Harden the jit.profile sampler: callback recovery and timer-stop ordering#663damiansirbu wants to merge 5 commits into
Conversation
jit_profile_callback ran exit(EXIT_FAILURE) after a failed lua_pcall, so one error in the sampling callback killed the whole process. The engine panic handler (CScriptEngine::lua_panic) already logs the stack and returns, so the error stays visible. Drop the exit, clear the callback thread stack, and keep profiling. The failed sample is dropped.
luaJIT_profile_stop cleared ps->g before profile_timer_stop, so the Windows timer thread could enter profile_trigger, read the now-null g, and dereference it. Join the timer thread first, then clear the state. No trigger runs after the join.
|
Draft while I fold in additional hardening surfaced by large-modpack validation. |
…frames whole allocprof_record cut the stack key mid-frame at the buffer bound, so a truncated frame read as an unattributable stack and lost bytes to "unknown". Cut at the last ';' instead, so a partial frame drops whole. Raise the CPU dumpstack buffer 2048->8192 and the alloc stack key 512->4096 so a 64-deep capture holds the whole stack without mid-frame truncation.
The CPU sampler slept a fixed interval, so a mod running on a fixed schedule could phase-lock to it and be systematically over- or under-sampled. Draw each sleep from an exponential around the mean, the same way the allocation profiler already draws its sample distance, so the sample times are never periodic.
The leaf+stack aggregation tables were 80MB of static image data (4096+16384 slots x 4104B) after the key-size increase. Allocate them with calloc on the first jit.allocprof.start instead: a process that never profiles holds no table memory. The tables stay alive after stop because consumers dump after allocprof.stop(); reset and slot reads are NULL-guarded for calls before the first start.
|
Pushed 3 more commits from running this in production. I use the profiler as the backend of an in-game profiling UI on a full GAMMA install (600+ mods), and these came out of that. The buffers were too small for real modpack stacks. The dumpstack buffer went 2048 -> 8192: the deepest stack I have captured in the field is 29 frames, and 2048 could truncate it. The allocprof keys went 512 -> 4096. A stack that still does not fit now cuts at the last The CPU sampler now jitters its interval. A fixed 5ms wakeup can phase-lock with per-frame work and with the 100/200ms throttles that half the scripts in a modpack use, and then it systematically over- or under-samples them. The timer thread now sleeps an exponential draw around the configured mean, the same trick the alloc sampler already uses for its byte distance. The rate stays the same: about 200 samples/s at i5, measured as 22549 samples over a 127s fight capture. The allocprof tables moved from static arrays to heap allocation on first start. After the key bump they had grown to 80MB of static data. The memory now exists only after the first jit.allocprof.start. The tables survive stop on purpose, because dumps happen after stop. I did several hours of in-game testing across many sessions, at least 2 of them pure profiling. The latest round: 2-minute combat captures on a vanilla-based install and on full GAMMA, 0% of samples at the depth-64 cap, and a VM-state split that reconciles with the per-stack weights. The stop path has been through many start/stop cycles with the timer-thread join in place. |
Summary
Robustness improvements to the jit.profile / jit.allocprof sampler from #641.
The profile callback recovers on its own and sampling continues uninterrupted.
The stop path joins the sampler thread before clearing its state, keeping shutdown and the timer thread cleanly separated.
Callback recovery keeps the session running
The profile callback runs under lua_pcall in jit_profile_callback (lib_jit.c).
Upstream LuaJIT hands a non-zero pcall status to the panic handler and then calls exit(EXIT_FAILURE), which suits the luajit CLI but is heavy for a game.
In this fork the panic handler already logs and returns.
CScriptEngine::lua_panic (script_engine.cpp:179) prints the stack and output, then returns 0.
The improvement leans on that: it drops the exit, clears the callback thread stack with lua_settop(L2, 0) so nothing accumulates across samples, and sampling carries on.
This matters most at the LuaJIT 2.0.4 32-bit allocation ceiling during a long capture, where an out-of-memory raise in the callback is absorbed and the run continues.
Timer thread joined before its state is cleared
On stop, luaJIT_profile_stop (lj_profile.c) cleared ps->g = NULL and then stopped the timer.
The Windows sampler runs on its own thread, which reads ps->g inside profile_trigger and dereferences it (g->hookmask, lj_profile.c:163-167).
Joining that thread before clearing its state keeps the two from overlapping on ps->g.
The stop now joins the sampler thread first, setting the abort flag and waiting on it (WaitForSingleObject(ps->thread, INFINITE), lj_profile.c:287-290).
ps->g clears once the thread has exited, so the timer path always reads a live g.
Files changed
Testing
Built locally (DX11, 0 errors) and run on a live GAMMA session with both profilers compiled in.
For the callback improvement, a probe armed a callback that raised every 10ms for about 6 seconds, roughly 600 raises, then stopped it.
The game stayed alive throughout and kept logging after the probe cleared the callback.
The timer improvement follows from the source above: the deref is direct, and joining first keeps the clear ordered after the thread exits.
The sampler ran a full session with a clean stop.