Oh boy, that is one subtle bug, nice catch! It is indeed unintional and here’s why it happens:
-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
while true do
Can you spot the bug? Hint: It’s in the
If none of your handlers ever actually yields, it will keep reusing the same
runEventHandlerInFreeThread thread… and the
... argument of that thread remains on the stack even through the subsequent handler calls, so anything in the
... can’t be garbage collected until one of the handlers yields, requiring the creation of a new
runEventHandlerInFreeThread thread, and resulting in the eventual garbage collection of the old thread.
So basically, specifically the first set of arguments you pass to a
:Fire call in your game after a handler yields will be held onto until the next time that any handler yields.
I’m not sure what to do about this. It’s a pretty obscure edge case I don’t think will ever actually come up in practice and even when it does it would only leak a small fixed amount of memory. Since solving it will incur a slight performance penalty I’m hesitant to do it with there being so little chance of it mattering. If you want to solve it, you can change
runEventHandlerInFreeThread to this:
local function runEventHandlerInFreeThread()
while true do
And the creation of the
freeRunnerThread to this:
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
coroutine.resume(freeRunnerThread) --> Add this
To basically “clear” the initial step of the coroutine and only invoke handlers via the yield.
Kinda unfortunate that we can’t load LVM bytecode… if we could I would be able to solve this via LVM assembly at no perf cost.