I've encountered a space leak in the Reflex library when using the Applicative instance of Behavior. The space leak occurs when a Behavior is constructed with the Applicative instance and sampled, as shown in the following snippet:
sample ((<>) <$> current dynA <*> current dynB)
However, when the Behavior is constructed by calling current on a Dynamic which is constructed with the Applicative instance, there is no space leak:
sample ( current ( (<>) <$> dynA <*> dynB ))
I've profiled my program and found that the space leak is related to DEAD_WEAK objects created by the behaviorPull closure and retained by the accumMaybeMDyn closure.
I'm currently working around it by using a forked version of reflex-vty which defines _vtyResult_picture :: Dynamic t V.Picture instead of _vtyResult_picture :: Behavior t V.Picture (see plow-technologies/reflex-vty@e426a01) but I believe a proper fix belongs in Reflex since the documentation suggests that sampling a Behavior for outputs by the host framework is the recommended pattern.
I'm using GHC 9.2.4 and reflex-0.8.2.1 but I've also reproduced it with the develop branch of Reflex. I'm attaching the simplest reproducer I could came up with. It can be called with constant-memory or increasing-memory as an argument, the later demos the space leak. I've also attatched the SVG rendering of the .hp files for each run (GitHub won't allow the .hp files)


{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
module Main where
import Control.Concurrent
import Control.Monad
import Control.Monad.Fix
import Control.Monad.IO.Class
import Control.Monad.Identity
import Control.Monad.Ref
import Data.Dependent.Sum
import Data.IORef
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Time
import Reflex
import Reflex.Host.Class
import System.Environment
import System.Exit
type MonadTestApp t m =
( Reflex t,
MonadHold t m,
MonadHold t (Performable m),
MonadFix m,
MonadFix (Performable m),
ReflexHost t,
PostBuild t m,
PerformEvent t m,
MonadIO m,
MonadIO (Performable m),
MonadIO (HostFrame t),
Ref m ~ IORef,
Ref (HostFrame t) ~ IORef,
MonadRef (HostFrame t),
NotReady t m,
TriggerEvent t m
)
type TestApp t m =
MonadTestApp t m =>
m (Behavior t T.Text)
-- | Run a program written in the framework. This will do all the necessary
-- work to integrate the Reflex-based guest program with the outside world
-- via IO.
host ::
(forall t m. TestApp t m) ->
IO ()
host myGuest =
-- Use the Spider implementation of Reflex.
runSpiderHost $ do
(postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
events <- liftIO newChan
-- Evaluate our user's program to set up the data flow graph.
(b, fc@(FireCommand fire)) <-
hostPerformEventT $
flip runPostBuildT postBuild $
flip runTriggerEventT events myGuest
mPostBuildTrigger <- readRef postBuildTriggerRef
forM_ mPostBuildTrigger $ \postBuildTrigger ->
fire [postBuildTrigger :=> Identity ()] $ return ()
-- Begin our event processing loop.
forever $ do
ers <- liftIO $ readChan events
liftIO . T.putStr . T.unlines
=<< fireEventTriggerRefs fc ers (sample b)
where
fireEventTriggerRefs ::
(Monad (ReadPhase m), MonadIO m) =>
FireCommand t m ->
[DSum (EventTriggerRef t) TriggerInvocation] ->
ReadPhase m a ->
m [a]
fireEventTriggerRefs (FireCommand fire) ers rcb = do
mes <- liftIO $
forM ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do
me <- readIORef er
return $! fmap (\e -> e `seq` e :=> Identity a) me
a <- fire (catMaybes mes) rcb
liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
return a
-- | This guest does not have a space leak
guestDynApplicative :: TestApp t m
guestDynApplicative = do
(messages1D, messages2D) <- twoMessageBuffers
pure $ current $ fmap (T.unlines . reverse) $ (<>) <$> messages1D <*> messages2D
-- | This guest does have a space leak
guestBhvApplicative :: TestApp t m
guestBhvApplicative = do
(messages1D, messages2D) <- twoMessageBuffers
pure $ fmap (T.unlines . reverse) $ (<>) <$> current messages1D <*> current messages2D
twoMessageBuffers ::
( Reflex t,
MonadIO m,
MonadHold t m,
TriggerEvent t m,
MonadFix m,
PostBuild t m,
PerformEvent t m,
MonadIO (Performable m)
) =>
m (Dynamic t [T.Text], Dynamic t [T.Text])
twoMessageBuffers = do
message1E <- ("message1" <$) <$> (tickLossy 0.5 =<< liftIO getCurrentTime)
let acc10 x xs = x : take 9 xs
messages1D <- foldDyn acc10 [] message1E
-- The 'never' in the following line causes a space leak when 'messages2D' is
-- turned into a Behavior with 'current' and this Behavior value is then used in
-- an 'Applicative' expression (see guestBhvApplicative).
messages2D <- foldDyn acc10 [] never
pure (messages1D, messages2D)
main :: IO ()
main =
getArgs >>= \case
["constant-mem"] -> host guestDynApplicative
["increasing-mem"] -> host guestBhvApplicative
_ -> die "Usage: repro-leak ( constant-mem | increasing-mem )"
I've encountered a space leak in the Reflex library when using the Applicative instance of Behavior. The space leak occurs when a Behavior is constructed with the Applicative instance and sampled, as shown in the following snippet:
However, when the Behavior is constructed by calling current on a Dynamic which is constructed with the Applicative instance, there is no space leak:
I've profiled my program and found that the space leak is related to DEAD_WEAK objects created by the behaviorPull closure and retained by the accumMaybeMDyn closure.
I'm currently working around it by using a forked version of
reflex-vtywhich defines_vtyResult_picture :: Dynamic t V.Pictureinstead of_vtyResult_picture :: Behavior t V.Picture(see plow-technologies/reflex-vty@e426a01) but I believe a proper fix belongs in Reflex since the documentation suggests that sampling aBehaviorfor outputs by the host framework is the recommended pattern.I'm using GHC 9.2.4 and reflex-0.8.2.1 but I've also reproduced it with the
developbranch of Reflex. I'm attaching the simplest reproducer I could came up with. It can be called withconstant-memoryorincreasing-memoryas an argument, the later demos the space leak. I've also attatched the SVG rendering of the.hpfiles for each run (GitHub won't allow the.hpfiles){-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} module Main where import Control.Concurrent import Control.Monad import Control.Monad.Fix import Control.Monad.IO.Class import Control.Monad.Identity import Control.Monad.Ref import Data.Dependent.Sum import Data.IORef import Data.Maybe import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Time import Reflex import Reflex.Host.Class import System.Environment import System.Exit type MonadTestApp t m = ( Reflex t, MonadHold t m, MonadHold t (Performable m), MonadFix m, MonadFix (Performable m), ReflexHost t, PostBuild t m, PerformEvent t m, MonadIO m, MonadIO (Performable m), MonadIO (HostFrame t), Ref m ~ IORef, Ref (HostFrame t) ~ IORef, MonadRef (HostFrame t), NotReady t m, TriggerEvent t m ) type TestApp t m = MonadTestApp t m => m (Behavior t T.Text) -- | Run a program written in the framework. This will do all the necessary -- work to integrate the Reflex-based guest program with the outside world -- via IO. host :: (forall t m. TestApp t m) -> IO () host myGuest = -- Use the Spider implementation of Reflex. runSpiderHost $ do (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef events <- liftIO newChan -- Evaluate our user's program to set up the data flow graph. (b, fc@(FireCommand fire)) <- hostPerformEventT $ flip runPostBuildT postBuild $ flip runTriggerEventT events myGuest mPostBuildTrigger <- readRef postBuildTriggerRef forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return () -- Begin our event processing loop. forever $ do ers <- liftIO $ readChan events liftIO . T.putStr . T.unlines =<< fireEventTriggerRefs fc ers (sample b) where fireEventTriggerRefs :: (Monad (ReadPhase m), MonadIO m) => FireCommand t m -> [DSum (EventTriggerRef t) TriggerInvocation] -> ReadPhase m a -> m [a] fireEventTriggerRefs (FireCommand fire) ers rcb = do mes <- liftIO $ forM ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do me <- readIORef er return $! fmap (\e -> e `seq` e :=> Identity a) me a <- fire (catMaybes mes) rcb liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb return a -- | This guest does not have a space leak guestDynApplicative :: TestApp t m guestDynApplicative = do (messages1D, messages2D) <- twoMessageBuffers pure $ current $ fmap (T.unlines . reverse) $ (<>) <$> messages1D <*> messages2D -- | This guest does have a space leak guestBhvApplicative :: TestApp t m guestBhvApplicative = do (messages1D, messages2D) <- twoMessageBuffers pure $ fmap (T.unlines . reverse) $ (<>) <$> current messages1D <*> current messages2D twoMessageBuffers :: ( Reflex t, MonadIO m, MonadHold t m, TriggerEvent t m, MonadFix m, PostBuild t m, PerformEvent t m, MonadIO (Performable m) ) => m (Dynamic t [T.Text], Dynamic t [T.Text]) twoMessageBuffers = do message1E <- ("message1" <$) <$> (tickLossy 0.5 =<< liftIO getCurrentTime) let acc10 x xs = x : take 9 xs messages1D <- foldDyn acc10 [] message1E -- The 'never' in the following line causes a space leak when 'messages2D' is -- turned into a Behavior with 'current' and this Behavior value is then used in -- an 'Applicative' expression (see guestBhvApplicative). messages2D <- foldDyn acc10 [] never pure (messages1D, messages2D) main :: IO () main = getArgs >>= \case ["constant-mem"] -> host guestDynApplicative ["increasing-mem"] -> host guestBhvApplicative _ -> die "Usage: repro-leak ( constant-mem | increasing-mem )"