Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Emitter memory leak #330

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from

Conversation

RiskoZoSlovenska
Copy link
Contributor

Listeners created by the Emitter:once() method get put into an internal table, but are never removed from said table, causing them to never be GC'd. This PR fixes this issue by making the internal table's keys be weak.

(It also slightly simplifies the code used to clean the listener tables).

Listeners created by `:once()` were put into an internal table and never removed.
Fixed this by making the table weak-keyed.
@GitSparTV
Copy link

I have a feeling this won't work.
Was this tested?
Because numbers are not a subject of being weak for GC as I remember

@RiskoZoSlovenska
Copy link
Contributor Author

@GitSparTV The table is not array-like; keys are Listener objects, which should work fine here.

I stumbled upon this issue when I noticed that some of my objects (which used Listeners created by Emitter:once()) weren't being GC'd, and this fix solved that.

@GitSparTV
Copy link

Does :once create Listener object?

@RiskoZoSlovenska
Copy link
Contributor Author

It does

function Emitter:once(eventName, callback, errorHandler)
local listener = Listener(self, eventName, callback, errorHandler)
insert(self._listeners[listener.eventName], listener)
once[listener] = true
return listener
end

@GitSparTV
Copy link

Ok last question, when the emitter is not yet emitted, will the listener object live? What if GC cycle happens before emitting?

@RiskoZoSlovenska
Copy link
Contributor Author

There should also be a reference to the listener in the self._listeners[listener.eventName] table (from which it is removed correctly when the event is fired/the listener is removed).

@GitSparTV
Copy link

Nice

@SinisterRectus
Copy link
Owner

Looking at my code, it looks like I never clean false emitters from the table unless the emission hits the "once" code? I think I meant to add a call to clean somewhere else, like after emitter removal, but never went back to review it. I wonder if we should do that here.

@RiskoZoSlovenska
Copy link
Contributor Author

if v == listener then
listeners[i] = false
return true
end
Pretty sure the listeners[i] = false could be replaced with a table.remove(listeners, i) call (of course, you'd have to iterate backwards).


for i = 1, #listeners do
local listener = listeners[i]
if listener then
if once[listener] then
listeners[i] = false
needsCleaning = true
end
listener:fire(...)
end
end
Same here. You wouldn't even need clean() if you did that unless you were planning to somehow optimize it.

@SinisterRectus
Copy link
Owner

Listeners are intentionally marked for deletion instead of directly removed to avoid unexpected behavior due to table contraction while actively iterating that table. Marked listeners are cleaned only after the emit loop exits and all listeners have been visited. I now remember that this is also why I do not clean listeners in the removeListener method: This method can be called during an event callback, and therefore, during listener table iteration.

@SinisterRectus
Copy link
Owner

My emitter implementation in the sandbox branch does not use the "once" table, so that leak is fixed there and will eventually merge into an active 3.0 branch (probably dev).

I've also just committed a change to the sandbox so that listener tables are now correctly marked in the removeListener method.

There is still the issue where the listener table can theoretically grow unbounded if a user continuously adds and removes listeners. To fix this, I think I need to use a different data structure than a simple Lua table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants