on master help sign in
Add interleave and concatMapInterleave operations
commit2ca80e7503f3d6d9575c9151afcd4bd2feddf325 authorHarendra Kumar <harendra@composewell.com> dateMon 19 Sep 2022, 02:49 (4 years ago) parentf3ac108
6 files changed+714−278
src/Streamly/Internal/Data/Stream/Async.hs+117−1 src/Streamly/Internal/Data/Stream/Async/Channel.hs+58−5 src/Streamly/Internal/Data/Stream/Async/Channel/Core.hs → src/Streamly/Internal/Data/Stream/Async/Channel/Append.hs+3−271 src/Streamly/Internal/Data/Stream/Async/Channel/Interleave.hs+248 src/Streamly/Internal/Data/Stream/Async/Channel/Operations.hs+285 streamly.cabal+3−1
src/Streamly/Internal/Data/Stream/Async.hs+117−1
@@ -18,6 +18,10 @@
1818
1919module Streamly.Internal.Data.Stream.Async
2020 (
21+ -- * Imports
22+ -- $setup
23+
24+ -- * Types
2125 MonadAsync
2226
2327 -- * Configuration
@@ -47,13 +51,17 @@ module Streamly.Internal.Data.Stream.Async
4751 -- XXX experimental binary ops, move to parallel?
4852 , append
4953 , appendWith
54+ , interleave
55+ , interleaveWith
5056
5157 , apply
5258 , concatList
5359 , concat
5460 , concatWith
5561 , concatMap
62+ , concatMapInterleave
5663 , concatMapWith
64+ , concatMapInterleaveWith
5765 )
5866where
5967
@@ -66,9 +74,23 @@ import Streamly.Internal.Data.Stream.Async.Channel
6674import Prelude hiding (mapM, sequence, concat, concatMap)
6775
6876-- $setup
77+--
78+-- Imports for example snippets in this module.
79+--
6980-- >>> :m
81+-- >>> import Control.Concurrent (threadDelay)
82+-- >>> import qualified Streamly.Data.Array.Unboxed as Array
83+-- >>> import qualified Streamly.Data.Fold as Fold
84+-- >>> import qualified Streamly.Data.Parser as Parser
7085-- >>> import qualified Streamly.Data.Stream as Stream
7186-- >>> import qualified Streamly.Internal.Data.Stream.Async as Async
87+-- >>> import Prelude hiding (concatMap, concat)
88+-- >>> :{
89+-- delay n = do
90+-- threadDelay (n * 1000000) -- sleep for n seconds
91+-- putStrLn (show n ++ " sec") -- print "n sec"
92+-- return n -- IO Int
93+-- :}
7294
7395-------------------------------------------------------------------------------
7496-- Useful operations
@@ -104,6 +126,15 @@ appendWith modifier stream1 stream2 =
104126 $ appendWithK
105127 modifier (Stream.toStreamK stream1) (Stream.toStreamK stream2)
106128
129+-- | Like 'interleave' but with a Config modifier.
130+{-# INLINE interleaveWith #-}
131+interleaveWith :: MonadAsync m =>
132+ (Config -> Config) -> Stream m a -> Stream m a -> Stream m a
133+interleaveWith modifier stream1 stream2 =
134+ Stream.fromStreamK
135+ $ interleaveWithK
136+ modifier (Stream.toStreamK stream1) (Stream.toStreamK stream2)
137+
107138-- | Binary operation to evaluate two streams concurrently prioritizing the
108139-- left stream.
109140--
@@ -116,10 +147,29 @@ appendWith modifier stream1 stream2 =
116147--
117148-- >>> append = Async.appendWith id
118149--
150+-- The following code finishes in 4 seconds:
151+--
152+-- >>> stream1 = Stream.fromEffect (delay 4)
153+-- >>> stream2 = Stream.fromEffect (delay 2)
154+-- >>> Stream.fold Fold.toList $ stream1 `Async.append` stream2
155+-- 2 sec
156+-- 4 sec
157+-- [2,4]
158+--
119159{-# INLINE append #-}
120160append :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
121161append = appendWith id
122162
163+-- | Like 'append' but interleaves the streams fairly instead of prioritizing
164+-- the left stream. This schedules all streams in a round robin fashion over
165+-- limited number of threads.
166+--
167+-- >>> interleave = Async.interleaveWith id
168+--
169+{-# INLINE interleave #-}
170+interleave :: MonadAsync m => Stream m a -> Stream m a -> Stream m a
171+interleave = interleaveWith id
172+
123173-- concatMapWith modifier f stream = concatWith modifier $ fmap f stream
124174
125175-- | Like 'concatMap' but we can also specify the concurrent channel's
@@ -133,15 +183,81 @@ concatMapWith modifier f stream =
133183 $ concatMapWithK
134184 modifier (Stream.toStreamK . f) (Stream.toStreamK stream)
135185
186+-- | Like 'concatMapInterleave' but we can also specify the concurrent
187+-- channel's configuration parameters using Config modifiers.
188+--
189+{-# INLINE concatMapInterleaveWith #-}
190+concatMapInterleaveWith :: MonadAsync m =>
191+ (Config -> Config) -> (a -> Stream m b) -> Stream m a -> Stream m b
192+concatMapInterleaveWith modifier f stream =
193+ Stream.fromStreamK
194+ $ concatMapInterleaveWithK
195+ modifier (Stream.toStreamK . f) (Stream.toStreamK stream)
196+
136197-- | Map each element of the input to a stream and then concurrently evaluate
137−-- and concatenate the resulting streams.
198+-- and concatenate the resulting streams. Multiple streams may be evaluated
199+-- concurrently but earlier streams are perferred. Output from the streams are
200+-- used as they arrive.
138201--
139202-- >>> concatMap = Async.concatMapWith id
140203--
204+-- >>> f cfg xs = Stream.fold Fold.toList $ Async.concatMapWith cfg id $ Stream.fromList xs
205+--
206+-- The following streams finish in 4 seconds:
207+--
208+-- >>> stream1 = Stream.fromEffect (delay 4)
209+-- >>> stream2 = Stream.fromEffect (delay 2)
210+-- >>> stream3 = Stream.fromEffect (delay 1)
211+-- >>> f id [stream1, stream2, stream3]
212+-- 1 sec
213+-- 2 sec
214+-- 4 sec
215+-- [1,2,4]
216+--
217+-- Limiting threads to 2 schedules the third stream only after one of the first
218+-- two has finished, releasing a thread:
219+--
220+-- >>> f (Async.maxThreads 2) [stream1, stream2, stream3]
221+-- ...
222+-- [2,1,4]
223+--
224+-- When used with a Single thread it behaves like serial concatMap:
225+--
226+-- >>> f (Async.maxThreads 1) [stream1, stream2, stream3]
227+-- ...
228+-- [4,2,1]
229+--
230+-- >>> stream1 = Stream.fromList [1,2,3]
231+-- >>> stream2 = Stream.fromList [4,5,6]
232+-- >>> f (Async.maxThreads 1) [stream1, stream2]
233+-- [1,2,3,4,5,6]
234+--
141235{-# INLINE concatMap #-}
142236concatMap :: MonadAsync m => (a -> Stream m b) -> Stream m a -> Stream m b
143237concatMap = concatMapWith id
144238
239+-- | Map each element of the input to a stream and then concurrently evaluate
240+-- and interleave the resulting streams. Unlike 'concatMap' which prefers to
241+-- evaluate the earlier stream first, this schedules all streams in a round
242+-- robin fashion over the available threads.
243+--
244+-- >>> concatMapInterleave = Async.concatMapInterleaveWith id
245+--
246+-- When used with a single thread it behaves like serial interleaving:
247+--
248+-- >>> f cfg xs = Stream.fold Fold.toList $ Async.concatMapInterleaveWith cfg id $ Stream.fromList xs
249+--
250+-- >>> stream1 = Stream.fromList [1,2,3]
251+-- >>> stream2 = Stream.fromList [4,5,6]
252+-- >>> f (Async.maxThreads 1) [stream1, stream2]
253+-- [1,4,2,5,3,6]
254+--
255+-- /Works only on finite number of streams/
256+--
257+{-# INLINE concatMapInterleave #-}
258+concatMapInterleave :: MonadAsync m => (a -> Stream m b) -> Stream m a -> Stream m b
259+concatMapInterleave = concatMapInterleaveWith id
260+
145261-- | Like 'concat' but we can also specify the concurrent channel's
146262-- configuration parameters using Config modifiers.
147263--
src/Streamly/Internal/Data/Stream/Async/Channel.hs+58−5
@@ -23,7 +23,6 @@ module Streamly.Internal.Data.Stream.Async.Channel
2323 , constRate
2424
2525 -- * Operations
26− , newChannel
2726 , fromChannel
2827 , toChannel
2928 , withChannelK
@@ -35,19 +34,25 @@ module Streamly.Internal.Data.Stream.Async.Channel
3534 -- * Stream operations
3635 , evalWithK
3736 , appendWithK
38− , concatMapWithK
37+ , interleaveWithK
38+ , concatMapWithK -- appendMap
39+ , concatMapInterleaveWithK -- interleaveMap
3940 )
4041where
4142
4243import Control.Monad.IO.Class (MonadIO(liftIO))
4344import Streamly.Internal.Control.Concurrent (MonadAsync, askRunInIO)
45+import Streamly.Internal.Data.Stream.Async.Channel.Operations
46+ (fromChannel, toChannel, toChannelK)
4447import Streamly.Internal.Data.SVar.Type (adaptState)
4548
4649import qualified Streamly.Internal.Data.Stream as Stream
50+import qualified Streamly.Internal.Data.Stream.Async.Channel.Append as Append
51+import qualified Streamly.Internal.Data.Stream.Async.Channel.Interleave
52+ as Interleave
4753import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
4854
4955import Streamly.Internal.Data.Stream.Async.Channel.Type
50−import Streamly.Internal.Data.Stream.Async.Channel.Core
5156
5257-- | Allocate a channel and evaluate the stream using the channel and the
5358-- supplied evaluator function. The evaluator is run in a worker thread.
@@ -62,7 +67,22 @@ withChannelK evaluator modifier input = K.concatEffect action
6267 where
6368
6469 action = do
65− chan <- newChannel modifier
70+ chan <- Append.newChannel modifier
71+ toChannelK chan (evaluator chan input)
72+ return $ Stream.toStreamK $ fromChannel chan
73+
74+{-# INLINE withInterleaveChannelK #-}
75+withInterleaveChannelK :: MonadAsync m =>
76+ (Channel m b -> K.Stream m a -> K.Stream m b)
77+ -> (Config -> Config)
78+ -> K.Stream m a
79+ -> K.Stream m b
80+withInterleaveChannelK evaluator modifier input = K.concatEffect action
81+
82+ where
83+
84+ action = do
85+ chan <- Interleave.newChannel modifier
6686 toChannelK chan (evaluator chan input)
6787 return $ Stream.toStreamK $ fromChannel chan
6888
@@ -115,13 +135,35 @@ appendWithK modifier stream1 stream2 = K.concatEffect action
115135 where
116136
117137 action = do
118− chan <- newChannel modifier
138+ chan <- Append.newChannel modifier
139+ toChannelK chan stream2
140+ toChannelK chan stream1
141+ return $ Stream.toStreamK $ fromChannel chan
142+
143+-- | Create a new channel and add both the streams to it for async evaluation.
144+-- The output stream is the result of the evaluation.
145+{-# INLINE interleaveWithK #-}
146+interleaveWithK :: MonadAsync m =>
147+ (Config -> Config) -> K.Stream m a -> K.Stream m a -> K.Stream m a
148+interleaveWithK modifier stream1 stream2 = K.concatEffect action
149+
150+ where
151+
152+ action = do
153+ chan <- Interleave.newChannel modifier
119154 toChannelK chan stream2
120155 toChannelK chan stream1
121156 return $ Stream.toStreamK $ fromChannel chan
122157
123158-- | Evaluate the first stream in the current thread and add the second stream
124159-- to the supplied channel. This is to be used by a worker thread.
160+--
161+-- This can be used with concatMapWith:
162+--
163+-- @
164+-- concatMap = K.concatMapWith (_appendWithChanK chan) f stream
165+-- @
166+--
125167{-# INLINE _appendWithChanK #-}
126168_appendWithChanK :: MonadAsync m =>
127169 Channel m a -> K.Stream m a -> K.Stream m a -> K.Stream m a
@@ -192,3 +234,14 @@ concatMapWithChanK chan f stream =
192234concatMapWithK :: MonadAsync m =>
193235 (Config -> Config) -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
194236concatMapWithK modifier f = withChannelK (`concatMapWithChanK` f) modifier
237+
238+-- | Allocate a channel and use it to concurrently evaluate the streams
239+-- generated by the mapped function.
240+--
241+-- >> concatMapInterleaveWithK modifier f = withInterleaveChannelK (`concatMapWithChanK` f) modifier
242+--
243+{-# INLINE concatMapInterleaveWithK #-}
244+concatMapInterleaveWithK :: MonadAsync m =>
245+ (Config -> Config) -> (a -> K.Stream m b) -> K.Stream m a -> K.Stream m b
246+concatMapInterleaveWithK modifier f =
247+ withInterleaveChannelK (`concatMapWithChanK` f) modifier
src/Streamly/Internal/Data/Stream/Async/Channel/Core.hs → src/Streamly/Internal/Data/Stream/Async/Channel/Append.hsrenamed+3−271
@@ -1,14 +1,5 @@
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−
101-- |
11−-- Module : Streamly.Internal.Data.Stream.Async.Channel.Core
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Append
123-- Copyright : (c) 2017 Composewell Technologies
134-- License : BSD-3-Clause
145-- Maintainer : streamly@composewell.com
@@ -22,38 +13,23 @@
2213-- use INLINABLE and SPECIALIZE on it which makes it specialized but it is not
2314-- actually inlined.
2415
25−module Streamly.Internal.Data.Stream.Async.Channel.Core
16+module Streamly.Internal.Data.Stream.Async.Channel.Append
2617 (
2718 newChannel
28− , toChannel
29− , toChannelK
30− , fromChannel
31− , fromChannelK
3219 )
3320where
3421
35−#include "inline.hs"
36−
3722import Control.Concurrent (myThreadId)
3823import Control.Concurrent.MVar (newEmptyMVar)
39−import Control.Exception (fromException)
40−import Control.Monad (when)
41−import Control.Monad.Catch (throwM, MonadThrow)
4224import Control.Monad.IO.Class (MonadIO(liftIO))
4325import Control.Monad.Trans.Control (MonadBaseControl, restoreM)
44−import Data.IORef (IORef, newIORef, readIORef, mkWeakIORef, writeIORef)
45−import Data.Maybe (isNothing)
26+import Data.IORef (IORef, newIORef, readIORef)
4627import Streamly.Internal.Control.Concurrent
4728 (MonadRunInIO, MonadAsync, RunInIO(..), askRunInIO)
4829import Streamly.Internal.Data.Atomics
4930 (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)
5331
5432import 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
5733import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
5834
5935import Streamly.Internal.Data.Stream.Async.Channel.Consumer
@@ -61,18 +37,6 @@ import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
6137import Streamly.Internal.Data.Stream.Async.Channel.Type
6238import Streamly.Internal.Data.Stream.Async.Channel.Worker
6339
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−
7640------------------------------------------------------------------------------
7741-- Creating a channel
7842------------------------------------------------------------------------------
@@ -263,7 +227,6 @@ getLifoSVar mrun cfg = do
263227 -> (Channel m a -> m Bool)
264228 -> (Channel m a -> IO Bool)
265229 -> (IORef [(RunInIO m, K.Stream m a)]
266− -- -> State Stream m a
267230 -> Channel m a
268231 -> Maybe WorkerInfo
269232 -> m())
@@ -317,11 +280,6 @@ getLifoSVar mrun cfg = do
317280 workLoopLIFOLimited
318281 in return sv
319282
320−-- type Channel m a = SVar K.Stream m a
321−-- data Config = Config
322−
323−-- XXX openChannel?
324−
325283-- | Create a new async style concurrent stream evaluation channel. The monad
326284-- state used to run the stream actions is taken from the call site of
327285-- newChannel.
@@ -332,229 +290,3 @@ newChannel :: (MonadAsync m) =>
332290newChannel modifier = do
333291 mrun <- askRunInIO
334292 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/Interleave.hsnew file+248
@@ -0,0 +1,248 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Async.Channel.Interleave
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.Interleave
10+ (
11+ newChannel
12+ )
13+where
14+
15+#include "inline.hs"
16+
17+import Control.Concurrent (myThreadId)
18+import Control.Concurrent.MVar (newEmptyMVar)
19+import Control.Monad.IO.Class (MonadIO(liftIO))
20+import Control.Monad.Trans.Control (MonadBaseControl, restoreM)
21+import Data.Concurrent.Queue.MichaelScott (LinkedQueue, newQ, nullQ, tryPopR, pushL)
22+import Data.IORef (newIORef, readIORef)
23+import Streamly.Internal.Control.Concurrent
24+ (MonadRunInIO, MonadAsync, RunInIO(..), askRunInIO)
25+
26+import qualified Data.Set as Set
27+import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
28+
29+import Streamly.Internal.Data.Stream.Async.Channel.Consumer
30+import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
31+import Streamly.Internal.Data.Stream.Async.Channel.Type
32+import Streamly.Internal.Data.Stream.Async.Channel.Worker
33+
34+------------------------------------------------------------------------------
35+-- Creating a channel
36+------------------------------------------------------------------------------
37+
38+data WorkerStatus = Continue | Suspend
39+
40+-- XXX we can use the Ahead style sequence/heap mechanism to make the best
41+-- effort to always try to finish the streams on the left side of an expression
42+-- first as long as possible.
43+
44+{-# INLINE enqueueFIFO #-}
45+enqueueFIFO ::
46+ Channel m a
47+ -> LinkedQueue (RunInIO m, K.Stream m a)
48+ -> (RunInIO m, K.Stream m a)
49+ -> IO ()
50+enqueueFIFO sv q m = do
51+ pushL q m
52+ ringDoorBell sv
53+
54+-- XXX we can remove sv as it is derivable from st
55+
56+{-# INLINE workLoopFIFO #-}
57+workLoopFIFO
58+ :: MonadRunInIO m
59+ => LinkedQueue (RunInIO m, K.Stream m a)
60+ -> Channel m a
61+ -> Maybe WorkerInfo
62+ -> m ()
63+workLoopFIFO q sv winfo = run
64+
65+ where
66+
67+ stop = liftIO $ sendStop sv winfo
68+ run = do
69+ work <- liftIO $ tryPopR q
70+ case work of
71+ Nothing -> stop
72+ Just (RunInIO runin, m) -> do
73+ r <- liftIO
74+ $ runin
75+ $ K.foldStreamShared
76+ undefined yieldk single (return Continue) m
77+ res <- restoreM r
78+ case res of
79+ Continue -> run
80+ Suspend -> stop
81+
82+ single a = do
83+ res <- liftIO $ sendYield sv winfo (ChildYield a)
84+ return $ if res then Continue else Suspend
85+
86+ -- XXX in general we would like to yield "n" elements from a single stream
87+ -- before moving on to the next. Single element granularity could be too
88+ -- expensive in certain cases. Similarly, we can use time limit for
89+ -- yielding.
90+ yieldk a r = do
91+ res <- liftIO $ sendYield sv winfo (ChildYield a)
92+ runInIO <- askRunInIO
93+ liftIO $ enqueueFIFO sv q (runInIO, r)
94+ return $ if res then Continue else Suspend
95+
96+{-# INLINE workLoopFIFOLimited #-}
97+workLoopFIFOLimited
98+ :: forall m a. MonadRunInIO m
99+ => LinkedQueue (RunInIO m, K.Stream m a)
100+ -> Channel m a
101+ -> Maybe WorkerInfo
102+ -> m ()
103+workLoopFIFOLimited q sv winfo = run
104+
105+ where
106+
107+ stop = liftIO $ sendStop sv winfo
108+ incrContinue = liftIO (incrementYieldLimit sv) >> return Continue
109+ run = do
110+ work <- liftIO $ tryPopR q
111+ case work of
112+ Nothing -> stop
113+ Just (RunInIO runin, m) -> do
114+ yieldLimitOk <- liftIO $ decrementYieldLimit sv
115+ if yieldLimitOk
116+ then do
117+ r <- liftIO
118+ $ runin
119+ $ K.foldStreamShared
120+ undefined yieldk single incrContinue m
121+ res <- restoreM r
122+ case res of
123+ Continue -> run
124+ Suspend -> stop
125+ else liftIO $ do
126+ enqueueFIFO sv q (RunInIO runin, m)
127+ incrementYieldLimit sv
128+ sendStop sv winfo
129+
130+ single a = do
131+ res <- liftIO $ sendYield sv winfo (ChildYield a)
132+ return $ if res then Continue else Suspend
133+
134+ yieldk a r = do
135+ res <- liftIO $ sendYield sv winfo (ChildYield a)
136+ runInIO <- askRunInIO
137+ liftIO $ enqueueFIFO sv q (runInIO, r)
138+ yieldLimitOk <- liftIO $ decrementYieldLimit sv
139+ if res && yieldLimitOk
140+ then return Continue
141+ else liftIO $ do
142+ incrementYieldLimit sv
143+ return Suspend
144+
145+-------------------------------------------------------------------------------
146+-- SVar creation
147+-------------------------------------------------------------------------------
148+
149+-- XXX we have this function in this file because passing runStreamLIFO as a
150+-- function argument to this function results in a perf degradation of more
151+-- than 10%. Need to investigate what the root cause is.
152+-- Interestingly, the same thing does not make any difference for Ahead.
153+getFifoSVar :: forall m a. (MonadIO m, MonadBaseControl IO m) =>
154+ RunInIO m -> Config -> IO (Channel m a)
155+getFifoSVar mrun cfg = do
156+ outQ <- newIORef ([], 0)
157+ outQMv <- newEmptyMVar
158+ active <- newIORef 0
159+ wfw <- newIORef False
160+ running <- newIORef Set.empty
161+ q <- newQ
162+ yl <- case getYieldLimit cfg of
163+ Nothing -> return Nothing
164+ Just x -> Just <$> newIORef x
165+ rateInfo <- getYieldRateInfo cfg
166+
167+ stats <- newSVarStats
168+ tid <- myThreadId
169+
170+ let isWorkFinished _ = nullQ q
171+ let isWorkFinishedLimited sv = do
172+ yieldsDone <-
173+ case remainingWork sv of
174+ Just ref -> do
175+ n <- readIORef ref
176+ return (n <= 0)
177+ Nothing -> return False
178+ qEmpty <- nullQ q
179+ return $ qEmpty || yieldsDone
180+
181+ let getSVar :: Channel m a
182+ -> (Channel m a -> m [ChildEvent a])
183+ -> (Channel m a -> m Bool)
184+ -> (Channel m a -> IO Bool)
185+ -> (LinkedQueue (RunInIO m, K.Stream m a)
186+ -> Channel m a
187+ -> Maybe WorkerInfo
188+ -> m())
189+ -> Channel m a
190+ getSVar sv readOutput postProc workDone wloop = Channel
191+ { outputQueue = outQ
192+ , remainingWork = yl
193+ , maxBufferLimit = getMaxBuffer cfg
194+ , maxWorkerLimit = min (getMaxThreads cfg) (getMaxBuffer cfg)
195+ , yieldRateInfo = rateInfo
196+ , outputDoorBell = outQMv
197+ , readOutputQ = readOutput sv
198+ , postProcess = postProc sv
199+ , workerThreads = running
200+ , workLoop = wloop q sv
201+ , enqueue = enqueueFIFO sv q
202+ , isWorkDone = workDone sv
203+ , isQueueDone = workDone sv
204+ , needDoorBell = wfw
205+ , svarMrun = mrun
206+ , workerCount = active
207+ , accountThread = delThread sv
208+ , workerStopMVar = undefined
209+ , svarRef = Nothing
210+ , svarInspectMode = getInspectMode cfg
211+ , svarCreator = tid
212+ , svarStats = stats
213+ }
214+
215+ let sv =
216+ case getStreamRate cfg of
217+ Nothing ->
218+ case getYieldLimit cfg of
219+ Nothing -> getSVar sv readOutputQBounded
220+ postProcessBounded
221+ isWorkFinished
222+ workLoopFIFO
223+ Just _ -> getSVar sv readOutputQBounded
224+ postProcessBounded
225+ isWorkFinishedLimited
226+ workLoopFIFOLimited
227+ Just _ ->
228+ case getYieldLimit cfg of
229+ Nothing -> getSVar sv readOutputQPaced
230+ postProcessPaced
231+ isWorkFinished
232+ workLoopFIFO
233+ Just _ -> getSVar sv readOutputQPaced
234+ postProcessPaced
235+ isWorkFinishedLimited
236+ workLoopFIFOLimited
237+ in return sv
238+
239+-- | Create a new async style concurrent stream evaluation channel. The monad
240+-- state used to run the stream actions is taken from the call site of
241+-- newChannel.
242+{-# INLINABLE newChannel #-}
243+{-# SPECIALIZE newChannel :: (Config -> Config) -> IO (Channel IO a) #-}
244+newChannel :: (MonadAsync m) =>
245+ (Config -> Config) -> m (Channel m a)
246+newChannel modifier = do
247+ mrun <- askRunInIO
248+ liftIO $ getFifoSVar mrun (modifier defaultConfig)
src/Streamly/Internal/Data/Stream/Async/Channel/Operations.hsnew file+285
@@ -0,0 +1,285 @@
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.Operations
12+-- Copyright : (c) 2017 Composewell Technologies
13+-- License : BSD-3-Clause
14+-- Maintainer : streamly@composewell.com
15+-- Stability : experimental
16+-- Portability : GHC
17+
18+module Streamly.Internal.Data.Stream.Async.Channel.Operations
19+ (
20+ toChannel
21+ , toChannelK
22+ , fromChannel
23+ , fromChannelK
24+ )
25+where
26+
27+#include "inline.hs"
28+
29+import Control.Exception (fromException)
30+import Control.Monad (when)
31+import Control.Monad.Catch (throwM, MonadThrow)
32+import Control.Monad.IO.Class (MonadIO(liftIO))
33+import Control.Monad.Trans.Control (MonadBaseControl)
34+import Data.IORef (newIORef, readIORef, mkWeakIORef, writeIORef)
35+import Data.Maybe (isNothing)
36+import Streamly.Internal.Control.Concurrent (MonadAsync, askRunInIO)
37+import Streamly.Internal.Data.Stream.Type (Stream)
38+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
39+import System.Mem (performMajorGC)
40+
41+import qualified Streamly.Internal.Data.Stream as Stream
42+import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
43+import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
44+
45+import Streamly.Internal.Data.Stream.Async.Channel.Consumer
46+import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
47+import Streamly.Internal.Data.Stream.Async.Channel.Type
48+
49+import Prelude hiding (map, concat, concatMap)
50+
51+#if __GLASGOW_HASKELL__ < 810
52+#ifdef INSPECTION
53+import Control.Exception (Exception)
54+import Control.Monad.Catch (MonadThrow)
55+import Control.Monad.Trans.Control (MonadBaseControl)
56+import Data.Typeable (Typeable)
57+import Test.Inspection (inspect, hasNoTypeClassesExcept)
58+#endif
59+#endif
60+
61+------------------------------------------------------------------------------
62+-- Generating streams from a channel
63+------------------------------------------------------------------------------
64+
65+-- $concurrentEval
66+--
67+-- Usually a channel is used to concurrently evaluate multiple actions in a
68+-- stream using many worker threads that push the results to the channel and a
69+-- single puller that pulls them from channel generating the evaluated stream.
70+--
71+-- @
72+-- input stream
73+-- |
74+-- <-----------------|<--------worker
75+-- | exceptions |
76+-- output stream <---Channel<------worker
77+-- |
78+-- |<--------worker
79+--
80+-- @
81+--
82+-- The puller itself schedules the worker threads based on demand.
83+-- Exceptions are propagated from the worker threads to the puller.
84+
85+-------------------------------------------------------------------------------
86+-- Write a stream to a channel
87+-------------------------------------------------------------------------------
88+
89+-- XXX Should be a Fold, singleton API could be called joinChannel, or the fold
90+-- can be called joinChannel.
91+
92+-- | Write a stream to an 'SVar' in a non-blocking manner. The stream can then
93+-- be read back from the SVar using 'fromSVar'.
94+toChannelK :: (MonadIO m, MonadBaseControl IO m) =>
95+ Channel m a -> K.Stream m a -> m ()
96+toChannelK sv m = do
97+ runIn <- askRunInIO
98+ liftIO $ enqueue sv (runIn, m)
99+
100+-- INLINE for fromStreamK/toStreamK fusion
101+
102+-- | Send a stream to a given channel for concurrent evaluation.
103+{-# INLINE toChannel #-}
104+toChannel :: (MonadIO m, MonadBaseControl IO m) =>
105+ Channel m a -> Stream m a -> m ()
106+toChannel chan = toChannelK chan . Stream.toStreamK
107+
108+{-
109+-- | Send a stream of streams to a concurrent channel for evaluation.
110+{-# INLINE joinChannel #-}
111+joinChannel :: Channel m a -> Fold m (Stream m a) ()
112+joinChannel = undefined
113+-}
114+
115+-------------------------------------------------------------------------------
116+-- Read a stream from a channel
117+-------------------------------------------------------------------------------
118+
119+-- | Pull a stream from an SVar.
120+{-# NOINLINE fromChannelRaw #-}
121+fromChannelRaw :: (MonadIO m, MonadThrow m) => Channel m a -> K.Stream m a
122+fromChannelRaw sv = K.MkStream $ \st yld sng stp -> do
123+ list <- readOutputQ sv
124+ -- Reversing the output is important to guarantee that we process the
125+ -- outputs in the same order as they were generated by the constituent
126+ -- streams.
127+ K.foldStream st yld sng stp $ processEvents $ reverse list
128+
129+ where
130+
131+ allDone stp = do
132+ when (svarInspectMode sv) $ do
133+ t <- liftIO $ getTime Monotonic
134+ liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
135+ liftIO $ printSVar sv "SVar Done"
136+ stp
137+
138+ {-# INLINE processEvents #-}
139+ processEvents [] = K.MkStream $ \st yld sng stp -> do
140+ done <- postProcess sv
141+ if done
142+ then allDone stp
143+ else K.foldStream st yld sng stp $ fromChannelRaw sv
144+
145+ processEvents (ev : es) = K.MkStream $ \st yld sng stp -> do
146+ let rest = processEvents es
147+ case ev of
148+ ChildYield a -> yld a rest
149+ ChildStop tid e -> do
150+ accountThread sv tid
151+ case e of
152+ Nothing -> K.foldStream st yld sng stp rest
153+ Just ex ->
154+ case fromException ex of
155+ Just ThreadAbort ->
156+ K.foldStream st yld sng stp rest
157+ Nothing -> liftIO (cleanupSVar sv) >> throwM ex
158+
159+#if __GLASGOW_HASKELL__ < 810
160+#ifdef INSPECTION
161+-- Use of GHC constraint tuple (GHC.Classes.(%,,%)) in fromStreamVar leads to
162+-- space leak because the tuple gets allocated in every recursive call and each
163+-- allocation holds on to the previous allocation. This test is to make sure
164+-- that we do not use the constraint tuple type class.
165+--
166+inspect $ hasNoTypeClassesExcept 'fromStreamVar
167+ [ ''Monad
168+ , ''Applicative
169+ , ''MonadThrow
170+ , ''Exception
171+ , ''MonadIO
172+ , ''MonadBaseControl
173+ , ''Typeable
174+ , ''Functor
175+ ]
176+#endif
177+#endif
178+
179+-- XXX fromChannel Should not be called multiple times, we can add a
180+-- safeguard for that. Or we can replicate the stream so that we can distribute
181+-- it to multiple consumers. or should we use an explicit dupChannel for that?
182+
183+{-# INLINE fromChannelK #-}
184+fromChannelK :: MonadAsync m => Channel m a -> K.Stream m a
185+fromChannelK sv =
186+ K.mkStream $ \st yld sng stp -> do
187+ ref <- liftIO $ newIORef ()
188+ _ <- liftIO $ mkWeakIORef ref hook
189+
190+ startChannel sv
191+ -- We pass a copy of sv to fromStreamVar, so that we know that it has
192+ -- no other references, when that copy gets garbage collected "ref"
193+ -- will get garbage collected and our hook will be called.
194+ K.foldStreamShared st yld sng stp $
195+ fromChannelRaw sv{svarRef = Just ref}
196+ where
197+
198+ hook = do
199+ when (svarInspectMode sv) $ do
200+ r <- liftIO $ readIORef (svarStopTime (svarStats sv))
201+ when (isNothing r) $
202+ printSVar sv "SVar Garbage Collected"
203+ cleanupSVar sv
204+ -- If there are any SVars referenced by this SVar a GC will prompt
205+ -- them to be cleaned up quickly.
206+ when (svarInspectMode sv) performMajorGC
207+
208+-- | Generate a stream of results from concurrent evaluations from a channel.
209+-- Evaluation of the channel does not start until this API is called. This API
210+-- must not be called more than once on a channel. It kicks off evaluation of
211+-- the channel by dispatching concurrent workers and ensures that as long there
212+-- is work queued on the channel workers are dispatched proportional to the
213+-- demand by the consumer.
214+--
215+{-# INLINE fromChannel #-}
216+fromChannel :: MonadAsync m => Channel m a -> Stream m a
217+fromChannel = Stream.fromStreamK . fromChannelK
218+
219+data FromSVarState t m a =
220+ FromSVarInit
221+ | FromSVarRead (Channel m a)
222+ | FromSVarLoop (Channel m a) [ChildEvent a]
223+ | FromSVarDone (Channel m a)
224+
225+-- | Like 'fromSVar' but generates a StreamD style stream instead of CPS.
226+--
227+{-# INLINE_NORMAL _fromChannelD #-}
228+_fromChannelD :: (MonadIO m, MonadThrow m) => Channel m a -> D.Stream m a
229+_fromChannelD svar = D.Stream step FromSVarInit
230+ where
231+
232+ {-# INLINE_LATE step #-}
233+ step _ FromSVarInit = do
234+ ref <- liftIO $ newIORef ()
235+ _ <- liftIO $ mkWeakIORef ref hook
236+ -- when this copy of svar gets garbage collected "ref" will get
237+ -- garbage collected and our GC hook will be called.
238+ let sv = svar{svarRef = Just ref}
239+ return $ D.Skip (FromSVarRead sv)
240+
241+ where
242+
243+ {-# NOINLINE hook #-}
244+ hook = do
245+ when (svarInspectMode svar) $ do
246+ r <- liftIO $ readIORef (svarStopTime (svarStats svar))
247+ when (isNothing r) $
248+ printSVar svar "SVar Garbage Collected"
249+ cleanupSVar svar
250+ -- If there are any SVars referenced by this SVar a GC will prompt
251+ -- them to be cleaned up quickly.
252+ when (svarInspectMode svar) performMajorGC
253+
254+ step _ (FromSVarRead sv) = do
255+ list <- readOutputQ sv
256+ -- Reversing the output is important to guarantee that we process the
257+ -- outputs in the same order as they were generated by the constituent
258+ -- streams.
259+ return $ D.Skip $ FromSVarLoop sv (Prelude.reverse list)
260+
261+ step _ (FromSVarLoop sv []) = do
262+ done <- postProcess sv
263+ return $ D.Skip $ if done
264+ then FromSVarDone sv
265+ else FromSVarRead sv
266+
267+ step _ (FromSVarLoop sv (ev : es)) = do
268+ case ev of
269+ ChildYield a -> return $ D.Yield a (FromSVarLoop sv es)
270+ ChildStop tid e -> do
271+ accountThread sv tid
272+ case e of
273+ Nothing -> return $ D.Skip (FromSVarLoop sv es)
274+ Just ex ->
275+ case fromException ex of
276+ Just ThreadAbort ->
277+ return $ D.Skip (FromSVarLoop sv es)
278+ Nothing -> liftIO (cleanupSVar sv) >> throwM ex
279+
280+ step _ (FromSVarDone sv) = do
281+ when (svarInspectMode sv) $ do
282+ t <- liftIO $ getTime Monotonic
283+ liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
284+ liftIO $ printSVar sv "SVar Done"
285+ return D.Stop
streamly.cabal+3−1
@@ -451,7 +451,9 @@ library
451451 , Streamly.Internal.Data.Stream.Async.Channel.Worker
452452 , Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
453453 , Streamly.Internal.Data.Stream.Async.Channel.Consumer
454− , Streamly.Internal.Data.Stream.Async.Channel.Core
454+ , Streamly.Internal.Data.Stream.Async.Channel.Append
455+ , Streamly.Internal.Data.Stream.Async.Channel.Interleave
456+ , Streamly.Internal.Data.Stream.Async.Channel.Operations
455457 , Streamly.Internal.Data.Stream.Async.Channel
456458 , Streamly.Internal.Data.Stream.Async
457459 , Streamly.Internal.Data.Stream.Parallel