on master help sign in
Move common parts of Channel out of Async module
commit51fb1b8c2984e86c1d7551ff40e91f32cab49be5 authorHarendra Kumar <harendra@composewell.com> dateThu 22 Sep 2022, 00:02 (4 years ago) parent09ed99d
11 files changed+1236−1071
src/Streamly/Internal/Data/Stream/Async/Channel.hs+3−2 src/Streamly/Internal/Data/Stream/Async/Channel/Append.hs+21−21 src/Streamly/Internal/Data/Stream/Async/Channel/Consumer.hs+17−40 src/Streamly/Internal/Data/Stream/Async/Channel/Dispatcher.hs+28−337 src/Streamly/Internal/Data/Stream/Async/Channel/Interleave.hs+21−21 src/Streamly/Internal/Data/Stream/Async/Channel/Operations.hs+14−9 src/Streamly/Internal/Data/Stream/Async/Channel/Type.hs+48−506 src/Streamly/Internal/Data/Stream/Channel/Dispatcher.hs+297 src/Streamly/Internal/Data/Stream/Channel/Types.hs+710 src/Streamly/Internal/Data/Stream/Async/Channel/Worker.hs → src/Streamly/Internal/Data/Stream/Channel/Worker.hs+73−134 streamly.cabal+4−1
src/Streamly/Internal/Data/Stream/Async/Channel.hs+3−2
@@ -43,7 +43,7 @@ where
4343import Control.Monad.IO.Class (MonadIO(liftIO))
4444import Streamly.Internal.Control.Concurrent (MonadAsync, askRunInIO)
4545import Streamly.Internal.Data.Stream.Async.Channel.Operations
46− (fromChannel, toChannel, toChannelK)
46+ (fromChannel, fromChannelK, toChannel, toChannelK)
4747import Streamly.Internal.Data.SVar.Type (adaptState)
4848
4949import qualified Streamly.Internal.Data.Stream as Stream
@@ -53,6 +53,7 @@ import qualified Streamly.Internal.Data.Stream.Async.Channel.Interleave
5353import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
5454
5555import Streamly.Internal.Data.Stream.Async.Channel.Type
56+import Streamly.Internal.Data.Stream.Channel.Types
5657
5758-- | Allocate a channel and evaluate the stream using the channel and the
5859-- supplied evaluator function. The evaluator is run in a worker thread.
@@ -69,7 +70,7 @@ withChannelK evaluator modifier input = K.concatEffect action
6970 action = do
7071 chan <- Append.newChannel modifier
7172 toChannelK chan (evaluator chan input)
72− return $ Stream.toStreamK $ fromChannel chan
73+ return $ fromChannelK chan
7374
7475{-# INLINE withInterleaveChannelK #-}
7576withInterleaveChannelK :: MonadAsync m =>
src/Streamly/Internal/Data/Stream/Async/Channel/Append.hs+21−21
@@ -28,14 +28,14 @@ import Streamly.Internal.Control.Concurrent
2828 (MonadRunInIO, MonadAsync, RunInIO(..), askRunInIO)
2929import Streamly.Internal.Data.Atomics
3030 (atomicModifyIORefCAS, atomicModifyIORefCAS_)
31+import Streamly.Internal.Data.Stream.Channel.Dispatcher (delThread)
3132
3233import qualified Data.Set as Set
3334import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
3435
3536import Streamly.Internal.Data.Stream.Async.Channel.Consumer
36−import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
3737import Streamly.Internal.Data.Stream.Async.Channel.Type
38−import Streamly.Internal.Data.Stream.Async.Channel.Worker
38+import Streamly.Internal.Data.Stream.Channel.Types
3939
4040------------------------------------------------------------------------------
4141-- Creating a channel
@@ -53,7 +53,7 @@ enqueueLIFO ::
5353 -> IO ()
5454enqueueLIFO sv q m = do
5555 atomicModifyIORefCAS_ q $ \ms -> m : ms
56− ringDoorBell sv
56+ ringDoorBell (needDoorBell sv) (outputDoorBell sv)
5757
5858data WorkerStatus = Continue | Suspend
5959
@@ -69,11 +69,10 @@ workLoopLIFO q sv winfo = run
6969
7070 where
7171
72− stop = liftIO $ sendStop sv winfo
7372 run = do
7473 work <- dequeue
7574 case work of
76− Nothing -> stop
75+ Nothing -> liftIO $ stop sv winfo
7776 Just (RunInIO runin, m) -> do
7877 -- XXX when we finish we need to send the monadic state back to
7978 -- the parent so that the state can be merged back. We capture
@@ -92,14 +91,14 @@ workLoopLIFO q sv winfo = run
9291 res <- restoreM r
9392 case res of
9493 Continue -> run
95− Suspend -> stop
94+ Suspend -> liftIO $ stop sv winfo
9695
9796 single a = do
98− res <- liftIO $ sendYield sv winfo (ChildYield a)
97+ res <- liftIO $ yield sv winfo a
9998 return $ if res then Continue else Suspend
10099
101100 yieldk a r = do
102− res <- liftIO $ sendYield sv winfo (ChildYield a)
101+ res <- liftIO $ yield sv winfo a
103102 if res
104103 then K.foldStreamShared undefined yieldk single (return Continue) r
105104 else do
@@ -128,12 +127,13 @@ workLoopLIFOLimited q sv winfo = run
128127
129128 where
130129
131− incrContinue = liftIO (incrementYieldLimit sv) >> return Continue
132− stop = liftIO $ sendStop sv winfo
130+ incrContinue =
131+ liftIO (incrementYieldLimit (remainingWork sv)) >> return Continue
132+
133133 run = do
134134 work <- dequeue
135135 case work of
136− Nothing -> stop
136+ Nothing -> liftIO $ stop sv winfo
137137 Just (RunInIO runin, m) -> do
138138 -- XXX This is just a best effort minimization of concurrency
139139 -- to the yield limit. If the stream is made of concurrent
@@ -141,7 +141,7 @@ workLoopLIFOLimited q sv winfo = run
141141 -- streams before executing the action. This can be done
142142 -- though, by sharing the yield limit ref with downstream
143143 -- actions via state passing. Just a todo.
144− yieldLimitOk <- liftIO $ decrementYieldLimit sv
144+ yieldLimitOk <- liftIO $ decrementYieldLimit (remainingWork sv)
145145 if yieldLimitOk
146146 then do
147147 r <- liftIO $ runin $
@@ -154,28 +154,28 @@ workLoopLIFOLimited q sv winfo = run
154154 res <- restoreM r
155155 case res of
156156 Continue -> run
157− Suspend -> stop
157+ Suspend -> liftIO $ stop sv winfo
158158 -- Avoid any side effects, undo the yield limit decrement if we
159159 -- never yielded anything.
160160 else liftIO $ do
161161 enqueueLIFO sv q (RunInIO runin, m)
162− incrementYieldLimit sv
163− sendStop sv winfo
162+ incrementYieldLimit (remainingWork sv)
163+ stop sv winfo
164164
165165 single a = do
166− res <- liftIO $ sendYield sv winfo (ChildYield a)
166+ res <- liftIO $ yield sv winfo a
167167 return $ if res then Continue else Suspend
168168
169169 -- XXX can we pass on the yield limit downstream to limit the concurrency
170170 -- of constituent streams.
171171 yieldk a r = do
172− res <- liftIO $ sendYield sv winfo (ChildYield a)
173− yieldLimitOk <- liftIO $ decrementYieldLimit sv
172+ res <- liftIO $ yield sv winfo a
173+ yieldLimitOk <- liftIO $ decrementYieldLimit (remainingWork sv)
174174 if res && yieldLimitOk
175175 then K.foldStreamShared undefined yieldk single incrContinue r
176176 else do
177177 runInIO <- askRunInIO
178− liftIO $ incrementYieldLimit sv
178+ liftIO $ incrementYieldLimit (remainingWork sv)
179179 liftIO $ enqueueLIFO sv q (runInIO, r)
180180 return Suspend
181181
@@ -205,7 +205,7 @@ getLifoSVar mrun cfg = do
205205 yl <- case getYieldLimit cfg of
206206 Nothing -> return Nothing
207207 Just x -> Just <$> newIORef x
208− rateInfo <- getYieldRateInfo cfg
208+ rateInfo <- newRateInfo cfg
209209
210210 stats <- newSVarStats
211211 tid <- myThreadId
@@ -248,7 +248,7 @@ getLifoSVar mrun cfg = do
248248 , needDoorBell = wfw
249249 , svarMrun = mrun
250250 , workerCount = active
251− , accountThread = delThread sv
251+ , accountThread = delThread running
252252 , workerStopMVar = undefined
253253 , svarRef = Nothing
254254 , svarInspectMode = getInspectMode cfg
src/Streamly/Internal/Data/Stream/Async/Channel/Consumer.hs+17−40
@@ -9,52 +9,39 @@
99module Streamly.Internal.Data.Stream.Async.Channel.Consumer
1010 (
1111 -- * Read Output
12− readOutputQBasic
13− , readOutputQRaw
14− , readOutputQPaced
12+ readOutputQPaced
1513 , readOutputQBounded
1614
1715 -- * Postprocess Hook After Reading
1816 , postProcessPaced
1917 , postProcessBounded
20−
21− -- * Release Resources
22− , cleanupSVar
2318 )
2419where
2520
2621import Control.Monad.Trans.Control (MonadBaseControl)
27−import Control.Concurrent (throwTo)
2822import Control.Monad (when, void)
2923import Control.Monad.IO.Class (MonadIO(liftIO))
30−import Data.IORef (IORef, readIORef, writeIORef)
31−import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS)
24+import Data.IORef (readIORef)
3225
33−import Streamly.Internal.Data.Stream.Async.Channel.Type
3426import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
27+import Streamly.Internal.Data.Stream.Async.Channel.Type
28+import Streamly.Internal.Data.Stream.Channel.Dispatcher
29+import Streamly.Internal.Data.Stream.Channel.Types
3530
3631-------------------------------------------------------------------------------
3732-- Reading from the workers' output queue/buffer
3833-------------------------------------------------------------------------------
3934
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)
35+{-# INLINE readOutputQChan #-}
36+readOutputQChan :: Channel m a -> IO ([ChildEvent a], Int)
37+readOutputQChan sv = do
38+ let ss = if svarInspectMode sv then Just (svarStats sv) else Nothing
39+ in readOutputQRaw (outputQueue sv) ss
5340
5441readOutputQBounded :: (MonadIO m, MonadBaseControl IO m) =>
5542 Channel m a -> m [ChildEvent a]
5643readOutputQBounded sv = do
57− (list, len) <- liftIO $ readOutputQRaw sv
44+ (list, len) <- liftIO $ readOutputQChan sv
5845 -- When there is no output seen we dispatch more workers to help
5946 -- out if there is work pending in the work queue.
6047 if len <= 0
@@ -78,12 +65,12 @@ readOutputQBounded sv = do
7865 {-# INLINE blockingRead #-}
7966 blockingRead = do
8067 sendWorkerWait sendWorkerDelay (dispatchWorker 0) sv
81− liftIO (fst `fmap` readOutputQRaw sv)
68+ liftIO (fst `fmap` readOutputQChan sv)
8269
8370readOutputQPaced :: (MonadIO m, MonadBaseControl IO m) =>
8471 Channel m a -> m [ChildEvent a]
8572readOutputQPaced sv = do
86− (list, len) <- liftIO $ readOutputQRaw sv
73+ (list, len) <- liftIO $ readOutputQChan sv
8774 if len <= 0
8875 then blockingRead
8976 else do
@@ -97,11 +84,11 @@ readOutputQPaced sv = do
9784 {-# INLINE blockingRead #-}
9885 blockingRead = do
9986 sendWorkerWait sendWorkerDelayPaced dispatchWorkerPaced sv
100− liftIO (fst `fmap` readOutputQRaw sv)
87+ liftIO (fst `fmap` readOutputQChan sv)
10188
10289postProcessPaced :: (MonadIO m, MonadBaseControl IO m) => Channel m a -> m Bool
10390postProcessPaced sv = do
104− workersDone <- allThreadsDone sv
91+ workersDone <- allThreadsDone (workerThreads sv)
10592 -- XXX If during consumption we figure out we are getting delayed then we
10693 -- should trigger dispatch there as well. We should try to check on the
10794 -- workers after consuming every n item from the buffer?
@@ -113,7 +100,7 @@ postProcessPaced sv = do
113100 -- Note that we need to guarantee a worker since the work is not
114101 -- finished, therefore we cannot just rely on dispatchWorkerPaced
115102 -- which may or may not send a worker.
116− noWorker <- allThreadsDone sv
103+ noWorker <- allThreadsDone (workerThreads sv)
117104 when noWorker $ pushWorker 0 sv
118105 return r
119106 else return False
@@ -121,7 +108,7 @@ postProcessPaced sv = do
121108postProcessBounded :: (MonadIO m, MonadBaseControl IO m) =>
122109 Channel m a -> m Bool
123110postProcessBounded sv = do
124− workersDone <- allThreadsDone sv
111+ workersDone <- allThreadsDone (workerThreads sv)
125112 -- There may still be work pending even if there are no workers pending
126113 -- because all the workers may return if the outputQueue becomes full. In
127114 -- that case send off a worker to kickstart the work again.
@@ -141,13 +128,3 @@ postProcessBounded sv = do
141128 -- void $ dispatchWorker sv
142129 return r
143130 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/Dispatcher.hs+28−337
@@ -9,22 +9,8 @@
99--
1010module Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
1111 (
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−
2512 -- * Dispatching
26− , recordMaxWorkers
27− , pushWorker
13+ pushWorker
2814 , dispatchWorker
2915 , dispatchWorkerPaced
3016 , sendWorkerWait
@@ -35,338 +21,33 @@ module Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
3521where
3622
3723import 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(..))
24+import Control.Concurrent (takeMVar, threadDelay)
25+import Control.Exception (assert)
4326import Control.Monad (when, void)
4427import Control.Monad.IO.Class (MonadIO(liftIO))
4528import Data.Maybe (fromJust, fromMaybe)
46−import Data.IORef (IORef, modifyIORef, newIORef, readIORef, writeIORef)
29+import Data.IORef (modifyIORef, newIORef, readIORef, writeIORef)
4730import Streamly.Internal.Control.ForkLifted (doFork)
48−import Streamly.Internal.Data.Atomics
49− (atomicModifyIORefCAS, atomicModifyIORefCAS_, writeBarrier,
50− storeLoadBarrier)
31+import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS_, storeLoadBarrier)
5132import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
5233import Streamly.Internal.Data.Time.Units
53− (AbsTime, NanoSecond64(..), MicroSecond64(..), diffAbsTime64,
54− fromRelTime64, toRelTime64, showNanoSecond64, showRelTime64)
55−import System.IO (hPutStrLn, stderr)
56−
57−-- import qualified Data.Heap as H
58−import qualified Data.Set as S
34+ (MicroSecond64(..), diffAbsTime64, fromRelTime64, toRelTime64)
5935
6036import Streamly.Internal.Data.Stream.Async.Channel.Type
61−import Streamly.Internal.Data.Stream.Async.Channel.Worker
62−
63−-------------------------------------------------------------------------------
64−-- Worker latency data processing
65−-------------------------------------------------------------------------------
66−
67−-- Every once in a while workers update the latencies and check the yield rate.
68−-- They return if we are above the expected yield rate. If we check too often
69−-- it may impact performance, if we check less often we may have a stale
70−-- picture. We update every minThreadDelay but we translate that into a yield
71−-- count based on latency so that the checking overhead is little.
72−--
73−-- XXX use a generation count to indicate that the value is updated. If the
74−-- value is updated an existing worker must check it again on the next yield.
75−-- Otherwise it is possible that we may keep updating it and because of the mod
76−-- worker keeps skipping it.
77−updateWorkerPollingInterval :: YieldRateInfo -> NanoSecond64 -> IO ()
78−updateWorkerPollingInterval yinfo latency = do
79− let periodRef = workerPollingInterval yinfo
80− cnt = max 1 $ minThreadDelay `div` latency
81− period = min cnt (fromIntegral magicMaxBuffer)
82−
83− writeIORef periodRef (fromIntegral period)
84−
85−{-# INLINE recordMinMaxLatency #-}
86−recordMinMaxLatency :: Channel m a -> NanoSecond64 -> IO ()
87−recordMinMaxLatency sv new = do
88− let ss = svarStats sv
89− minLat <- readIORef (minWorkerLatency ss)
90− when (new < minLat || minLat == 0) $
91− writeIORef (minWorkerLatency ss) new
92−
93− maxLat <- readIORef (maxWorkerLatency ss)
94− when (new > maxLat) $ writeIORef (maxWorkerLatency ss) new
95−
96−recordAvgLatency :: Channel m a -> (Count, NanoSecond64) -> IO ()
97−recordAvgLatency sv (count, time) = do
98− let ss = svarStats sv
99− modifyIORef (avgWorkerLatency ss) $
100− \(cnt, t) -> (cnt + count, t + time)
101−
102−-- Pour the pending latency stats into a collection bucket
103−{-# INLINE collectWorkerPendingLatency #-}
104−collectWorkerPendingLatency
105− :: IORef (Count, Count, NanoSecond64)
106− -> IORef (Count, Count, NanoSecond64)
107− -> IO (Count, Maybe (Count, NanoSecond64))
108−collectWorkerPendingLatency cur col = do
109− (fcount, count, time) <- atomicModifyIORefCAS cur $ \v -> ((0,0,0), v)
110−
111− (fcnt, cnt, t) <- readIORef col
112− let totalCount = fcnt + fcount
113− latCount = cnt + count
114− latTime = t + time
115− writeIORef col (totalCount, latCount, latTime)
116−
117− assert (latCount == 0 || latTime /= 0) (return ())
118− let latPair =
119− if latCount > 0 && latTime > 0
120− then Just (latCount, latTime)
121− else Nothing
122− return (totalCount, latPair)
123−
124−{-# INLINE shouldUseCollectedBatch #-}
125−shouldUseCollectedBatch
126− :: Count
127− -> NanoSecond64
128− -> NanoSecond64
129− -> NanoSecond64
130− -> Bool
131−shouldUseCollectedBatch collectedYields collectedTime newLat prevLat =
132− let r = fromIntegral newLat / fromIntegral prevLat :: Double
133− in (collectedYields > fromIntegral magicMaxBuffer)
134− || (collectedTime > minThreadDelay)
135− || (prevLat > 0 && (r > 2 || r < 0.5))
136− || (prevLat == 0)
137−
138−-- Returns a triple, (1) yield count since last collection, (2) the base time
139−-- when we started counting, (3) average latency in the last measurement
140−-- period. The former two are used for accurate measurement of the going rate
141−-- whereas the average is used for future estimates e.g. how many workers
142−-- should be maintained to maintain the rate.
143−-- CAUTION! keep it in sync with getWorkerLatency
144−collectLatency :: Channel m a
145− -> YieldRateInfo
146− -> Bool
147− -> IO (Count, AbsTime, NanoSecond64)
148−collectLatency sv yinfo drain = do
149− let cur = workerPendingLatency yinfo
150− col = workerCollectedLatency yinfo
151− longTerm = svarAllTimeLatency yinfo
152− measured = workerMeasuredLatency yinfo
153−
154− (newCount, newLatPair) <- collectWorkerPendingLatency cur col
155− (lcount, ltime) <- readIORef longTerm
156− prevLat <- readIORef measured
157−
158− let newLcount = lcount + newCount
159− retWith lat = return (newLcount, ltime, lat)
160−
161− case newLatPair of
162− Nothing -> retWith prevLat
163− Just (count, time) -> do
164− let newLat = time `div` fromIntegral count
165− when (svarInspectMode sv) $ recordMinMaxLatency sv newLat
166− -- When we have collected a significant sized batch we compute the
167− -- new latency using that batch and return the new latency,
168− -- otherwise we return the previous latency derived from the
169− -- previous batch.
170− if shouldUseCollectedBatch newCount time newLat prevLat || drain
171− then do
172− -- XXX make this NOINLINE?
173− updateWorkerPollingInterval yinfo (max newLat prevLat)
174− when (svarInspectMode sv) $ recordAvgLatency sv (count, time)
175− writeIORef col (0, 0, 0)
176− writeIORef measured ((prevLat + newLat) `div` 2)
177− modifyIORef longTerm $ \(_, t) -> (newLcount, t)
178− retWith newLat
179− else retWith prevLat
180−
181−-------------------------------------------------------------------------------
182−-- Dumping the SVar for debug/diag
183−-------------------------------------------------------------------------------
184−
185−dumpSVarStats :: Channel m a -> SVarStats -> IO String
186−dumpSVarStats sv ss = do
187− case yieldRateInfo sv of
188− Nothing -> return ()
189− Just yinfo -> do
190− _ <- liftIO $ collectLatency sv yinfo True
191− return ()
192−
193− dispatches <- readIORef $ totalDispatches ss
194− maxWrk <- readIORef $ maxWorkers ss
195− maxOq <- readIORef $ maxOutQSize ss
196− -- maxHp <- readIORef $ maxHeapSize ss
197− minLat <- readIORef $ minWorkerLatency ss
198− maxLat <- readIORef $ maxWorkerLatency ss
199− (avgCnt, avgTime) <- readIORef $ avgWorkerLatency ss
200− (svarCnt, svarGainLossCnt, svarLat) <- case yieldRateInfo sv of
201− Nothing -> return (0, 0, 0)
202− Just yinfo -> do
203− (cnt, startTime) <- readIORef $ svarAllTimeLatency yinfo
204− if cnt > 0
205− then do
206− t <- readIORef (svarStopTime ss)
207− gl <- readIORef (svarGainedLostYields yinfo)
208− case t of
209− Nothing -> do
210− now <- getTime Monotonic
211− let interval = diffAbsTime64 now startTime
212− return (cnt, gl, interval `div` fromIntegral cnt)
213− Just stopTime -> do
214− let interval = diffAbsTime64 stopTime startTime
215− return (cnt, gl, interval `div` fromIntegral cnt)
216− else return (0, 0, 0)
217−
218− return $ unlines
219− [ "total dispatches = " <> show dispatches
220− , "max workers = " <> show maxWrk
221− , "max outQSize = " <> show maxOq
222− <> (if minLat > 0
223− then "\nmin worker latency = " <> showNanoSecond64 minLat
224− else "")
225− <> (if maxLat > 0
226− then "\nmax worker latency = " <> showNanoSecond64 maxLat
227− else "")
228− <> (if avgCnt > 0
229− then let lat = avgTime `div` fromIntegral avgCnt
230− in "\navg worker latency = " <> showNanoSecond64 lat
231− else "")
232− <> (if svarLat > 0
233− then "\nSVar latency = " <> showRelTime64 svarLat
234− else "")
235− <> (if svarCnt > 0
236− then "\nSVar yield count = " <> show svarCnt
237− else "")
238− <> (if svarGainLossCnt > 0
239− then "\nSVar gain/loss yield count = " <> show svarGainLossCnt
240− else "")
241− ]
242−
243−{-# NOINLINE dumpSVar #-}
244−dumpSVar :: Channel m a -> IO String
245−dumpSVar sv = do
246− (oqList, oqLen) <- readIORef $ outputQueue sv
247− db <- tryReadMVar $ outputDoorBell sv
248−
249− waiting <- readIORef $ needDoorBell sv
250− rthread <- readIORef $ workerThreads sv
251− workers <- readIORef $ workerCount sv
252− stats <- dumpSVarStats sv (svarStats sv)
253−
254− return $ unlines
255− [
256− "Creator tid = " <> show (svarCreator sv)
257− , "---------CURRENT STATE-----------"
258− , "outputQueue length computed = " <> show (length oqList)
259− , "outputQueue length maintained = " <> show oqLen
260− -- XXX print the types of events in the outputQueue, first 5
261− , "outputDoorBell = " <> show db
262− ]
263− <> unlines
264− [ "needDoorBell = " <> show waiting
265− , "running threads = " <> show rthread
266− -- XXX print the status of first 5 threads
267− , "running thread count = " <> show workers
268− ]
269− <> "---------STATS-----------\n"
270− <> stats
271−
272−-- MVar diagnostics has some overhead - around 5% on AsyncT null benchmark, we
273−-- can keep it on in production to debug problems quickly if and when they
274−-- happen, but it may result in unexpected output when threads are left hanging
275−-- until they are GCed because the consumer went away.
276−
277−{-# NOINLINE mvarExcHandler #-}
278−mvarExcHandler :: Channel m a -> String -> BlockedIndefinitelyOnMVar -> IO ()
279−mvarExcHandler sv label e@BlockedIndefinitelyOnMVar = do
280− svInfo <- dumpSVar sv
281− hPutStrLn stderr $ label <> " " <> "BlockedIndefinitelyOnMVar\n" <> svInfo
282− throwIO e
283−
284−{-# NOINLINE stmExcHandler #-}
285−stmExcHandler :: Channel m a -> String -> BlockedIndefinitelyOnSTM -> IO ()
286−stmExcHandler sv label e@BlockedIndefinitelyOnSTM = do
287− svInfo <- dumpSVar sv
288− hPutStrLn stderr $ label <> " " <> "BlockedIndefinitelyOnSTM\n" <> svInfo
289− throwIO e
290−
291−withDiagMVar :: Channel m a -> String -> IO () -> IO ()
292−withDiagMVar sv label action =
293− if svarInspectMode sv
294− then
295− action `catches` [ Handler (mvarExcHandler sv label)
296− , Handler (stmExcHandler sv label)
297− ]
298− else action
299−
300−printSVar :: Channel m a -> String -> IO ()
301−printSVar sv how = do
302− svInfo <- dumpSVar sv
303− hPutStrLn stderr $ "\n" <> how <> "\n" <> svInfo
304−
305−-------------------------------------------------------------------------------
306−-- Thread accounting
307−-------------------------------------------------------------------------------
308−
309−-- Thread tracking is needed for two reasons:
310−--
311−-- 1) Killing threads on exceptions. Threads may not be left to go away by
312−-- themselves because they may run for significant times before going away or
313−-- worse they may be stuck in IO and never go away.
314−--
315−-- 2) To know when all threads are done and the stream has ended.
316−
317−{-# NOINLINE addThread #-}
318−addThread :: MonadIO m => Channel m a -> ThreadId -> m ()
319−addThread sv tid =
320− liftIO $ modifyIORef (workerThreads sv) (S.insert tid)
321−
322−-- This is cheaper than modifyThread because we do not have to send a
323−-- outputDoorBell This can make a difference when more workers are being
324−-- dispatched.
325−{-# INLINE delThread #-}
326−delThread :: MonadIO m => Channel m a -> ThreadId -> m ()
327−delThread sv tid =
328− liftIO $ modifyIORef (workerThreads sv) (S.delete tid)
329−
330−-- If present then delete else add. This takes care of out of order add and
331−-- delete i.e. a delete arriving before we even added a thread.
332−-- This occurs when the forked thread is done even before the 'addThread' right
333−-- after the fork gets a chance to run.
334−{-# INLINE modifyThread #-}
335−modifyThread :: MonadIO m => Channel m a -> ThreadId -> m ()
336−modifyThread sv tid = do
337− changed <- liftIO $ atomicModifyIORefCAS (workerThreads sv) $ \old ->
338− if S.member tid old
339− then let new = S.delete tid old in (new, new)
340− else let new = S.insert tid old in (new, old)
341− when (null changed) $
342− liftIO $ do
343− writeBarrier
344− void $ tryPutMVar (outputDoorBell sv) ()
345−
346−-- | This is safe even if we are adding more threads concurrently because if
347−-- a child thread is adding another thread then anyway 'workerThreads' will
348−-- not be empty.
349−{-# INLINE allThreadsDone #-}
350−allThreadsDone :: MonadIO m => Channel m a -> m Bool
351−allThreadsDone sv = liftIO $ S.null <$> readIORef (workerThreads sv)
37+import Streamly.Internal.Data.Stream.Channel.Dispatcher
38+import Streamly.Internal.Data.Stream.Channel.Types
39+import Streamly.Internal.Data.Stream.Channel.Worker
35240
35341-------------------------------------------------------------------------------
35442-- Dispatching workers
35543-------------------------------------------------------------------------------
35644
357−{-# NOINLINE recordMaxWorkers #-}
358−recordMaxWorkers :: MonadIO m => Channel m a -> m ()
359−recordMaxWorkers sv = liftIO $ do
360− active <- readIORef (workerCount sv)
361− maxWrk <- readIORef (maxWorkers $ svarStats sv)
362− when (active > maxWrk) $ writeIORef (maxWorkers $ svarStats sv) active
363− modifyIORef (totalDispatches $ svarStats sv) (+1)
364−
36545{-# NOINLINE pushWorker #-}
36646pushWorker :: (MonadIO m, MonadBaseControl IO m) => Count -> Channel m a -> m ()
36747pushWorker yieldMax sv = do
36848 liftIO $ atomicModifyIORefCAS_ (workerCount sv) $ \n -> n + 1
369− when (svarInspectMode sv) $ recordMaxWorkers sv
49+ when (svarInspectMode sv)
50+ $ recordMaxWorkers (workerCount sv) (svarStats sv)
37051 -- This allocation matters when significant number of workers are being
37152 -- sent. We allocate it only when needed.
37253 winfo <-
@@ -381,8 +62,12 @@ pushWorker yieldMax sv = do
38162 , workerYieldCount = cntRef
38263 , workerLatencyStart = lat
38364 }
384− doFork (workLoop sv winfo) (svarMrun sv) (handleChildException sv)
385− >>= addThread sv
65+ doFork (workLoop sv winfo) (svarMrun sv) exception
66+ >>= addThread (workerThreads sv)
67+
68+ where
69+
70+ exception = handleChildException (outputQueue sv) (outputDoorBell sv)
38671
38772-- Returns:
38873-- True: can dispatch more
@@ -454,7 +139,9 @@ dispatchWorkerPaced sv = do
454139 (svarYields, svarElapsed, wLatency) <- do
455140 now <- liftIO $ getTime Monotonic
456141 (yieldCount, baseTime, lat) <-
457− liftIO $ collectLatency sv yinfo False
142+ liftIO
143+ $ collectLatency
144+ (svarInspectMode sv) (svarStats sv) yinfo False
458145 let elapsed = fromRelTime64 $ diffAbsTime64 now baseTime
459146 let latency =
460147 if lat == 0
@@ -492,7 +179,7 @@ dispatchWorkerPaced sv = do
492179 -- here as it is not a reliable way to ensure there are
493180 -- definitely no active workers. When workerCount is 0 we may
494181 -- still have a Stop event waiting in the outputQueue.
495− done <- allThreadsDone sv
182+ done <- allThreadsDone (workerThreads sv)
496183 when done $ void $ do
497184 let us = fromRelTime64 (toRelTime64 s) :: MicroSecond64
498185 liftIO $ threadDelay (fromIntegral us)
@@ -502,7 +189,7 @@ dispatchWorkerPaced sv = do
502189 assert (yields > 0) (return ())
503190 updateGainedLostYields yinfo yields
504191
505− done <- allThreadsDone sv
192+ done <- allThreadsDone (workerThreads sv)
506193 when done $ void $ dispatchWorker yields sv
507194 return False
508195 ManyWorkers netWorkers yields -> do
@@ -627,8 +314,12 @@ sendWorkerWait delay dispatch sv = do
627314 if canDoMore
628315 then sendWorkerWait delay dispatch sv
629316 else do
630− liftIO $ withDiagMVar sv "sendWorkerWait: nothing to do"
631− $ takeMVar (outputDoorBell sv)
317+ liftIO
318+ $ withDiagMVar
319+ (svarInspectMode sv)
320+ (dumpSVar sv)
321+ "sendWorkerWait: nothing to do"
322+ $ takeMVar (outputDoorBell sv)
632323 (_, len) <- liftIO $ readIORef (outputQueue sv)
633324 when (len <= 0) $ sendWorkerWait delay dispatch sv
634325
src/Streamly/Internal/Data/Stream/Async/Channel/Interleave.hs+21−21
@@ -22,14 +22,14 @@ import Data.Concurrent.Queue.MichaelScott (LinkedQueue, newQ, nullQ, tryPopR, pu
2222import Data.IORef (newIORef, readIORef)
2323import Streamly.Internal.Control.Concurrent
2424 (MonadRunInIO, MonadAsync, RunInIO(..), askRunInIO)
25+import Streamly.Internal.Data.Stream.Channel.Dispatcher (delThread)
2526
2627import qualified Data.Set as Set
2728import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
2829
2930import Streamly.Internal.Data.Stream.Async.Channel.Consumer
30−import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
3131import Streamly.Internal.Data.Stream.Async.Channel.Type
32−import Streamly.Internal.Data.Stream.Async.Channel.Worker
32+import Streamly.Internal.Data.Stream.Channel.Types
3333
3434------------------------------------------------------------------------------
3535-- Creating a channel
@@ -49,7 +49,7 @@ enqueueFIFO ::
4949 -> IO ()
5050enqueueFIFO sv q m = do
5151 pushL q m
52− ringDoorBell sv
52+ ringDoorBell (needDoorBell sv) (outputDoorBell sv)
5353
5454-- XXX we can remove sv as it is derivable from st
5555
@@ -64,11 +64,10 @@ workLoopFIFO q sv winfo = run
6464
6565 where
6666
67− stop = liftIO $ sendStop sv winfo
6867 run = do
6968 work <- liftIO $ tryPopR q
7069 case work of
71− Nothing -> stop
70+ Nothing -> liftIO $ stop sv winfo
7271 Just (RunInIO runin, m) -> do
7372 r <- liftIO
7473 $ runin
@@ -77,10 +76,10 @@ workLoopFIFO q sv winfo = run
7776 res <- restoreM r
7877 case res of
7978 Continue -> run
80− Suspend -> stop
79+ Suspend -> liftIO $ stop sv winfo
8180
8281 single a = do
83− res <- liftIO $ sendYield sv winfo (ChildYield a)
82+ res <- liftIO $ yield sv winfo a
8483 return $ if res then Continue else Suspend
8584
8685 -- XXX in general we would like to yield "n" elements from a single stream
@@ -88,7 +87,7 @@ workLoopFIFO q sv winfo = run
8887 -- expensive in certain cases. Similarly, we can use time limit for
8988 -- yielding.
9089 yieldk a r = do
91− res <- liftIO $ sendYield sv winfo (ChildYield a)
90+ res <- liftIO $ yield sv winfo a
9291 runInIO <- askRunInIO
9392 liftIO $ enqueueFIFO sv q (runInIO, r)
9493 return $ if res then Continue else Suspend
@@ -104,14 +103,15 @@ workLoopFIFOLimited q sv winfo = run
104103
105104 where
106105
107− stop = liftIO $ sendStop sv winfo
108− incrContinue = liftIO (incrementYieldLimit sv) >> return Continue
106+ incrContinue =
107+ liftIO (incrementYieldLimit (remainingWork sv)) >> return Continue
108+
109109 run = do
110110 work <- liftIO $ tryPopR q
111111 case work of
112− Nothing -> stop
112+ Nothing -> liftIO $ stop sv winfo
113113 Just (RunInIO runin, m) -> do
114− yieldLimitOk <- liftIO $ decrementYieldLimit sv
114+ yieldLimitOk <- liftIO $ decrementYieldLimit (remainingWork sv)
115115 if yieldLimitOk
116116 then do
117117 r <- liftIO
@@ -121,25 +121,25 @@ workLoopFIFOLimited q sv winfo = run
121121 res <- restoreM r
122122 case res of
123123 Continue -> run
124− Suspend -> stop
124+ Suspend -> liftIO $ stop sv winfo
125125 else liftIO $ do
126126 enqueueFIFO sv q (RunInIO runin, m)
127− incrementYieldLimit sv
128− sendStop sv winfo
127+ incrementYieldLimit (remainingWork sv)
128+ stop sv winfo
129129
130130 single a = do
131− res <- liftIO $ sendYield sv winfo (ChildYield a)
131+ res <- liftIO $ yield sv winfo a
132132 return $ if res then Continue else Suspend
133133
134134 yieldk a r = do
135− res <- liftIO $ sendYield sv winfo (ChildYield a)
135+ res <- liftIO $ yield sv winfo a
136136 runInIO <- askRunInIO
137137 liftIO $ enqueueFIFO sv q (runInIO, r)
138− yieldLimitOk <- liftIO $ decrementYieldLimit sv
138+ yieldLimitOk <- liftIO $ decrementYieldLimit (remainingWork sv)
139139 if res && yieldLimitOk
140140 then return Continue
141141 else liftIO $ do
142− incrementYieldLimit sv
142+ incrementYieldLimit (remainingWork sv)
143143 return Suspend
144144
145145-------------------------------------------------------------------------------
@@ -162,7 +162,7 @@ getFifoSVar mrun cfg = do
162162 yl <- case getYieldLimit cfg of
163163 Nothing -> return Nothing
164164 Just x -> Just <$> newIORef x
165− rateInfo <- getYieldRateInfo cfg
165+ rateInfo <- newRateInfo cfg
166166
167167 stats <- newSVarStats
168168 tid <- myThreadId
@@ -204,7 +204,7 @@ getFifoSVar mrun cfg = do
204204 , needDoorBell = wfw
205205 , svarMrun = mrun
206206 , workerCount = active
207− , accountThread = delThread sv
207+ , accountThread = delThread running
208208 , workerStopMVar = undefined
209209 , svarRef = Nothing
210210 , svarInspectMode = getInspectMode cfg
src/Streamly/Internal/Data/Stream/Async/Channel/Operations.hs+14−9
@@ -42,9 +42,9 @@ import qualified Streamly.Internal.Data.Stream as Stream
4242import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
4343import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
4444
45−import Streamly.Internal.Data.Stream.Async.Channel.Consumer
4645import Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
4746import Streamly.Internal.Data.Stream.Async.Channel.Type
47+import Streamly.Internal.Data.Stream.Channel.Types
4848
4949import Prelude hiding (map, concat, concatMap)
5050
@@ -91,6 +91,7 @@ import Test.Inspection (inspect, hasNoTypeClassesExcept)
9191
9292-- | Write a stream to an 'SVar' in a non-blocking manner. The stream can then
9393-- be read back from the SVar using 'fromSVar'.
94+{-# INLINE toChannelK #-}
9495toChannelK :: (MonadIO m, MonadBaseControl IO m) =>
9596 Channel m a -> K.Stream m a -> m ()
9697toChannelK sv m = do
@@ -132,7 +133,7 @@ fromChannelRaw sv = K.MkStream $ \st yld sng stp -> do
132133 when (svarInspectMode sv) $ do
133134 t <- liftIO $ getTime Monotonic
134135 liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
135− liftIO $ printSVar sv "SVar Done"
136+ liftIO $ printSVar (dumpSVar sv) "SVar Done"
136137 stp
137138
138139 {-# INLINE processEvents #-}
@@ -154,7 +155,9 @@ fromChannelRaw sv = K.MkStream $ \st yld sng stp -> do
154155 case fromException ex of
155156 Just ThreadAbort ->
156157 K.foldStream st yld sng stp rest
157− Nothing -> liftIO (cleanupSVar sv) >> throwM ex
158+ Nothing -> do
159+ liftIO (cleanupSVar (workerThreads sv))
160+ throwM ex
158161
159162#if __GLASGOW_HASKELL__ < 810
160163#ifdef INSPECTION
@@ -199,8 +202,8 @@ fromChannelK sv =
199202 when (svarInspectMode sv) $ do
200203 r <- liftIO $ readIORef (svarStopTime (svarStats sv))
201204 when (isNothing r) $
202− printSVar sv "SVar Garbage Collected"
203− cleanupSVar sv
205+ printSVar (dumpSVar sv) "SVar Garbage Collected"
206+ cleanupSVar (workerThreads sv)
204207 -- If there are any SVars referenced by this SVar a GC will prompt
205208 -- them to be cleaned up quickly.
206209 when (svarInspectMode sv) performMajorGC
@@ -245,8 +248,8 @@ _fromChannelD svar = D.Stream step FromSVarInit
245248 when (svarInspectMode svar) $ do
246249 r <- liftIO $ readIORef (svarStopTime (svarStats svar))
247250 when (isNothing r) $
248− printSVar svar "SVar Garbage Collected"
249− cleanupSVar svar
251+ printSVar (dumpSVar svar) "SVar Garbage Collected"
252+ cleanupSVar (workerThreads svar)
250253 -- If there are any SVars referenced by this SVar a GC will prompt
251254 -- them to be cleaned up quickly.
252255 when (svarInspectMode svar) performMajorGC
@@ -275,11 +278,13 @@ _fromChannelD svar = D.Stream step FromSVarInit
275278 case fromException ex of
276279 Just ThreadAbort ->
277280 return $ D.Skip (FromSVarLoop sv es)
278− Nothing -> liftIO (cleanupSVar sv) >> throwM ex
281+ Nothing -> do
282+ liftIO (cleanupSVar (workerThreads sv))
283+ throwM ex
279284
280285 step _ (FromSVarDone sv) = do
281286 when (svarInspectMode sv) $ do
282287 t <- liftIO $ getTime Monotonic
283288 liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
284− liftIO $ printSVar sv "SVar Done"
289+ liftIO $ printSVar (dumpSVar sv) "SVar Done"
285290 return D.Stop
src/Streamly/Internal/Data/Stream/Async/Channel/Type.hs+48−506
@@ -8,222 +8,24 @@
88
99module Streamly.Internal.Data.Stream.Async.Channel.Type
1010 (
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
11+ Channel(..)
12+ , yield
13+ , stop
14+ , dumpSVar
5615 )
5716where
5817
5918import Control.Concurrent (ThreadId)
6019import Control.Concurrent.MVar (MVar)
61−import Control.Exception (SomeException(..), Exception)
62−import Data.Int (Int64)
63−import Data.IORef (IORef, newIORef)
20+import Data.IORef (IORef)
6421import Data.Set (Set)
6522
6623import Streamly.Internal.Control.Concurrent (RunInIO)
24+import Streamly.Internal.Data.Stream.Channel.Dispatcher (dumpSVarStats)
25+import Streamly.Internal.Data.Stream.Channel.Worker (sendYield, sendStop)
6726import 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)
11227
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−}
28+import Streamly.Internal.Data.Stream.Channel.Types
22729
22830-- IMPORTANT NOTE: we cannot update the SVar after generating it as we have
22931-- references to the original SVar stored in several functions which will keep
@@ -310,303 +112,43 @@ data Channel m a = Channel
310112 , svarCreator :: ThreadId
311113 }
312114
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)
115+{-# INLINE yield #-}
116+yield :: Channel m a -> Maybe WorkerInfo -> a -> IO Bool
117+yield sv winfo x =
118+ sendYield
119+ (maxBufferLimit sv)
120+ (maxWorkerLimit sv)
121+ (workerCount sv)
122+ winfo
123+ (yieldRateInfo sv)
124+ (outputQueue sv)
125+ (outputDoorBell sv)
126+ (ChildYield x)
127+
128+{-# INLINE stop #-}
129+stop :: Channel m a -> Maybe WorkerInfo -> IO ()
130+stop sv winfo =
131+ sendStop
132+ (workerCount sv)
133+ winfo
134+ (yieldRateInfo sv)
135+ (outputQueue sv)
136+ (outputDoorBell sv)
137+
138+{-# NOINLINE dumpSVar #-}
139+dumpSVar :: Channel m a -> IO String
140+dumpSVar sv = do
141+ xs <- sequence
142+ [ return (dumpCreator (svarCreator sv))
143+ , return "---------CURRENT STATE-----------"
144+ , dumpOutputQ (outputQueue sv)
145+ -- XXX print the types of events in the outputQueue, first 5
146+ , dumpDoorBell (outputDoorBell sv)
147+ , dumpNeedDoorBell (needDoorBell sv)
148+ , dumpRunningThreads (workerThreads sv)
149+ -- XXX print the status of first 5 threads
150+ , dumpWorkerCount (workerCount sv)
151+ , return "---------STATS-----------\n"
152+ , dumpSVarStats (svarInspectMode sv) (yieldRateInfo sv) (svarStats sv)
153+ ]
154+ return $ concat xs
src/Streamly/Internal/Data/Stream/Channel/Dispatcher.hsnew file+297
@@ -0,0 +1,297 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.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.Channel.Dispatcher
11+ (
12+ -- * Latency collection
13+ minThreadDelay
14+ , collectLatency
15+
16+ -- * Thread accounting
17+ , addThread
18+ , delThread
19+ , modifyThread
20+ , allThreadsDone
21+ , recordMaxWorkers
22+
23+ -- * Diagnostics
24+ , dumpSVarStats
25+ )
26+where
27+
28+import Data.Set (Set)
29+import Control.Concurrent (MVar, ThreadId)
30+import Control.Concurrent.MVar (tryPutMVar)
31+import Control.Exception (assert)
32+import Control.Monad (when, void)
33+import Control.Monad.IO.Class (MonadIO(liftIO))
34+import Data.IORef (IORef, modifyIORef, readIORef, writeIORef)
35+#if __GLASGOW_HASKELL__ < 804
36+import Data.Semigroup ((<>))
37+#endif
38+import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS, writeBarrier)
39+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
40+import Streamly.Internal.Data.Time.Units
41+ ( AbsTime, NanoSecond64(..), diffAbsTime64, showNanoSecond64
42+ , showRelTime64)
43+
44+import qualified Data.Set as S
45+
46+import Streamly.Internal.Data.Stream.Channel.Types
47+
48+-------------------------------------------------------------------------------
49+-- Worker latency data processing
50+-------------------------------------------------------------------------------
51+
52+-- | This is a magic number and it is overloaded, and used at several places to
53+-- achieve batching:
54+--
55+-- 1. If we have to sleep to slowdown this is the minimum period that we
56+-- accumulate before we sleep. Also, workers do not stop until this much
57+-- sleep time is accumulated.
58+-- 3. Collected latencies are computed and transferred to measured latency
59+-- after a minimum of this period.
60+minThreadDelay :: NanoSecond64
61+minThreadDelay = 1000000
62+
63+-- Every once in a while workers update the latencies and check the yield rate.
64+-- They return if we are above the expected yield rate. If we check too often
65+-- it may impact performance, if we check less often we may have a stale
66+-- picture. We update every minThreadDelay but we translate that into a yield
67+-- count based on latency so that the checking overhead is little.
68+--
69+-- XXX use a generation count to indicate that the value is updated. If the
70+-- value is updated an existing worker must check it again on the next yield.
71+-- Otherwise it is possible that we may keep updating it and because of the mod
72+-- worker keeps skipping it.
73+updateWorkerPollingInterval :: YieldRateInfo -> NanoSecond64 -> IO ()
74+updateWorkerPollingInterval yinfo latency = do
75+ let periodRef = workerPollingInterval yinfo
76+ cnt = max 1 $ minThreadDelay `div` latency
77+ period = min cnt (fromIntegral magicMaxBuffer)
78+
79+ writeIORef periodRef (fromIntegral period)
80+
81+{-# INLINE recordMinMaxLatency #-}
82+recordMinMaxLatency :: SVarStats -> NanoSecond64 -> IO ()
83+recordMinMaxLatency ss new = do
84+ minLat <- readIORef (minWorkerLatency ss)
85+ when (new < minLat || minLat == 0) $
86+ writeIORef (minWorkerLatency ss) new
87+
88+ maxLat <- readIORef (maxWorkerLatency ss)
89+ when (new > maxLat) $ writeIORef (maxWorkerLatency ss) new
90+
91+recordAvgLatency :: SVarStats -> (Count, NanoSecond64) -> IO ()
92+recordAvgLatency ss (count, time) = do
93+ modifyIORef (avgWorkerLatency ss) $
94+ \(cnt, t) -> (cnt + count, t + time)
95+
96+-- Pour the pending latency stats into a collection bucket
97+{-# INLINE collectWorkerPendingLatency #-}
98+collectWorkerPendingLatency
99+ :: IORef (Count, Count, NanoSecond64)
100+ -> IORef (Count, Count, NanoSecond64)
101+ -> IO (Count, Maybe (Count, NanoSecond64))
102+collectWorkerPendingLatency cur col = do
103+ (fcount, count, time) <- atomicModifyIORefCAS cur $ \v -> ((0,0,0), v)
104+
105+ (fcnt, cnt, t) <- readIORef col
106+ let totalCount = fcnt + fcount
107+ latCount = cnt + count
108+ latTime = t + time
109+ writeIORef col (totalCount, latCount, latTime)
110+
111+ assert (latCount == 0 || latTime /= 0) (return ())
112+ let latPair =
113+ if latCount > 0 && latTime > 0
114+ then Just (latCount, latTime)
115+ else Nothing
116+ return (totalCount, latPair)
117+
118+{-# INLINE shouldUseCollectedBatch #-}
119+shouldUseCollectedBatch
120+ :: Count
121+ -> NanoSecond64
122+ -> NanoSecond64
123+ -> NanoSecond64
124+ -> Bool
125+shouldUseCollectedBatch collectedYields collectedTime newLat prevLat =
126+ let r = fromIntegral newLat / fromIntegral prevLat :: Double
127+ in (collectedYields > fromIntegral magicMaxBuffer)
128+ || (collectedTime > minThreadDelay)
129+ || (prevLat > 0 && (r > 2 || r < 0.5))
130+ || (prevLat == 0)
131+
132+-- Returns a triple, (1) yield count since last collection, (2) the base time
133+-- when we started counting, (3) average latency in the last measurement
134+-- period. The former two are used for accurate measurement of the going rate
135+-- whereas the average is used for future estimates e.g. how many workers
136+-- should be maintained to maintain the rate.
137+-- CAUTION! keep it in sync with getWorkerLatency
138+collectLatency ::
139+ Bool
140+ -> SVarStats
141+ -> YieldRateInfo
142+ -> Bool
143+ -> IO (Count, AbsTime, NanoSecond64)
144+collectLatency inspect ss yinfo drain = do
145+ let cur = workerPendingLatency yinfo
146+ col = workerCollectedLatency yinfo
147+ longTerm = svarAllTimeLatency yinfo
148+ measured = workerMeasuredLatency yinfo
149+
150+ (newCount, newLatPair) <- collectWorkerPendingLatency cur col
151+ (lcount, ltime) <- readIORef longTerm
152+ prevLat <- readIORef measured
153+
154+ let newLcount = lcount + newCount
155+ retWith lat = return (newLcount, ltime, lat)
156+
157+ case newLatPair of
158+ Nothing -> retWith prevLat
159+ Just (count, time) -> do
160+ let newLat = time `div` fromIntegral count
161+ when inspect $ recordMinMaxLatency ss newLat
162+ -- When we have collected a significant sized batch we compute the
163+ -- new latency using that batch and return the new latency,
164+ -- otherwise we return the previous latency derived from the
165+ -- previous batch.
166+ if shouldUseCollectedBatch newCount time newLat prevLat || drain
167+ then do
168+ -- XXX make this NOINLINE?
169+ updateWorkerPollingInterval yinfo (max newLat prevLat)
170+ when inspect $ recordAvgLatency ss (count, time)
171+ writeIORef col (0, 0, 0)
172+ writeIORef measured ((prevLat + newLat) `div` 2)
173+ modifyIORef longTerm $ \(_, t) -> (newLcount, t)
174+ retWith newLat
175+ else retWith prevLat
176+
177+-------------------------------------------------------------------------------
178+-- Dumping the SVar for debug/diag
179+-------------------------------------------------------------------------------
180+
181+dumpSVarStats :: Bool -> Maybe YieldRateInfo -> SVarStats -> IO String
182+dumpSVarStats inspect rateInfo ss = do
183+ case rateInfo of
184+ Nothing -> return ()
185+ Just yinfo -> do
186+ _ <- liftIO $ collectLatency inspect ss yinfo True
187+ return ()
188+
189+ dispatches <- readIORef $ totalDispatches ss
190+ maxWrk <- readIORef $ maxWorkers ss
191+ maxOq <- readIORef $ maxOutQSize ss
192+ -- maxHp <- readIORef $ maxHeapSize ss
193+ minLat <- readIORef $ minWorkerLatency ss
194+ maxLat <- readIORef $ maxWorkerLatency ss
195+ (avgCnt, avgTime) <- readIORef $ avgWorkerLatency ss
196+ (svarCnt, svarGainLossCnt, svarLat) <- case rateInfo of
197+ Nothing -> return (0, 0, 0)
198+ Just yinfo -> do
199+ (cnt, startTime) <- readIORef $ svarAllTimeLatency yinfo
200+ if cnt > 0
201+ then do
202+ t <- readIORef (svarStopTime ss)
203+ gl <- readIORef (svarGainedLostYields yinfo)
204+ case t of
205+ Nothing -> do
206+ now <- getTime Monotonic
207+ let interval = diffAbsTime64 now startTime
208+ return (cnt, gl, interval `div` fromIntegral cnt)
209+ Just stopTime -> do
210+ let interval = diffAbsTime64 stopTime startTime
211+ return (cnt, gl, interval `div` fromIntegral cnt)
212+ else return (0, 0, 0)
213+
214+ return $ unlines
215+ [ "total dispatches = " <> show dispatches
216+ , "max workers = " <> show maxWrk
217+ , "max outQSize = " <> show maxOq
218+ <> (if minLat > 0
219+ then "\nmin worker latency = " <> showNanoSecond64 minLat
220+ else "")
221+ <> (if maxLat > 0
222+ then "\nmax worker latency = " <> showNanoSecond64 maxLat
223+ else "")
224+ <> (if avgCnt > 0
225+ then let lat = avgTime `div` fromIntegral avgCnt
226+ in "\navg worker latency = " <> showNanoSecond64 lat
227+ else "")
228+ <> (if svarLat > 0
229+ then "\nSVar latency = " <> showRelTime64 svarLat
230+ else "")
231+ <> (if svarCnt > 0
232+ then "\nSVar yield count = " <> show svarCnt
233+ else "")
234+ <> (if svarGainLossCnt > 0
235+ then "\nSVar gain/loss yield count = " <> show svarGainLossCnt
236+ else "")
237+ ]
238+
239+-------------------------------------------------------------------------------
240+-- Thread accounting
241+-------------------------------------------------------------------------------
242+
243+-- Thread tracking is needed for two reasons:
244+--
245+-- 1) Killing threads on exceptions. Threads may not be left to go away by
246+-- themselves because they may run for significant times before going away or
247+-- worse they may be stuck in IO and never go away.
248+--
249+-- 2) To know when all threads are done and the stream has ended.
250+
251+{-# NOINLINE addThread #-}
252+addThread :: MonadIO m => IORef (Set ThreadId) -> ThreadId -> m ()
253+addThread workerSet tid =
254+ liftIO $ modifyIORef workerSet (S.insert tid)
255+
256+-- This is cheaper than modifyThread because we do not have to send a
257+-- outputDoorBell This can make a difference when more workers are being
258+-- dispatched.
259+{-# INLINE delThread #-}
260+delThread :: MonadIO m => IORef (Set ThreadId) -> ThreadId -> m ()
261+delThread workerSet tid =
262+ liftIO $ modifyIORef workerSet (S.delete tid)
263+
264+-- If present then delete else add. This takes care of out of order add and
265+-- delete i.e. a delete arriving before we even added a thread.
266+-- This occurs when the forked thRead is done even before the 'addThread' right
267+-- after the fork gets a chance to run.
268+{-# INLINE modifyThread #-}
269+modifyThread :: MonadIO m => IORef (Set ThreadId) -> MVar () -> ThreadId -> m ()
270+modifyThread workerSet bell tid = do
271+ changed <- liftIO $ atomicModifyIORefCAS workerSet $ \old ->
272+ if S.member tid old
273+ then let new = S.delete tid old in (new, new)
274+ else let new = S.insert tid old in (new, old)
275+ when (null changed) $
276+ liftIO $ do
277+ writeBarrier
278+ void $ tryPutMVar bell ()
279+
280+-- | This is safe even if we are adding more threads concurrently because if
281+-- a child thread is adding another thread then anyway 'workerThreads' will
282+-- not be empty.
283+{-# INLINE allThreadsDone #-}
284+allThreadsDone :: MonadIO m => IORef (Set ThreadId) -> m Bool
285+allThreadsDone ref = liftIO $ S.null <$> readIORef ref
286+
287+-------------------------------------------------------------------------------
288+-- Dispatching workers
289+-------------------------------------------------------------------------------
290+
291+{-# NOINLINE recordMaxWorkers #-}
292+recordMaxWorkers :: MonadIO m => IORef Int -> SVarStats -> m ()
293+recordMaxWorkers countRef ss = liftIO $ do
294+ active <- readIORef countRef
295+ maxWrk <- readIORef (maxWorkers ss)
296+ when (active > maxWrk) $ writeIORef (maxWorkers ss) active
297+ modifyIORef (totalDispatches ss) (+1)
src/Streamly/Internal/Data/Stream/Channel/Types.hsnew file+710
@@ -0,0 +1,710 @@
1+-- |
2+-- Module : Streamly.Internal.Data.Stream.Channel.Types
3+-- Copyright : (c) 2017 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+-- A Channel is a place where streams join and new streams start. This module
10+-- defines low level data structures and functions to build channels. For
11+-- concrete Channels see the Channel modules of specific stream types.
12+--
13+-- A Channel is a conduit to the output from multiple streams running
14+-- concurrently and asynchronously. A channel can be thought of as an
15+-- asynchronous IO handle. We can write any number of streams to a channel in a
16+-- non-blocking manner and then read them back at any time at any pace. The
17+-- channel would run the streams asynchronously and accumulate results. A
18+-- channel may not really execute the stream completely and accumulate all the
19+-- results. However, it ensures that the reader can read the results at
20+-- whatever pace it wants to read. The channel monitors and adapts to the
21+-- consumer's pace.
22+--
23+-- A channel is a mini scheduler, it has an associated workLoop that holds the
24+-- stream tasks to be picked and run by a pool of worker threads. It has an
25+-- associated output queue where the output stream elements are placed by the
26+-- worker threads. An outputDoorBell is used by the worker threads to intimate the
27+-- consumer thread about availability of new results in the output queue. More
28+-- workers are added to the channel by 'fromChannel' on demand if the output
29+-- produced is not keeping pace with the consumer. On bounded channels, workers
30+-- block on the output queue to provide throttling of the producer when the
31+-- consumer is not pulling fast enough. The number of workers may even get
32+-- reduced depending on the consuming pace.
33+--
34+module Streamly.Internal.Data.Stream.Channel.Types
35+ (
36+ -- * Types
37+ Count (..)
38+ , Limit (..)
39+ , ThreadAbort (..)
40+ , ChildEvent (..)
41+
42+ -- * Stats
43+ , SVarStats (..)
44+ , newSVarStats
45+
46+ -- * Rate Control
47+ , WorkerInfo (..)
48+ , LatencyRange (..)
49+ , YieldRateInfo (..)
50+ , newRateInfo
51+
52+ -- * Output queue
53+ , readOutputQRaw
54+ , readOutputQBasic
55+ , ringDoorBell
56+
57+ -- * Yield Limit
58+ , decrementYieldLimit
59+ , incrementYieldLimit
60+
61+ -- * Configuration
62+ , Rate (..)
63+ , Config
64+
65+ -- ** Default config
66+ , magicMaxBuffer
67+ , defaultConfig
68+
69+ -- ** Set config
70+ , maxThreads
71+ , maxBuffer
72+ , maxYields
73+ , inspectMode
74+
75+ , rate
76+ , avgRate
77+ , minRate
78+ , maxRate
79+ , constRate
80+
81+ -- ** Get config
82+ , getMaxThreads
83+ , getMaxBuffer
84+ , getStreamRate
85+ , getStreamLatency
86+ , setStreamLatency
87+ , getYieldLimit
88+ , getInspectMode
89+
90+ -- * Cleanup
91+ , cleanupSVar
92+
93+ -- * Diagnostics
94+ , dumpCreator
95+ , dumpOutputQ
96+ , dumpDoorBell
97+ , dumpNeedDoorBell
98+ , dumpRunningThreads
99+ , dumpWorkerCount
100+
101+ , withDiagMVar
102+ , printSVar
103+ )
104+where
105+
106+import Control.Concurrent (ThreadId, myThreadId, throwTo, MVar, tryReadMVar)
107+import Control.Concurrent.MVar (tryPutMVar)
108+import Control.Exception
109+ ( SomeException(..), Exception, catches, throwIO, Handler(..)
110+ , BlockedIndefinitelyOnMVar(..), BlockedIndefinitelyOnSTM(..))
111+import Control.Monad (void, when)
112+import Data.Int (Int64)
113+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
114+import Data.Set (Set)
115+import Streamly.Internal.Data.Atomics
116+ (atomicModifyIORefCAS, atomicModifyIORefCAS_, storeLoadBarrier)
117+import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
118+import Streamly.Internal.Data.Time.Units (AbsTime, NanoSecond64(..))
119+import System.IO (hPutStrLn, stderr)
120+
121+import qualified Data.Set as Set
122+
123+------------------------------------------------------------------------------
124+-- Common types
125+------------------------------------------------------------------------------
126+
127+newtype Count = Count Int64
128+ deriving ( Eq
129+ , Read
130+ , Show
131+ , Enum
132+ , Bounded
133+ , Num
134+ , Real
135+ , Integral
136+ , Ord
137+ )
138+
139+-- XXX We can use maxBound for unlimited?
140+
141+-- This is essentially a 'Maybe Word' type
142+data Limit = Unlimited | Limited Word deriving Show
143+
144+instance Eq Limit where
145+ Unlimited == Unlimited = True
146+ Unlimited == Limited _ = False
147+ Limited _ == Unlimited = False
148+ Limited x == Limited y = x == y
149+
150+instance Ord Limit where
151+ Unlimited <= Unlimited = True
152+ Unlimited <= Limited _ = False
153+ Limited _ <= Unlimited = True
154+ Limited x <= Limited y = x <= y
155+
156+------------------------------------------------------------------------------
157+-- Parent child thread communication type
158+------------------------------------------------------------------------------
159+
160+data ThreadAbort = ThreadAbort deriving Show
161+
162+instance Exception ThreadAbort
163+
164+-- | Events that a child thread may send to a parent thread.
165+data ChildEvent a =
166+ ChildYield a
167+ | ChildStop ThreadId (Maybe SomeException)
168+
169+-- | We measure the individual worker latencies to estimate the number of workers
170+-- needed or the amount of time we have to sleep between dispatches to achieve
171+-- a particular rate when controlled pace mode it used.
172+data WorkerInfo = WorkerInfo
173+ {
174+ -- | 0 means unlimited
175+ workerYieldMax :: Count
176+ -- | total number of yields by the worker till now
177+ , workerYieldCount :: IORef Count
178+ -- | yieldCount at start, timestamp
179+ , workerLatencyStart :: IORef (Count, AbsTime)
180+ }
181+
182+data LatencyRange = LatencyRange
183+ { minLatency :: NanoSecond64
184+ , maxLatency :: NanoSecond64
185+ } deriving Show
186+
187+-- | Rate control.
188+data YieldRateInfo = YieldRateInfo
189+ { svarLatencyTarget :: NanoSecond64
190+ , svarLatencyRange :: LatencyRange
191+ , svarRateBuffer :: Int
192+
193+ -- | [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
194+ -- read by the worker threads
195+ , svarGainedLostYields :: IORef Count
196+
197+ -- | Actual latency/througput as seen from the consumer side, we count the
198+ -- yields and the time it took to generates those yields. This is used to
199+ -- increase or decrease the number of workers needed to achieve the desired
200+ -- rate. The idle time of workers is adjusted in this, so that we only
201+ -- account for the rate when the consumer actually demands data.
202+ -- XXX interval latency is enough, we can move this under diagnostics build
203+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
204+ -- read by the worker threads
205+ , svarAllTimeLatency :: IORef (Count, AbsTime)
206+
207+ -- XXX Worker latency specified by the user to be used before the first
208+ -- actual measurement arrives. Not yet implemented
209+ , workerBootstrapLatency :: Maybe NanoSecond64
210+
211+ -- | After how many yields the worker should update the latency information.
212+ -- If the latency is high, this count is kept lower and vice-versa. XXX If
213+ -- the latency suddenly becomes too high this count may remain too high for
214+ -- long time, in such cases the consumer can change it.
215+ -- 0 means no latency computation
216+ -- XXX this is derivable from workerMeasuredLatency, can be removed.
217+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
218+ -- read by the worker threads
219+ , workerPollingInterval :: IORef Count
220+
221+ -- | This is in progress latency stats maintained by the workers which we
222+ -- empty into workerCollectedLatency stats at certain intervals - whenever
223+ -- we process the stream elements yielded in this period. The first count
224+ -- is all yields, the second count is only those yields for which the
225+ -- latency was measured to be non-zero (note that if the timer resolution
226+ -- is low the measured latency may be zero e.g. on JS platform).
227+ -- [LOCKING] Locked access. Modified by the consumer thread as well as
228+ -- worker threads. Workers modify it periodically based on
229+ -- workerPollingInterval and not on every yield to reduce the locking
230+ -- overhead.
231+ -- (allYieldCount, yieldCount, timeTaken)
232+ , workerPendingLatency :: IORef (Count, Count, NanoSecond64)
233+
234+ -- | This is the second level stat which is an accmulation from
235+ -- workerPendingLatency stats. We keep accumulating latencies in this
236+ -- bucket until we have stats for a sufficient period and then we reset it
237+ -- to start collecting for the next period and retain the computed average
238+ -- latency for the last period in workerMeasuredLatency.
239+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
240+ -- read by the worker threads
241+ -- (allYieldCount, yieldCount, timeTaken)
242+ , workerCollectedLatency :: IORef (Count, Count, NanoSecond64)
243+
244+ -- | Latency as measured by workers, aggregated for the last period.
245+ -- [LOCKING] Unlocked access. Modified by the consumer thread and unsafely
246+ -- read by the worker threads
247+ , workerMeasuredLatency :: IORef NanoSecond64
248+ }
249+
250+data SVarStats = SVarStats {
251+ totalDispatches :: IORef Int
252+ , maxWorkers :: IORef Int
253+ , maxOutQSize :: IORef Int
254+ , maxHeapSize :: IORef Int
255+ , maxWorkQSize :: IORef Int
256+ , avgWorkerLatency :: IORef (Count, NanoSecond64)
257+ , minWorkerLatency :: IORef NanoSecond64
258+ , maxWorkerLatency :: IORef NanoSecond64
259+ , svarStopTime :: IORef (Maybe AbsTime)
260+}
261+
262+-------------------------------------------------------------------------------
263+-- Config
264+-------------------------------------------------------------------------------
265+
266+-- | Specifies the stream yield rate in yields per second (@Hertz@).
267+-- We keep accumulating yield credits at 'rateGoal'. At any point of time we
268+-- allow only as many yields as we have accumulated as per 'rateGoal' since the
269+-- start of time. If the consumer or the producer is slower or faster, the
270+-- actual rate may fall behind or exceed 'rateGoal'. We try to recover the gap
271+-- between the two by increasing or decreasing the pull rate from the producer.
272+-- However, if the gap becomes more than 'rateBuffer' we try to recover only as
273+-- much as 'rateBuffer'.
274+--
275+-- 'rateLow' puts a bound on how low the instantaneous rate can go when
276+-- recovering the rate gap. In other words, it determines the maximum yield
277+-- latency. Similarly, 'rateHigh' puts a bound on how high the instantaneous
278+-- rate can go when recovering the rate gap. In other words, it determines the
279+-- minimum yield latency. We reduce the latency by increasing concurrency,
280+-- therefore we can say that it puts an upper bound on concurrency.
281+--
282+-- If the 'rateGoal' is 0 or negative the stream never yields a value.
283+-- If the 'rateBuffer' is 0 or negative we do not attempt to recover.
284+--
285+-- /Since: 0.5.0 ("Streamly")/
286+--
287+-- @since 0.8.0
288+data Rate = Rate
289+ { rateLow :: Double -- ^ The lower rate limit
290+ , rateGoal :: Double -- ^ The target rate we want to achieve
291+ , rateHigh :: Double -- ^ The upper rate limit
292+ , rateBuffer :: Int -- ^ Maximum slack from the goal
293+ }
294+
295+-- XXX we can put the resettable fields in a oneShotConfig field and others in
296+-- a persistentConfig field. That way reset would be fast and scalable
297+-- irrespective of the number of fields.
298+--
299+-- XXX make all these Limited types and use phantom types to distinguish them
300+
301+-- | An abstract type for specifying the configuration parameters of a
302+-- 'Channel'. Use @Config -> Config@ modifier functions to modify the
303+-- default configuration. See the modifiers for default values.
304+--
305+data Config = Config
306+ { -- one shot configuration, automatically reset for each API call
307+ -- streamVar :: Maybe (SVar t m a)
308+ _yieldLimit :: Maybe Count
309+
310+ -- persistent configuration, state that remains valid until changed by
311+ -- an explicit setting via a combinator.
312+ , _threadsHigh :: Limit
313+ , _bufferHigh :: Limit
314+
315+ -- XXX these two can be collapsed into a single type
316+ , _streamLatency :: Maybe NanoSecond64 -- bootstrap latency
317+ , _maxStreamRate :: Maybe Rate
318+ , _inspectMode :: Bool
319+ }
320+
321+-------------------------------------------------------------------------------
322+-- State defaults and reset
323+-------------------------------------------------------------------------------
324+
325+-- | A magical value for the buffer size arrived at by running the smallest
326+-- possible task and measuring the optimal value of the buffer for that. This
327+-- is obviously dependent on hardware, this figure is based on a 2.2GHz intel
328+-- core-i7 processor.
329+magicMaxBuffer :: Word
330+magicMaxBuffer = 1500
331+
332+defaultMaxThreads, defaultMaxBuffer :: Limit
333+defaultMaxThreads = Limited magicMaxBuffer
334+defaultMaxBuffer = Limited magicMaxBuffer
335+
336+-- | The fields prefixed by an _ are not to be accessed or updated directly but
337+-- via smart accessor APIs. Use get/set routines instead of directly accessing
338+-- the Config fields
339+defaultConfig :: Config
340+defaultConfig = Config
341+ { -- streamVar = Nothing
342+ _yieldLimit = Nothing
343+ , _threadsHigh = defaultMaxThreads
344+ , _bufferHigh = defaultMaxBuffer
345+ , _maxStreamRate = Nothing
346+ , _streamLatency = Nothing
347+ , _inspectMode = False
348+ }
349+
350+-------------------------------------------------------------------------------
351+-- Smart get/set routines for State
352+-------------------------------------------------------------------------------
353+
354+maxYields :: Maybe Int64 -> Config -> Config
355+maxYields lim st =
356+ st { _yieldLimit =
357+ case lim of
358+ Nothing -> Nothing
359+ Just n ->
360+ if n <= 0
361+ then Just 0
362+ else Just (fromIntegral n)
363+ }
364+
365+getYieldLimit :: Config -> Maybe Count
366+getYieldLimit = _yieldLimit
367+
368+-- | Specify the maximum number of threads that can be spawned by the channel.
369+-- A value of 0 resets the thread limit to default, a negative value means
370+-- there is no limit. The default value is 1500.
371+--
372+-- When the actions in a stream are IO bound, having blocking IO calls, this
373+-- option can be used to control the maximum number of in-flight IO requests.
374+-- When the actions are CPU bound this option can be used to control the amount
375+-- of CPU used by the stream.
376+--
377+maxThreads :: Int -> Config -> Config
378+maxThreads n st =
379+ st { _threadsHigh =
380+ if n < 0
381+ then Unlimited
382+ else if n == 0
383+ then defaultMaxThreads
384+ else Limited (fromIntegral n)
385+ }
386+
387+getMaxThreads :: Config -> Limit
388+getMaxThreads = _threadsHigh
389+
390+-- | Specify the maximum size of the buffer for storing the results from
391+-- concurrent computations. If the buffer becomes full we stop spawning more
392+-- concurrent tasks until there is space in the buffer.
393+-- A value of 0 resets the buffer size to default, a negative value means
394+-- there is no limit. The default value is 1500.
395+--
396+-- CAUTION! using an unbounded 'maxBuffer' value (i.e. a negative value)
397+-- coupled with an unbounded 'maxThreads' value is a recipe for disaster in
398+-- presence of infinite streams, or very large streams. Especially, it must
399+-- not be used when 'pure' is used in 'ZipAsyncM' streams as 'pure' in
400+-- applicative zip streams generates an infinite stream causing unbounded
401+-- concurrent generation with no limit on the buffer or threads.
402+--
403+maxBuffer :: Int -> Config -> Config
404+maxBuffer n st =
405+ st { _bufferHigh =
406+ if n < 0
407+ then Unlimited
408+ else if n == 0
409+ then defaultMaxBuffer
410+ else Limited (fromIntegral n)
411+ }
412+
413+getMaxBuffer :: Config -> Limit
414+getMaxBuffer = _bufferHigh
415+
416+-- | Specify the pull rate of a channel.
417+-- A 'Nothing' value resets the rate to default which is unlimited. When the
418+-- rate is specified, concurrent production may be ramped up or down
419+-- automatically to achieve the specified yield rate. The specific behavior for
420+-- different styles of 'Rate' specifications is documented under 'Rate'. The
421+-- effective maximum production rate achieved by a channel is governed by:
422+--
423+-- * The 'maxThreads' limit
424+-- * The 'maxBuffer' limit
425+-- * The maximum rate that the stream producer can achieve
426+-- * The maximum rate that the stream consumer can achieve
427+--
428+rate :: Maybe Rate -> Config -> Config
429+rate r st = st { _maxStreamRate = r }
430+
431+getStreamRate :: Config -> Maybe Rate
432+getStreamRate = _maxStreamRate
433+
434+setStreamLatency :: Int -> Config -> Config
435+setStreamLatency n st =
436+ st { _streamLatency =
437+ if n <= 0
438+ then Nothing
439+ else Just (fromIntegral n)
440+ }
441+
442+getStreamLatency :: Config -> Maybe NanoSecond64
443+getStreamLatency = _streamLatency
444+
445+inspectMode :: Config -> Config
446+inspectMode st = st { _inspectMode = True }
447+
448+getInspectMode :: Config -> Bool
449+getInspectMode = _inspectMode
450+
451+-------------------------------------------------------------------------------
452+-- Initialization
453+-------------------------------------------------------------------------------
454+
455+newRateInfo :: Config -> IO (Maybe YieldRateInfo)
456+newRateInfo st = do
457+ -- convert rate in Hertz to latency in Nanoseconds
458+ let rateToLatency r = if r <= 0 then maxBound else round $ 1.0e9 / r
459+ case getStreamRate st of
460+ Just (Rate low goal high buf) ->
461+ let l = rateToLatency goal
462+ minl = rateToLatency high
463+ maxl = rateToLatency low
464+ in mkYieldRateInfo l (LatencyRange minl maxl) buf
465+ Nothing -> return Nothing
466+
467+ where
468+
469+ mkYieldRateInfo latency latRange buf = do
470+ measured <- newIORef 0
471+ wcur <- newIORef (0,0,0)
472+ wcol <- newIORef (0,0,0)
473+ now <- getTime Monotonic
474+ wlong <- newIORef (0,now)
475+ period <- newIORef 1
476+ gainLoss <- newIORef (Count 0)
477+
478+ return $ Just YieldRateInfo
479+ { svarLatencyTarget = latency
480+ , svarLatencyRange = latRange
481+ , svarRateBuffer = buf
482+ , svarGainedLostYields = gainLoss
483+ , workerBootstrapLatency = getStreamLatency st
484+ , workerPollingInterval = period
485+ , workerMeasuredLatency = measured
486+ , workerPendingLatency = wcur
487+ , workerCollectedLatency = wcol
488+ , svarAllTimeLatency = wlong
489+ }
490+
491+newSVarStats :: IO SVarStats
492+newSVarStats = do
493+ disp <- newIORef 0
494+ maxWrk <- newIORef 0
495+ maxOq <- newIORef 0
496+ maxHs <- newIORef 0
497+ maxWq <- newIORef 0
498+ avgLat <- newIORef (0, NanoSecond64 0)
499+ maxLat <- newIORef (NanoSecond64 0)
500+ minLat <- newIORef (NanoSecond64 0)
501+ stpTime <- newIORef Nothing
502+
503+ return SVarStats
504+ { totalDispatches = disp
505+ , maxWorkers = maxWrk
506+ , maxOutQSize = maxOq
507+ , maxHeapSize = maxHs
508+ , maxWorkQSize = maxWq
509+ , avgWorkerLatency = avgLat
510+ , minWorkerLatency = minLat
511+ , maxWorkerLatency = maxLat
512+ , svarStopTime = stpTime
513+ }
514+
515+-------------------------------------------------------------------------------
516+-- Rate
517+-------------------------------------------------------------------------------
518+
519+-- | Same as @rate (Just $ Rate (r/2) r (2*r) maxBound)@
520+--
521+-- Specifies the average production rate of a stream in number of yields
522+-- per second (i.e. @Hertz@). Concurrent production is ramped up or down
523+-- automatically to achieve the specified average yield rate. The rate can
524+-- go down to half of the specified rate on the lower side and double of
525+-- the specified rate on the higher side.
526+--
527+avgRate :: Double -> Config -> Config
528+avgRate r = rate (Just $ Rate (r/2) r (2*r) maxBound)
529+
530+-- | Same as @rate (Just $ Rate r r (2*r) maxBound)@
531+--
532+-- Specifies the minimum rate at which the stream should yield values. As
533+-- far as possible the yield rate would never be allowed to go below the
534+-- specified rate, even though it may possibly go above it at times, the
535+-- upper limit is double of the specified rate.
536+--
537+minRate :: Double -> Config -> Config
538+minRate r = rate (Just $ Rate r r (2*r) maxBound)
539+
540+-- | Same as @rate (Just $ Rate (r/2) r r maxBound)@
541+--
542+-- Specifies the maximum rate at which the stream should yield values. As
543+-- far as possible the yield rate would never be allowed to go above the
544+-- specified rate, even though it may possibly go below it at times, the
545+-- lower limit is half of the specified rate. This can be useful in
546+-- applications where certain resource usage must not be allowed to go
547+-- beyond certain limits.
548+--
549+maxRate :: Double -> Config -> Config
550+maxRate r = rate (Just $ Rate (r/2) r r maxBound)
551+
552+-- | Same as @rate (Just $ Rate r r r 0)@
553+--
554+-- Specifies a constant yield rate. If for some reason the actual rate
555+-- goes above or below the specified rate we do not try to recover it by
556+-- increasing or decreasing the rate in future. This can be useful in
557+-- applications like graphics frame refresh where we need to maintain a
558+-- constant refresh rate.
559+--
560+constRate :: Double -> Config -> Config
561+constRate r = rate (Just $ Rate r r r 0)
562+
563+-------------------------------------------------------------------------------
564+-- Channel yield count
565+-------------------------------------------------------------------------------
566+
567+-- XXX Can we make access to remainingWork and yieldRateInfo fields in sv
568+-- faster, along with the fields in sv required by send?
569+-- XXX make it noinline
570+--
571+-- XXX we may want to employ an increment and decrement in batches when the
572+-- througput is high or when the cost of synchronization is high. For example
573+-- if the application is distributed then inc/dec of a shared variable may be
574+-- very costly.
575+
576+-- | A worker decrements the yield limit before it executes an action. However,
577+-- the action may not result in an element being yielded, in that case we have
578+-- to increment the yield limit.
579+--
580+-- Note that we need it to be an Int type so that we have the ability to undo a
581+-- decrement that takes it below zero.
582+{-# INLINE decrementYieldLimit #-}
583+decrementYieldLimit :: Maybe (IORef Count) -> IO Bool
584+decrementYieldLimit remaining =
585+ case remaining of
586+ Nothing -> return True
587+ Just ref -> do
588+ r <- atomicModifyIORefCAS ref $ \x -> (x - 1, x)
589+ return $ r >= 1
590+
591+{-# INLINE incrementYieldLimit #-}
592+incrementYieldLimit :: Maybe (IORef Count) -> IO ()
593+incrementYieldLimit remaining =
594+ case remaining of
595+ Nothing -> return ()
596+ Just ref -> atomicModifyIORefCAS_ ref (+ 1)
597+
598+-------------------------------------------------------------------------------
599+-- Output queue
600+-------------------------------------------------------------------------------
601+
602+{-# INLINE readOutputQBasic #-}
603+readOutputQBasic :: IORef ([ChildEvent a], Int) -> IO ([ChildEvent a], Int)
604+readOutputQBasic q = atomicModifyIORefCAS q $ \x -> (([],0), x)
605+
606+{-# INLINE readOutputQRaw #-}
607+readOutputQRaw ::
608+ IORef ([ChildEvent a], Int) -> Maybe SVarStats -> IO ([ChildEvent a], Int)
609+readOutputQRaw q stats = do
610+ (list, len) <- readOutputQBasic q
611+ case stats of
612+ Just ss -> do
613+ let ref = maxOutQSize ss
614+ oqLen <- readIORef ref
615+ when (len > oqLen) $ writeIORef ref len
616+ Nothing -> return ()
617+ return (list, len)
618+
619+{-# INLINE ringDoorBell #-}
620+ringDoorBell :: IORef Bool -> MVar () -> IO ()
621+ringDoorBell needBell bell = do
622+ storeLoadBarrier
623+ w <- readIORef needBell
624+ when w $ do
625+ -- Note: the sequence of operations is important for correctness here.
626+ -- We need to set the flag to false strictly before sending the
627+ -- outputDoorBell, otherwise the outputDoorBell may get processed too
628+ -- early and then we may set the flag to False to later making the
629+ -- consumer lose the flag, even without receiving a outputDoorBell.
630+ atomicModifyIORefCAS_ needBell (const False)
631+ void $ tryPutMVar bell ()
632+
633+-------------------------------------------------------------------------------
634+-- Diagnostics
635+-------------------------------------------------------------------------------
636+
637+dumpCreator :: Show a => a -> String
638+dumpCreator tid = "Creator tid = " <> show tid
639+
640+dumpOutputQ :: (Foldable t, Show a1) => IORef (t a2, a1) -> IO String
641+dumpOutputQ q = do
642+ (oqList, oqLen) <- readIORef q
643+ return $ unlines
644+ [ "outputQueue length computed = " <> show (length oqList)
645+ , "outputQueue length maintained = " <> show oqLen
646+ ]
647+
648+dumpDoorBell :: Show a => MVar a -> IO String
649+dumpDoorBell mvar = do
650+ db <- tryReadMVar mvar
651+ return $ "outputDoorBell = " <> show db
652+
653+dumpNeedDoorBell :: Show a => IORef a -> IO String
654+dumpNeedDoorBell ref = do
655+ waiting <- readIORef ref
656+ return $ "needDoorBell = " <> show waiting
657+
658+dumpRunningThreads :: Show a => IORef a -> IO String
659+dumpRunningThreads ref = do
660+ rthread <- readIORef ref
661+ return $ "running threads = " <> show rthread
662+
663+dumpWorkerCount :: Show a => IORef a -> IO String
664+dumpWorkerCount ref = do
665+ workers <- readIORef ref
666+ return $ "running thread count = " <> show workers
667+
668+{-# NOINLINE mvarExcHandler #-}
669+mvarExcHandler :: IO String -> String -> BlockedIndefinitelyOnMVar -> IO ()
670+mvarExcHandler dump label e@BlockedIndefinitelyOnMVar = do
671+ svInfo <- dump
672+ hPutStrLn stderr $ label <> " " <> "BlockedIndefinitelyOnMVar\n" <> svInfo
673+ throwIO e
674+
675+{-# NOINLINE stmExcHandler #-}
676+stmExcHandler :: IO String -> String -> BlockedIndefinitelyOnSTM -> IO ()
677+stmExcHandler dump label e@BlockedIndefinitelyOnSTM = do
678+ svInfo <- dump
679+ hPutStrLn stderr $ label <> " " <> "BlockedIndefinitelyOnSTM\n" <> svInfo
680+ throwIO e
681+
682+-- | MVar diagnostics has some overhead - around 5% on AsyncT null benchmark, we
683+-- can keep it on in production to debug problems quickly if and when they
684+-- happen, but it may result in unexpected output when threads are left hanging
685+-- until they are GCed because the consumer went away.
686+withDiagMVar :: Bool -> IO String -> String -> IO () -> IO ()
687+withDiagMVar inspect dump label action =
688+ if inspect
689+ then
690+ action `catches` [ Handler (mvarExcHandler dump label)
691+ , Handler (stmExcHandler dump label)
692+ ]
693+ else action
694+
695+printSVar :: IO String -> String -> IO ()
696+printSVar dump how = do
697+ svInfo <- dump
698+ hPutStrLn stderr $ "\n" <> how <> "\n" <> svInfo
699+
700+-------------------------------------------------------------------------------
701+-- Cleanup
702+-------------------------------------------------------------------------------
703+
704+-- Can be called from a worker as well, avoids throwing to itself
705+cleanupSVar :: IORef (Set ThreadId) -> IO ()
706+cleanupSVar workerSet = do
707+ workers <- readIORef workerSet
708+ self <- myThreadId
709+ Prelude.mapM_ (`throwTo` ThreadAbort)
710+ (Prelude.filter (/= self) $ Set.toList workers)
src/Streamly/Internal/Data/Stream/Async/Channel/Worker.hs → src/Streamly/Internal/Data/Stream/Channel/Worker.hsrenamed+73−134
@@ -6,34 +6,18 @@
66-- Stability : experimental
77-- Portability : GHC
88--
9−--
10−module Streamly.Internal.Data.Stream.Async.Channel.Worker
11− (
12− -- * Adjusting Limits
13− decrementYieldLimit
14− , incrementYieldLimit
9+-- Collecting results from child workers in a streamed fashion
1510
16− -- * Rate Control
17− , Work (..)
18− , isBeyondMaxRate
11+module Streamly.Internal.Data.Stream.Channel.Worker
12+ (
13+ Work (..)
1914 , estimateWorkers
20− , updateYieldCount
21− , minThreadDelay
22− , workerRateControl
23− , workerUpdateLatency
2415
2516 -- * Send Events
26− , send
27− , ringDoorBell
28−
29− -- ** Yield
17+ , sendWithDoorBell
3018 , sendYield
31−
32− -- ** Stop
3319 , sendStop
34−
35− -- ** Exception
36− , handleChildException
20+ , handleChildException -- XXX rename to sendException
3721 )
3822where
3923
@@ -43,48 +27,12 @@ import Control.Exception (SomeException(..), assert)
4327import Control.Monad (when, void)
4428import Data.IORef (IORef, readIORef, writeIORef)
4529import Streamly.Internal.Data.Atomics
46− (atomicModifyIORefCAS, atomicModifyIORefCAS_, writeBarrier,
47− storeLoadBarrier)
30+ (atomicModifyIORefCAS, atomicModifyIORefCAS_, writeBarrier)
4831import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
4932import Streamly.Internal.Data.Time.Units
5033 (AbsTime, NanoSecond64(..), diffAbsTime64, fromRelTime64)
5134
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)
35+import Streamly.Internal.Data.Stream.Channel.Types
8836
8937-------------------------------------------------------------------------------
9038-- Yield control
@@ -106,20 +54,6 @@ isBeyondMaxYield cnt winfo =
10654-- Sending results from worker
10755-------------------------------------------------------------------------------
10856
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−
12357{-# INLINE sendWithDoorBell #-}
12458sendWithDoorBell ::
12559 IORef ([ChildEvent a], Int) -> MVar () -> ChildEvent a -> IO Int
@@ -140,11 +74,6 @@ sendWithDoorBell q bell msg = do
14074 void $ tryPutMVar bell ()
14175 return oldlen
14276
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−
14877-------------------------------------------------------------------------------
14978-- Collect and update worker latency
15079-------------------------------------------------------------------------------
@@ -206,17 +135,6 @@ data Work
206135 | ManyWorkers Int Count
207136 deriving Show
208137
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−
220138-- | Another magic number! When we have to start more workers to cover up a
221139-- number of yields that we are lagging by then we cannot start one worker for
222140-- each yield because that may be a very big number and if the latency of the
@@ -333,16 +251,16 @@ estimateWorkers workerLimit svarYields gainLossYields
333251 Unlimited -> n
334252 Limited x -> min n (fromIntegral x)
335253
336−isBeyondMaxRate :: Channel m a -> YieldRateInfo -> IO Bool
337−isBeyondMaxRate sv yinfo = do
338− (count, tstamp, wLatency) <- getWorkerLatency yinfo
254+isBeyondMaxRate :: Limit -> IORef Int -> YieldRateInfo -> IO Bool
255+isBeyondMaxRate workerLimit workerCount rateInfo = do
256+ (count, tstamp, wLatency) <- getWorkerLatency rateInfo
339257 now <- getTime Monotonic
340258 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
259+ let targetLat = svarLatencyTarget rateInfo
260+ gainLoss <- readIORef (svarGainedLostYields rateInfo)
261+ let work = estimateWorkers workerLimit count gainLoss duration
262+ wLatency targetLat (svarLatencyRange rateInfo)
263+ cnt <- readIORef workerCount
346264 return $ case work of
347265 -- XXX set the worker's maxYields or polling interval based on yields
348266 PartialWorker _yields -> cnt > 1
@@ -354,29 +272,31 @@ isBeyondMaxRate sv yinfo = do
354272-- and we should stop based on the aggregate yields. However, latency update
355273-- period can be based on individual worker yields.
356274{-# 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)
275+checkRatePeriodic ::
276+ Limit
277+ -> IORef Int
278+ -> YieldRateInfo
279+ -> WorkerInfo
280+ -> Count
281+ -> IO Bool
282+checkRatePeriodic workerLimit workerCount rateInfo workerInfo ycnt = do
283+ i <- readIORef (workerPollingInterval rateInfo)
364284 -- XXX use generation count to check if the interval has been updated
365285 if i /= 0 && (ycnt `mod` i) == 0
366286 then do
367− workerUpdateLatency yinfo winfo
287+ workerUpdateLatency rateInfo workerInfo
368288 -- XXX not required for parallel streams
369− isBeyondMaxRate sv yinfo
289+ isBeyondMaxRate workerLimit workerCount rateInfo
370290 else return False
371291
372−-- CAUTION! this also updates the yield count and therefore should be called
292+-- | CAUTION! this also updates the yield count and therefore should be called
373293-- only when we are actually yielding an element.
374294{-# 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)
295+workerRateControl :: Limit -> IORef Int -> YieldRateInfo -> WorkerInfo -> IO Bool
296+workerRateControl workerLimit workerCount rateInfo workerInfo = do
297+ cnt <- updateYieldCount workerInfo
298+ beyondMaxRate <- checkRatePeriodic workerLimit workerCount rateInfo workerInfo cnt
299+ return $ not (isBeyondMaxYield cnt workerInfo || beyondMaxRate)
380300
381301-------------------------------------------------------------------------------
382302-- Send a yield event
@@ -385,24 +305,35 @@ workerRateControl sv yinfo winfo = do
385305-- XXX we should do rate control here but not latency update in case of ahead
386306-- streams. latency update must be done when we yield directly to outputQueue
387307-- or when we yield to heap.
388−--
389−-- returns whether the worker should continue (True) or stop (False).
308+
309+-- | Returns whether the worker should continue (True) or stop (False).
390310{-# 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
311+sendYield ::
312+ Limit
313+ -> Limit
314+ -> IORef Int
315+ -> Maybe WorkerInfo
316+ -> Maybe YieldRateInfo
317+ -> IORef ([ChildEvent a], Int)
318+ -> MVar ()
319+ -> ChildEvent a
320+ -> IO Bool
321+sendYield bufferLimit workerLimit workerCount workerInfo rateInfo q bell msg =
322+ do
323+ oldlen <- sendWithDoorBell q bell msg
324+ bufferSpaceOk <-
325+ case bufferLimit of
396326 Unlimited -> return True
397327 Limited lim -> do
398− active <- readIORef (workerCount sv)
328+ active <- readIORef workerCount
399329 return $ (oldlen + 1) < (fromIntegral lim - active)
400330 rateLimitOk <-
401− case mwinfo of
331+ case workerInfo of
402332 Just winfo ->
403− case yieldRateInfo sv of
333+ case rateInfo of
404334 Nothing -> return True
405− Just yinfo -> workerRateControl sv yinfo winfo
335+ Just yinfo ->
336+ workerRateControl workerLimit workerCount yinfo winfo
406337 Nothing -> return True
407338 return $ bufferSpaceOk && rateLimitOk
408339
@@ -417,18 +348,26 @@ workerStopUpdate winfo info = do
417348 when (i /= 0) $ workerUpdateLatency info winfo
418349
419350{-# 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
351+sendStop ::
352+ IORef Int
353+ -> Maybe WorkerInfo
354+ -> Maybe YieldRateInfo
355+ -> IORef ([ChildEvent a], Int)
356+ -> MVar ()
357+ -> IO ()
358+sendStop workerCount workerInfo rateInfo q bell = do
359+ atomicModifyIORefCAS_ workerCount $ \n -> n - 1
360+ case (workerInfo, rateInfo) of
361+ (Just winfo, Just rinfo) ->
362+ workerStopUpdate winfo rinfo
426363 _ ->
427364 return ()
428− myThreadId >>= \tid -> void $ send sv (ChildStop tid Nothing)
365+ myThreadId >>= \tid ->
366+ void $ sendWithDoorBell q bell (ChildStop tid Nothing)
429367
430368{-# NOINLINE handleChildException #-}
431−handleChildException :: Channel m a -> SomeException -> IO ()
432−handleChildException sv e = do
369+handleChildException ::
370+ IORef ([ChildEvent a], Int) -> MVar () -> SomeException -> IO ()
371+handleChildException q bell e = do
433372 tid <- myThreadId
434− void $ send sv (ChildStop tid (Just e))
373+ void $ sendWithDoorBell q bell (ChildStop tid (Just e))
streamly.cabal+4−1
@@ -447,8 +447,11 @@ library
447447
448448 , Streamly.Internal.Data.Stream.Serial
449449 , Streamly.Internal.Data.Stream.WSerial
450+ , Streamly.Internal.Data.Stream.Channel.Types
451+ , Streamly.Internal.Data.Stream.Channel.Dispatcher
452+ , Streamly.Internal.Data.Stream.Channel.Worker
453+
450454 , Streamly.Internal.Data.Stream.Async.Channel.Type
451− , Streamly.Internal.Data.Stream.Async.Channel.Worker
452455 , Streamly.Internal.Data.Stream.Async.Channel.Dispatcher
453456 , Streamly.Internal.Data.Stream.Async.Channel.Consumer
454457 , Streamly.Internal.Data.Stream.Async.Channel.Append