-
Notifications
You must be signed in to change notification settings - Fork 71
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
Memory leak with dynamic behavior switching #261
Comments
Nice, can you share how you made the pretty graph? |
Sure! I just used eventlog2html 😄 More specifically:
and build with I often also build with |
The same problem is present with just dynamic event switching - no need to bring {-# language BlockArguments #-}
module Main where
import Control.Monad
import Data.Functor
import Reactive.Banana
import Reactive.Banana.Frameworks
import System.Mem
import System.Mem.Weak
withGhcDebug = id
main :: IO ()
main = withGhcDebug do
(ah1, fire1) <- newAddHandler
actuate =<< compile do
e <- fromAddHandler ah1
let e2 = observeE $ e $> do
accumE () (id <$ e)
e3 <- switchE never e2
reactimate $ return <$> e3
performGC
putStrLn "Running"
replicateM_ 10000 $ do
fire1 ()
performGC I'll try and solve this leak first. |
Ok, the fix for both of these leaks isn't hard - we can just modify doAddChild (P parent) (P child) = do
level1 <- _levelP <$> readRef child
level2 <- _levelP <$> readRef parent
let level = level1 `max` (level2 + 1)
w <- parent `connectChild` P child
-- Remove any dead children. These three lines are new.
let alive w = maybe False (const True) <$> deRefWeak w
children' <- filterM alive . _childrenP =<< readRef parent
modify' parent $ set childrenP children'
modify' child $ set levelP level . update parentsP (w:) But I'm not particularly happy with this solution. When the |
When implementing this, I was hoping to use finalizers to remove dead children — i.e. when Hm. Finalizers are run concurrently, but to keep our sanity, changes to the network need to be sequential and scheduled (e.g. using a writer part of (One issue that I didn't think deeply enough about is the question of how fast we can remove transitive dependencies. I.e. event |
@HeinrichApfelmus I've also thought about using finalizers, but the whole thing seems a lot more complex/action-at-a-distance than it needs to be. As far as I'm aware, we always have the entire graph right in front of us, through a I don't like finalizers partly because it's unclear when they will run, but more that it's unclear if they will run at all! I'd hate to be in a position where I accumulate just enough garbage to impact performance, but not enough to trigger the right generation GC to solve the problem. |
Yes and no. The trouble is twofold:
I do agree that the documentation on finalizers is rather pessimistic. However, I feel that we may not have a choice, and in practice, it does not seem too bad (well, if it is bad, then we can report this as a bug in GHC. 😄) |
Yea, I was thinking over 1 yesterday! Thanks for sharing. Something I also want to do is try modelling our graph in Alloy and to use a model checker to work out the complexities here! |
Ok, I might have another fix: connectChild parent child = do
w <- mkWeakNodeValue child child
modify' parent $ update childrenP (w:)
+
+ -- Add a finalizer to remove the child from the parent when the child dies.
+ case child of
+ P c@(Ref r _) -> addFinalizer r $ removeParents c
+ _ -> return ()
+
mkWeakNodeValue child (P parent) -- child keeps parent alive The idea is pretty trivial - when a {-# language BlockArguments #-}
module Main where
import Control.Monad
import Control.Monad.IO.Class
import Data.Functor
import Reactive.Banana
import Reactive.Banana.Frameworks
import System.Mem
import System.Mem.Weak
import Control.Concurrent (threadDelay, yield)
withGhcDebug = id
main :: IO ()
main = withGhcDebug do
(ah1, fire1) <- newAddHandler
actuate =<< compile do
e <- fromAddHandler ah1
e2 <- execute $ e $> do
accumE () (id <$ e)
reactimate $ return () <$ e2
performGC
putStrLn "Running"
replicateM_ 10000 $ do
fire1 ()
performMajorGC
-- yield so finalizers can run.
yield
putStrLn "Done" Ran with Some noise, but that clear blue line is the signal - a clear leak. With the fix above, we get: But I also have to run 10x the amount of iterations otherwise it terminates too quickly! So I think I've got a good handle on at least one fix. I think the way to proceed from here is to add a finalizer when we call |
A note to myself as to why we can't just use
This is why we think we need help from the GC. When Note that if we used dynamic event switching and switched out of I need to think about promptly cleaning up a whole sequence of |
…c-evaluation Perform evaluation steps `Network` using `GraphGC` ### Overview This pull request completely changes the way that dependencies between `Pulse` are tracked and used in order to perform an `Evaluation.step` for a `Network`. We use the machinery provided by `GraphGC` to * track dependencies between `Pulse`, using `insertEdge` and `clearPredecessors`. * traverse the `Pulse` in dependency order with early exit, using `walkSuccessors_`. ### Comments * This should fix many remaining issues with garbage collection for `Pulse`, specifically #261 * I think that in order to fix *all* remaining issues for `Pulse`, we may have to look at garbage collection and `Vault`. * This pull request doesn't do anything for `Latch`. Still, 🥳! ### Obsoletes * #182 * sadly, #243
Thank you @ocharles ! I have revisited this problem and decided to redesign the low-level implementation entirely in order to separate concerns better. I have created a The following program now runs in constant space: {-# language BlockArguments #-}
module Main where
import Control.Monad
import Control.Monad.IO.Class
import Data.Functor
import Reactive.Banana
import Reactive.Banana.Frameworks
import System.Mem
import System.Mem.Weak
import Control.Concurrent (threadDelay, yield)
withGhcDebug = id
main :: IO ()
main = withGhcDebug do
(ah1, fire1) <- newAddHandler
actuate =<< compile do
e <- fromAddHandler ah1
e2 <- execute $ e $> do
accumE () never -- previously: accumE () (id <$ e)
reactimate $ return () <$ e2
performGC
putStrLn "Running"
replicateM_ 10000 $ do
fire1 ()
performMajorGC
-- yield so finalizers can run.
yield
putStrLn "Done" … but the program with |
Fixed it: The issue was an accumulation of finalizers, which was due to a short-cut that I took when implementing Unfortunately, the earlier program mentioned in #261 (comment) , involving |
I have fixed an additional space leak with But now, as of commit 29776bd , the heap profile for all programs mentioned in this thread is constant. 🥳 Here is the heap profile for @ocharles' initial example program:
In addition, several variants of the space leaks reported here are now part of the automated test suite — the tests will fail if the network grows unexpectedly. I think that it's time to successfully close this issue. 💪 If you do find more space leaks, please don't hesitate to bring them to my attention! |
What a marathon effort! I've been reading these updates with bated breath, each one delivering a fraction but leaving me wanting more. Congratulations 🎉 |
Thanks! 😊 What do you think — maybe I could pitch the story of these update to Netflix? 🤔 |
I'd watch it 🤷 |
The following program:
Leaks memory:
I've also modified
doAddChild
to print out the number of children in a parent:This shows that a node has a 50000 children at the end. My guess is this is the
Event
e
- wheneverexecute
fires we attach a new stepper toe
, but this child is never removed - even thoughswitchB
should be discarding them.The text was updated successfully, but these errors were encountered: