on develop help sign in
Switch cacheEvent's weakSelf to a FastWeak
011edf2Ryan Trinkle
reflex.cabal+4
@@ -78,6 +78,7 @@ library
78 Reflex.Dynamic,
78 Reflex.Dynamic,
79 Reflex.Dynamic.Uniq,
79 Reflex.Dynamic.Uniq,
80 Reflex.DynamicWriter,
80 Reflex.DynamicWriter,
81+ Reflex.FastWeak,
81 Reflex.FunctorMaybe,
82 Reflex.FunctorMaybe,
82 Reflex.Host.Class,
83 Reflex.Host.Class,
83 Reflex.Patch,
84 Reflex.Patch,
@@ -136,6 +137,9 @@ library
136 if flag(experimental-dependent-sum-instances)
137 if flag(experimental-dependent-sum-instances)
137 ghc-options: -DEXPERIMENTAL_DEPENDENT_SUM_INSTANCES
138 ghc-options: -DEXPERIMENTAL_DEPENDENT_SUM_INSTANCES
138
139
140+ if impl(ghcjs)
141+ build-depends: ghcjs-base
142+
139test-suite semantics
143test-suite semantics
140 type: exitcode-stdio-1.0
144 type: exitcode-stdio-1.0
141 main-is: semantics.hs
145 main-is: semantics.hs
src/Data/FastWeakBag.hsnew file+123
@@ -0,0 +1,123 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE ExistentialQuantification #-}
4+#ifdef USE_REFLEX_OPTIMIZER
5+{-# OPTIONS_GHC -fplugin=Reflex.Optimizer #-}
6+#endif
7+-- | This module defines the 'FastWeakBag' type, which represents a mutable
8+-- collection of items that does not cause the items to be retained in memory.
9+-- This is useful for situations where a value needs to be inspected or modified
10+-- if it is still alive, but can be ignored if it is dead.
11+module Data.FastWeakBag
12+ ( FastWeakBag
13+ , FastWeakBagTicket
14+ , empty
15+ , singleton
16+ , insert
17+ , traverse
18+ , remove
19+ -- * Internal functions
20+ -- These will not always be available.
21+ , _weakBag_children --TODO: Don't export this
22+ ) where
23+
24+import Control.Exception
25+import Control.Monad hiding (forM_, mapM_)
26+import Control.Monad.IO.Class
27+import Data.Foldable (forM_, mapM_)
28+import Data.IntMap.Strict (IntMap)
29+import qualified Data.IntMap.Strict as IntMap
30+import Data.IORef
31+import Reflex.FastWeak
32+import System.Mem.Weak
33+
34+import Prelude hiding (mapM_, traverse)
35+
36+-- | A @FastWeakBag a@ holds a set of values of type @a@, but does not retain them -
37+-- that is, they can still be garbage-collected. As long as the @a@s remain
38+-- alive, the 'FastWeakBag' will continue to refer to them.
39+data FastWeakBag a = FastWeakBag
40+ { _weakBag_nextId :: {-# UNPACK #-} !(IORef Int) --TODO: what if this wraps around?
41+ , _weakBag_children :: {-# UNPACK #-} !(IORef (IntMap (Weak a)))
42+ }
43+
44+-- | When inserting an item into a 'FastWeakBag', a 'FastWeakBagTicket' is returned. If
45+-- the caller retains the ticket, the item is guranteed to stay in memory (and
46+-- thus in the 'FastWeakBag'). The ticket can also be used to remove the item from
47+-- the 'FastWeakBag' prematurely (i.e. while it is still alive), using 'remove'.
48+data FastWeakBagTicket = forall a. FastWeakBagTicket
49+ { _weakBagTicket_weakItem :: {-# UNPACK #-} !(Weak a)
50+ , _weakBagTicket_item :: {-# NOUNPACK #-} !a
51+ }
52+
53+-- | Insert an item into a 'FastWeakBag'.
54+{-# INLINE insert #-}
55+insert :: a -- ^ The item
56+ -> FastWeakBag a -- ^ The 'FastWeakBag' to insert into
57+ -> FastWeak b -- ^ An arbitrary value to be used in the following
58+ -- callback
59+ -> (b -> IO ()) -- ^ A callback to be invoked when the item is removed
60+ -- (whether automatically by the item being garbage
61+ -- collected or manually via 'remove')
62+ -> IO FastWeakBagTicket -- ^ Returns a 'FastWeakBagTicket' that ensures the item
63+ -- is retained and allows the item to be removed.
64+insert a (FastWeakBag nextId children) wb finalizer = {-# SCC "insert" #-} do
65+ a' <- evaluate a
66+ myId <- atomicModifyIORef' nextId $ \n -> (succ n, n)
67+ let cleanup = do
68+ mb <- getFastWeakValue wb
69+ forM_ mb $ \b -> do
70+ csWithoutMe <- atomicModifyIORef children $ \cs ->
71+ let !csWithoutMe = IntMap.delete myId cs
72+ in (csWithoutMe, csWithoutMe)
73+ when (IntMap.null csWithoutMe) $ finalizer b
74+ return ()
75+ return ()
76+ wa <- mkWeakPtr a' $ Just cleanup
77+ atomicModifyIORef' children $ \cs -> (IntMap.insert myId wa cs, ())
78+ return $ FastWeakBagTicket
79+ { _weakBagTicket_weakItem = wa
80+ , _weakBagTicket_item = a'
81+ }
82+
83+-- | Create an empty 'FastWeakBag'.
84+{-# INLINE empty #-}
85+empty :: IO (FastWeakBag a)
86+empty = {-# SCC "empty" #-} do
87+ nextId <- newIORef 1
88+ children <- newIORef IntMap.empty
89+ let bag = FastWeakBag
90+ { _weakBag_nextId = nextId
91+ , _weakBag_children = children
92+ }
93+ return bag
94+
95+-- | Create a 'FastWeakBag' with one item; equivalent to creating the 'FastWeakBag' with
96+-- 'empty', then using 'insert'.
97+{-# INLINE singleton #-}
98+singleton :: a -> FastWeak b -> (b -> IO ()) -> IO (FastWeakBag a, FastWeakBagTicket)
99+singleton a wb finalizer = {-# SCC "singleton" #-} do
100+ bag <- empty
101+ ticket <- insert a bag wb finalizer
102+ return (bag, ticket)
103+
104+{-# INLINE traverse #-}
105+-- | Visit every node in the given list. If new nodes are appended during the
106+-- traversal, they will not be visited. Every live node that was in the list
107+-- when the traversal began will be visited exactly once; however, no guarantee
108+-- is made about the order of the traversal.
109+traverse :: MonadIO m => FastWeakBag a -> (a -> m ()) -> m ()
110+traverse (FastWeakBag _ children) f = {-# SCC "traverse" #-} do
111+ cs <- liftIO $ readIORef children
112+ forM_ cs $ \c -> do
113+ ma <- liftIO $ deRefWeak c
114+ mapM_ f ma
115+
116+-- | Remove an item from the 'FastWeakBag'; does nothing if invoked multiple times
117+-- on the same 'FastWeakBagTicket'.
118+{-# INLINE remove #-}
119+remove :: FastWeakBagTicket -> IO ()
120+remove (FastWeakBagTicket w _) = {-# SCC "remove" #-} finalize w
121+--TODO: Should 'remove' also drop the reference to the item?
122+
123+--TODO: can/should we provide a null FastWeakBagTicket?
src/Reflex/FastWeak.hsnew file+75
@@ -0,0 +1,75 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ForeignFunctionInterface #-}
3+{-# LANGUAGE JavaScriptFFI #-}
4+{-# LANGUAGE MagicHash #-}
5+module Reflex.FastWeak
6+ ( FastWeakTicket
7+ , FastWeak
8+ , fastWeakTicket
9+ , fastWeakTicketValue
10+ , getFastWeakTicketWeak
11+ , getFastWeakValue
12+ ) where
13+
14+#ifdef ghcjs_HOST_OS
15+import GHCJS.Types
16+import Unsafe.Coerce
17+#else
18+#endif
19+
20+
21+#ifdef ghcjs_HOST_OS
22+type Ref = JSVal
23+#else
24+type Ref = ()
25+#endif
26+
27+newtype FastWeakTicket a = FastWeakTicket Ref
28+
29+newtype FastWeak a = FastWeak Ref
30+
31+-- Just designed to mirror JSVal, so that we can unsafeCoerce between the two
32+data Val a = Val { unVal :: a }
33+
34+-- Do we have a concern about accidentally retaining the ticket here?
35+fastWeakTicketValue :: FastWeakTicket a -> a
36+#ifdef ghcjs_HOST_OS
37+fastWeakTicketValue t = unVal (unsafeCoerce (js_ticketVal t))
38+
39+foreign import javascript unsafe "$r = $1.val;" js_ticketVal :: FastWeakTicket a -> JSVal
40+#else
41+fastWeakTicketValue = error "fastWeakTicketValue not implemented"
42+#endif
43+
44+getFastWeakValue :: FastWeak a -> IO (Maybe a)
45+#ifdef ghcjs_HOST_OS
46+getFastWeakValue w = do
47+ r <- js_weakVal w
48+ case js_isNull r of
49+ True -> return Nothing
50+ False -> return (unsafeCoerce r)
51+
52+foreign import javascript unsafe "$1 === null" js_isNull :: JSVal -> Bool
53+
54+foreign import javascript unsafe "$r = ($1.ticket === null) ? null : $1.ticket.val;" js_weakVal :: FastWeak a -> IO JSVal
55+#else
56+getFastWeakValue = error "getFastWeakValue not implemented"
57+#endif
58+
59+-- I think it's fine if this is lazy - it'll retain the 'a', but so would the output; we just need to make sure it's forced before we start relying on the associated FastWeak to actually be weak
60+fastWeakTicket :: a -> FastWeakTicket a
61+#ifdef ghcjs_HOST_OS
62+fastWeakTicket v = js_fastWeakTicket (unsafeCoerce (Val v))
63+
64+foreign import javascript unsafe "$r = new h$FastWeakTicket($1);" js_fastWeakTicket :: JSVal -> FastWeakTicket a
65+#else
66+fastWeakTicket = error "fastWeakTicket not implemented"
67+#endif
68+
69+-- Needs IO so that it can force the value - otherwise, could end up with a reference to the Ticket, which would retain the value
70+#ifdef ghcjs_HOST_OS
71+foreign import javascript unsafe "$r = $1.weak;" getFastWeakTicketWeak :: FastWeakTicket a -> IO (FastWeak a)
72+#else
73+getFastWeakTicketWeak :: FastWeakTicket a -> IO (FastWeak a)
74+getFastWeakTicketWeak = error "fastWeakTicketWeak not implemented"
75+#endif
src/Reflex/Spider/Internal.hs+39−30
@@ -8,9 +8,7 @@
8{-# LANGUAGE FunctionalDependencies #-}
8{-# LANGUAGE FunctionalDependencies #-}
9{-# LANGUAGE GADTs #-}
9{-# LANGUAGE GADTs #-}
10{-# LANGUAGE GeneralizedNewtypeDeriving #-}
10{-# LANGUAGE GeneralizedNewtypeDeriving #-}
11−#if defined(DEBUG) || defined(DEBUG_CYCLES)
12{-# LANGUAGE LambdaCase #-}
11{-# LANGUAGE LambdaCase #-}
13−#endif
14{-# LANGUAGE MagicHash #-}
12{-# LANGUAGE MagicHash #-}
15{-# LANGUAGE MultiParamTypeClasses #-}
13{-# LANGUAGE MultiParamTypeClasses #-}
16{-# LANGUAGE MultiWayIf #-}
14{-# LANGUAGE MultiWayIf #-}
@@ -62,6 +60,7 @@ import Data.Word
62import GHC.Exts
60import GHC.Exts
63import GHC.IORef (IORef (..))
61import GHC.IORef (IORef (..))
64import GHC.Stack
62import GHC.Stack
63+import Reflex.FastWeak
65import Reflex.FunctorMaybe
64import Reflex.FunctorMaybe
66import System.IO.Unsafe
65import System.IO.Unsafe
67import System.Mem.Weak
66import System.Mem.Weak
@@ -75,6 +74,8 @@ import Data.Monoid (mempty)
75import Data.Tree (Forest, Tree (..), drawForest)
74import Data.Tree (Forest, Tree (..), drawForest)
76#endif
75#endif
77
76
77+import Data.FastWeakBag (FastWeakBag)
78+import qualified Data.FastWeakBag as FastWeakBag
78import Data.WeakBag (WeakBag, WeakBagTicket, _weakBag_children)
79import Data.WeakBag (WeakBag, WeakBagTicket, _weakBag_children)
79import qualified Data.WeakBag as WeakBag
80import qualified Data.WeakBag as WeakBag
80#ifndef SPECIALIZE_TO_SPIDERTIMELINE_GLOBAL
81#ifndef SPECIALIZE_TO_SPIDERTIMELINE_GLOBAL
@@ -252,10 +253,9 @@ pushCheap f e = Event $ \sub -> do
252 return (subscription, occ')
253 return (subscription, occ')
253
254
254data CacheSubscribed x a
255data CacheSubscribed x a
255− = CacheSubscribed { _cacheSubscribed_subscribers :: {-# UNPACK #-} !(WeakBag (Subscriber x a))
256+ = CacheSubscribed { _cacheSubscribed_subscribers :: {-# UNPACK #-} !(FastWeakBag (Subscriber x a))
256− , _cacheSubscribed_weakSelf :: {-# UNPACK #-} !(IORef (Weak (CacheSubscribed x a)))
257 , _cacheSubscribed_parent :: {-# UNPACK #-} !(EventSubscription x)
257 , _cacheSubscribed_parent :: {-# UNPACK #-} !(EventSubscription x)
258− , _cacheSubscribed_cached :: {-# UNPACK #-} !(IORef (Maybe (CacheSubscribed x a)))
258+ , _cacheSubscribed_cached :: {-# UNPACK #-} !(IORef (Maybe (FastWeakTicket (CacheSubscribed x a))))
259 , _cacheSubscribed_occurrence :: {-# UNPACK #-} !(IORef (Maybe a))
259 , _cacheSubscribed_occurrence :: {-# UNPACK #-} !(IORef (Maybe a))
260 }
260 }
261
261
@@ -263,57 +263,59 @@ data CacheSubscribed x a
263-- repeatedly
263-- repeatedly
264--
264--
265--TODO: Try a caching strategy where we subscribe directly to the parent when
265--TODO: Try a caching strategy where we subscribe directly to the parent when
266−--there's only one subscriber, and then build our own WeakBag only when a second
266+--there's only one subscriber, and then build our own FastWeakBag only when a second
267--subscriber joins
267--subscriber joins
268{-# NOINLINE cacheEvent #-}
268{-# NOINLINE cacheEvent #-}
269−cacheEvent :: HasSpiderTimeline x => Event x a -> Event x a
269+cacheEvent :: forall x a. HasSpiderTimeline x => Event x a -> Event x a
270cacheEvent e =
270cacheEvent e =
271#ifdef DEBUG_TRACE_EVENTS
271#ifdef DEBUG_TRACE_EVENTS
272 withStackOneLine $ \callSite -> Event $
272 withStackOneLine $ \callSite -> Event $
273#else
273#else
274 Event $
274 Event $
275#endif
275#endif
276+ let mSubscribedRef :: IORef (Maybe (FastWeakTicket (CacheSubscribed x a)))
276− let !mSubscribedRef = unsafeNewIORef e Nothing
277+ !mSubscribedRef = unsafeNewIORef e Nothing
277 in \sub -> {-# SCC "cacheEvent" #-} do
278 in \sub -> {-# SCC "cacheEvent" #-} do
278#ifdef DEBUG_TRACE_EVENTS
279#ifdef DEBUG_TRACE_EVENTS
279 unless (BS8.null callSite) $ liftIO $ BS8.hPutStrLn stderr callSite
280 unless (BS8.null callSite) $ liftIO $ BS8.hPutStrLn stderr callSite
280#endif
281#endif
281− mSubscribed <- liftIO $ readIORef mSubscribedRef
282+ subscribedTicket <- liftIO (readIORef mSubscribedRef) >>= \case
282− case mSubscribed of
283+ Just subscribedTicket -> return subscribedTicket
283− Just subscribed -> liftIO $ do
284− ticket <- WeakBag.insert sub (_cacheSubscribed_subscribers subscribed) (_cacheSubscribed_weakSelf subscribed) cleanupCacheSubscribed
285− occ <- readIORef $ _cacheSubscribed_occurrence subscribed
286− let es = EventSubscribed
287− { eventSubscribedHeightRef = eventSubscribedHeightRef $ _eventSubscription_subscribed $ _cacheSubscribed_parent subscribed
288− , eventSubscribedRetained = toAny subscribed
289− }
290− return (EventSubscription (WeakBag.remove ticket >> touch ticket) es, occ)
291 Nothing -> do
284 Nothing -> do
292− weakSelf <- liftIO $ newIORef $ error "cacheEvent: weakSelf not yet intialized"
285+ subscribers <- liftIO FastWeakBag.empty
293− (subscribers, ticket) <- liftIO $ WeakBag.singleton sub weakSelf cleanupCacheSubscribed
294− occRef <- liftIO $ newIORef undefined
286+ occRef <- liftIO $ newIORef Nothing -- This should never be read prior to being set below
295 (parentSub, occ) <- subscribeAndRead e $ Subscriber
287 (parentSub, occ) <- subscribeAndRead e $ Subscriber
296 { subscriberPropagate = \a -> do
288 { subscriberPropagate = \a -> do
297 liftIO $ writeIORef occRef $ Just a
289 liftIO $ writeIORef occRef $ Just a
298 scheduleClear occRef
290 scheduleClear occRef
299− propagate a subscribers
291+ propagateFast a subscribers
300 , subscriberInvalidateHeight = \old -> do
292 , subscriberInvalidateHeight = \old -> do
301− WeakBag.traverse subscribers $ invalidateSubscriberHeight old
293+ FastWeakBag.traverse subscribers $ invalidateSubscriberHeight old
302 , subscriberRecalculateHeight = \new -> do
294 , subscriberRecalculateHeight = \new -> do
303− WeakBag.traverse subscribers $ recalculateSubscriberHeight new
295+ FastWeakBag.traverse subscribers $ recalculateSubscriberHeight new
304 }
296 }
305− liftIO $ writeIORef occRef occ
297+ when (isJust occ) $ do
298+ liftIO $ writeIORef occRef occ -- Set the initial value of occRef; we don't need to do this if occ is Nothing
306− when (isJust occ) $ scheduleClear occRef
299+ scheduleClear occRef
307 let !subscribed = CacheSubscribed
300 let !subscribed = CacheSubscribed
308 { _cacheSubscribed_subscribers = subscribers
301 { _cacheSubscribed_subscribers = subscribers
309− , _cacheSubscribed_weakSelf = weakSelf
310 , _cacheSubscribed_parent = parentSub
302 , _cacheSubscribed_parent = parentSub
311 , _cacheSubscribed_cached = mSubscribedRef
303 , _cacheSubscribed_cached = mSubscribedRef
312 , _cacheSubscribed_occurrence = occRef
304 , _cacheSubscribed_occurrence = occRef
313 }
305 }
306+ !subscribedTicket = fastWeakTicket subscribed
314− liftIO $ writeIORef mSubscribedRef $ Just subscribed
307+ liftIO $ writeIORef mSubscribedRef $ Just subscribedTicket
315− liftIO $ writeIORef weakSelf =<< evaluate =<< mkWeakPtrWithDebug subscribed "PushSubscribed"
308+ return subscribedTicket
309+ liftIO $ do
310+ let subscribed = fastWeakTicketValue subscribedTicket
311+ subscribedWeak <- getFastWeakTicketWeak subscribedTicket
312+ ticket <- FastWeakBag.insert sub (_cacheSubscribed_subscribers subscribed) subscribedWeak cleanupCacheSubscribed
313+ occ <- readIORef $ _cacheSubscribed_occurrence subscribed
314+ let es = EventSubscribed
315+ { eventSubscribedHeightRef = eventSubscribedHeightRef $ _eventSubscription_subscribed $ _cacheSubscribed_parent subscribed
316+ , eventSubscribedRetained = toAny subscribed
317+ }
316− return (EventSubscription (WeakBag.remove ticket >> touch ticket) (EventSubscribed (eventSubscribedHeightRef $ _eventSubscription_subscribed parentSub) (toAny subscribed)), occ)
318+ return (EventSubscription (FastWeakBag.remove ticket >> touch ticket) es, occ)
317
319
318cleanupCacheSubscribed :: CacheSubscribed x a -> IO ()
320cleanupCacheSubscribed :: CacheSubscribed x a -> IO ()
319cleanupCacheSubscribed self = do
321cleanupCacheSubscribed self = do
@@ -497,6 +499,13 @@ propagate a subscribers = withIncreasedDepth $ do
497 --TODO: Should we check if nodes already have their values before propagating? Maybe we're re-doing work
499 --TODO: Should we check if nodes already have their values before propagating? Maybe we're re-doing work
498 WeakBag.traverse subscribers $ \s -> subscriberPropagate s a
500 WeakBag.traverse subscribers $ \s -> subscriberPropagate s a
499
501
502+-- | Propagate everything at the current height
503+propagateFast :: a -> FastWeakBag (Subscriber x a) -> EventM x ()
504+propagateFast a subscribers = withIncreasedDepth $ do
505+ -- Note: in the following traversal, we do not visit nodes that are added to the list during our traversal; they are new events, which will necessarily have full information already, so there is no need to traverse them
506+ --TODO: Should we check if nodes already have their values before propagating? Maybe we're re-doing work
507+ FastWeakBag.traverse subscribers $ \s -> subscriberPropagate s a
508+
500--------------------------------------------------------------------------------
509--------------------------------------------------------------------------------
501-- EventSubscribed
510-- EventSubscribed
502--------------------------------------------------------------------------------
511--------------------------------------------------------------------------------