on master help sign in
Implement explicit concurrency via channels
62c4903Harendra Kumar
src/Streamly/Internal/Data/Stream/Async.hsnew file+204
@@ -0,0 +1,204 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+-- Non-parallelizable stream combinators like unfoldrM, iterateM etc. can be
10+-- evaluated concurrently with the stream consumer by using `eval`.
11+-- Parallelizable combinators like repeatM, replicateM can generate the stream
12+-- concurrently using 'concatMap'.
13+
14+-- Single effects related functionality can be moved to
15+-- Data.Async/Control.Async.
16+-- Common Channel functionality to Data.Channel.
17+-- Stream channel to Data.Stream.Channel.
18+
19+module Streamly.Internal.Data.Stream.Async
20+ (
21+ MonadAsync
22+
23+ -- * Configuration
24+ , Config
25+ , maxThreads
26+ , maxBuffer
27+ -- maxYields
28+ , Rate(..)
29+ , rate
30+ , avgRate
31+ , minRate
32+ , maxRate
33+ , constRate
34+
35+ -- * Combinators
36+ -- | Stream combinators using Async channel
37+
38+ -- XXX Move to parallel module
39+ , eval
40+ , evalWith
41+
42+ , mapM
43+ , mapMWith
44+ , sequence
45+ , sequenceWith
46+
47+ -- XXX experimental binary ops, move to parallel?
48+ , append
49+ , appendWith
50+
51+ , apply
52+ , concatList
53+ , concat
54+ , concatWith
55+ , concatMap
56+ , concatMapWith
57+ )
58+where
59+
60+import Streamly.Internal.Control.Concurrent (MonadAsync)
61+import Streamly.Internal.Data.Stream.Type (Stream)
62+
63+import qualified Streamly.Internal.Data.Stream as Stream
64+
65+import Streamly.Internal.Data.Stream.Async.Channel
66+import Prelude hiding (mapM, sequence, concat, concatMap)
67+
68+-- $setup
69+-- >>> :m
70+-- >>> import qualified Streamly.Data.Stream as Stream
71+-- >>> import qualified Streamly.Internal.Data.Stream.Async as Async
72+
73+-------------------------------------------------------------------------------
74+-- Useful operations
75+-------------------------------------------------------------------------------
76+
77+-- | Like 'eval' but can specify a config modifier to change the concurrent
78+-- channel parameters.
79+{-# INLINE evalWith #-}
80+evalWith :: MonadAsync m => (Config -> Config) -> Stream m a -> Stream m a
81+evalWith cfg stream =
82+ -- Stream.fromStreamD $ evalWithD cfg $ Stream.toStreamD stream
83+ Stream.fromStreamK $ evalWithK cfg $ Stream.toStreamK stream
84+
85+-- | Evaluate a stream asynchronously using a channel and serve the consumer
86+-- from the evaluation buffer.
87+--
88+-- >>> eval = Async.evalWith id
89+--
90+{-# INLINE eval #-}
91+eval :: MonadAsync m => Stream m a -> Stream m a
92+eval = evalWith id
93+
94+-------------------------------------------------------------------------------
95+-- Concurrent combinators for "Stream" type
96+-------------------------------------------------------------------------------
97+
98+-- | Like 'append' but with a Config modifier.
99+{-# INLINE appendWith #-}
100+appendWith :: MonadAsync m =>
101+ (Config -> Config) -> Stream m a -> Stream m a -> Stream m a
102+appendWith modifier stream1 stream2 =
103+ Stream.fromStreamK
104+ $ appendWithK
105+ modifier (Stream.toStreamK stream1) (Stream.toStreamK stream2)
106+
107+-- | Binary operation to evaluate two streams concurrently prioritizing the
108+-- left stream.
109+--
110+-- If you want to combine more than two streams you almost always want the
111+-- 'concat' operation instead. The performance of this operation degrades
112+-- rapidly when more streams are combined as each operation adds one more
113+-- concurrent channel. On the other hand, 'concat' uses a single channel for
114+-- all streams. However, with this operation you can precisely control the
115+-- scheduling by creating arbitrary shape expression trees.
116+--
117+-- >>> append = Async.appendWith id
118+--
119+{-# INLINE append #-}
120+append :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
121+append = appendWith id
122+
123+-- concatMapWith modifier f stream = concatWith modifier $ fmap f stream
124+
125+-- | Like 'concatMap' but we can also specify the concurrent channel's
126+-- configuration parameters using Config modifiers.
127+--
128+{-# INLINE concatMapWith #-}
129+concatMapWith :: MonadAsync m =>
130+ (Config -> Config) -> (a -> Stream m b) -> Stream m a -> Stream m b
131+concatMapWith modifier f stream =
132+ Stream.fromStreamK
133+ $ concatMapWithK
134+ modifier (Stream.toStreamK . f) (Stream.toStreamK stream)
135+
136+-- | Map each element of the input to a stream and then concurrently evaluate
137+-- and concatenate the resulting streams.
138+--
139+-- >>> concatMap = Async.concatMapWith id
140+--
141+{-# INLINE concatMap #-}
142+concatMap :: MonadAsync m => (a -> Stream m b) -> Stream m a -> Stream m b
143+concatMap = concatMapWith id
144+
145+-- | Like 'concat' but we can also specify the concurrent channel's
146+-- configuration parameters using Config modifiers.
147+--
148+-- >>> concatWith modifier = Async.concatMapWith modifier id
149+--
150+{-# INLINE concatWith #-}
151+concatWith :: MonadAsync m =>
152+ (Config -> Config) -> Stream m (Stream m a) -> Stream m a
153+concatWith modifier = concatMapWith modifier id
154+
155+-- | Evaluate the streams in the input stream concurrently and combine them.
156+--
157+-- >>> concat = Async.concatWith id
158+--
159+{-# INLINE concat #-}
160+concat :: MonadAsync m => Stream m (Stream m a) -> Stream m a
161+concat = concatWith id
162+
163+-- | Like 'concat' but works on a list of streams.
164+--
165+-- >>> concatList = Async.concat . Stream.fromList
166+--
167+{-# INLINE concatList #-}
168+concatList :: MonadAsync m => [Stream m a] -> Stream m a
169+concatList = concat . Stream.fromList
170+
171+{-# INLINE apply #-}
172+{-# SPECIALIZE apply :: Stream IO (a -> b) -> Stream IO a -> Stream IO b #-}
173+apply :: MonadAsync m => Stream m (a -> b) -> Stream m a -> Stream m b
174+apply stream1 stream2 = concatMap (\g -> concatMap (pure . g) stream2) stream1
175+
176+-- |
177+-- >>> mapMWith modifier f = Async.concatMapWith modifier (Stream.fromEffect . f)
178+--
179+{-# INLINE mapMWith #-}
180+mapMWith :: MonadAsync m =>
181+ (Config -> Config) -> (a -> m b) -> Stream m a -> Stream m b
182+mapMWith modifier f = concatMapWith modifier (Stream.fromEffect . f)
183+
184+-- |
185+-- >>> mapM = Async.mapMWith id
186+{-# INLINE mapM #-}
187+mapM :: MonadAsync m => (a -> m b) -> Stream m a -> Stream m b
188+mapM = mapMWith id
189+
190+-- |
191+-- >>> sequenceWith modifier = Async.mapMWith modifier id
192+--
193+{-# INLINE sequenceWith #-}
194+sequenceWith :: MonadAsync m =>
195+ (Config -> Config) -> Stream m (m a) -> Stream m a
196+sequenceWith modifier = mapMWith modifier id
197+
198+-- |
199+-- >>> sequence = Async.sequenceWith id
200+--
201+{-# INLINE sequence #-}
202+sequence :: MonadAsync m =>
203+ Stream m (m a) -> Stream m a
204+sequence = sequenceWith id
src/Streamly/Internal/Data/Stream/Async/Channel.hsnew file+194
@@ -0,0 +1,194 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+
9+module Streamly.Internal.Data.Stream.Async.Channel
10+ (
11+ -- * Channel
12+ Channel
13+
14+ -- * Configuration
15+ , Config
16+ , maxThreads
17+ , maxBuffer
18+ , Rate(..)
19+ , rate
20+ , avgRate
21+ , minRate
22+ , maxRate
23+ , constRate
24+
25+ -- * Operations
26+ , newChannel
27+ , fromChannel
28+ , toChannel
29+ , withChannelK
30+ , concatMapWithChanK
31+
32+ -- XXX Move to the higher level Async module. Low level module would have
33+ -- only explicit channel operations.
34+
35+ -- * Stream operations
36+ , evalWithK
37+ , appendWithK
38+ , concatMapWithK
39+ )
40+where
41+
42+import Control.Monad.IO.Class (MonadIO(liftIO))
43+import Streamly.Internal.Control.Concurrent (MonadAsync, askRunInIO)
44+import Streamly.Internal.Data.SVar.Type (adaptState)
45+
46+import qualified Streamly.Internal.Data.Stream as Stream
47+import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
48+
49+import Streamly.Internal.Data.Stream.Async.Channel.Type
50+import Streamly.Internal.Data.Stream.Async.Channel.Core
51+
52+-- | Allocate a channel and evaluate the stream using the channel and the
53+-- supplied evaluator function. The evaluator is run in a worker thread.
54+{-# INLINE withChannelK #-}
55+withChannelK :: MonadAsync m =>
56+ (Channel m b -> K.Stream m a -> K.Stream m b)
57+ -> (Config -> Config)
58+ -> K.Stream m a
59+ -> K.Stream m b
60+withChannelK evaluator modifier input = K.concatEffect action
61+
62+ where
63+
64+ action = do
65+ chan <- newChannel modifier
66+ toChannelK chan (evaluator chan input)
67+ return $ Stream.toStreamK $ fromChannel chan
68+
69+-------------------------------------------------------------------------------
70+-- Evaluating a stream
71+-------------------------------------------------------------------------------
72+
73+-- XXX This should go in the parallel module, this is not specific to async.
74+
75+-- | Evaluate the stream asynchronously in a worker thread separate from the
76+-- consumer thread.
77+--
78+-- >> evalWithK = withChannelK (const id)
79+--
80+{-# INLINABLE evalWithK #-}
81+evalWithK :: MonadAsync m => (Config -> Config) -> K.Stream m a -> K.Stream m a
82+evalWithK = withChannelK (const id)
83+
84+{-
85+{-# INLINE_NORMAL evalWithD #-}
86+evalWithD :: MonadAsync m => (Config -> Config) -> D.Stream m a -> D.Stream m a
87+evalWithD modifier m = D.Stream step Nothing
88+ where
89+
90+ step _ Nothing = do
91+ chan <- newChannel modifier
92+ sendFirstWorker chan (D.toStreamK m)
93+ -- XXX should use an unfold to make this efficient
94+ return $ D.Skip $ Just $ fromChannelD chan
95+
96+ step gst (Just (D.UnStream step1 st)) = do
97+ r <- step1 gst st
98+ return $ case r of
99+ D.Yield a s -> D.Yield a (Just $ D.Stream step1 s)
100+ D.Skip s -> D.Skip (Just $ D.Stream step1 s)
101+ D.Stop -> D.Stop
102+-}
103+
104+-------------------------------------------------------------------------------
105+-- appending two streams
106+-------------------------------------------------------------------------------
107+
108+-- | Create a new channel and add both the streams to it for async evaluation.
109+-- The output stream is the result of the evaluation.
110+{-# INLINE appendWithK #-}
111+appendWithK :: MonadAsync m =>
112+ (Config -> Config) -> K.Stream m a -> K.Stream m a -> K.Stream m a
113+appendWithK modifier stream1 stream2 = K.concatEffect action
114+
115+ where
116+
117+ action = do
118+ chan <- newChannel modifier
119+ toChannelK chan stream2
120+ toChannelK chan stream1
121+ return $ Stream.toStreamK $ fromChannel chan
122+
123+-- | Evaluate the first stream in the current thread and add the second stream
124+-- to the supplied channel. This is to be used by a worker thread.
125+{-# INLINE _appendWithChanK #-}
126+_appendWithChanK :: MonadAsync m =>
127+ Channel m a -> K.Stream m a -> K.Stream m a -> K.Stream m a
128+_appendWithChanK chan stream1 stream2 =
129+ K.before (toChannelK chan stream2) stream1
130+
131+-------------------------------------------------------------------------------
132+-- concat
133+-------------------------------------------------------------------------------
134+
135+-- | @concatMapDivK useTail useHead stream@, divides the stream in head and
136+-- tail, maps a stream generator on the head and maps an action on the tail of
137+-- a stream. Returns the stream generated by the head.
138+{-# INLINE concatMapDivK #-}
139+concatMapDivK :: Monad m =>
140+ (K.Stream m a -> m ())
141+ -> (a -> K.Stream m b)
142+ -> K.Stream m a
143+ -> K.Stream m b
144+concatMapDivK useTail useHead stream =
145+ K.mkStream $ \st yld sng stp -> do
146+ let foldShared = K.foldStreamShared st yld sng stp
147+ single a = foldShared $ useHead a
148+ yieldk a r = useTail r >> single a
149+ in K.foldStreamShared (adaptState st) yieldk single stp stream
150+
151+-- | A runner function takes a queuing function @q@ and a stream, it splits the
152+-- input stream, queuing the tail and using the head to generate a stream.
153+-- 'mkEnqueue' takes a runner function and generates the queuing function @q@.
154+-- Note that @q@ and the runner are mutually recursive, mkEnqueue ties the knot
155+-- between the two.
156+{-# INLINE mkEnqueue #-}
157+mkEnqueue :: MonadAsync m =>
158+ Channel m b
159+ -> ((K.Stream m a -> m ()) -> K.Stream m a -> K.Stream m b)
160+ -> m (K.Stream m a -> m ())
161+mkEnqueue chan runner = do
162+ runInIO <- askRunInIO
163+ return
164+ $ let q stream = liftIO $ enqueue chan (runInIO, runner q stream) in q
165+
166+-- XXX Can be renamed to concatMapWithK if we move concatMapWithK to higher
167+-- level module. We can keep only Channel based ops in this module.
168+
169+-- | Takes the head element of the input stream and queues the tail of the
170+-- stream to the channel, then maps the supplied function on the head and
171+-- evaluates the resulting stream.
172+--
173+-- This function is designed to be used by worker threads on a channel to
174+-- concurrently map and evaluate a stream.
175+{-# INLINE concatMapWithChanK #-}
176+concatMapWithChanK :: MonadAsync m =>
177+ Channel m b -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
178+concatMapWithChanK chan f stream =
179+ let run q = concatMapDivK q f
180+ in K.concatMapEffect (`run` stream) (mkEnqueue chan run)
181+ -- K.concatMapWith (_appendWithChanK chan) f stream
182+
183+-- XXX Add a deep evaluation variant that evaluates individual elements in the
184+-- generated streams in parallel.
185+
186+-- | Allocate a channel and use it to concurrently evaluate the streams
187+-- generated by the mapped function.
188+--
189+-- >> concatMapWithK modifier f = withChannelK (`concatMapWithChanK` f) modifier
190+--
191+{-# INLINE concatMapWithK #-}
192+concatMapWithK :: MonadAsync m =>
193+ (Config -> Config) -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
194+concatMapWithK modifier f = withChannelK (`concatMapWithChanK` f) modifier
src/Streamly/Internal/Data/Stream/Async/Channel/Consumer.hsnew file+153
@@ -0,0 +1,153 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Consumer
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+module Streamly.Internal.Data.Stream.Async.Channel.Consumer
10+ (
11+ -- * Read Output
12+ readOutputQBasic
13+ , readOutputQRaw
14+ , readOutputQPaced
15+ , readOutputQBounded
16+
17+ -- * Postprocess Hook After Reading
18+ , postProcessPaced
19+ , postProcessBounded
20+
21+ -- * Release Resources
22+ , cleanupSVar
23+ )
24+where
25+
26+import Control.Monad.Trans.Control (MonadBaseControl)
27+import Control.Concurrent (throwTo)
28+import Control.Monad (when, void)
29+import Control.Monad.IO.Class (MonadIO(liftIO))
30+import Data.IORef (IORef, readIORef, writeIORef)
31+import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS)
32+
33+import Streamly.Internal.Data.Stream.Async.Channel.Type
34+import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
35+
36+-------------------------------------------------------------------------------
37+-- Reading from the workers' output queue/buffer
38+-------------------------------------------------------------------------------
39+
40+{-# INLINE readOutputQBasic #-}
41+readOutputQBasic :: IORef ([ChildEvent a], Int) -> IO ([ChildEvent a], Int)
42+readOutputQBasic q = atomicModifyIORefCAS q $ \x -> (([],0), x)
43+
44+{-# INLINE readOutputQRaw #-}
45+readOutputQRaw :: Channel m a -> IO ([ChildEvent a], Int)
46+readOutputQRaw sv = do
47+ (list, len) <- readOutputQBasic (outputQueue sv)
48+ when (svarInspectMode sv) $ do
49+ let ref = maxOutQSize $ svarStats sv
50+ oqLen <- readIORef ref
51+ when (len > oqLen) $ writeIORef ref len
52+ return (list, len)
53+
54+readOutputQBounded :: (MonadIO m, MonadBaseControl IO m) =>
55+ Channel m a -> m [ChildEvent a]
56+readOutputQBounded sv = do
57+ (list, len) <- liftIO $ readOutputQRaw sv
58+ -- When there is no output seen we dispatch more workers to help
59+ -- out if there is work pending in the work queue.
60+ if len <= 0
61+ then blockingRead
62+ else do
63+ -- send a worker proactively, if needed, even before we start
64+ -- processing the output. This may degrade single processor
65+ -- perf but improves multi-processor, because of more
66+ -- parallelism
67+ sendOneWorker
68+ return list
69+
70+ where
71+
72+ sendOneWorker = do
73+ cnt <- liftIO $ readIORef $ workerCount sv
74+ when (cnt <= 0) $ do
75+ done <- liftIO $ isWorkDone sv
76+ when (not done) (pushWorker 0 sv)
77+
78+ {-# INLINE blockingRead #-}
79+ blockingRead = do
80+ sendWorkerWait sendWorkerDelay (dispatchWorker 0) sv
81+ liftIO (fst `fmap` readOutputQRaw sv)
82+
83+readOutputQPaced :: (MonadIO m, MonadBaseControl IO m) =>
84+ Channel m a -> m [ChildEvent a]
85+readOutputQPaced sv = do
86+ (list, len) <- liftIO $ readOutputQRaw sv
87+ if len <= 0
88+ then blockingRead
89+ else do
90+ -- XXX send a worker proactively, if needed, even before we start
91+ -- processing the output.
92+ void $ dispatchWorkerPaced sv
93+ return list
94+
95+ where
96+
97+ {-# INLINE blockingRead #-}
98+ blockingRead = do
99+ sendWorkerWait sendWorkerDelayPaced dispatchWorkerPaced sv
100+ liftIO (fst `fmap` readOutputQRaw sv)
101+
102+postProcessPaced :: (MonadIO m, MonadBaseControl IO m) => Channel m a -> m Bool
103+postProcessPaced sv = do
104+ workersDone <- allThreadsDone sv
105+ -- XXX If during consumption we figure out we are getting delayed then we
106+ -- should trigger dispatch there as well. We should try to check on the
107+ -- workers after consuming every n item from the buffer?
108+ if workersDone
109+ then do
110+ r <- liftIO $ isWorkDone sv
111+ when (not r) $ do
112+ void $ dispatchWorkerPaced sv
113+ -- Note that we need to guarantee a worker since the work is not
114+ -- finished, therefore we cannot just rely on dispatchWorkerPaced
115+ -- which may or may not send a worker.
116+ noWorker <- allThreadsDone sv
117+ when noWorker $ pushWorker 0 sv
118+ return r
119+ else return False
120+
121+postProcessBounded :: (MonadIO m, MonadBaseControl IO m) =>
122+ Channel m a -> m Bool
123+postProcessBounded sv = do
124+ workersDone <- allThreadsDone sv
125+ -- There may still be work pending even if there are no workers pending
126+ -- because all the workers may return if the outputQueue becomes full. In
127+ -- that case send off a worker to kickstart the work again.
128+ --
129+ -- Note that isWorkDone can only be safely checked if all workers are done.
130+ -- When some workers are in progress they may have decremented the yield
131+ -- Limit and later ending up incrementing it again. If we look at the yield
132+ -- limit in that window we may falsely say that it is 0 and therefore we
133+ -- are done.
134+ if workersDone
135+ then do
136+ r <- liftIO $ isWorkDone sv
137+ -- Note that we need to guarantee a worker, therefore we cannot just
138+ -- use dispatchWorker which may or may not send a worker.
139+ when (not r) (pushWorker 0 sv)
140+ -- XXX do we need to dispatch many here?
141+ -- void $ dispatchWorker sv
142+ return r
143+ else return False
144+
145+-------------------------------------------------------------------------------
146+-- Cleanup
147+-------------------------------------------------------------------------------
148+
149+cleanupSVar :: Channel m a -> IO ()
150+cleanupSVar sv = do
151+ workers <- readIORef (workerThreads sv)
152+ Prelude.mapM_ (`throwTo` ThreadAbort)
153+ workers
src/Streamly/Internal/Data/Stream/Async/Channel/Core.hsnew file+560
@@ -0,0 +1,560 @@
1+#ifdef __HADDOCK_VERSION__
2+#undef INSPECTION
3+#endif
4+
5+#ifdef INSPECTION
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-}
8+#endif
9+
10+-- |
11+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Core
12+-- Copyright : (c) 2017 Composewell Technologies
13+-- License : BSD-3-Clause
14+-- Maintainer : streamly@composewell.com
15+-- Stability : experimental
16+-- Portability : GHC
17+--
18+-- The functions in this module are separated from the combinators using
19+-- these because of a GHC issue. We need to have newChannel specialized but
20+-- not inlined. If we keep it in the same module as its users we cannot achieve
21+-- that and the code becomes bloated. But if it is in a separate module we can
22+-- use INLINABLE and SPECIALIZE on it which makes it specialized but it is not
23+-- actually inlined.
24+
25+module Streamly.Internal.Data.Stream.Async.Channel.Core
26+ (
27+ newChannel
28+ , toChannel
29+ , toChannelK
30+ , fromChannel
31+ , fromChannelK
32+ )
33+where
34+
35+#include "inline.hs"
36+
37+import Control.Concurrent (myThreadId)
38+import Control.Concurrent.MVar (newEmptyMVar)
39+import Control.Exception (fromException)
40+import Control.Monad (when)
41+import Control.Monad.Catch (throwM, MonadThrow)
42+import Control.Monad.IO.Class (MonadIO(liftIO))
43+import Control.Monad.Trans.Control (MonadBaseControl, restoreM)
44+import Data.IORef (IORef, newIORef, readIORef, mkWeakIORef, writeIORef)
45+import Data.Maybe (isNothing)
46+import Streamly.Internal.Control.Concurrent
47+ (MonadRunInIO, MonadAsync, RunInIO(..), askRunInIO)
48+import Streamly.Internal.Data.Atomics
49+ (atomicModifyIORefCAS, atomicModifyIORefCAS_)
50+import Streamly.Internal.Data.Stream.Type (Stream)
51+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
52+import System.Mem (performMajorGC)
53+
54+import qualified Data.Set as Set
55+import qualified Streamly.Internal.Data.Stream as Stream
56+import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
57+import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
58+
59+import Streamly.Internal.Data.Stream.Async.Channel.Consumer
60+import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
61+import Streamly.Internal.Data.Stream.Async.Channel.Type
62+import Streamly.Internal.Data.Stream.Async.Channel.Worker
63+
64+import Prelude hiding (map, concat, concatMap)
65+
66+#if __GLASGOW_HASKELL__ < 810
67+#ifdef INSPECTION
68+import Control.Exception (Exception)
69+import Control.Monad.Catch (MonadThrow)
70+import Control.Monad.Trans.Control (MonadBaseControl)
71+import Data.Typeable (Typeable)
72+import Test.Inspection (inspect, hasNoTypeClassesExcept)
73+#endif
74+#endif
75+
76+------------------------------------------------------------------------------
77+-- Creating a channel
78+------------------------------------------------------------------------------
79+
80+-- Note: For purely right associated expressions this queue should have at most
81+-- one element. It grows to more than one when we have left associcated
82+-- expressions. Large left associated compositions can grow this to a
83+-- large size
84+{-# INLINE enqueueLIFO #-}
85+enqueueLIFO ::
86+ Channel m a
87+ -> IORef [(RunInIO m, K.Stream m a)]
88+ -> (RunInIO m, K.Stream m a)
89+ -> IO ()
90+enqueueLIFO sv q m = do
91+ atomicModifyIORefCAS_ q $ \ms -> m : ms
92+ ringDoorBell sv
93+
94+data WorkerStatus = Continue | Suspend
95+
96+{-# INLINE workLoopLIFO #-}
97+workLoopLIFO
98+ :: MonadRunInIO m
99+ => IORef [(RunInIO m, K.Stream m a)]
100+ -- -> State Stream m a
101+ -> Channel m a
102+ -> Maybe WorkerInfo
103+ -> m ()
104+workLoopLIFO q sv winfo = run
105+
106+ where
107+
108+ stop = liftIO $ sendStop sv winfo
109+ run = do
110+ work <- dequeue
111+ case work of
112+ Nothing -> stop
113+ Just (RunInIO runin, m) -> do
114+ -- XXX when we finish we need to send the monadic state back to
115+ -- the parent so that the state can be merged back. We capture
116+ -- and return the state in the stop continuation.
117+ --
118+ -- Instead of using the run function we can just restore the
119+ -- monad state here. That way it can work easily for
120+ -- distributed case as well.
121+ r <- liftIO $ runin $
122+ K.foldStreamShared
123+ undefined
124+ yieldk
125+ single
126+ (return Continue)
127+ m
128+ res <- restoreM r
129+ case res of
130+ Continue -> run
131+ Suspend -> stop
132+
133+ single a = do
134+ res <- liftIO $ sendYield sv winfo (ChildYield a)
135+ return $ if res then Continue else Suspend
136+
137+ yieldk a r = do
138+ res <- liftIO $ sendYield sv winfo (ChildYield a)
139+ if res
140+ then K.foldStreamShared undefined yieldk single (return Continue) r
141+ else do
142+ runInIO <- askRunInIO
143+ liftIO $ enqueueLIFO sv q (runInIO, r)
144+ return Suspend
145+
146+ dequeue = liftIO $ atomicModifyIORefCAS q $ \case
147+ [] -> ([], Nothing)
148+ x : xs -> (xs, Just x)
149+
150+-- We duplicate workLoop for yield limit and no limit cases because it has
151+-- around 40% performance overhead in the worst case.
152+--
153+-- XXX we can pass yinfo directly as an argument here so that we do not have to
154+-- make a check every time.
155+{-# INLINE workLoopLIFOLimited #-}
156+workLoopLIFOLimited
157+ :: forall m a. MonadRunInIO m
158+ => IORef [(RunInIO m, K.Stream m a)]
159+ -- -> State Stream m a
160+ -> Channel m a
161+ -> Maybe WorkerInfo
162+ -> m ()
163+workLoopLIFOLimited q sv winfo = run
164+
165+ where
166+
167+ incrContinue = liftIO (incrementYieldLimit sv) >> return Continue
168+ stop = liftIO $ sendStop sv winfo
169+ run = do
170+ work <- dequeue
171+ case work of
172+ Nothing -> stop
173+ Just (RunInIO runin, m) -> do
174+ -- XXX This is just a best effort minimization of concurrency
175+ -- to the yield limit. If the stream is made of concurrent
176+ -- streams we do not reserve the yield limit in the constituent
177+ -- streams before executing the action. This can be done
178+ -- though, by sharing the yield limit ref with downstream
179+ -- actions via state passing. Just a todo.
180+ yieldLimitOk <- liftIO $ decrementYieldLimit sv
181+ if yieldLimitOk
182+ then do
183+ r <- liftIO $ runin $
184+ K.foldStreamShared
185+ undefined
186+ yieldk
187+ single
188+ incrContinue
189+ m
190+ res <- restoreM r
191+ case res of
192+ Continue -> run
193+ Suspend -> stop
194+ -- Avoid any side effects, undo the yield limit decrement if we
195+ -- never yielded anything.
196+ else liftIO $ do
197+ enqueueLIFO sv q (RunInIO runin, m)
198+ incrementYieldLimit sv
199+ sendStop sv winfo
200+
201+ single a = do
202+ res <- liftIO $ sendYield sv winfo (ChildYield a)
203+ return $ if res then Continue else Suspend
204+
205+ -- XXX can we pass on the yield limit downstream to limit the concurrency
206+ -- of constituent streams.
207+ yieldk a r = do
208+ res <- liftIO $ sendYield sv winfo (ChildYield a)
209+ yieldLimitOk <- liftIO $ decrementYieldLimit sv
210+ if res && yieldLimitOk
211+ then K.foldStreamShared undefined yieldk single incrContinue r
212+ else do
213+ runInIO <- askRunInIO
214+ liftIO $ incrementYieldLimit sv
215+ liftIO $ enqueueLIFO sv q (runInIO, r)
216+ return Suspend
217+
218+ dequeue = liftIO $ atomicModifyIORefCAS q $ \case
219+ [] -> ([], Nothing)
220+ x : xs -> (xs, Just x)
221+
222+-------------------------------------------------------------------------------
223+-- SVar creation
224+-- This code belongs in SVar.hs but is kept here for perf reasons
225+-------------------------------------------------------------------------------
226+
227+-- XXX we have this function in this file because passing runStreamLIFO as a
228+-- function argument to this function results in a perf degradation of more
229+-- than 10%. Need to investigate what the root cause is.
230+-- Interestingly, the same thing does not make any difference for Ahead.
231+-- {-# INLINABLE getLifoSVar #-}
232+getLifoSVar :: forall m a. (MonadIO m, MonadBaseControl IO m) =>
233+ RunInIO m -> Config -> IO (Channel m a)
234+getLifoSVar mrun cfg = do
235+ outQ <- newIORef ([], 0)
236+ outQMv <- newEmptyMVar
237+ active <- newIORef 0
238+ wfw <- newIORef False
239+ running <- newIORef Set.empty
240+ q <- newIORef ([] :: [(RunInIO m, K.Stream m a)])
241+ yl <- case getYieldLimit cfg of
242+ Nothing -> return Nothing
243+ Just x -> Just <$> newIORef x
244+ rateInfo <- getYieldRateInfo cfg
245+
246+ stats <- newSVarStats
247+ tid <- myThreadId
248+
249+ let isWorkFinished _ = null <$> readIORef q
250+
251+ let isWorkFinishedLimited sv = do
252+ yieldsDone <-
253+ case remainingWork sv of
254+ Just ref -> do
255+ n <- readIORef ref
256+ return (n <= 0)
257+ Nothing -> return False
258+ qEmpty <- null <$> readIORef q
259+ return $ qEmpty || yieldsDone
260+
261+ let getSVar :: Channel m a
262+ -> (Channel m a -> m [ChildEvent a])
263+ -> (Channel m a -> m Bool)
264+ -> (Channel m a -> IO Bool)
265+ -> (IORef [(RunInIO m, K.Stream m a)]
266+ -- -> State Stream m a
267+ -> Channel m a
268+ -> Maybe WorkerInfo
269+ -> m())
270+ -> Channel m a
271+ getSVar sv readOutput postProc workDone wloop = Channel
272+ { outputQueue = outQ
273+ , remainingWork = yl
274+ , maxBufferLimit = getMaxBuffer cfg
275+ , maxWorkerLimit = min (getMaxThreads cfg) (getMaxBuffer cfg)
276+ , yieldRateInfo = rateInfo
277+ , outputDoorBell = outQMv
278+ , readOutputQ = readOutput sv
279+ , postProcess = postProc sv
280+ , workerThreads = running
281+ , workLoop = wloop q sv
282+ , enqueue = enqueueLIFO sv q
283+ , isWorkDone = workDone sv
284+ , isQueueDone = workDone sv
285+ , needDoorBell = wfw
286+ , svarMrun = mrun
287+ , workerCount = active
288+ , accountThread = delThread sv
289+ , workerStopMVar = undefined
290+ , svarRef = Nothing
291+ , svarInspectMode = getInspectMode cfg
292+ , svarCreator = tid
293+ , svarStats = stats
294+ }
295+
296+ let sv =
297+ case getStreamRate cfg of
298+ Nothing ->
299+ case getYieldLimit cfg of
300+ Nothing -> getSVar sv readOutputQBounded
301+ postProcessBounded
302+ isWorkFinished
303+ workLoopLIFO
304+ Just _ -> getSVar sv readOutputQBounded
305+ postProcessBounded
306+ isWorkFinishedLimited
307+ workLoopLIFOLimited
308+ Just _ ->
309+ case getYieldLimit cfg of
310+ Nothing -> getSVar sv readOutputQPaced
311+ postProcessPaced
312+ isWorkFinished
313+ workLoopLIFO
314+ Just _ -> getSVar sv readOutputQPaced
315+ postProcessPaced
316+ isWorkFinishedLimited
317+ workLoopLIFOLimited
318+ in return sv
319+
320+-- type Channel m a = SVar K.Stream m a
321+-- data Config = Config
322+
323+-- XXX openChannel?
324+
325+-- | Create a new async style concurrent stream evaluation channel. The monad
326+-- state used to run the stream actions is taken from the call site of
327+-- newChannel.
328+{-# INLINABLE newChannel #-}
329+{-# SPECIALIZE newChannel :: (Config -> Config) -> IO (Channel IO a) #-}
330+newChannel :: (MonadAsync m) =>
331+ (Config -> Config) -> m (Channel m a)
332+newChannel modifier = do
333+ mrun <- askRunInIO
334+ liftIO $ getLifoSVar mrun (modifier defaultConfig)
335+
336+------------------------------------------------------------------------------
337+-- Generating streams from a channel
338+------------------------------------------------------------------------------
339+
340+-- $concurrentEval
341+--
342+-- Usually a channel is used to concurrently evaluate multiple actions in a
343+-- stream using many worker threads that push the results to the channel and a
344+-- single puller that pulls them from channel generating the evaluated stream.
345+--
346+-- @
347+-- input stream
348+-- |
349+-- <-----------------|<--------worker
350+-- | exceptions |
351+-- output stream <---Channel<------worker
352+-- |
353+-- |<--------worker
354+--
355+-- @
356+--
357+-- The puller itself schedules the worker threads based on demand.
358+-- Exceptions are propagated from the worker threads to the puller.
359+
360+-------------------------------------------------------------------------------
361+-- Write a stream to a channel
362+-------------------------------------------------------------------------------
363+
364+-- XXX Should be a Fold, singleton API could be called joinChannel, or the fold
365+-- can be called joinChannel.
366+
367+-- | Write a stream to an 'SVar' in a non-blocking manner. The stream can then
368+-- be read back from the SVar using 'fromSVar'.
369+toChannelK :: (MonadIO m, MonadBaseControl IO m) =>
370+ Channel m a -> K.Stream m a -> m ()
371+toChannelK sv m = do
372+ runIn <- askRunInIO
373+ liftIO $ enqueue sv (runIn, m)
374+
375+-- INLINE for fromStreamK/toStreamK fusion
376+
377+-- | Send a stream to a given channel for concurrent evaluation.
378+{-# INLINE toChannel #-}
379+toChannel :: (MonadIO m, MonadBaseControl IO m) =>
380+ Channel m a -> Stream m a -> m ()
381+toChannel chan = toChannelK chan . Stream.toStreamK
382+
383+{-
384+-- | Send a stream of streams to a concurrent channel for evaluation.
385+{-# INLINE joinChannel #-}
386+joinChannel :: Channel m a -> Fold m (Stream m a) ()
387+joinChannel = undefined
388+-}
389+
390+-------------------------------------------------------------------------------
391+-- Read a stream from a channel
392+-------------------------------------------------------------------------------
393+
394+-- | Pull a stream from an SVar.
395+{-# NOINLINE fromChannelRaw #-}
396+fromChannelRaw :: (MonadIO m, MonadThrow m) => Channel m a -> K.Stream m a
397+fromChannelRaw sv = K.MkStream $ \st yld sng stp -> do
398+ list <- readOutputQ sv
399+ -- Reversing the output is important to guarantee that we process the
400+ -- outputs in the same order as they were generated by the constituent
401+ -- streams.
402+ K.foldStream st yld sng stp $ processEvents $ reverse list
403+
404+ where
405+
406+ allDone stp = do
407+ when (svarInspectMode sv) $ do
408+ t <- liftIO $ getTime Monotonic
409+ liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
410+ liftIO $ printSVar sv "SVar Done"
411+ stp
412+
413+ {-# INLINE processEvents #-}
414+ processEvents [] = K.MkStream $ \st yld sng stp -> do
415+ done <- postProcess sv
416+ if done
417+ then allDone stp
418+ else K.foldStream st yld sng stp $ fromChannelRaw sv
419+
420+ processEvents (ev : es) = K.MkStream $ \st yld sng stp -> do
421+ let rest = processEvents es
422+ case ev of
423+ ChildYield a -> yld a rest
424+ ChildStop tid e -> do
425+ accountThread sv tid
426+ case e of
427+ Nothing -> K.foldStream st yld sng stp rest
428+ Just ex ->
429+ case fromException ex of
430+ Just ThreadAbort ->
431+ K.foldStream st yld sng stp rest
432+ Nothing -> liftIO (cleanupSVar sv) >> throwM ex
433+
434+#if __GLASGOW_HASKELL__ < 810
435+#ifdef INSPECTION
436+-- Use of GHC constraint tuple (GHC.Classes.(%,,%)) in fromStreamVar leads to
437+-- space leak because the tuple gets allocated in every recursive call and each
438+-- allocation holds on to the previous allocation. This test is to make sure
439+-- that we do not use the constraint tuple type class.
440+--
441+inspect $ hasNoTypeClassesExcept 'fromStreamVar
442+ [ ''Monad
443+ , ''Applicative
444+ , ''MonadThrow
445+ , ''Exception
446+ , ''MonadIO
447+ , ''MonadBaseControl
448+ , ''Typeable
449+ , ''Functor
450+ ]
451+#endif
452+#endif
453+
454+-- XXX fromChannel Should not be called multiple times, we can add a
455+-- safeguard for that. Or we can replicate the stream so that we can distribute
456+-- it to multiple consumers. or should we use an explicit dupChannel for that?
457+
458+{-# INLINE fromChannelK #-}
459+fromChannelK :: MonadAsync m => Channel m a -> K.Stream m a
460+fromChannelK sv =
461+ K.mkStream $ \st yld sng stp -> do
462+ ref <- liftIO $ newIORef ()
463+ _ <- liftIO $ mkWeakIORef ref hook
464+
465+ startChannel sv
466+ -- We pass a copy of sv to fromStreamVar, so that we know that it has
467+ -- no other references, when that copy gets garbage collected "ref"
468+ -- will get garbage collected and our hook will be called.
469+ K.foldStreamShared st yld sng stp $
470+ fromChannelRaw sv{svarRef = Just ref}
471+ where
472+
473+ hook = do
474+ when (svarInspectMode sv) $ do
475+ r <- liftIO $ readIORef (svarStopTime (svarStats sv))
476+ when (isNothing r) $
477+ printSVar sv "SVar Garbage Collected"
478+ cleanupSVar sv
479+ -- If there are any SVars referenced by this SVar a GC will prompt
480+ -- them to be cleaned up quickly.
481+ when (svarInspectMode sv) performMajorGC
482+
483+-- | Generate a stream of results from concurrent evaluations from a channel.
484+-- Evaluation of the channel does not start until this API is called. This API
485+-- must not be called more than once on a channel. It kicks off evaluation of
486+-- the channel by dispatching concurrent workers and ensures that as long there
487+-- is work queued on the channel workers are dispatched proportional to the
488+-- demand by the consumer.
489+--
490+{-# INLINE fromChannel #-}
491+fromChannel :: MonadAsync m => Channel m a -> Stream m a
492+fromChannel = Stream.fromStreamK . fromChannelK
493+
494+data FromSVarState t m a =
495+ FromSVarInit
496+ | FromSVarRead (Channel m a)
497+ | FromSVarLoop (Channel m a) [ChildEvent a]
498+ | FromSVarDone (Channel m a)
499+
500+-- | Like 'fromSVar' but generates a StreamD style stream instead of CPS.
501+--
502+{-# INLINE_NORMAL _fromChannelD #-}
503+_fromChannelD :: (MonadIO m, MonadThrow m) => Channel m a -> D.Stream m a
504+_fromChannelD svar = D.Stream step FromSVarInit
505+ where
506+
507+ {-# INLINE_LATE step #-}
508+ step _ FromSVarInit = do
509+ ref <- liftIO $ newIORef ()
510+ _ <- liftIO $ mkWeakIORef ref hook
511+ -- when this copy of svar gets garbage collected "ref" will get
512+ -- garbage collected and our GC hook will be called.
513+ let sv = svar{svarRef = Just ref}
514+ return $ D.Skip (FromSVarRead sv)
515+
516+ where
517+
518+ {-# NOINLINE hook #-}
519+ hook = do
520+ when (svarInspectMode svar) $ do
521+ r <- liftIO $ readIORef (svarStopTime (svarStats svar))
522+ when (isNothing r) $
523+ printSVar svar "SVar Garbage Collected"
524+ cleanupSVar svar
525+ -- If there are any SVars referenced by this SVar a GC will prompt
526+ -- them to be cleaned up quickly.
527+ when (svarInspectMode svar) performMajorGC
528+
529+ step _ (FromSVarRead sv) = do
530+ list <- readOutputQ sv
531+ -- Reversing the output is important to guarantee that we process the
532+ -- outputs in the same order as they were generated by the constituent
533+ -- streams.
534+ return $ D.Skip $ FromSVarLoop sv (Prelude.reverse list)
535+
536+ step _ (FromSVarLoop sv []) = do
537+ done <- postProcess sv
538+ return $ D.Skip $ if done
539+ then FromSVarDone sv
540+ else FromSVarRead sv
541+
542+ step _ (FromSVarLoop sv (ev : es)) = do
543+ case ev of
544+ ChildYield a -> return $ D.Yield a (FromSVarLoop sv es)
545+ ChildStop tid e -> do
546+ accountThread sv tid
547+ case e of
548+ Nothing -> return $ D.Skip (FromSVarLoop sv es)
549+ Just ex ->
550+ case fromException ex of
551+ Just ThreadAbort ->
552+ return $ D.Skip (FromSVarLoop sv es)
553+ Nothing -> liftIO (cleanupSVar sv) >> throwM ex
554+
555+ step _ (FromSVarDone sv) = do
556+ when (svarInspectMode sv) $ do
557+ t <- liftIO $ getTime Monotonic
558+ liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
559+ liftIO $ printSVar sv "SVar Done"
560+ return D.Stop
src/Streamly/Internal/Data/Stream/Async/Channel/Dispatcher.hsnew file+673
@@ -0,0 +1,673 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+--
10+module Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
11+ (
12+ -- * Latency collection
13+ collectLatency
14+
15+ -- * Diagnostics
16+ , withDiagMVar
17+ , dumpSVar
18+ , printSVar
19+
20+ -- * Thread accounting
21+ , delThread
22+ , modifyThread
23+ , allThreadsDone
24+
25+ -- * Dispatching
26+ , recordMaxWorkers
27+ , pushWorker
28+ , dispatchWorker
29+ , dispatchWorkerPaced
30+ , sendWorkerWait
31+ , startChannel
32+ , sendWorkerDelay
33+ , sendWorkerDelayPaced
34+ )
35+where
36+
37+import Control.Monad.Trans.Control (MonadBaseControl)
38+import Control.Concurrent (takeMVar, tryReadMVar, ThreadId, threadDelay)
39+import Control.Concurrent.MVar (tryPutMVar)
40+import Control.Exception
41+ (assert, catches, throwIO, Handler(..), BlockedIndefinitelyOnMVar(..),
42+ BlockedIndefinitelyOnSTM(..))
43+import Control.Monad (when, void)
44+import Control.Monad.IO.Class (MonadIO(liftIO))
45+import Data.Maybe (fromJust, fromMaybe)
46+import Data.IORef (IORef, modifyIORef, newIORef, readIORef, writeIORef)
47+#if __GLASGOW_HASKELL__ < 804
48+import Data.Semigroup ((<>))
49+#endif
50+import Streamly.Internal.Control.ForkLifted (doFork)
51+import Streamly.Internal.Data.Atomics
52+ (atomicModifyIORefCAS, atomicModifyIORefCAS_, writeBarrier,
53+ storeLoadBarrier)
54+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
55+import Streamly.Internal.Data.Time.Units
56+ (AbsTime, NanoSecond64(..), MicroSecond64(..), diffAbsTime64,
57+ fromRelTime64, toRelTime64, showNanoSecond64, showRelTime64)
58+import System.IO (hPutStrLn, stderr)
59+
60+-- import qualified Data.Heap as H
61+import qualified Data.Set as S
62+
63+import Streamly.Internal.Data.Stream.Async.Channel.Type
64+import Streamly.Internal.Data.Stream.Async.Channel.Worker
65+
66+-------------------------------------------------------------------------------
67+-- Worker latency data processing
68+-------------------------------------------------------------------------------
69+
70+-- Every once in a while workers update the latencies and check the yield rate.
71+-- They return if we are above the expected yield rate. If we check too often
72+-- it may impact performance, if we check less often we may have a stale
73+-- picture. We update every minThreadDelay but we translate that into a yield
74+-- count based on latency so that the checking overhead is little.
75+--
76+-- XXX use a generation count to indicate that the value is updated. If the
77+-- value is updated an existing worker must check it again on the next yield.
78+-- Otherwise it is possible that we may keep updating it and because of the mod
79+-- worker keeps skipping it.
80+updateWorkerPollingInterval :: YieldRateInfo -> NanoSecond64 -> IO ()
81+updateWorkerPollingInterval yinfo latency = do
82+ let periodRef = workerPollingInterval yinfo
83+ cnt = max 1 $ minThreadDelay `div` latency
84+ period = min cnt (fromIntegral magicMaxBuffer)
85+
86+ writeIORef periodRef (fromIntegral period)
87+
88+{-# INLINE recordMinMaxLatency #-}
89+recordMinMaxLatency :: Channel m a -> NanoSecond64 -> IO ()
90+recordMinMaxLatency sv new = do
91+ let ss = svarStats sv
92+ minLat <- readIORef (minWorkerLatency ss)
93+ when (new < minLat || minLat == 0) $
94+ writeIORef (minWorkerLatency ss) new
95+
96+ maxLat <- readIORef (maxWorkerLatency ss)
97+ when (new > maxLat) $ writeIORef (maxWorkerLatency ss) new
98+
99+recordAvgLatency :: Channel m a -> (Count, NanoSecond64) -> IO ()
100+recordAvgLatency sv (count, time) = do
101+ let ss = svarStats sv
102+ modifyIORef (avgWorkerLatency ss) $
103+ \(cnt, t) -> (cnt + count, t + time)
104+
105+-- Pour the pending latency stats into a collection bucket
106+{-# INLINE collectWorkerPendingLatency #-}
107+collectWorkerPendingLatency
108+ :: IORef (Count, Count, NanoSecond64)
109+ -> IORef (Count, Count, NanoSecond64)
110+ -> IO (Count, Maybe (Count, NanoSecond64))
111+collectWorkerPendingLatency cur col = do
112+ (fcount, count, time) <- atomicModifyIORefCAS cur $ \v -> ((0,0,0), v)
113+
114+ (fcnt, cnt, t) <- readIORef col
115+ let totalCount = fcnt + fcount
116+ latCount = cnt + count
117+ latTime = t + time
118+ writeIORef col (totalCount, latCount, latTime)
119+
120+ assert (latCount == 0 || latTime /= 0) (return ())
121+ let latPair =
122+ if latCount > 0 && latTime > 0
123+ then Just (latCount, latTime)
124+ else Nothing
125+ return (totalCount, latPair)
126+
127+{-# INLINE shouldUseCollectedBatch #-}
128+shouldUseCollectedBatch
129+ :: Count
130+ -> NanoSecond64
131+ -> NanoSecond64
132+ -> NanoSecond64
133+ -> Bool
134+shouldUseCollectedBatch collectedYields collectedTime newLat prevLat =
135+ let r = fromIntegral newLat / fromIntegral prevLat :: Double
136+ in (collectedYields > fromIntegral magicMaxBuffer)
137+ || (collectedTime > minThreadDelay)
138+ || (prevLat > 0 && (r > 2 || r < 0.5))
139+ || (prevLat == 0)
140+
141+-- Returns a triple, (1) yield count since last collection, (2) the base time
142+-- when we started counting, (3) average latency in the last measurement
143+-- period. The former two are used for accurate measurement of the going rate
144+-- whereas the average is used for future estimates e.g. how many workers
145+-- should be maintained to maintain the rate.
146+-- CAUTION! keep it in sync with getWorkerLatency
147+collectLatency :: Channel m a
148+ -> YieldRateInfo
149+ -> Bool
150+ -> IO (Count, AbsTime, NanoSecond64)
151+collectLatency sv yinfo drain = do
152+ let cur = workerPendingLatency yinfo
153+ col = workerCollectedLatency yinfo
154+ longTerm = svarAllTimeLatency yinfo
155+ measured = workerMeasuredLatency yinfo
156+
157+ (newCount, newLatPair) <- collectWorkerPendingLatency cur col
158+ (lcount, ltime) <- readIORef longTerm
159+ prevLat <- readIORef measured
160+
161+ let newLcount = lcount + newCount
162+ retWith lat = return (newLcount, ltime, lat)
163+
164+ case newLatPair of
165+ Nothing -> retWith prevLat
166+ Just (count, time) -> do
167+ let newLat = time `div` fromIntegral count
168+ when (svarInspectMode sv) $ recordMinMaxLatency sv newLat
169+ -- When we have collected a significant sized batch we compute the
170+ -- new latency using that batch and return the new latency,
171+ -- otherwise we return the previous latency derived from the
172+ -- previous batch.
173+ if shouldUseCollectedBatch newCount time newLat prevLat || drain
174+ then do
175+ -- XXX make this NOINLINE?
176+ updateWorkerPollingInterval yinfo (max newLat prevLat)
177+ when (svarInspectMode sv) $ recordAvgLatency sv (count, time)
178+ writeIORef col (0, 0, 0)
179+ writeIORef measured ((prevLat + newLat) `div` 2)
180+ modifyIORef longTerm $ \(_, t) -> (newLcount, t)
181+ retWith newLat
182+ else retWith prevLat
183+
184+-------------------------------------------------------------------------------
185+-- Dumping the SVar for debug/diag
186+-------------------------------------------------------------------------------
187+
188+dumpSVarStats :: Channel m a -> SVarStats -> IO String
189+dumpSVarStats sv ss = do
190+ case yieldRateInfo sv of
191+ Nothing -> return ()
192+ Just yinfo -> do
193+ _ <- liftIO $ collectLatency sv yinfo True
194+ return ()
195+
196+ dispatches <- readIORef $ totalDispatches ss
197+ maxWrk <- readIORef $ maxWorkers ss
198+ maxOq <- readIORef $ maxOutQSize ss
199+ -- maxHp <- readIORef $ maxHeapSize ss
200+ minLat <- readIORef $ minWorkerLatency ss
201+ maxLat <- readIORef $ maxWorkerLatency ss
202+ (avgCnt, avgTime) <- readIORef $ avgWorkerLatency ss
203+ (svarCnt, svarGainLossCnt, svarLat) <- case yieldRateInfo sv of
204+ Nothing -> return (0, 0, 0)
205+ Just yinfo -> do
206+ (cnt, startTime) <- readIORef $ svarAllTimeLatency yinfo
207+ if cnt > 0
208+ then do
209+ t <- readIORef (svarStopTime ss)
210+ gl <- readIORef (svarGainedLostYields yinfo)
211+ case t of
212+ Nothing -> do
213+ now <- getTime Monotonic
214+ let interval = diffAbsTime64 now startTime
215+ return (cnt, gl, interval `div` fromIntegral cnt)
216+ Just stopTime -> do
217+ let interval = diffAbsTime64 stopTime startTime
218+ return (cnt, gl, interval `div` fromIntegral cnt)
219+ else return (0, 0, 0)
220+
221+ return $ unlines
222+ [ "total dispatches = " <> show dispatches
223+ , "max workers = " <> show maxWrk
224+ , "max outQSize = " <> show maxOq
225+ <> (if minLat > 0
226+ then "\nmin worker latency = " <> showNanoSecond64 minLat
227+ else "")
228+ <> (if maxLat > 0
229+ then "\nmax worker latency = " <> showNanoSecond64 maxLat
230+ else "")
231+ <> (if avgCnt > 0
232+ then let lat = avgTime `div` fromIntegral avgCnt
233+ in "\navg worker latency = " <> showNanoSecond64 lat
234+ else "")
235+ <> (if svarLat > 0
236+ then "\nSVar latency = " <> showRelTime64 svarLat
237+ else "")
238+ <> (if svarCnt > 0
239+ then "\nSVar yield count = " <> show svarCnt
240+ else "")
241+ <> (if svarGainLossCnt > 0
242+ then "\nSVar gain/loss yield count = " <> show svarGainLossCnt
243+ else "")
244+ ]
245+
246+{-# NOINLINE dumpSVar #-}
247+dumpSVar :: Channel m a -> IO String
248+dumpSVar sv = do
249+ (oqList, oqLen) <- readIORef $ outputQueue sv
250+ db <- tryReadMVar $ outputDoorBell sv
251+
252+ waiting <- readIORef $ needDoorBell sv
253+ rthread <- readIORef $ workerThreads sv
254+ workers <- readIORef $ workerCount sv
255+ stats <- dumpSVarStats sv (svarStats sv)
256+
257+ return $ unlines
258+ [
259+ "Creator tid = " <> show (svarCreator sv)
260+ , "---------CURRENT STATE-----------"
261+ , "outputQueue length computed = " <> show (length oqList)
262+ , "outputQueue length maintained = " <> show oqLen
263+ -- XXX print the types of events in the outputQueue, first 5
264+ , "outputDoorBell = " <> show db
265+ ]
266+ <> unlines
267+ [ "needDoorBell = " <> show waiting
268+ , "running threads = " <> show rthread
269+ -- XXX print the status of first 5 threads
270+ , "running thread count = " <> show workers
271+ ]
272+ <> "---------STATS-----------\n"
273+ <> stats
274+
275+-- MVar diagnostics has some overhead - around 5% on AsyncT null benchmark, we
276+-- can keep it on in production to debug problems quickly if and when they
277+-- happen, but it may result in unexpected output when threads are left hanging
278+-- until they are GCed because the consumer went away.
279+
280+{-# NOINLINE mvarExcHandler #-}
281+mvarExcHandler :: Channel m a -> String -> BlockedIndefinitelyOnMVar -> IO ()
282+mvarExcHandler sv label e@BlockedIndefinitelyOnMVar = do
283+ svInfo <- dumpSVar sv
284+ hPutStrLn stderr $ label <> " " <> "BlockedIndefinitelyOnMVar\n" <> svInfo
285+ throwIO e
286+
287+{-# NOINLINE stmExcHandler #-}
288+stmExcHandler :: Channel m a -> String -> BlockedIndefinitelyOnSTM -> IO ()
289+stmExcHandler sv label e@BlockedIndefinitelyOnSTM = do
290+ svInfo <- dumpSVar sv
291+ hPutStrLn stderr $ label <> " " <> "BlockedIndefinitelyOnSTM\n" <> svInfo
292+ throwIO e
293+
294+withDiagMVar :: Channel m a -> String -> IO () -> IO ()
295+withDiagMVar sv label action =
296+ if svarInspectMode sv
297+ then
298+ action `catches` [ Handler (mvarExcHandler sv label)
299+ , Handler (stmExcHandler sv label)
300+ ]
301+ else action
302+
303+printSVar :: Channel m a -> String -> IO ()
304+printSVar sv how = do
305+ svInfo <- dumpSVar sv
306+ hPutStrLn stderr $ "\n" <> how <> "\n" <> svInfo
307+
308+-------------------------------------------------------------------------------
309+-- Thread accounting
310+-------------------------------------------------------------------------------
311+
312+-- Thread tracking is needed for two reasons:
313+--
314+-- 1) Killing threads on exceptions. Threads may not be left to go away by
315+-- themselves because they may run for significant times before going away or
316+-- worse they may be stuck in IO and never go away.
317+--
318+-- 2) To know when all threads are done and the stream has ended.
319+
320+{-# NOINLINE addThread #-}
321+addThread :: MonadIO m => Channel m a -> ThreadId -> m ()
322+addThread sv tid =
323+ liftIO $ modifyIORef (workerThreads sv) (S.insert tid)
324+
325+-- This is cheaper than modifyThread because we do not have to send a
326+-- outputDoorBell This can make a difference when more workers are being
327+-- dispatched.
328+{-# INLINE delThread #-}
329+delThread :: MonadIO m => Channel m a -> ThreadId -> m ()
330+delThread sv tid =
331+ liftIO $ modifyIORef (workerThreads sv) (S.delete tid)
332+
333+-- If present then delete else add. This takes care of out of order add and
334+-- delete i.e. a delete arriving before we even added a thread.
335+-- This occurs when the forked thread is done even before the 'addThread' right
336+-- after the fork gets a chance to run.
337+{-# INLINE modifyThread #-}
338+modifyThread :: MonadIO m => Channel m a -> ThreadId -> m ()
339+modifyThread sv tid = do
340+ changed <- liftIO $ atomicModifyIORefCAS (workerThreads sv) $ \old ->
341+ if S.member tid old
342+ then let new = S.delete tid old in (new, new)
343+ else let new = S.insert tid old in (new, old)
344+ when (null changed) $
345+ liftIO $ do
346+ writeBarrier
347+ void $ tryPutMVar (outputDoorBell sv) ()
348+
349+-- | This is safe even if we are adding more threads concurrently because if
350+-- a child thread is adding another thread then anyway 'workerThreads' will
351+-- not be empty.
352+{-# INLINE allThreadsDone #-}
353+allThreadsDone :: MonadIO m => Channel m a -> m Bool
354+allThreadsDone sv = liftIO $ S.null <$> readIORef (workerThreads sv)
355+
356+-------------------------------------------------------------------------------
357+-- Dispatching workers
358+-------------------------------------------------------------------------------
359+
360+{-# NOINLINE recordMaxWorkers #-}
361+recordMaxWorkers :: MonadIO m => Channel m a -> m ()
362+recordMaxWorkers sv = liftIO $ do
363+ active <- readIORef (workerCount sv)
364+ maxWrk <- readIORef (maxWorkers $ svarStats sv)
365+ when (active > maxWrk) $ writeIORef (maxWorkers $ svarStats sv) active
366+ modifyIORef (totalDispatches $ svarStats sv) (+1)
367+
368+{-# NOINLINE pushWorker #-}
369+pushWorker :: (MonadIO m, MonadBaseControl IO m) => Count -> Channel m a -> m ()
370+pushWorker yieldMax sv = do
371+ liftIO $ atomicModifyIORefCAS_ (workerCount sv) $ \n -> n + 1
372+ when (svarInspectMode sv) $ recordMaxWorkers sv
373+ -- This allocation matters when significant number of workers are being
374+ -- sent. We allocate it only when needed.
375+ winfo <-
376+ case yieldRateInfo sv of
377+ Nothing -> return Nothing
378+ Just _ -> liftIO $ do
379+ cntRef <- newIORef 0
380+ t <- getTime Monotonic
381+ lat <- newIORef (0, t)
382+ return $ Just WorkerInfo
383+ { workerYieldMax = yieldMax
384+ , workerYieldCount = cntRef
385+ , workerLatencyStart = lat
386+ }
387+ doFork (workLoop sv winfo) (svarMrun sv) (handleChildException sv)
388+ >>= addThread sv
389+
390+-- Returns:
391+-- True: can dispatch more
392+-- False: cannot dispatch any more
393+dispatchWorker :: (MonadIO m, MonadBaseControl IO m) => Count -> Channel m a -> m Bool
394+dispatchWorker yieldCount sv = do
395+ let workerLimit = maxWorkerLimit sv
396+ -- XXX in case of Ahead streams we should not send more than one worker
397+ -- when the work queue is done but heap is not done.
398+ done <- liftIO $ isWorkDone sv
399+ -- Note, "done" may not mean that the work is actually finished if there
400+ -- are workers active, because there may be a worker which has not yet
401+ -- queued the leftover work.
402+ if not done
403+ then do
404+ qDone <- liftIO $ isQueueDone sv
405+ -- This count may not be accurate as it is decremented by the workers
406+ -- and we have no synchronization with that decrement.
407+ active <- liftIO $ readIORef $ workerCount sv
408+ if not qDone
409+ then do
410+ -- Note that we may deadlock if the previous workers (tasks in the
411+ -- stream) wait/depend on the future workers (tasks in the stream)
412+ -- executing. In that case we should either configure the maxWorker
413+ -- count to higher or use parallel style instead of ahead or async
414+ -- style.
415+ limit <- case remainingWork sv of
416+ Nothing -> return workerLimit
417+ Just ref -> do
418+ n <- liftIO $ readIORef ref
419+ case yieldRateInfo sv of
420+ Just _ -> return workerLimit
421+ Nothing ->
422+ return $
423+ case workerLimit of
424+ Unlimited -> Limited (fromIntegral n)
425+ Limited lim -> Limited $ min lim (fromIntegral n)
426+
427+ -- XXX for ahead streams shall we take the heap yields into account
428+ -- for controlling the dispatch? We should not dispatch if the heap
429+ -- has already got the limit covered.
430+ let dispatch = pushWorker yieldCount sv >> return True
431+ in case limit of
432+ Unlimited -> dispatch
433+ -- Note that the use of remainingWork and workerCount is not
434+ -- atomic and the counts may even have changed between reading
435+ -- and using them here, so this is just approximate logic and
436+ -- we cannot rely on it for correctness. We may actually
437+ -- dispatch more workers than required.
438+ Limited lim | lim > fromIntegral active -> dispatch
439+ _ -> return False
440+ else do
441+ when (active <= 0) $ pushWorker 0 sv
442+ return False
443+ else return False
444+
445+-- XXX in case of ahead style stream we need to take the heap size into account
446+-- because we return the workers on the basis of that which causes a condition
447+-- where we keep dispatching and they keep returning. So we must have exactly
448+-- the same logic for not dispatching and for returning.
449+--
450+-- Returns:
451+-- True: can dispatch more
452+-- False: full, no more dispatches
453+dispatchWorkerPaced :: (MonadIO m, MonadBaseControl IO m) =>
454+ Channel m a -> m Bool
455+dispatchWorkerPaced sv = do
456+ let yinfo = fromJust $ yieldRateInfo sv
457+ (svarYields, svarElapsed, wLatency) <- do
458+ now <- liftIO $ getTime Monotonic
459+ (yieldCount, baseTime, lat) <-
460+ liftIO $ collectLatency sv yinfo False
461+ let elapsed = fromRelTime64 $ diffAbsTime64 now baseTime
462+ let latency =
463+ if lat == 0
464+ then fromMaybe lat (workerBootstrapLatency yinfo)
465+ else lat
466+
467+ return (yieldCount, elapsed, latency)
468+
469+ if wLatency == 0
470+ -- Need to measure the latency with a single worker before we can perform
471+ -- any computation.
472+ then return False
473+ else do
474+ let workerLimit = maxWorkerLimit sv
475+ let targetLat = svarLatencyTarget yinfo
476+ let range = svarLatencyRange yinfo
477+ gainLoss <- liftIO $ readIORef (svarGainedLostYields yinfo)
478+ let work = estimateWorkers workerLimit svarYields gainLoss svarElapsed
479+ wLatency targetLat range
480+
481+ -- XXX we need to take yieldLimit into account here. If we are at the
482+ -- end of the limit as well as the time, we should not be sleeping.
483+ -- If we are not actually planning to dispatch any more workers we need
484+ -- to take that in account.
485+ case work of
486+ BlockWait s -> do
487+ assert (s >= 0) (return ())
488+ -- XXX note that when we return from here we will block waiting
489+ -- for the result from the existing worker. If that takes too
490+ -- long we won't be able to send another worker until the
491+ -- result arrives.
492+ --
493+ -- Sleep only if there are no active workers, otherwise we will
494+ -- defer the output of those. Note we cannot use workerCount
495+ -- here as it is not a reliable way to ensure there are
496+ -- definitely no active workers. When workerCount is 0 we may
497+ -- still have a Stop event waiting in the outputQueue.
498+ done <- allThreadsDone sv
499+ when done $ void $ do
500+ let us = fromRelTime64 (toRelTime64 s) :: MicroSecond64
501+ liftIO $ threadDelay (fromIntegral us)
502+ dispatchWorker 1 sv
503+ return False
504+ PartialWorker yields -> do
505+ assert (yields > 0) (return ())
506+ updateGainedLostYields yinfo yields
507+
508+ done <- allThreadsDone sv
509+ when done $ void $ dispatchWorker yields sv
510+ return False
511+ ManyWorkers netWorkers yields -> do
512+ assert (netWorkers >= 1) (return ())
513+ assert (yields >= 0) (return ())
514+ updateGainedLostYields yinfo yields
515+
516+ let periodRef = workerPollingInterval yinfo
517+ ycnt = max 1 $ yields `div` fromIntegral netWorkers
518+ period = min ycnt (fromIntegral magicMaxBuffer)
519+
520+ old <- liftIO $ readIORef periodRef
521+ when (period < old) $
522+ liftIO $ writeIORef periodRef period
523+
524+ cnt <- liftIO $ readIORef $ workerCount sv
525+ if cnt < netWorkers
526+ then do
527+ let total = netWorkers - cnt
528+ batch = max 1 $ fromIntegral $
529+ minThreadDelay `div` targetLat
530+ -- XXX stagger the workers over a period?
531+ -- XXX cannot sleep, as that would mean we cannot process
532+ -- the outputs. need to try a different mechanism to
533+ -- stagger.
534+ -- when (total > batch) $
535+ -- liftIO $ threadDelay $ nanoToMicroSecs minThreadDelay
536+ dispatchN (min total batch)
537+ else return False
538+
539+ where
540+
541+ updateGainedLostYields yinfo yields = do
542+ let buf = fromIntegral $ svarRateBuffer yinfo
543+ when (yields /= 0 && abs yields > buf) $ do
544+ let delta =
545+ if yields > 0
546+ then yields - buf
547+ else yields + buf
548+ liftIO $ modifyIORef (svarGainedLostYields yinfo) (+ delta)
549+
550+ dispatchN n =
551+ if n == 0
552+ then return True
553+ else do
554+ r <- dispatchWorker 0 sv
555+ if r
556+ then dispatchN (n - 1)
557+ else return False
558+
559+{-# NOINLINE sendWorkerWait #-}
560+sendWorkerWait
561+ :: MonadIO m
562+ => (Channel m a -> IO ())
563+ -> (Channel m a -> m Bool)
564+ -> Channel m a
565+ -> m ()
566+sendWorkerWait delay dispatch sv = do
567+ -- Note that we are guaranteed to have at least one outstanding worker when
568+ -- we enter this function. So if we sleep we are guaranteed to be woken up
569+ -- by an outputDoorBell, when the worker exits.
570+
571+ liftIO $ delay sv
572+ (_, n) <- liftIO $ readIORef (outputQueue sv)
573+ when (n <= 0) $ do
574+ -- The queue may be empty temporarily if the worker has dequeued the
575+ -- work item but has not enqueued the remaining part yet. For the same
576+ -- reason, a worker may come back if it tries to dequeue and finds the
577+ -- queue empty, even though the whole work has not finished yet.
578+
579+ -- If we find that the queue is empty, but it may be empty
580+ -- temporarily, when we checked it. If that's the case we might
581+ -- sleep indefinitely unless the active workers produce some
582+ -- output. We may deadlock specially if the otuput from the active
583+ -- workers depends on the future workers that we may never send.
584+ -- So in case the queue was temporarily empty set a flag to inform
585+ -- the enqueue to send us a doorbell.
586+
587+ -- Note that this is just a best effort mechanism to avoid a
588+ -- deadlock. Deadlocks may still happen if for some weird reason
589+ -- the consuming computation shares an MVar or some other resource
590+ -- with the producing computation and gets blocked on that resource
591+ -- and therefore cannot do any pushworker to add more threads to
592+ -- the producer. In such cases the programmer should use a parallel
593+ -- style so that all the producers are scheduled immediately and
594+ -- unconditionally. We can also use a separate monitor thread to
595+ -- push workers instead of pushing them from the consumer, but then
596+ -- we are no longer using pull based concurrency rate adaptation.
597+ --
598+ -- XXX update this in the tutorial.
599+ --
600+ -- Having pending active workers does not mean that we are guaranteed
601+ -- to be woken up if we sleep. In case of Ahead streams, there may be
602+ -- queued items in the heap even though the outputQueue is empty, and
603+ -- we may have active workers which are deadlocked on those items to be
604+ -- processed by the consumer. We should either guarantee that any
605+ -- worker, before returning, clears the heap or we send a worker to
606+ -- clear it. Normally we always send a worker if no output is seen, but
607+ -- if the thread limit is reached or we are using pacing then we may
608+ -- not send a worker. See the concurrentApplication test in the tests,
609+ -- that test case requires at least one yield from the producer to not
610+ -- deadlock, if the last workers output is stuck in the heap then this
611+ -- test fails. This problem can be extended to n threads when the
612+ -- consumer may depend on the evaluation of next n items in the
613+ -- producer stream.
614+
615+ -- register for the outputDoorBell before we check the queue so that if
616+ -- we sleep because the queue was empty we are guaranteed to get a
617+ -- doorbell on the next enqueue.
618+
619+ liftIO $ atomicModifyIORefCAS_ (needDoorBell sv) $ const True
620+ liftIO storeLoadBarrier
621+ canDoMore <- dispatch sv
622+
623+ -- XXX test for the case when we miss sending a worker when the worker
624+ -- count is more than 1500.
625+ --
626+ -- XXX Assert here that if the heap is not empty then there is at
627+ -- least one outstanding worker. Otherwise we could be sleeping
628+ -- forever.
629+
630+ if canDoMore
631+ then sendWorkerWait delay dispatch sv
632+ else do
633+ liftIO $ withDiagMVar sv "sendWorkerWait: nothing to do"
634+ $ takeMVar (outputDoorBell sv)
635+ (_, len) <- liftIO $ readIORef (outputQueue sv)
636+ when (len <= 0) $ sendWorkerWait delay dispatch sv
637+
638+-- | Start the evaluation of the channel's work queue by kicking off a worker.
639+-- Note: Work queue must not be empty otherwise the worker will exit without
640+-- doing anything.
641+startChannel :: (MonadIO m, MonadBaseControl IO m) =>
642+ Channel m a -> m ()
643+startChannel chan = do
644+ case yieldRateInfo chan of
645+ Nothing -> pushWorker 0 chan
646+ Just yinfo ->
647+ if svarLatencyTarget yinfo == maxBound
648+ then liftIO $ threadDelay maxBound
649+ else pushWorker 1 chan
650+
651+sendWorkerDelayPaced :: Channel m a -> IO ()
652+sendWorkerDelayPaced _ = return ()
653+
654+sendWorkerDelay :: Channel m a -> IO ()
655+sendWorkerDelay _sv =
656+ -- XXX we need a better way to handle this than hardcoded delays. The
657+ -- delays may be different for different systems.
658+ -- If there is a usecase where this is required we can create a combinator
659+ -- to set it as a config in the state.
660+ {-
661+ do
662+ ncpu <- getNumCapabilities
663+ if ncpu <= 1
664+ then
665+ if (svarStyle sv == AheadVar)
666+ then threadDelay 100
667+ else threadDelay 25
668+ else
669+ if (svarStyle sv == AheadVar)
670+ then threadDelay 100
671+ else threadDelay 10
672+ -}
673+ return ()
src/Streamly/Internal/Data/Stream/Async/Channel/Type.hsnew file+612
@@ -0,0 +1,612 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Type
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+
9+module Streamly.Internal.Data.Stream.Async.Channel.Type
10+ (
11+ -- * Parent child communication
12+ ThreadAbort (..)
13+ , ChildEvent (..)
14+
15+ -- * SVar
16+ , Count (..)
17+ , Limit (..)
18+ , SVarStats (..)
19+ , WorkerInfo (..)
20+ , LatencyRange (..)
21+ , YieldRateInfo (..)
22+ , Channel(..)
23+
24+ -- * Channel configuration
25+ , Rate (..)
26+ , Config
27+
28+ -- ** Default config
29+ , magicMaxBuffer
30+ , defaultConfig
31+
32+ -- ** Type cast
33+
34+ -- ** State accessors
35+ , maxThreads
36+ , maxBuffer
37+ , maxYields
38+ , inspectMode
39+
40+ , rate
41+ , avgRate
42+ , minRate
43+ , maxRate
44+ , constRate
45+
46+ , getMaxThreads
47+ , getMaxBuffer
48+ , getStreamRate
49+ , getStreamLatency
50+ , setStreamLatency
51+ , getYieldLimit
52+ , getInspectMode
53+
54+ , getYieldRateInfo
55+ , newSVarStats
56+ )
57+where
58+
59+import Control.Concurrent (ThreadId)
60+import Control.Concurrent.MVar (MVar)
61+import Control.Exception (SomeException(..), Exception)
62+import Data.Int (Int64)
63+import Data.IORef (IORef, newIORef)
64+import Data.Set (Set)
65+
66+import Streamly.Internal.Control.Concurrent (RunInIO)
67+import Streamly.Internal.Data.Stream.StreamK.Type (Stream)
68+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
69+import Streamly.Internal.Data.Time.Units (AbsTime, NanoSecond64(..))
70+
71+newtype Count = Count Int64
72+ deriving ( Eq
73+ , Read
74+ , Show
75+ , Enum
76+ , Bounded
77+ , Num
78+ , Real
79+ , Integral
80+ , Ord
81+ )
82+
83+-- XXX We can use maxBound for unlimited?
84+
85+-- This is essentially a 'Maybe Word' type
86+data Limit = Unlimited | Limited Word deriving Show
87+
88+instance Eq Limit where
89+ Unlimited == Unlimited = True
90+ Unlimited == Limited _ = False
91+ Limited _ == Unlimited = False
92+ Limited x == Limited y = x == y
93+
94+instance Ord Limit where
95+ Unlimited <= Unlimited = True
96+ Unlimited <= Limited _ = False
97+ Limited _ <= Unlimited = True
98+ Limited x <= Limited y = x <= y
99+
100+------------------------------------------------------------------------------
101+-- Parent child thread communication type
102+------------------------------------------------------------------------------
103+
104+data ThreadAbort = ThreadAbort deriving Show
105+
106+instance Exception ThreadAbort
107+
108+-- | Events that a child thread may send to a parent thread.
109+data ChildEvent a =
110+ ChildYield a
111+ | ChildStop ThreadId (Maybe SomeException)
112+
113+-- | An SVar or a Stream Var is a conduit to the output from multiple streams
114+-- running concurrently and asynchronously. An SVar can be thought of as an
115+-- asynchronous IO handle. We can write any number of streams to an SVar in a
116+-- non-blocking manner and then read them back at any time at any pace. The
117+-- SVar would run the streams asynchronously and accumulate results. An SVar
118+-- may not really execute the stream completely and accumulate all the results.
119+-- However, it ensures that the reader can read the results at whatever paces
120+-- it wants to read. The SVar monitors and adapts to the consumer's pace.
121+--
122+-- An SVar is a mini scheduler, it has an associated workLoop that holds the
123+-- stream tasks to be picked and run by a pool of worker threads. It has an
124+-- associated output queue where the output stream elements are placed by the
125+-- worker threads. A outputDoorBell is used by the worker threads to intimate the
126+-- consumer thread about availability of new results in the output queue. More
127+-- workers are added to the SVar by 'fromStreamVar' on demand if the output
128+-- produced is not keeping pace with the consumer. On bounded SVars, workers
129+-- block on the output queue to provide throttling of the producer when the
130+-- consumer is not pulling fast enough. The number of workers may even get
131+-- reduced depending on the consuming pace.
132+--
133+-- New work is enqueued either at the time of creation of the SVar or as a
134+-- result of executing the parallel combinators i.e. '<|' and '<|>' when the
135+-- already enqueued computations get evaluated. See 'joinStreamVarAsync'.
136+
137+-- We measure the individual worker latencies to estimate the number of workers
138+-- needed or the amount of time we have to sleep between dispatches to achieve
139+-- a particular rate when controlled pace mode it used.
140+data WorkerInfo = WorkerInfo
141+ { workerYieldMax :: Count -- 0 means unlimited
142+ -- total number of yields by the worker till now
143+ , workerYieldCount :: IORef Count
144+ -- yieldCount at start, timestamp
145+ , workerLatencyStart :: IORef (Count, AbsTime)
146+ }
147+
148+data LatencyRange = LatencyRange
149+ { minLatency :: NanoSecond64
150+ , maxLatency :: NanoSecond64
151+ } deriving Show
152+
153+-- Rate control.
154+data YieldRateInfo = YieldRateInfo
155+ { svarLatencyTarget :: NanoSecond64
156+ , svarLatencyRange :: LatencyRange
157+ , svarRateBuffer :: Int
158+
159+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
160+ -- read by the worker threads
161+ , svarGainedLostYields :: IORef Count
162+
163+ -- Actual latency/througput as seen from the consumer side, we count the
164+ -- yields and the time it took to generates those yields. This is used to
165+ -- increase or decrease the number of workers needed to achieve the desired
166+ -- rate. The idle time of workers is adjusted in this, so that we only
167+ -- account for the rate when the consumer actually demands data.
168+ -- XXX interval latency is enough, we can move this under diagnostics build
169+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
170+ -- read by the worker threads
171+ , svarAllTimeLatency :: IORef (Count, AbsTime)
172+
173+ -- XXX Worker latency specified by the user to be used before the first
174+ -- actual measurement arrives. Not yet implemented
175+ , workerBootstrapLatency :: Maybe NanoSecond64
176+
177+ -- After how many yields the worker should update the latency information.
178+ -- If the latency is high, this count is kept lower and vice-versa. XXX If
179+ -- the latency suddenly becomes too high this count may remain too high for
180+ -- long time, in such cases the consumer can change it.
181+ -- 0 means no latency computation
182+ -- XXX this is derivable from workerMeasuredLatency, can be removed.
183+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
184+ -- read by the worker threads
185+ , workerPollingInterval :: IORef Count
186+
187+ -- This is in progress latency stats maintained by the workers which we
188+ -- empty into workerCollectedLatency stats at certain intervals - whenever
189+ -- we process the stream elements yielded in this period. The first count
190+ -- is all yields, the second count is only those yields for which the
191+ -- latency was measured to be non-zero (note that if the timer resolution
192+ -- is low the measured latency may be zero e.g. on JS platform).
193+ -- [LOCKING] Locked access. Modified by the consumer thread as well as
194+ -- worker threads. Workers modify it periodically based on
195+ -- workerPollingInterval and not on every yield to reduce the locking
196+ -- overhead.
197+ -- (allYieldCount, yieldCount, timeTaken)
198+ , workerPendingLatency :: IORef (Count, Count, NanoSecond64)
199+
200+ -- This is the second level stat which is an accmulation from
201+ -- workerPendingLatency stats. We keep accumulating latencies in this
202+ -- bucket until we have stats for a sufficient period and then we reset it
203+ -- to start collecting for the next period and retain the computed average
204+ -- latency for the last period in workerMeasuredLatency.
205+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
206+ -- read by the worker threads
207+ -- (allYieldCount, yieldCount, timeTaken)
208+ , workerCollectedLatency :: IORef (Count, Count, NanoSecond64)
209+
210+ -- Latency as measured by workers, aggregated for the last period.
211+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
212+ -- read by the worker threads
213+ , workerMeasuredLatency :: IORef NanoSecond64
214+ }
215+
216+data SVarStats = SVarStats {
217+ totalDispatches :: IORef Int
218+ , maxWorkers :: IORef Int
219+ , maxOutQSize :: IORef Int
220+ , maxHeapSize :: IORef Int
221+ , maxWorkQSize :: IORef Int
222+ , avgWorkerLatency :: IORef (Count, NanoSecond64)
223+ , minWorkerLatency :: IORef NanoSecond64
224+ , maxWorkerLatency :: IORef NanoSecond64
225+ , svarStopTime :: IORef (Maybe AbsTime)
226+}
227+
228+-- IMPORTANT NOTE: we cannot update the SVar after generating it as we have
229+-- references to the original SVar stored in several functions which will keep
230+-- pointing to the original data and the new updates won't reflect there.
231+-- Any updateable parts must be kept in mutable references (IORef).
232+
233+-- XXX Since we have stream specific channels now, we can remove functions like
234+-- enqueue, readOuputQ, postProcess, workLoop etc from this.
235+
236+-- | A mutable channel to evaluate multiple streams concurrently and provide
237+-- the combined results as output stream.
238+data Channel m a = Channel
239+ {
240+ svarMrun :: RunInIO m
241+ -- FORWARD FLOW: Flow of data from the workers to the consumer
242+
243+ -- Shared output queue (events, length)
244+ -- XXX For better efficiency we can try a preallocated array type (perhaps
245+ -- something like a vector) that allows an O(1) append. That way we will
246+ -- avoid constructing and reversing the list. Possibly we can also avoid
247+ -- the GC copying overhead. When the size increases we should be able to
248+ -- allocate the array in chunks.
249+ --
250+ -- [LOCKING] Frequent locked access. This is updated by workers on each
251+ -- yield and once in a while read by the consumer thread. This could have
252+ -- big locking overhead if the number of workers is high.
253+ --
254+ -- XXX We can use a per-CPU data structure to reduce the locking overhead.
255+ -- However, a per-cpu structure cannot guarantee the exact sequence in
256+ -- which the elements were added, though that may not be important.
257+ -- XXX We can send a bundle of events of one type coaleseced together in an
258+ -- unboxed structure.
259+ , outputQueue :: IORef ([ChildEvent a], Int)
260+
261+ -- [LOCKING] Infrequent MVar. Used when the outputQ transitions from empty
262+ -- to non-empty, or a work item is queued by a worker to the work queue and
263+ -- needDoorBell is set by the consumer.
264+ , outputDoorBell :: MVar () -- signal the consumer about output
265+ -- XXX Can we use IO instead of m here?
266+ , readOutputQ :: m [ChildEvent a]
267+ , postProcess :: m Bool
268+
269+ -- Scheduling --
270+
271+ -- Combined/aggregate parameters
272+ -- This is capped to maxBufferLimit if set to more than that. Otherwise
273+ -- potentially each worker may yield one value to the buffer in the worst
274+ -- case exceeding the requested buffer size.
275+ , maxWorkerLimit :: Limit
276+ , maxBufferLimit :: Limit
277+
278+ -- [LOCKING] Read only access by consumer when dispatching a worker.
279+ -- Decremented by workers when picking work and undo decrement if the
280+ -- worker does not yield a value.
281+ , remainingWork :: Maybe (IORef Count)
282+ , yieldRateInfo :: Maybe YieldRateInfo
283+
284+ , enqueue :: (RunInIO m, Stream m a) -> IO ()
285+ , isWorkDone :: IO Bool
286+ , isQueueDone :: IO Bool
287+ , needDoorBell :: IORef Bool
288+ , workLoop :: Maybe WorkerInfo -> m ()
289+
290+ -- Shared, thread tracking
291+ -- [LOCKING] Updated unlocked, only by consumer thread.
292+ , workerThreads :: IORef (Set ThreadId)
293+
294+ -- [LOCKING] Updated locked, by consumer thread when dispatching a worker
295+ -- and by the worker threads when the thread stops. This is read unsafely
296+ -- at several places where we want to rely on an approximate value.
297+ , workerCount :: IORef Int
298+ -- XXX Can we use IO instead of m here?
299+ , accountThread :: ThreadId -> m ()
300+ , workerStopMVar :: MVar ()
301+
302+ -- cleanup: to track garbage collection of SVar --
303+ , svarRef :: Maybe (IORef ())
304+
305+ -- Stats --
306+ , svarStats :: SVarStats
307+
308+ -- Diagnostics --
309+ , svarInspectMode :: Bool
310+ , svarCreator :: ThreadId
311+ }
312+
313+-------------------------------------------------------------------------------
314+-- Overall state threaded around a stream
315+-------------------------------------------------------------------------------
316+
317+-- | Specifies the stream yield rate in yields per second (@Hertz@).
318+-- We keep accumulating yield credits at 'rateGoal'. At any point of time we
319+-- allow only as many yields as we have accumulated as per 'rateGoal' since the
320+-- start of time. If the consumer or the producer is slower or faster, the
321+-- actual rate may fall behind or exceed 'rateGoal'. We try to recover the gap
322+-- between the two by increasing or decreasing the pull rate from the producer.
323+-- However, if the gap becomes more than 'rateBuffer' we try to recover only as
324+-- much as 'rateBuffer'.
325+--
326+-- 'rateLow' puts a bound on how low the instantaneous rate can go when
327+-- recovering the rate gap. In other words, it determines the maximum yield
328+-- latency. Similarly, 'rateHigh' puts a bound on how high the instantaneous
329+-- rate can go when recovering the rate gap. In other words, it determines the
330+-- minimum yield latency. We reduce the latency by increasing concurrency,
331+-- therefore we can say that it puts an upper bound on concurrency.
332+--
333+-- If the 'rateGoal' is 0 or negative the stream never yields a value.
334+-- If the 'rateBuffer' is 0 or negative we do not attempt to recover.
335+--
336+-- /Since: 0.5.0 ("Streamly")/
337+--
338+-- @since 0.8.0
339+data Rate = Rate
340+ { rateLow :: Double -- ^ The lower rate limit
341+ , rateGoal :: Double -- ^ The target rate we want to achieve
342+ , rateHigh :: Double -- ^ The upper rate limit
343+ , rateBuffer :: Int -- ^ Maximum slack from the goal
344+ }
345+
346+-- XXX we can put the resettable fields in a oneShotConfig field and others in
347+-- a persistentConfig field. That way reset would be fast and scalable
348+-- irrespective of the number of fields.
349+--
350+-- XXX make all these Limited types and use phantom types to distinguish them
351+
352+-- | An abstract type for specifying the configuration parameters of a
353+-- 'Channel'. Use @Config -> Config@ modifier functions to modify the
354+-- default configuration. See the modifiers for default values.
355+--
356+data Config = Config
357+ { -- one shot configuration, automatically reset for each API call
358+ -- streamVar :: Maybe (SVar t m a)
359+ _yieldLimit :: Maybe Count
360+
361+ -- persistent configuration, state that remains valid until changed by
362+ -- an explicit setting via a combinator.
363+ , _threadsHigh :: Limit
364+ , _bufferHigh :: Limit
365+
366+ -- XXX these two can be collapsed into a single type
367+ , _streamLatency :: Maybe NanoSecond64 -- bootstrap latency
368+ , _maxStreamRate :: Maybe Rate
369+ , _inspectMode :: Bool
370+ }
371+
372+-------------------------------------------------------------------------------
373+-- State defaults and reset
374+-------------------------------------------------------------------------------
375+
376+-- A magical value for the buffer size arrived at by running the smallest
377+-- possible task and measuring the optimal value of the buffer for that. This
378+-- is obviously dependent on hardware, this figure is based on a 2.2GHz intel
379+-- core-i7 processor.
380+magicMaxBuffer :: Word
381+magicMaxBuffer = 1500
382+
383+defaultMaxThreads, defaultMaxBuffer :: Limit
384+defaultMaxThreads = Limited magicMaxBuffer
385+defaultMaxBuffer = Limited magicMaxBuffer
386+
387+-- The fields prefixed by an _ are not to be accessed or updated directly but
388+-- via smart accessor APIs.
389+defaultConfig :: Config
390+defaultConfig = Config
391+ { -- streamVar = Nothing
392+ _yieldLimit = Nothing
393+ , _threadsHigh = defaultMaxThreads
394+ , _bufferHigh = defaultMaxBuffer
395+ , _maxStreamRate = Nothing
396+ , _streamLatency = Nothing
397+ , _inspectMode = False
398+ }
399+
400+-------------------------------------------------------------------------------
401+-- Smart get/set routines for State
402+-------------------------------------------------------------------------------
403+
404+-- Use get/set routines instead of directly accessing the State fields
405+maxYields :: Maybe Int64 -> Config -> Config
406+maxYields lim st =
407+ st { _yieldLimit =
408+ case lim of
409+ Nothing -> Nothing
410+ Just n ->
411+ if n <= 0
412+ then Just 0
413+ else Just (fromIntegral n)
414+ }
415+
416+getYieldLimit :: Config -> Maybe Count
417+getYieldLimit = _yieldLimit
418+
419+-- | Specify the maximum number of threads that can be spawned by the channel.
420+-- A value of 0 resets the thread limit to default, a negative value means
421+-- there is no limit. The default value is 1500.
422+--
423+-- When the actions in a stream are IO bound, having blocking IO calls, this
424+-- option can be used to control the maximum number of in-flight IO requests.
425+-- When the actions are CPU bound this option can be used to control the amount
426+-- of CPU used by the stream.
427+--
428+maxThreads :: Int -> Config -> Config
429+maxThreads n st =
430+ st { _threadsHigh =
431+ if n < 0
432+ then Unlimited
433+ else if n == 0
434+ then defaultMaxThreads
435+ else Limited (fromIntegral n)
436+ }
437+
438+getMaxThreads :: Config -> Limit
439+getMaxThreads = _threadsHigh
440+
441+-- | Specify the maximum size of the buffer for storing the results from
442+-- concurrent computations. If the buffer becomes full we stop spawning more
443+-- concurrent tasks until there is space in the buffer.
444+-- A value of 0 resets the buffer size to default, a negative value means
445+-- there is no limit. The default value is 1500.
446+--
447+-- CAUTION! using an unbounded 'maxBuffer' value (i.e. a negative value)
448+-- coupled with an unbounded 'maxThreads' value is a recipe for disaster in
449+-- presence of infinite streams, or very large streams. Especially, it must
450+-- not be used when 'pure' is used in 'ZipAsyncM' streams as 'pure' in
451+-- applicative zip streams generates an infinite stream causing unbounded
452+-- concurrent generation with no limit on the buffer or threads.
453+--
454+maxBuffer :: Int -> Config -> Config
455+maxBuffer n st =
456+ st { _bufferHigh =
457+ if n < 0
458+ then Unlimited
459+ else if n == 0
460+ then defaultMaxBuffer
461+ else Limited (fromIntegral n)
462+ }
463+
464+getMaxBuffer :: Config -> Limit
465+getMaxBuffer = _bufferHigh
466+
467+-- | Specify the pull rate of a channel.
468+-- A 'Nothing' value resets the rate to default which is unlimited. When the
469+-- rate is specified, concurrent production may be ramped up or down
470+-- automatically to achieve the specified yield rate. The specific behavior for
471+-- different styles of 'Rate' specifications is documented under 'Rate'. The
472+-- effective maximum production rate achieved by a channel is governed by:
473+--
474+-- * The 'maxThreads' limit
475+-- * The 'maxBuffer' limit
476+-- * The maximum rate that the stream producer can achieve
477+-- * The maximum rate that the stream consumer can achieve
478+--
479+rate :: Maybe Rate -> Config -> Config
480+rate r st = st { _maxStreamRate = r }
481+
482+getStreamRate :: Config -> Maybe Rate
483+getStreamRate = _maxStreamRate
484+
485+setStreamLatency :: Int -> Config -> Config
486+setStreamLatency n st =
487+ st { _streamLatency =
488+ if n <= 0
489+ then Nothing
490+ else Just (fromIntegral n)
491+ }
492+
493+getStreamLatency :: Config -> Maybe NanoSecond64
494+getStreamLatency = _streamLatency
495+
496+inspectMode :: Config -> Config
497+inspectMode st = st { _inspectMode = True }
498+
499+getInspectMode :: Config -> Bool
500+getInspectMode = _inspectMode
501+
502+-------------------------------------------------------------------------------
503+-- Creating an SVar
504+-------------------------------------------------------------------------------
505+
506+getYieldRateInfo :: Config -> IO (Maybe YieldRateInfo)
507+getYieldRateInfo st = do
508+ -- convert rate in Hertz to latency in Nanoseconds
509+ let rateToLatency r = if r <= 0 then maxBound else round $ 1.0e9 / r
510+ case getStreamRate st of
511+ Just (Rate low goal high buf) ->
512+ let l = rateToLatency goal
513+ minl = rateToLatency high
514+ maxl = rateToLatency low
515+ in mkYieldRateInfo l (LatencyRange minl maxl) buf
516+ Nothing -> return Nothing
517+
518+ where
519+
520+ mkYieldRateInfo latency latRange buf = do
521+ measured <- newIORef 0
522+ wcur <- newIORef (0,0,0)
523+ wcol <- newIORef (0,0,0)
524+ now <- getTime Monotonic
525+ wlong <- newIORef (0,now)
526+ period <- newIORef 1
527+ gainLoss <- newIORef (Count 0)
528+
529+ return $ Just YieldRateInfo
530+ { svarLatencyTarget = latency
531+ , svarLatencyRange = latRange
532+ , svarRateBuffer = buf
533+ , svarGainedLostYields = gainLoss
534+ , workerBootstrapLatency = getStreamLatency st
535+ , workerPollingInterval = period
536+ , workerMeasuredLatency = measured
537+ , workerPendingLatency = wcur
538+ , workerCollectedLatency = wcol
539+ , svarAllTimeLatency = wlong
540+ }
541+
542+newSVarStats :: IO SVarStats
543+newSVarStats = do
544+ disp <- newIORef 0
545+ maxWrk <- newIORef 0
546+ maxOq <- newIORef 0
547+ maxHs <- newIORef 0
548+ maxWq <- newIORef 0
549+ avgLat <- newIORef (0, NanoSecond64 0)
550+ maxLat <- newIORef (NanoSecond64 0)
551+ minLat <- newIORef (NanoSecond64 0)
552+ stpTime <- newIORef Nothing
553+
554+ return SVarStats
555+ { totalDispatches = disp
556+ , maxWorkers = maxWrk
557+ , maxOutQSize = maxOq
558+ , maxHeapSize = maxHs
559+ , maxWorkQSize = maxWq
560+ , avgWorkerLatency = avgLat
561+ , minWorkerLatency = minLat
562+ , maxWorkerLatency = maxLat
563+ , svarStopTime = stpTime
564+ }
565+
566+-------------------------------------------------------------------------------
567+-- Rate
568+-------------------------------------------------------------------------------
569+
570+-- | Same as @rate (Just $ Rate (r/2) r (2*r) maxBound)@
571+--
572+-- Specifies the average production rate of a stream in number of yields
573+-- per second (i.e. @Hertz@). Concurrent production is ramped up or down
574+-- automatically to achieve the specified average yield rate. The rate can
575+-- go down to half of the specified rate on the lower side and double of
576+-- the specified rate on the higher side.
577+--
578+avgRate :: Double -> Config -> Config
579+avgRate r = rate (Just $ Rate (r/2) r (2*r) maxBound)
580+
581+-- | Same as @rate (Just $ Rate r r (2*r) maxBound)@
582+--
583+-- Specifies the minimum rate at which the stream should yield values. As
584+-- far as possible the yield rate would never be allowed to go below the
585+-- specified rate, even though it may possibly go above it at times, the
586+-- upper limit is double of the specified rate.
587+--
588+minRate :: Double -> Config -> Config
589+minRate r = rate (Just $ Rate r r (2*r) maxBound)
590+
591+-- | Same as @rate (Just $ Rate (r/2) r r maxBound)@
592+--
593+-- Specifies the maximum rate at which the stream should yield values. As
594+-- far as possible the yield rate would never be allowed to go above the
595+-- specified rate, even though it may possibly go below it at times, the
596+-- lower limit is half of the specified rate. This can be useful in
597+-- applications where certain resource usage must not be allowed to go
598+-- beyond certain limits.
599+--
600+maxRate :: Double -> Config -> Config
601+maxRate r = rate (Just $ Rate (r/2) r r maxBound)
602+
603+-- | Same as @rate (Just $ Rate r r r 0)@
604+--
605+-- Specifies a constant yield rate. If for some reason the actual rate
606+-- goes above or below the specified rate we do not try to recover it by
607+-- increasing or decreasing the rate in future. This can be useful in
608+-- applications like graphics frame refresh where we need to maintain a
609+-- constant refresh rate.
610+--
611+constRate :: Double -> Config -> Config
612+constRate r = rate (Just $ Rate r r r 0)
src/Streamly/Internal/Data/Stream/Async/Channel/Worker.hsnew file+434
@@ -0,0 +1,434 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Worker
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+--
10+module Streamly.Internal.Data.Stream.Async.Channel.Worker
11+ (
12+ -- * Adjusting Limits
13+ decrementYieldLimit
14+ , incrementYieldLimit
15+
16+ -- * Rate Control
17+ , Work (..)
18+ , isBeyondMaxRate
19+ , estimateWorkers
20+ , updateYieldCount
21+ , minThreadDelay
22+ , workerRateControl
23+ , workerUpdateLatency
24+
25+ -- * Send Events
26+ , send
27+ , ringDoorBell
28+
29+ -- ** Yield
30+ , sendYield
31+
32+ -- ** Stop
33+ , sendStop
34+
35+ -- ** Exception
36+ , handleChildException
37+ )
38+where
39+
40+import Control.Concurrent (myThreadId)
41+import Control.Concurrent.MVar (MVar, tryPutMVar)
42+import Control.Exception (SomeException(..), assert)
43+import Control.Monad (when, void)
44+import Data.IORef (IORef, readIORef, writeIORef)
45+import Streamly.Internal.Data.Atomics
46+ (atomicModifyIORefCAS, atomicModifyIORefCAS_, writeBarrier,
47+ storeLoadBarrier)
48+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
49+import Streamly.Internal.Data.Time.Units
50+ (AbsTime, NanoSecond64(..), diffAbsTime64, fromRelTime64)
51+
52+import Streamly.Internal.Data.Stream.Async.Channel.Type
53+
54+------------------------------------------------------------------------------
55+-- Collecting results from child workers in a streamed fashion
56+------------------------------------------------------------------------------
57+
58+-- XXX Can we make access to remainingWork and yieldRateInfo fields in sv
59+-- faster, along with the fields in sv required by send?
60+-- XXX make it noinline
61+--
62+-- XXX we may want to employ an increment and decrement in batches when the
63+-- througput is high or when the cost of synchronization is high. For example
64+-- if the application is distributed then inc/dec of a shared variable may be
65+-- very costly.
66+--
67+-- A worker decrements the yield limit before it executes an action. However,
68+-- the action may not result in an element being yielded, in that case we have
69+-- to increment the yield limit.
70+--
71+-- Note that we need it to be an Int type so that we have the ability to undo a
72+-- decrement that takes it below zero.
73+{-# INLINE decrementYieldLimit #-}
74+decrementYieldLimit :: Channel m a -> IO Bool
75+decrementYieldLimit sv =
76+ case remainingWork sv of
77+ Nothing -> return True
78+ Just ref -> do
79+ r <- atomicModifyIORefCAS ref $ \x -> (x - 1, x)
80+ return $ r >= 1
81+
82+{-# INLINE incrementYieldLimit #-}
83+incrementYieldLimit :: Channel m a -> IO ()
84+incrementYieldLimit sv =
85+ case remainingWork sv of
86+ Nothing -> return ()
87+ Just ref -> atomicModifyIORefCAS_ ref (+ 1)
88+
89+-------------------------------------------------------------------------------
90+-- Yield control
91+-------------------------------------------------------------------------------
92+
93+updateYieldCount :: WorkerInfo -> IO Count
94+updateYieldCount winfo = do
95+ cnt <- readIORef (workerYieldCount winfo)
96+ let cnt1 = cnt + 1
97+ writeIORef (workerYieldCount winfo) cnt1
98+ return cnt1
99+
100+isBeyondMaxYield :: Count -> WorkerInfo -> Bool
101+isBeyondMaxYield cnt winfo =
102+ let ymax = workerYieldMax winfo
103+ in ymax /= 0 && cnt >= ymax
104+
105+-------------------------------------------------------------------------------
106+-- Sending results from worker
107+-------------------------------------------------------------------------------
108+
109+{-# INLINE ringDoorBell #-}
110+ringDoorBell :: Channel m a -> IO ()
111+ringDoorBell sv = do
112+ storeLoadBarrier
113+ w <- readIORef $ needDoorBell sv
114+ when w $ do
115+ -- Note: the sequence of operations is important for correctness here.
116+ -- We need to set the flag to false strictly before sending the
117+ -- outputDoorBell, otherwise the outputDoorBell may get processed too
118+ -- early and then we may set the flag to False to later making the
119+ -- consumer lose the flag, even without receiving a outputDoorBell.
120+ atomicModifyIORefCAS_ (needDoorBell sv) (const False)
121+ void $ tryPutMVar (outputDoorBell sv) ()
122+
123+{-# INLINE sendWithDoorBell #-}
124+sendWithDoorBell ::
125+ IORef ([ChildEvent a], Int) -> MVar () -> ChildEvent a -> IO Int
126+sendWithDoorBell q bell msg = do
127+ -- XXX can the access to outputQueue be made faster somehow?
128+ oldlen <- atomicModifyIORefCAS q $ \(es, n) ->
129+ ((msg : es, n + 1), n)
130+ when (oldlen <= 0) $ do
131+ -- The wake up must happen only after the store has finished otherwise
132+ -- we can have lost wakeup problems.
133+ writeBarrier
134+ -- Since multiple workers can try this at the same time, it is possible
135+ -- that we may put a spurious MVar after the consumer has already seen
136+ -- the output. But that's harmless, at worst it may cause the consumer
137+ -- to read the queue again and find it empty.
138+ -- The important point is that the consumer is guaranteed to receive a
139+ -- doorbell if something was added to the queue after it empties it.
140+ void $ tryPutMVar bell ()
141+ return oldlen
142+
143+-- | This function is used by the producer threads to queue output for the
144+-- consumer thread to consume. Returns whether the queue has more space.
145+send :: Channel m a -> ChildEvent a -> IO Int
146+send sv = sendWithDoorBell (outputQueue sv) (outputDoorBell sv)
147+
148+-------------------------------------------------------------------------------
149+-- Collect and update worker latency
150+-------------------------------------------------------------------------------
151+
152+workerCollectLatency :: WorkerInfo -> IO (Maybe (Count, NanoSecond64))
153+workerCollectLatency winfo = do
154+ (cnt0, t0) <- readIORef (workerLatencyStart winfo)
155+ cnt1 <- readIORef (workerYieldCount winfo)
156+ let cnt = cnt1 - cnt0
157+
158+ if cnt > 0
159+ then do
160+ t1 <- getTime Monotonic
161+ let period = fromRelTime64 $ diffAbsTime64 t1 t0
162+ writeIORef (workerLatencyStart winfo) (cnt1, t1)
163+ return $ Just (cnt, period)
164+ else return Nothing
165+
166+-- XXX There are a number of gotchas in measuring latencies.
167+-- 1) We measure latencies only when a worker yields a value
168+-- 2) It is possible that a stream calls the stop continuation, in which case
169+-- the worker would not yield a value and we would not account that worker in
170+-- latencies. Even though this case should ideally be accounted we do not
171+-- account it because we cannot or do not distinguish it from the case
172+-- described next.
173+-- 3) It is possible that a worker returns without yielding anything because it
174+-- never got a chance to pick up work.
175+-- 4) If the system timer resolution is lower than the latency, the latency
176+-- computation turns out to be zero.
177+--
178+-- We can fix this if we measure the latencies by counting the work items
179+-- picked rather than based on the outputs yielded.
180+workerUpdateLatency :: YieldRateInfo -> WorkerInfo -> IO ()
181+workerUpdateLatency yinfo winfo = do
182+ r <- workerCollectLatency winfo
183+ case r of
184+ Just (cnt, period) -> do
185+ -- NOTE: On JS platform the timer resolution could be pretty low. When
186+ -- the timer resolution is low, measurement of latencies could be
187+ -- tricky. All the worker latencies will turn out to be zero if they
188+ -- are lower than the resolution. We only take into account those
189+ -- measurements which are more than the timer resolution.
190+
191+ let ref = workerPendingLatency yinfo
192+ (cnt1, t1) = if period > 0 then (cnt, period) else (0, 0)
193+ atomicModifyIORefCAS_ ref $
194+ \(fc, n, t) -> (fc + cnt, n + cnt1, t + t1)
195+ Nothing -> return ()
196+
197+-------------------------------------------------------------------------------
198+-- Worker rate control
199+-------------------------------------------------------------------------------
200+
201+-- We either block, or send one worker with limited yield count or one or more
202+-- workers with unlimited yield count.
203+data Work
204+ = BlockWait NanoSecond64
205+ | PartialWorker Count
206+ | ManyWorkers Int Count
207+ deriving Show
208+
209+-- | This is a magic number and it is overloaded, and used at several places to
210+-- achieve batching:
211+--
212+-- 1. If we have to sleep to slowdown this is the minimum period that we
213+-- accumulate before we sleep. Also, workers do not stop until this much
214+-- sleep time is accumulated.
215+-- 3. Collected latencies are computed and transferred to measured latency
216+-- after a minimum of this period.
217+minThreadDelay :: NanoSecond64
218+minThreadDelay = 1000000
219+
220+-- | Another magic number! When we have to start more workers to cover up a
221+-- number of yields that we are lagging by then we cannot start one worker for
222+-- each yield because that may be a very big number and if the latency of the
223+-- workers is low these number of yields could be very high. We assume that we
224+-- run each extra worker for at least this much time.
225+rateRecoveryTime :: NanoSecond64
226+rateRecoveryTime = 1000000
227+
228+-- | Get the worker latency without resetting workerPendingLatency
229+-- Returns (total yield count, base time, measured latency)
230+-- CAUTION! keep it in sync with collectLatency
231+getWorkerLatency :: YieldRateInfo -> IO (Count, AbsTime, NanoSecond64)
232+getWorkerLatency yinfo = do
233+ let cur = workerPendingLatency yinfo
234+ col = workerCollectedLatency yinfo
235+ longTerm = svarAllTimeLatency yinfo
236+ measured = workerMeasuredLatency yinfo
237+
238+ (curTotalCount, curCount, curTime) <- readIORef cur
239+ (colTotalCount, colCount, colTime) <- readIORef col
240+ (lcount, ltime) <- readIORef longTerm
241+ prevLat <- readIORef measured
242+
243+ let latCount = colCount + curCount
244+ latTime = colTime + curTime
245+ totalCount = colTotalCount + curTotalCount
246+ newLat =
247+ if latCount > 0 && latTime > 0
248+ then let lat = latTime `div` fromIntegral latCount
249+ -- XXX Give more weight to new?
250+ in (lat + prevLat) `div` 2
251+ else prevLat
252+ return (lcount + totalCount, ltime, newLat)
253+
254+-- XXX we can use phantom types to distinguish the duration/latency/expectedLat
255+estimateWorkers
256+ :: Limit
257+ -> Count
258+ -> Count
259+ -> NanoSecond64
260+ -> NanoSecond64
261+ -> NanoSecond64
262+ -> LatencyRange
263+ -> Work
264+estimateWorkers workerLimit svarYields gainLossYields
265+ svarElapsed wLatency targetLat range =
266+ -- XXX we can have a maxEfficiency combinator as well which runs the
267+ -- producer at the maximal efficiency i.e. the number of workers are chosen
268+ -- such that the latency is minimum or within a range. Or we can call it
269+ -- maxWorkerLatency.
270+ --
271+ let
272+ -- How many workers do we need to achieve the required rate?
273+ --
274+ -- When the workers are IO bound we can increase the throughput by
275+ -- increasing the number of workers as long as the IO device has enough
276+ -- capacity to process all the requests concurrently. If the IO
277+ -- bandwidth is saturated increasing the workers won't help. Also, if
278+ -- the CPU utilization in processing all these requests exceeds the CPU
279+ -- bandwidth, then increasing the number of workers won't help.
280+ --
281+ -- When the workers are purely CPU bound, increasing the workers beyond
282+ -- the number of CPUs won't help.
283+ --
284+ -- TODO - measure the CPU and IO requirements of the workers. Have a
285+ -- way to specify the max bandwidth of the underlying IO mechanism and
286+ -- use that to determine the max rate of workers, and also take the CPU
287+ -- bandwidth into account. We can also discover the IO bandwidth if we
288+ -- know that we are not CPU bound, then how much steady state rate are
289+ -- we able to achieve. Design tests for CPU bound and IO bound cases.
290+
291+ -- Calculate how many yields are we ahead or behind to match the exact
292+ -- required rate. Based on that we increase or decrease the effective
293+ -- workers.
294+ --
295+ -- When the worker latency is lower than required latency we begin with
296+ -- a yield and then wait rather than first waiting and then yielding.
297+ targetYields = (svarElapsed + wLatency + targetLat - 1) `div` targetLat
298+ effectiveYields = svarYields + gainLossYields
299+ deltaYields = fromIntegral targetYields - effectiveYields
300+
301+ -- We recover the deficit by running at a higher/lower rate for a
302+ -- certain amount of time. To keep the effective rate in reasonable
303+ -- limits we use rateRecoveryTime, minLatency and maxLatency.
304+ in if deltaYields > 0
305+ then
306+ let deltaYieldsFreq :: Double
307+ deltaYieldsFreq =
308+ fromIntegral deltaYields /
309+ fromIntegral rateRecoveryTime
310+ yieldsFreq = 1.0 / fromIntegral targetLat
311+ totalYieldsFreq = yieldsFreq + deltaYieldsFreq
312+ requiredLat = NanoSecond64 $ round $ 1.0 / totalYieldsFreq
313+ adjustedLat = min (max requiredLat (minLatency range))
314+ (maxLatency range)
315+ in assert (adjustedLat > 0) $
316+ if wLatency <= adjustedLat
317+ then PartialWorker deltaYields
318+ else let workers = withLimit $ wLatency `div` adjustedLat
319+ limited = min workers (fromIntegral deltaYields)
320+ in ManyWorkers (fromIntegral limited) deltaYields
321+ else
322+ let expectedDuration = fromIntegral effectiveYields * targetLat
323+ sleepTime = expectedDuration - svarElapsed
324+ maxSleepTime = maxLatency range - wLatency
325+ s = min sleepTime maxSleepTime
326+ in assert (sleepTime >= 0) $
327+ -- if s is less than 0 it means our maxSleepTime is less
328+ -- than the worker latency.
329+ if s > 0 then BlockWait s else ManyWorkers 1 (Count 0)
330+ where
331+ withLimit n =
332+ case workerLimit of
333+ Unlimited -> n
334+ Limited x -> min n (fromIntegral x)
335+
336+isBeyondMaxRate :: Channel m a -> YieldRateInfo -> IO Bool
337+isBeyondMaxRate sv yinfo = do
338+ (count, tstamp, wLatency) <- getWorkerLatency yinfo
339+ now <- getTime Monotonic
340+ let duration = fromRelTime64 $ diffAbsTime64 now tstamp
341+ let targetLat = svarLatencyTarget yinfo
342+ gainLoss <- readIORef (svarGainedLostYields yinfo)
343+ let work = estimateWorkers (maxWorkerLimit sv) count gainLoss duration
344+ wLatency targetLat (svarLatencyRange yinfo)
345+ cnt <- readIORef $ workerCount sv
346+ return $ case work of
347+ -- XXX set the worker's maxYields or polling interval based on yields
348+ PartialWorker _yields -> cnt > 1
349+ ManyWorkers n _ -> cnt > n
350+ BlockWait _ -> True
351+
352+-- XXX we should do rate control periodically based on the total yields rather
353+-- than based on the worker local yields as other workers may have yielded more
354+-- and we should stop based on the aggregate yields. However, latency update
355+-- period can be based on individual worker yields.
356+{-# NOINLINE checkRatePeriodic #-}
357+checkRatePeriodic :: Channel m a
358+ -> YieldRateInfo
359+ -> WorkerInfo
360+ -> Count
361+ -> IO Bool
362+checkRatePeriodic sv yinfo winfo ycnt = do
363+ i <- readIORef (workerPollingInterval yinfo)
364+ -- XXX use generation count to check if the interval has been updated
365+ if i /= 0 && (ycnt `mod` i) == 0
366+ then do
367+ workerUpdateLatency yinfo winfo
368+ -- XXX not required for parallel streams
369+ isBeyondMaxRate sv yinfo
370+ else return False
371+
372+-- CAUTION! this also updates the yield count and therefore should be called
373+-- only when we are actually yielding an element.
374+{-# NOINLINE workerRateControl #-}
375+workerRateControl :: Channel m a -> YieldRateInfo -> WorkerInfo -> IO Bool
376+workerRateControl sv yinfo winfo = do
377+ cnt <- updateYieldCount winfo
378+ beyondMaxRate <- checkRatePeriodic sv yinfo winfo cnt
379+ return $ not (isBeyondMaxYield cnt winfo || beyondMaxRate)
380+
381+-------------------------------------------------------------------------------
382+-- Send a yield event
383+-------------------------------------------------------------------------------
384+
385+-- XXX we should do rate control here but not latency update in case of ahead
386+-- streams. latency update must be done when we yield directly to outputQueue
387+-- or when we yield to heap.
388+--
389+-- returns whether the worker should continue (True) or stop (False).
390+{-# INLINE sendYield #-}
391+sendYield :: Channel m a -> Maybe WorkerInfo -> ChildEvent a -> IO Bool
392+sendYield sv mwinfo msg = do
393+ oldlen <- send sv msg
394+ let limit = maxBufferLimit sv
395+ bufferSpaceOk <- case limit of
396+ Unlimited -> return True
397+ Limited lim -> do
398+ active <- readIORef (workerCount sv)
399+ return $ (oldlen + 1) < (fromIntegral lim - active)
400+ rateLimitOk <-
401+ case mwinfo of
402+ Just winfo ->
403+ case yieldRateInfo sv of
404+ Nothing -> return True
405+ Just yinfo -> workerRateControl sv yinfo winfo
406+ Nothing -> return True
407+ return $ bufferSpaceOk && rateLimitOk
408+
409+-------------------------------------------------------------------------------
410+-- Send a Stop event
411+-------------------------------------------------------------------------------
412+
413+{-# INLINE workerStopUpdate #-}
414+workerStopUpdate :: WorkerInfo -> YieldRateInfo -> IO ()
415+workerStopUpdate winfo info = do
416+ i <- readIORef (workerPollingInterval info)
417+ when (i /= 0) $ workerUpdateLatency info winfo
418+
419+{-# INLINABLE sendStop #-}
420+sendStop :: Channel m a -> Maybe WorkerInfo -> IO ()
421+sendStop sv mwinfo = do
422+ atomicModifyIORefCAS_ (workerCount sv) $ \n -> n - 1
423+ case (mwinfo, yieldRateInfo sv) of
424+ (Just winfo, Just info) ->
425+ workerStopUpdate winfo info
426+ _ ->
427+ return ()
428+ myThreadId >>= \tid -> void $ send sv (ChildStop tid Nothing)
429+
430+{-# NOINLINE handleChildException #-}
431+handleChildException :: Channel m a -> SomeException -> IO ()
432+handleChildException sv e = do
433+ tid <- myThreadId
434+ void $ send sv (ChildStop tid (Just e))
streamly.cabal+9
@@ -446,6 +446,13 @@ library
446
446
447 , Streamly.Internal.Data.Stream.Serial
447 , Streamly.Internal.Data.Stream.Serial
448 , Streamly.Internal.Data.Stream.WSerial
448 , Streamly.Internal.Data.Stream.WSerial
449+ , Streamly.Internal.Data.Stream.Async.Channel.Type
450+ , Streamly.Internal.Data.Stream.Async.Channel.Worker
451+ , Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
452+ , Streamly.Internal.Data.Stream.Async.Channel.Consumer
453+ , Streamly.Internal.Data.Stream.Async.Channel.Core
454+ , Streamly.Internal.Data.Stream.Async.Channel
455+ , Streamly.Internal.Data.Stream.Async
449 , Streamly.Internal.Data.Stream.Parallel
456 , Streamly.Internal.Data.Stream.Parallel
450 , Streamly.Internal.Data.Stream.Ahead
457 , Streamly.Internal.Data.Stream.Ahead
451 , Streamly.Internal.Data.Stream.ZipAsync
458 , Streamly.Internal.Data.Stream.ZipAsync
@@ -561,6 +568,8 @@ library
561 , transformers >= 0.4 && < 0.7
568 , transformers >= 0.4 && < 0.7
562 , template-haskell >= 2.13 && < 2.20
569 , template-haskell >= 2.13 && < 2.20
563
570
571+ , monad-control
572+
564 -- The core streamly package
573 -- The core streamly package
565 , streamly-core == 0.1.0
574 , streamly-core == 0.1.0
566 , fusion-plugin-types >= 0.1 && < 0.2
575 , fusion-plugin-types >= 0.1 && < 0.2