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

Space leak when using the Applicative instance of Behavior #490

Open
albertov opened this issue Mar 27, 2023 · 0 comments
Open

Space leak when using the Applicative instance of Behavior #490

albertov opened this issue Mar 27, 2023 · 0 comments
Assignees

Comments

@albertov
Copy link

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)

repro-leak-constant-mem
repro-leak-increasing-mem

{-# 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 )"
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

No branches or pull requests

2 participants