2-- Module : Streamly.Internal.Data.Stream.Concurrent.Channel.Dispatcher
3-- Copyright : (c) 2017 Composewell Technologies
4-- License : BSD-3-Clause
5-- Maintainer : streamly@composewell.com
6-- Stability : experimental
10module Streamly.Internal.Data.Stream.Concurrent.Channel.Dispatcher
19 , sendWorkerDelayPaced
23import Control.Concurrent (takeMVar, threadDelay)
24import Control.Exception (assert)
25import Control.Monad (when, void)
26import Control.Monad.IO.Class (MonadIO(liftIO))
27import Data.Maybe (fromJust, fromMaybe)
28import Data.IORef (modifyIORef, newIORef, readIORef, writeIORef)
29import Streamly.Internal.Control.Concurrent (MonadRunInIO)
30import Streamly.Internal.Control.ForkLifted (doFork)
31import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS_, storeLoadBarrier)
32import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
33import Streamly.Internal.Data.Time.Units
34 (MicroSecond64(..), diffAbsTime64, fromRelTime64, toRelTime64)
36import Streamly.Internal.Data.Stream.Concurrent.Channel.Type
37import Streamly.Internal.Data.Stream.Channel.Dispatcher
38import Streamly.Internal.Data.Stream.Channel.Types
39import Streamly.Internal.Data.Stream.Channel.Worker
41-------------------------------------------------------------------------------
43-------------------------------------------------------------------------------
45{-# NOINLINE pushWorker #-}
46pushWorker :: MonadRunInIO m => Count -> Channel m a -> m ()
47pushWorker yieldMax sv = do
48 liftIO $ atomicModifyIORefCAS_ (workerCount sv) $ \n -> n + 1
49 when (svarInspectMode sv)
50 $ recordMaxWorkers (workerCount sv) (svarStats sv)
51 -- This allocation matters when significant number of workers are being
52 -- sent. We allocate it only when needed.
54 case yieldRateInfo sv of
55 Nothing -> return Nothing
58 t <- getTime Monotonic
59 lat <- newIORef (0, t)
60 return $ Just WorkerInfo
61 { workerYieldMax = yieldMax
62 , workerYieldCount = cntRef
63 , workerLatencyStart = lat
65 -- In case of lazy dispatch we dispatch workers only from the consumer
66 -- thread. In that case it is ok to use addThread here as it is guaranteed
67 -- that the thread will be added to the workerSet before the thread STOP
68 -- event is processed, because we do both of these actions in the same
69 -- consumer thread. However, in case of eager dispatch we may dispatch
70 -- workers from workers, in which case the thread Stop even may get
71 -- processed before the addThread occurs, so in that case we have to use
72 -- modifyThread which performs a toggle rather than adding or deleting.
74 -- XXX We can use addThread or modThread based on eager flag.
75 doFork (workLoop sv winfo) (svarMrun sv) exception >>= modThread
79 modThread = modifyThread (workerThreads sv) (outputDoorBell sv)
80 exception = handleChildException (outputQueue sv) (outputDoorBell sv)
82-- | Determine the maximum number of workers required based on 'maxWorkerLimit'
83-- and 'remainingWork'.
84{-# INLINE getEffectiveWorkerLimit #-}
85getEffectiveWorkerLimit :: MonadIO m => Channel m a -> m Limit
86getEffectiveWorkerLimit sv = do
87 let workerLimit = maxWorkerLimit sv
88 case remainingWork sv of
89 Nothing -> return workerLimit
91 n <- liftIO $ readIORef ref
92 case yieldRateInfo sv of
93 Just _ -> return workerLimit
97 Unlimited -> Limited (fromIntegral n)
98 Limited lim -> Limited $ min lim (fromIntegral n)
100-- | Determine whether the active threads are more than the max threads we are
101-- allowed to dispatch.
102{-# INLINE checkMaxThreads #-}
103checkMaxThreads :: MonadIO m => Int -> Channel m a -> m Bool
104checkMaxThreads active sv = do
105 -- Note that we may deadlock if the previous workers (tasks in the
106 -- stream) wait/depend on the future workers (tasks in the stream)
107 -- executing. In that case we should either configure the maxWorker
108 -- count to higher or use parallel style instead of ahead or async
110 limit <- getEffectiveWorkerLimit sv
114 -- Note that the use of remainingWork and workerCount is not
115 -- atomic and the counts may even have changed between reading
116 -- and using them here, so this is just approximate logic and
117 -- we cannot rely on it for correctness. We may actually
118 -- dispatch more workers than required.
119 Limited lim -> fromIntegral lim > active
121-- | Determine whether we would exceed max buffer if we dispatch more workers
122-- based on the current outputQueue size and active workers.
123{-# INLINE checkMaxBuffer #-}
124checkMaxBuffer :: MonadIO m => Int -> Channel m a -> m Bool
125checkMaxBuffer active sv = do
126 let limit = maxBufferLimit sv
128 Unlimited -> return True
130 (_, n) <- liftIO $ readIORef (outputQueue sv)
131 return $ fromIntegral lim > n + active
133dispatchWorker :: MonadRunInIO m =>
134 Count -> Channel m a -> m Bool
135dispatchWorker yieldCount sv = do
136 -- XXX in case of Ahead streams we should not send more than one worker
137 -- when the work queue is done but heap is not done.
138 -- XXX Should we have a single abstraction for checking q and
139 -- work instead checking the two separately?
140 done <- liftIO $ isWorkDone sv
141 -- Note, "done" may not mean that the work is actually finished if there
142 -- are workers active, because there may be a worker which has not yet
143 -- queued the leftover work.
146 qDone <- liftIO $ isQueueDone sv
147 -- This count may be more until the sendStop events are processed.
148 active <- liftIO $ readIORef $ workerCount sv
149 when (active < 0) $ error "dispatchWorker active negative"
152 -- XXX for ahead streams shall we take the heap yields into account
153 -- for controlling the dispatch? We should not dispatch if the heap
154 -- has already got the limit covered.
155 r <- checkMaxThreads active sv
158 r1 <- checkMaxBuffer active sv
160 then pushWorker yieldCount sv >> return True
164 when (active <= 0) $ do
165 r <- liftIO $ isWorkDone sv
166 when (not r) $ pushWorker 0 sv
170-- XXX in case of ahead style stream we need to take the heap size into account
171-- because we return the workers on the basis of that which causes a condition
172-- where we keep dispatching and they keep returning. So we must have exactly
173-- the same logic for not dispatching and for returning.
176-- True: can dispatch more
177-- False: full, no more dispatches
178dispatchWorkerPaced :: MonadRunInIO m =>
179 Channel m a -> m Bool
180dispatchWorkerPaced sv = do
181 let yinfo = fromJust $ yieldRateInfo sv
182 (svarYields, svarElapsed, wLatency) <- do
183 now <- liftIO $ getTime Monotonic
184 (yieldCount, baseTime, lat) <-
187 (svarInspectMode sv) (svarStats sv) yinfo False
188 let elapsed = fromRelTime64 $ diffAbsTime64 now baseTime
191 then fromMaybe lat (workerBootstrapLatency yinfo)
194 return (yieldCount, elapsed, latency)
197 -- Need to measure the latency with a single worker before we can perform
201 let workerLimit = maxWorkerLimit sv
202 let targetLat = svarLatencyTarget yinfo
203 let range = svarLatencyRange yinfo
204 gainLoss <- liftIO $ readIORef (svarGainedLostYields yinfo)
205 let work = estimateWorkers workerLimit svarYields gainLoss svarElapsed
206 wLatency targetLat range
208 -- XXX we need to take yieldLimit into account here. If we are at the
209 -- end of the limit as well as the time, we should not be sleeping.
210 -- If we are not actually planning to dispatch any more workers we need
211 -- to take that in account.
214 assert (s >= 0) (return ())
215 -- XXX note that when we return from here we will block waiting
216 -- for the result from the existing worker. If that takes too
217 -- long we won't be able to send another worker until the
220 -- Sleep only if there are no active workers, otherwise we will
221 -- defer the output of those. Note we cannot use workerCount
222 -- here as it is not a reliable way to ensure there are
223 -- definitely no active workers. When workerCount is 0 we may
224 -- still have a Stop event waiting in the outputQueue.
225 done <- allThreadsDone (workerThreads sv)
226 when done $ void $ do
227 let us = fromRelTime64 (toRelTime64 s) :: MicroSecond64
228 liftIO $ threadDelay (fromIntegral us)
231 PartialWorker yields -> do
232 assert (yields > 0) (return ())
233 updateGainedLostYields yinfo yields
235 done <- allThreadsDone (workerThreads sv)
236 when done $ void $ dispatchWorker yields sv
238 ManyWorkers netWorkers yields -> do
239 assert (netWorkers >= 1) (return ())
240 assert (yields >= 0) (return ())
241 updateGainedLostYields yinfo yields
243 let periodRef = workerPollingInterval yinfo
244 ycnt = max 1 $ yields `div` fromIntegral netWorkers
245 period = min ycnt (fromIntegral magicMaxBuffer)
247 old <- liftIO $ readIORef periodRef
248 when (period < old) $
249 liftIO $ writeIORef periodRef period
251 cnt <- liftIO $ readIORef $ workerCount sv
254 let total = netWorkers - cnt
255 batch = max 1 $ fromIntegral $
256 minThreadDelay `div` targetLat
257 -- XXX stagger the workers over a period?
258 -- XXX cannot sleep, as that would mean we cannot process
259 -- the outputs. need to try a different mechanism to
261 -- when (total > batch) $
262 -- liftIO $ threadDelay $ nanoToMicroSecs minThreadDelay
263 dispatchN (min total batch)
268 updateGainedLostYields yinfo yields = do
269 let buf = fromIntegral $ svarRateBuffer yinfo
270 when (yields /= 0 && abs yields > buf) $ do
275 liftIO $ modifyIORef (svarGainedLostYields yinfo) (+ delta)
281 r <- dispatchWorker 0 sv
283 then dispatchN (n - 1)
286{-# NOINLINE sendWorkerWait #-}
290 -> (Channel m a -> IO ())
291 -> (Channel m a -> m Bool)
294sendWorkerWait eagerEval delay dispatch sv = go
300 -- Note that we are guaranteed to have at least one outstanding worker
301 -- when we enter this function. So if we sleep we are guaranteed to be
302 -- woken up by an outputDoorBell, when the worker exits.
305 (_, n) <- liftIO $ readIORef (outputQueue sv)
306 when (n <= 0 || eagerEval) $ do
307 -- The queue may be empty temporarily if the worker has dequeued
308 -- the work item but has not enqueued the remaining part yet. For
309 -- the same reason, a worker may come back if it tries to dequeue
310 -- and finds the queue empty, even though the whole work has not
313 -- If we find that the queue is empty, but it may be empty
314 -- temporarily, when we checked it. If that's the case we might
315 -- sleep indefinitely unless the active workers produce some
316 -- output. We may deadlock specially if the otuput from the active
317 -- workers depends on the future workers that we may never send.
318 -- So in case the queue was temporarily empty set a flag to inform
319 -- the enqueue to send us a doorbell.
321 -- Note that this is just a best effort mechanism to avoid a
322 -- deadlock. Deadlocks may still happen if for some weird reason
323 -- the consuming computation shares an MVar or some other resource
324 -- with the producing computation and gets blocked on that resource
325 -- and therefore cannot do any pushworker to add more threads to
326 -- the producer. In such cases the programmer should use a parallel
327 -- style so that all the producers are scheduled immediately and
328 -- unconditionally. We can also use a separate monitor thread to
329 -- push workers instead of pushing them from the consumer, but then
330 -- we are no longer using pull based concurrency rate adaptation.
332 -- XXX update this in the tutorial.
334 -- Having pending active workers does not mean that we are
335 -- guaranteed to be woken up if we sleep. In case of Ahead streams,
336 -- there may be queued items in the heap even though the
337 -- outputQueue is empty, and we may have active workers which are
338 -- deadlocked on those items to be processed by the consumer. We
339 -- should either guarantee that any worker, before returning,
340 -- clears the heap or we send a worker to clear it. Normally we
341 -- always send a worker if no output is seen, but if the thread
342 -- limit is reached or we are using pacing then we may not send a
343 -- worker. See the concurrentApplication test in the tests, that
344 -- test case requires at least one yield from the producer to not
345 -- deadlock, if the last workers output is stuck in the heap then
346 -- this test fails. This problem can be extended to n threads when
347 -- the consumer may depend on the evaluation of next n items in the
350 -- register for the outputDoorBell before we check the queue so
351 -- that if we sleep because the queue was empty we are guaranteed
352 -- to get a doorbell on the next enqueue.
354 liftIO $ atomicModifyIORefCAS_ (doorBellOnWorkQ sv) $ const True
355 liftIO storeLoadBarrier
356 canDoMore <- dispatch sv
358 -- XXX test for the case when we miss sending a worker when the
359 -- worker count is more than 1500.
361 -- XXX Assert here that if the heap is not empty then there is at
362 -- least one outstanding worker. Otherwise we could be sleeping
372 "sendWorkerWait: nothing to do"
373 $ takeMVar (outputDoorBell sv)
374 (_, len) <- liftIO $ readIORef (outputQueue sv)
379 $ atomicModifyIORefCAS_ (doorBellOnWorkQ sv)
382-- | Start the evaluation of the channel's work queue by kicking off a worker.
383-- Note: Work queue must not be empty otherwise the worker will exit without
385startChannel :: MonadRunInIO m =>
387startChannel chan = do
388 case yieldRateInfo chan of
389 Nothing -> pushWorker 0 chan
391 if svarLatencyTarget yinfo == maxBound
392 then liftIO $ threadDelay maxBound
393 else pushWorker 1 chan
395sendWorkerDelayPaced :: Channel m a -> IO ()
396sendWorkerDelayPaced _ = return ()
398sendWorkerDelay :: Channel m a -> IO ()
400 -- XXX we need a better way to handle this than hardcoded delays. The
401 -- delays may be different for different systems.
402 -- If there is a usecase where this is required we can create a combinator
403 -- to set it as a config in the state.
406 ncpu <- getNumCapabilities
409 if (svarStyle sv == AheadVar)
413 if (svarStyle sv == AheadVar)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list