on edbb7b8 help sign in
689 lines 25 kB history raw
1#ifdef __HADDOCK_VERSION__
2#undef INSPECTION
3#endif
5#ifdef INSPECTION
6{-# LANGUAGE TemplateHaskell #-}
7{-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-}
8#endif
10-- |
11-- Module : Streamly.Internal.Data.Stream.SVar
12-- Copyright : (c) 2017 Composewell Technologies
13--
14-- License : BSD3
15-- Maintainer : streamly@composewell.com
16-- Stability : experimental
17-- Portability : GHC
18--
19--
20module Streamly.Internal.Data.Stream.SVar
21 (
22 -- * Unfold streams from SVar
23 -- $concurrentEval
24 fromSVar
25 , fromSVarD
26 -- , fromStreamVar
28 -- * Fold streams to SVar
29 , toSVar
30 , toSVarParallel
32 -- * Concurrent folds
33 -- $concurrentFolds
34 , fromConsumer
35 , pushToFold
36 , teeToSVar
37 , newFoldSVar
38 , newFoldSVarF
39 )
40where
42#include "inline.hs"
44import Control.Concurrent (myThreadId, takeMVar)
45import Control.Exception (fromException)
46import Control.Monad (when, void)
47import Control.Monad.Catch (throwM)
48import Control.Monad.IO.Class (MonadIO(liftIO))
49import Data.IORef (newIORef, readIORef, mkWeakIORef, writeIORef)
50import Data.Maybe (isNothing)
51import Streamly.Internal.Data.Atomics (atomicModifyIORefCAS_)
52import Streamly.Internal.Data.Fold.Type (Fold(..))
53import Streamly.Internal.Data.Time.Clock (Clock(Monotonic), getTime)
54import System.Mem (performMajorGC)
56import qualified Streamly.Internal.Data.Fold.Type as FL
57import qualified Streamly.Internal.Data.Stream.StreamD.Type as D
58import qualified Streamly.Internal.Data.Stream.StreamK.Type as K
60import Streamly.Internal.Data.SVar
62#if __GLASGOW_HASKELL__ < 810
63#ifdef INSPECTION
64import Control.Exception (Exception)
65import Control.Monad.Catch (MonadThrow)
66import Control.Monad.Trans.Control (MonadBaseControl)
67import Data.Typeable (Typeable)
68import Test.Inspection (inspect, hasNoTypeClassesExcept)
69#endif
70#endif
72------------------------------------------------------------------------------
73-- Generating streams from SVar
74------------------------------------------------------------------------------
76-- $concurrentEval
77--
78-- Usually the SVar is used to concurrently evaluate multiple actions in a
79-- stream using many worker threads that push the results to the SVar and a
80-- single puller that pulls them from SVar generating the evaluated stream.
81--
82-- @
83-- input stream
84-- |
85-- <-----------------|<--------worker
86-- | exceptions |
87-- output stream <------SVar<------worker
88-- |
89-- |<--------worker
90--
91-- @
92--
93-- The puller itself schedules the worker threads based on demand.
94-- Exceptions are propagated from the worker threads to the puller.
96-- | Pull a stream from an SVar.
97{-# NOINLINE fromStreamVar #-}
98fromStreamVar :: MonadAsync m => SVar K.Stream m a -> K.Stream m a
99fromStreamVar sv = K.MkStream $ \st yld sng stp -> do
100 list <- readOutputQ sv
101 -- Reversing the output is important to guarantee that we process the
102 -- outputs in the same order as they were generated by the constituent
103 -- streams.
104 K.foldStream st yld sng stp $ processEvents $ reverse list
106 where
108 allDone stp = do
109 when (svarInspectMode sv) $ do
110 t <- liftIO $ getTime Monotonic
111 liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
112 liftIO $ printSVar sv "SVar Done"
113 stp
115 {-# INLINE processEvents #-}
116 processEvents [] = K.MkStream $ \st yld sng stp -> do
117 done <- postProcess sv
118 if done
119 then allDone stp
120 else K.foldStream st yld sng stp $ fromStreamVar sv
122 processEvents (ev : es) = K.MkStream $ \st yld sng stp -> do
123 let rest = processEvents es
124 case ev of
125 ChildYield a -> yld a rest
126 ChildStop tid e -> do
127 accountThread sv tid
128 case e of
129 Nothing -> do
130 stop <- shouldStop tid
131 if stop
132 then liftIO (cleanupSVar sv) >> allDone stp
133 else K.foldStream st yld sng stp rest
134 Just ex ->
135 case fromException ex of
136 Just ThreadAbort ->
137 K.foldStream st yld sng stp rest
138 Nothing -> liftIO (cleanupSVar sv) >> throwM ex
139 shouldStop tid =
140 case svarStopStyle sv of
141 StopNone -> return False
142 StopAny -> return True
143 StopBy -> do
144 sid <- liftIO $ readIORef (svarStopBy sv)
145 return $ tid == sid
147#if __GLASGOW_HASKELL__ < 810
148#ifdef INSPECTION
149-- Use of GHC constraint tuple (GHC.Classes.(%,,%)) in fromStreamVar leads to
150-- space leak because the tuple gets allocated in every recursive call and each
151-- allocation holds on to the previous allocation. This test is to make sure
152-- that we do not use the constraint tuple type class.
153--
154inspect $ hasNoTypeClassesExcept 'fromStreamVar
155 [ ''Monad
156 , ''Applicative
157 , ''MonadThrow
158 , ''Exception
159 , ''MonadIO
160 , ''MonadBaseControl
161 , ''Typeable
162 , ''Functor
163 ]
164#endif
165#endif
167-- | Generate a stream from an SVar. An unevaluated stream can be pushed to an
168-- SVar using 'toSVar'. As we pull a stream from the SVar the input stream
169-- gets evaluated concurrently. The evaluation depends on the SVar style and
170-- the configuration parameters e.g. using the maxBuffer/maxThreads
171-- combinators.
172--
173{-# INLINE fromSVar #-}
174fromSVar :: (MonadAsync m, K.IsStream t) => SVar K.Stream m a -> t m a
175fromSVar sv =
176 K.mkStream $ \st yld sng stp -> do
177 ref <- liftIO $ newIORef ()
178 _ <- liftIO $ mkWeakIORef ref hook
179 -- We pass a copy of sv to fromStreamVar, so that we know that it has
180 -- no other references, when that copy gets garbage collected "ref"
181 -- will get garbage collected and our hook will be called.
182 K.foldStreamShared st yld sng stp $
183 K.fromStream $ fromStreamVar sv{svarRef = Just ref}
184 where
186 hook = do
187 when (svarInspectMode sv) $ do
188 r <- liftIO $ readIORef (svarStopTime (svarStats sv))
189 when (isNothing r) $
190 printSVar sv "SVar Garbage Collected"
191 cleanupSVar sv
192 -- If there are any SVars referenced by this SVar a GC will prompt
193 -- them to be cleaned up quickly.
194 when (svarInspectMode sv) performMajorGC
196data FromSVarState t m a =
197 FromSVarInit
198 | FromSVarRead (SVar t m a)
199 | FromSVarLoop (SVar t m a) [ChildEvent a]
200 | FromSVarDone (SVar t m a)
202-- | Like 'fromSVar' but generates a StreamD style stream instead of CPS.
203--
204{-# INLINE_NORMAL fromSVarD #-}
205fromSVarD :: (MonadAsync m) => SVar t m a -> D.Stream m a
206fromSVarD svar = D.Stream step FromSVarInit
207 where
209 {-# INLINE_LATE step #-}
210 step _ FromSVarInit = do
211 ref <- liftIO $ newIORef ()
212 _ <- liftIO $ mkWeakIORef ref hook
213 -- when this copy of svar gets garbage collected "ref" will get
214 -- garbage collected and our GC hook will be called.
215 let sv = svar{svarRef = Just ref}
216 return $ D.Skip (FromSVarRead sv)
218 where
220 {-# NOINLINE hook #-}
221 hook = do
222 when (svarInspectMode svar) $ do
223 r <- liftIO $ readIORef (svarStopTime (svarStats svar))
224 when (isNothing r) $
225 printSVar svar "SVar Garbage Collected"
226 cleanupSVar svar
227 -- If there are any SVars referenced by this SVar a GC will prompt
228 -- them to be cleaned up quickly.
229 when (svarInspectMode svar) performMajorGC
231 step _ (FromSVarRead sv) = do
232 list <- readOutputQ sv
233 -- Reversing the output is important to guarantee that we process the
234 -- outputs in the same order as they were generated by the constituent
235 -- streams.
236 return $ D.Skip $ FromSVarLoop sv (Prelude.reverse list)
238 step _ (FromSVarLoop sv []) = do
239 done <- postProcess sv
240 return $ D.Skip $ if done
241 then FromSVarDone sv
242 else FromSVarRead sv
244 step _ (FromSVarLoop sv (ev : es)) = do
245 case ev of
246 ChildYield a -> return $ D.Yield a (FromSVarLoop sv es)
247 ChildStop tid e -> do
248 accountThread sv tid
249 case e of
250 Nothing -> do
251 stop <- shouldStop tid
252 if stop
253 then do
254 liftIO (cleanupSVar sv)
255 return $ D.Skip (FromSVarDone sv)
256 else return $ D.Skip (FromSVarLoop sv es)
257 Just ex ->
258 case fromException ex of
259 Just ThreadAbort ->
260 return $ D.Skip (FromSVarLoop sv es)
261 Nothing -> liftIO (cleanupSVar sv) >> throwM ex
262 where
264 shouldStop tid =
265 case svarStopStyle sv of
266 StopNone -> return False
267 StopAny -> return True
268 StopBy -> do
269 sid <- liftIO $ readIORef (svarStopBy sv)
270 return $ tid == sid
272 step _ (FromSVarDone sv) = do
273 when (svarInspectMode sv) $ do
274 t <- liftIO $ getTime Monotonic
275 liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
276 liftIO $ printSVar sv "SVar Done"
277 return D.Stop
279------------------------------------------------------------------------------
280-- Writing streams to SVar
281------------------------------------------------------------------------------
283-- | Write a stream to an 'SVar' in a non-blocking manner. The stream is
284-- evaluated concurrently as it is read back from the SVar using 'fromSVar'.
285--
286toSVar :: (K.IsStream t, MonadAsync m) => SVar K.Stream m a -> t m a -> m ()
287toSVar sv m = toStreamVar sv $ K.toStream m
289-------------------------------------------------------------------------------
290-- Concurrent application
291-------------------------------------------------------------------------------
293-- | A fold to write a stream to an SVar. Unlike 'toSVar' this does not allow
294-- for concurrent evaluation of the stream, as the fold receives the input one
295-- element at a time, it just forwards the elements to the SVar. However, we
296-- can safely execute the fold in an independent thread, the SVar can act as a
297-- buffer decoupling the sender from the receiver. Also, we can have multiple
298-- folds running concurrently pusing the streams to the SVar.
299--
300{-# INLINE write #-}
301write :: MonadIO m => SVar t m a -> Maybe WorkerInfo -> Fold m a ()
302write svar winfo = Fold step initial extract
304 where
306 initial = return $ FL.Partial ()
308 -- XXX we can have a separate fold for unlimited buffer case to avoid a
309 -- branch in the step here.
310 step () x =
311 liftIO $ do
312 decrementBufferLimit svar
313 void $ send svar (ChildYield x)
314 return $ FL.Partial ()
316 extract () = liftIO $ sendStop svar winfo
318-- | Like write, but applies a yield limit.
319--
320{-# INLINE writeLimited #-}
321writeLimited :: MonadIO m
322 => SVar t m a -> Maybe WorkerInfo -> Fold m a ()
323writeLimited svar winfo = Fold step initial extract
325 where
327 initial = return $ FL.Partial True
329 step True x =
330 liftIO $ do
331 yieldLimitOk <- decrementYieldLimit svar
332 if yieldLimitOk
333 then do
334 decrementBufferLimit svar
335 void $ send svar (ChildYield x)
336 return $ FL.Partial True
337 else do
338 cleanupSVarFromWorker svar
339 sendStop svar winfo
340 return $ FL.Done ()
341 step False _ = return $ FL.Done ()
343 extract True = liftIO $ sendStop svar winfo
344 extract False = return ()
346-- Using StreamD the worker stream producing code can fuse with the code to
347-- queue output to the SVar giving some perf boost.
348--
349-- Note that StreamD can only be used in limited situations, specifically, we
350-- cannot implement joinStreamVarPar using this.
351--
352-- XXX make sure that the SVar passed is a Parallel style SVar.
354-- | Fold the supplied stream to the SVar asynchronously using Parallel
355-- concurrency style.
356-- {-# INLINE_NORMAL toSVarParallel #-}
357{-# INLINE toSVarParallel #-}
358toSVarParallel :: MonadAsync m
359 => State t m a -> SVar t m a -> D.Stream m a -> m ()
360toSVarParallel st sv xs =
361 if svarInspectMode sv
362 then forkWithDiag
363 else do
364 tid <-
365 case getYieldLimit st of
366 Nothing -> doFork (work Nothing)
367 (svarMrun sv)
368 (handleChildException sv)
369 Just _ -> doFork (workLim Nothing)
370 (svarMrun sv)
371 (handleChildException sv)
372 modifyThread sv tid
374 where
376 {-# NOINLINE work #-}
377 work info = D.fold (write sv info) xs
379 {-# NOINLINE workLim #-}
380 workLim info = D.fold (writeLimited sv info) xs
382 {-# NOINLINE forkWithDiag #-}
383 forkWithDiag = do
384 -- We do not use workerCount in case of ParallelVar but still there is
385 -- no harm in maintaining it correctly.
386 liftIO $ atomicModifyIORefCAS_ (workerCount sv) $ \n -> n + 1
387 recordMaxWorkers sv
388 -- This allocation matters when significant number of workers are being
389 -- sent. We allocate it only when needed. The overhead increases by 4x.
390 winfo <-
391 case yieldRateInfo sv of
392 Nothing -> return Nothing
393 Just _ -> liftIO $ do
394 cntRef <- newIORef 0
395 t <- getTime Monotonic
396 lat <- newIORef (0, t)
397 return $ Just WorkerInfo
398 { workerYieldMax = 0
399 , workerYieldCount = cntRef
400 , workerLatencyStart = lat
401 }
402 tid <-
403 case getYieldLimit st of
404 Nothing -> doFork (work winfo)
405 (svarMrun sv)
406 (handleChildException sv)
407 Just _ -> doFork (workLim winfo)
408 (svarMrun sv)
409 (handleChildException sv)
410 modifyThread sv tid
412-------------------------------------------------------------------------------
413-- Support for running folds concurrently
414-------------------------------------------------------------------------------
416-- $concurrentFolds
417--
418-- To run folds concurrently, we need to decouple the fold execution from the
419-- stream production. We use the SVar to do that, we have a single worker
420-- pushing the stream elements to the SVar and on the consumer side a fold
421-- driver pulls the values and folds them.
422--
423-- @
424--
425-- Fold worker <------SVar<------input stream
426-- | exceptions |
427-- --------------->
428--
429-- @
430--
431-- We need a channel for pushing exceptions from the fold worker to the stream
432-- pusher. The stream may be pushed to multiple folds at the same time. For
433-- that we need one SVar per fold:
434--
435-- @
436--
437-- Fold worker <------SVar<---
438-- | |
439-- Fold worker <------SVar<------input stream
440-- | |
441-- Fold worker <------SVar<---
442--
443-- @
444--
445-- Unlike in case concurrent stream evaluation, the puller does not drive the
446-- scheduling and concurrent execution of the stream. The stream is simply
447-- pushed by the stream producer at its own rate. The fold worker just pulls it
448-- and folds it.
449--
450-- Note: If the stream pusher terminates due to an exception, we do not
451-- actively terminate the fold. It gets cleaned up by the GC.
453-------------------------------------------------------------------------------
454-- Process events received by a fold consumer from a stream producer
455-------------------------------------------------------------------------------
457-- | Pull a stream from an SVar to fold it. Like 'fromSVar' except that it does
458-- not drive the evaluation of the stream. It just pulls whatever is available
459-- on the SVar. Also, when the fold stops it sends a notification to the stream
460-- pusher/producer. No exceptions are expected to be propagated from the stream
461-- pusher to the fold puller.
462--
463{-# NOINLINE fromProducer #-}
464fromProducer :: forall m a . MonadAsync m => SVar K.Stream m a -> K.Stream m a
465fromProducer sv = K.mkStream $ \st yld sng stp -> do
466 list <- readOutputQ sv
467 -- Reversing the output is important to guarantee that we process the
468 -- outputs in the same order as they were generated by the constituent
469 -- streams.
470 K.foldStream st yld sng stp $ processEvents $ reverse list
472 where
474 allDone :: m r -> m r
475 allDone stp = do
476 when (svarInspectMode sv) $ do
477 t <- liftIO $ getTime Monotonic
478 liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
479 liftIO $ printSVar sv "SVar Done"
480 sendStopToProducer sv
481 stp
483 {-# INLINE processEvents #-}
484 processEvents :: [ChildEvent a] -> K.Stream m a
485 processEvents [] = K.mkStream $ \st yld sng stp -> do
486 K.foldStream st yld sng stp $ fromProducer sv
488 processEvents (ev : es) = K.mkStream $ \_ yld _ stp -> do
489 let rest = processEvents es
490 case ev of
491 ChildYield a -> yld a rest
492 ChildStop tid e -> do
493 accountThread sv tid
494 case e of
495 Nothing -> allDone stp
496 Just _ -> error "Bug: fromProducer: received exception"
498-- | Create a Fold style SVar that runs a supplied fold function as the
499-- consumer. Any elements sent to the SVar are consumed by the supplied fold
500-- function.
501--
502{-# INLINE newFoldSVar #-}
503newFoldSVar :: (K.IsStream t, MonadAsync m)
504 => State K.Stream m a -> (t m a -> m b) -> m (SVar K.Stream m a)
505newFoldSVar stt f = do
506 -- Buffer size for the SVar is derived from the current state
507 sv <- newParallelVar StopAny (adaptState stt)
509 -- Add the producer thread-id to the SVar.
510 liftIO myThreadId >>= modifyThread sv
512 void $ doFork (void $ f $ K.fromStream $ fromProducer sv)
513 (svarMrun sv)
514 (handleFoldException sv)
515 return sv
517-- | Like 'fromProducer' but generates a StreamD style stream instead of
518-- StreamK.
519--
520{-# INLINE_NORMAL fromProducerD #-}
521fromProducerD :: (MonadAsync m) => SVar t m a -> D.Stream m a
522fromProducerD svar = D.Stream step (FromSVarRead svar)
523 where
525 {-# INLINE_LATE step #-}
526 step _ (FromSVarRead sv) = do
527 list <- readOutputQ sv
528 -- Reversing the output is important to guarantee that we process the
529 -- outputs in the same order as they were generated by the constituent
530 -- streams.
531 return $ D.Skip $ FromSVarLoop sv (Prelude.reverse list)
533 step _ (FromSVarLoop sv []) = return $ D.Skip $ FromSVarRead sv
534 step _ (FromSVarLoop sv (ev : es)) = do
535 case ev of
536 ChildYield a -> return $ D.Yield a (FromSVarLoop sv es)
537 ChildStop tid e -> do
538 accountThread sv tid
539 case e of
540 Nothing -> do
541 sendStopToProducer sv
542 return $ D.Skip (FromSVarDone sv)
543 Just _ -> error "Bug: fromProducer: received exception"
545 step _ (FromSVarDone sv) = do
546 when (svarInspectMode sv) $ do
547 t <- liftIO $ getTime Monotonic
548 liftIO $ writeIORef (svarStopTime (svarStats sv)) (Just t)
549 liftIO $ printSVar sv "SVar Done"
550 return D.Stop
552 step _ FromSVarInit = undefined
554-- | Like 'newFoldSVar' except that it uses a 'Fold' instead of a fold
555-- function.
556--
557{-# INLINE newFoldSVarF #-}
558newFoldSVarF :: MonadAsync m => State t m a -> Fold m a b -> m (SVar t m a)
559newFoldSVarF stt f = do
560 -- Buffer size for the SVar is derived from the current state
561 sv <- newParallelVar StopAny (adaptState stt)
562 -- Add the producer thread-id to the SVar.
563 liftIO myThreadId >>= modifyThread sv
564 void $ doFork (work sv) (svarMrun sv) (handleFoldException sv)
565 return sv
567 where
569 {-# NOINLINE work #-}
570 work sv = void $ D.fold f $ fromProducerD sv
572-------------------------------------------------------------------------------
573-- Process events received by the producer thread from the consumer side
574-------------------------------------------------------------------------------
576-- XXX currently only one event is sent by a fold consumer to the stream
577-- producer. But we can potentially have multiple events e.g. the fold step can
578-- generate exception more than once and the producer can ignore those
579-- exceptions or handle them and still keep driving the fold.
580--
581-- | Poll for events sent by the fold consumer to the stream pusher. The fold
582-- consumer can send a "Stop" event or an exception. When a "Stop" is received
583-- this function returns 'True'. If an exception is recieved then it throws the
584-- exception.
585--
586{-# NOINLINE fromConsumer #-}
587fromConsumer :: MonadAsync m => SVar K.Stream m a -> m Bool
588fromConsumer sv = do
589 (list, _) <- liftIO $ readOutputQBasic (outputQueueFromConsumer sv)
590 -- Reversing the output is important to guarantee that we process the
591 -- outputs in the same order as they were generated by the constituent
592 -- streams.
593 processEvents $ reverse list
595 where
597 {-# INLINE processEvents #-}
598 processEvents [] = return False
599 processEvents (ev : _) = do
600 case ev of
601 ChildStop _ e -> do
602 case e of
603 Nothing -> return True
604 Just ex -> throwM ex
605 ChildYield _ -> error "Bug: fromConsumer: invalid ChildYield event"
607-- | Push values from a stream to a fold worker via an SVar. Before pushing a
608-- value to the SVar it polls for events received from the fold consumer. If a
609-- stop event is received then it returns 'True' otherwise false. Propagates
610-- exceptions received from the fold consumer.
611--
612{-# INLINE pushToFold #-}
613pushToFold :: MonadAsync m => SVar K.Stream m a -> a -> m Bool
614pushToFold sv a = do
615 -- Check for exceptions before decrement so that we do not
616 -- block forever if the child already exited with an exception.
617 --
618 -- We avoid a race between the consumer fold sending an event and we
619 -- blocking on decrementBufferLimit by waking up the producer thread in
620 -- sendToProducer before any event is sent by the fold to the producer
621 -- stream.
622 let qref = outputQueueFromConsumer sv
623 done <- do
624 (_, n) <- liftIO $ readIORef qref
625 if (n > 0)
626 then fromConsumer sv
627 else return False
628 if done
629 then return True
630 else liftIO $ do
631 decrementBufferLimit sv
632 void $ send sv (ChildYield a)
633 return False
635------------------------------------------------------------------------------
636-- Clone and distribute a stream in parallel
637------------------------------------------------------------------------------
639-- XXX this could be written in StreamD style for better efficiency with fusion.
640--
641-- | Tap a stream and send the elements to the specified SVar in addition to
642-- yielding them again. The SVar runs a fold consumer. Elements are tapped and
643-- sent to the SVar until the fold finishes. Any exceptions from the fold
644-- evaluation are propagated in the current thread.
645--
646-- @
647--
648-- ------input stream---------output stream----->
649-- /|\\ |
650-- exceptions | | input
651-- | \\|/
652-- ----SVar
653-- |
654-- Fold
655--
656-- @
657--
658{-# INLINE teeToSVar #-}
659teeToSVar :: (K.IsStream t, MonadAsync m) =>
660 SVar K.Stream m a -> t m a -> t m a
661teeToSVar svr m = K.mkStream $ \st yld sng stp -> do
662 K.foldStreamShared st yld sng stp (go False m)
664 where
666 go False m0 = K.mkStream $ \st yld _ stp -> do
667 let drain = do
668 -- In general, a Stop event would come equipped with the result
669 -- of the fold. It is not used here but it would be useful in
670 -- applicative and distribute.
671 done <- fromConsumer svr
672 when (not done) $ do
673 liftIO $ withDiagMVar svr "teeToSVar: waiting to drain"
674 $ takeMVar (outputDoorBellFromConsumer svr)
675 drain
677 stopFold = do
678 liftIO $ sendStop svr Nothing
679 -- drain/wait until a stop event arrives from the fold.
680 drain
682 stop = stopFold >> stp
683 single a = do
684 done <- pushToFold svr a
685 yld a (go done (K.nilM stopFold))
686 yieldk a r = pushToFold svr a >>= \done -> yld a (go done r)
687 in K.foldStreamShared st yieldk single stop m0
689 go True m0 = m0