fix: use weakref in EventGenerator to prevent memory leak#3360
fix: use weakref in EventGenerator to prevent memory leak#3360
Conversation
1afe307 to
b939c4f
Compare
|
Performance benchmarks:
|
7e79eb8 to
7ce0b03
Compare
|
Thanks for this cleaning and PR. I just merged #3320, which adds a new function for creating the weakref and do some tests on it. Can you update this PR by merging with upstream main and evaluate whether the new function is useful for this PR? |
mesa/time/events.py
Outdated
| f"function {function} is not weakly referenceable. " | ||
| f"Use a user-defined function or method instead." | ||
| ) from e | ||
| # Here it will raise a value error if the function returns none |
There was a problem hiding this comment.
all of this is replaceable by the new function.
mesa/time/events.py
Outdated
| if weak_fun() is None: | ||
| raise ValueError( | ||
| "Cannot create EventGenerator with a function that is already garbage collected" | ||
| ) |
There was a problem hiding this comment.
we left this out of #3320, because it is very OS specific whether this adds value. I am still considering adding a check like this, but I need to check its behavior across different operating systems.
mesa/time/events.py
Outdated
| @property | ||
| def function(self) -> Callable: | ||
| """Get the function. If garbage collected, returns no-op.""" | ||
| func = self._function() | ||
|
|
||
| return func if func is not None else lambda: None |
There was a problem hiding this comment.
This property checks if the weak reference is dead (garbage collected) and then it will return none
There was a problem hiding this comment.
I know what the code does, I am asking why you deemed it necessary to put this into a property.
There was a problem hiding this comment.
Upon closer look, I realize the @property might be redundant.
We already check the weakref safely in _execute_and_reschedule:
func = self._function()
if func is None:
self.stop()
returnSince the safety check happens at execution time anyway, I'm okay to remove this @Property completely and rely on the check in _execute_and_reschedule.
mesa/time/events.py
Outdated
| return # Silent no-op (no error raised) | ||
|
|
||
| # Execute the function | ||
| func() |
There was a problem hiding this comment.
I suggest not calling this variable func. This is a protected term in Python.
There was a problem hiding this comment.
ok i will change it
tests/test_lifespan.py
Outdated
| assert lifetimes.Step.max() == 2 | ||
|
|
||
|
|
||
| class TestEventGeneratorMemoryLeak(unittest.TestCase): |
There was a problem hiding this comment.
I suggest moving these tests into test_events.py, rather than here.
There was a problem hiding this comment.
ok I will move it
There was a problem hiding this comment.
I meant all tests you added to test_lifespan.py
Hello @quaquel , I was going through the merged #3320 code and in the function this line will raise the value error on both the cases Question: Is blocking Case 1 intentional? Since my_func holds a reference, it shouldn't be garbage collected. Should we allow stored lambdas, or is the strict blocking deliberate to avoid edge cases? On the new function: Yes, function is definitely useful for this PR - it centralizes the weak reference logic and makes the code cleaner. I'll update my implementation to use it. |
|
Assigning a a lambda function to a variable defeats the purpose of a lambda as an anonymous function. Most linters will also flag it as a problem and it's considered an anti pattern. So, I am fine with just blocking lambda's in general. |
06c637d to
ed51889
Compare
a605785 to
0ed3c79
Compare
51c46bb to
8b1a4b2
Compare
4fc42d7 to
685b1cc
Compare
4f67f5f to
8d5fb89
Compare
|
@quaquel i have made the changes please check |
mesa/time/events.py
Outdated
| @@ -270,7 +273,16 @@ def _execute_and_reschedule(self) -> None: | |||
| if not self._active: | |||
| return | |||
|
|
|||
| self.function() | |||
| # Check weakref HERE (execution time), not in property getter | |||
| # This matches Event class behavior - weakref check during execution | |||
| fn = self._function() | |||
There was a problem hiding this comment.
| # Check weakref HERE (execution time), not in property getter | |
| # This matches Event class behavior - weakref check during execution | |
| fn = self.function() |
Summary
This PR fixes the memory leak by:
Bug / Issue
Pr for #3332
EventGenerator.function is a hard reference rather than a weak ref as used in Event. This can lead to memory leaks if a generator is used in combination with, e.g., an Agent. If the agent is removed from the model but remains part of an EventGenerator, it cannot be garbage-collected.
Implementation
As per the coversation in #3320
The behavior of both Event and EventGenerator should be the same
so,
__getstate__and__setstate__for the pickling issueTesting
It is work as expected
Event(5, lambda: 5)fail, whilea = lambada:10;Event(5, a)still works.Additional Notes
Eventclass weak reference pattern for consistency