on master help sign in
Add fold and parse for array streams

The test and benchmark code is taken from Adithya's original double ended lists based implementation.

commit3339c2082d9f86d6cb68d3ffdbfc4aa0e5fed892 authorHarendra Kumar <harendra@composewell.com> dateTue 27 Apr 2021, 02:37 (5 years ago) parent613a85f
7 files changed+329−11
benchmark/Streamly/Benchmark/Data/Array/Stream/Foreign.hs+74−6 hie.yaml+3−1 src/Streamly/Internal/Data/Array/Stream/Fold/Foreign.hs+1−1 src/Streamly/Internal/Data/Array/Stream/Foreign.hs+173−3 streamly.cabal+1 test/Streamly/Test/Data/Array/Stream/Foreign.hs+72 test/streamly-tests.cabal+5
benchmark/Streamly/Benchmark/Data/Array/Stream/Foreign.hs+74−6
@@ -23,18 +23,26 @@ module Main
2323 main
2424 ) where
2525
26+import Control.DeepSeq (NFData(..))
27+import Control.Monad (void)
28+import Control.Monad.Catch (MonadThrow)
2629import Data.Functor.Identity (runIdentity)
2730import Data.Word (Word8)
2831import System.IO (Handle)
32+import System.Random (randomRIO)
2933import Prelude hiding ()
3034
3135import qualified Streamly.Prelude as Stream
3236import qualified Streamly.Internal.Data.Array.Foreign as Array
3337import qualified Streamly.Internal.Data.Array.Stream.Foreign as ArrayStream
38+import qualified Streamly.Internal.Data.Fold as Fold
39+import qualified Streamly.Internal.Data.Parser.ParserD as ParserD
40+import qualified Streamly.Internal.Data.Stream.IsStream as Stream (arraysOf)
3441import qualified Streamly.Internal.FileSystem.Handle as Handle
3542import qualified Streamly.Internal.Unicode.Stream as Unicode
3643
3744import Gauge hiding (env)
45+import Streamly.Prelude (SerialT, MonadAsync, IsStream)
3846import Streamly.Benchmark.Common
3947import Streamly.Benchmark.Common.Handle
4048
@@ -44,6 +52,28 @@ import Streamly.Internal.Data.Stream.StreamD.Type (Step(..))
4452import Test.Inspection
4553#endif
4654
55+-------------------------------------------------------------------------------
56+-- Utilities
57+-------------------------------------------------------------------------------
58+
59+-- XXX these can be moved to the common module
60+
61+{-# INLINE sourceUnfoldrM #-}
62+sourceUnfoldrM :: (IsStream t, MonadAsync m) => Int -> Int -> t m Int
63+sourceUnfoldrM value n = Stream.unfoldrM step n
64+ where
65+ step cnt =
66+ if cnt > n + value
67+ then return Nothing
68+ else return (Just (cnt, cnt + 1))
69+
70+{-# INLINE benchIO #-}
71+benchIO
72+ :: NFData b
73+ => String -> (Int -> t IO a) -> (t IO a -> IO b) -> Benchmark
74+benchIO name src sink =
75+ bench name $ nfIO $ randomRIO (1,1) >>= sink . src
76+
4777-------------------------------------------------------------------------------
4878-- read chunked using toChunks
4979-------------------------------------------------------------------------------
@@ -183,6 +213,37 @@ o_1_space_copy_toChunks_group_ungroup env =
183213 ]
184214 ]
185215
216+-------------------------------------------------------------------------------
217+-- Parsers
218+-------------------------------------------------------------------------------
219+
220+{-# INLINE drainWhile #-}
221+drainWhile :: MonadThrow m => (a -> Bool) -> ParserD.Parser m a ()
222+drainWhile p = ParserD.takeWhile p Fold.drain
223+
224+-------------------------------------------------------------------------------
225+-- Folds and parsers
226+-------------------------------------------------------------------------------
227+
228+{-# INLINE fold #-}
229+fold :: SerialT IO (Array.Array Int) -> IO ()
230+fold s = void $ ArrayStream.fold Fold.drain s
231+
232+{-# INLINE parseArray #-}
233+parseArray :: Int -> SerialT IO (Array.Array Int) -> IO ()
234+parseArray value s = void $ ArrayStream.parse (drainWhile (< value)) s
235+
236+o_1_space_serial_array ::
237+ Int -> [Array.Array Int] -> [Array.Array Int] -> [Benchmark]
238+o_1_space_serial_array bound arraysSmall arraysBig =
239+ [ benchIO "fold (of 100)" (\_ -> Stream.fromList arraysSmall) fold
240+ , benchIO "fold (single)" (\_ -> Stream.fromList arraysBig) fold
241+ , benchIO "parse (of 100)" (\_ -> Stream.fromList arraysSmall)
242+ $ parseArray bound
243+ , benchIO "parse (single)" (\_ -> Stream.fromList arraysBig)
244+ $ parseArray bound
245+ ]
246+
186247-------------------------------------------------------------------------------
187248-- Driver
188249-------------------------------------------------------------------------------
@@ -193,13 +254,20 @@ moduleName = "Data.Array.Stream.Foreign"
193254main :: IO ()
194255main = do
195256 env <- mkHandleBenchEnv
196− defaultMain (allBenchmarks env)
257+ runWithCLIOptsEnv defaultStreamSize alloc (allBenchmarks env)
197258
198259 where
199260
200− allBenchmarks env =
201− [ bgroup (o_1_space_prefix moduleName)
202− ( o_1_space_read_chunked env
203− ++ o_1_space_copy_toChunks_group_ungroup env
204− )
261+ alloc value = do
262+ small <- Stream.toList $ Stream.arraysOf 100 $ sourceUnfoldrM value 0
263+ big <- Stream.toList $ Stream.arraysOf value $ sourceUnfoldrM value 0
264+ return (small, big)
265+
266+ allBenchmarks env arrays value =
267+ let (arraysSmall, arraysBig) = arrays
268+ in [ bgroup (o_1_space_prefix moduleName) $ Prelude.concat
269+ [ o_1_space_read_chunked env
270+ , o_1_space_serial_array value arraysSmall arraysBig
271+ , o_1_space_copy_toChunks_group_ungroup env
272+ ]
205273 ]
hie.yaml+3−1
@@ -51,7 +51,9 @@ cradle:
5151 - path: "./test/Streamly/Test/Data/Array.hs"
5252 component: "test:Data.Array"
5353 - path: "./test/Streamly/Test/Data/Array/Foreign.hs"
54− component: "Data.Array.Foreign"
54+ component: "test:Data.Array.Foreign"
55+ - path: "./test/Streamly/Test/Data/Array/Stream/Foreign.hs"
56+ component: "test:Data.Array.Stream.Foreign"
5557 - path: "./test/Streamly/Test/Data/Fold.hs"
5658 component: "test:Data.Fold"
5759 - path: "./test/Streamly/Test/Data/List.hs"
src/Streamly/Internal/Data/Array/Stream/Fold/Foreign.hs+1−1
@@ -23,7 +23,7 @@
2323-- >>> import qualified Streamly.Internal.Data.Stream.IsStream as Stream (arraysOf)
2424-- >>> import qualified Streamly.Prelude as Stream
2525--
26−-- >>> ArrayStream.fold (ArrayFold.fromFold (Fold.take 7 Fold.toList)) $ Stream.arraysOf 5 $ Stream.fromList "hello world"
26+-- >>> ArrayStream.foldArr (ArrayFold.fromFold (Fold.take 7 Fold.toList)) $ Stream.arraysOf 5 $ Stream.fromList "hello world"
2727-- "hello w"
2828--
2929module Streamly.Internal.Data.Array.Stream.Fold.Foreign
src/Streamly/Internal/Data/Array/Stream/Foreign.hs+173−3
@@ -22,6 +22,11 @@ module Streamly.Internal.Data.Array.Stream.Foreign
2222 , unlines
2323
2424 -- * Elimination
25+ -- ** Element Folds
26+ , fold
27+ , parse
28+ , parseD
29+
2530 -- ** Array Folds
2631 , foldArr
2732 , foldArr_
@@ -50,6 +55,9 @@ import Data.Bifunctor (second)
5055import Control.Exception (assert)
5156import Control.Monad.Catch (MonadThrow, throwM)
5257import Control.Monad.IO.Class (MonadIO(..))
58+#if __GLASGOW_HASKELL__ < 808
59+import Data.Semigroup (Semigroup(..))
60+#endif
5361import Data.Word (Word8)
5462import Foreign.ForeignPtr (touchForeignPtr)
5563import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
@@ -317,7 +325,65 @@ splitOnSuffix byte s =
317325 D.fromStreamD $ D.splitInnerBySuffix (A.breakOn byte) A.spliceTwo $ D.toStreamD s
318326
319327-------------------------------------------------------------------------------
320−-- Elimination
328+-- Elimination - Running folds
329+-------------------------------------------------------------------------------
330+
331+-- XXX This should be written using CPS (as foldK) if we want it to scale wrt
332+-- to the number of times it can be called on the same stream.
333+--
334+{-# INLINE_NORMAL foldD #-}
335+foldD :: forall m a b. (MonadIO m, Storable a) =>
336+ Fold m a b -> D.Stream m (Array a) -> m (b, D.Stream m (Array a))
337+foldD (Fold fstep initial extract) stream@(D.Stream step state) = do
338+ res <- initial
339+ case res of
340+ FL.Partial fs -> go SPEC state fs
341+ FL.Done fb -> return $! (fb, stream)
342+
343+ where
344+
345+ {-# INLINE go #-}
346+ go !_ st !fs = do
347+ r <- step defState st
348+ case r of
349+ D.Yield (Array (ForeignPtr start contents) (Ptr end)) s ->
350+ goArray SPEC s (ForeignPtr end contents) (Ptr start) fs
351+ D.Skip s -> go SPEC s fs
352+ D.Stop -> do
353+ b <- extract fs
354+ return (b, D.nil)
355+
356+ goArray !_ s fp@(ForeignPtr end _) !cur !fs
357+ | cur == Ptr end = do
358+ liftIO $ touchForeignPtr fp
359+ go SPEC s fs
360+ goArray !_ st fp@(ForeignPtr end contents) !cur !fs = do
361+ x <- liftIO $ peek cur
362+ res <- fstep fs x
363+ let elemSize = sizeOf (undefined :: a)
364+ next = cur `plusPtr` elemSize
365+ case res of
366+ FL.Done b -> do
367+ let !(Ptr curAddr) = next
368+ arr = Array (ForeignPtr curAddr contents) (Ptr end)
369+ return $! (b, D.cons arr (D.Stream step st))
370+ FL.Partial fs1 -> goArray SPEC st fp next fs1
371+
372+-- | Fold an array stream using the supplied 'Fold'. Returns the fold result
373+-- and the unconsumed stream.
374+--
375+-- /Internal/
376+--
377+{-# INLINE_NORMAL fold #-}
378+fold ::
379+ (MonadIO m, Storable a)
380+ => FL.Fold m a b
381+ -> SerialT m (A.Array a)
382+ -> m (b, SerialT m (A.Array a))
383+fold f s = fmap D.fromStreamD <$> foldD f (D.toStreamD s)
384+
385+-------------------------------------------------------------------------------
386+-- Fold to a single Array
321387-------------------------------------------------------------------------------
322388
323389-- When we have to take an array partially, take the last part of the array.
@@ -459,7 +525,7 @@ _toArraysOf n = FL.chunksOf n (A.writeNF n) FL.toStream
459525-}
460526
461527-------------------------------------------------------------------------------
462−-- Parsing
528+-- Elimination - running element parsers
463529-------------------------------------------------------------------------------
464530
465531-- GHC parser does not accept {-# ANN type [] NoSpecConstr #-}, so we need
@@ -467,6 +533,110 @@ _toArraysOf n = FL.chunksOf n (A.writeNF n) FL.toStream
467533{-# ANN type List NoSpecConstr #-}
468534newtype List a = List {getList :: [a]}
469535
536+-- This can be generalized to any type provided it can be unfolded to a stream
537+-- and it can be combined using a semigroup operation.
538+--
539+-- XXX This should be written using CPS (as parseK) if we want it to scale wrt
540+-- to the number of times it can be called on the same stream.
541+{-# INLINE_NORMAL parseD #-}
542+parseD ::
543+ forall m a b. (MonadIO m, MonadThrow m, Storable a)
544+ => PRD.Parser m a b
545+ -> D.Stream m (Array.Array a)
546+ -> m (b, D.Stream m (Array.Array a))
547+parseD (PRD.Parser pstep initial extract) stream@(D.Stream step state) = do
548+ res <- initial
549+ case res of
550+ PRD.IPartial s -> go SPEC state (List []) s
551+ PRD.IDone b -> return (b, stream)
552+ PRD.IError err -> throwM $ ParseError err
553+
554+ where
555+
556+ -- "backBuf" contains last few items in the stream that we may have to
557+ -- backtrack to.
558+ --
559+ -- XXX currently we are using a dumb list based approach for backtracking
560+ -- buffer. This can be replaced by a sliding/ring buffer using Data.Array.
561+ -- That will allow us more efficient random back and forth movement.
562+ {-# INLINE go #-}
563+ go !_ st backBuf !pst = do
564+ r <- step defState st
565+ case r of
566+ D.Yield (Array (ForeignPtr start contents) (Ptr end)) s ->
567+ gobuf SPEC s backBuf (ForeignPtr end contents) (Ptr start) pst
568+ D.Skip s -> go SPEC s backBuf pst
569+ D.Stop -> do
570+ b <- extract pst
571+ return (b, D.nil)
572+
573+ -- Use strictness on "cur" to keep it unboxed
574+ gobuf !_ s backBuf fp@(ForeignPtr end _) !cur !pst
575+ | cur == Ptr end = do
576+ liftIO $ touchForeignPtr fp
577+ go SPEC s backBuf pst
578+ gobuf !_ s backBuf fp@(ForeignPtr end contents) !cur !pst = do
579+ x <- liftIO $ peek cur
580+ pRes <- pstep pst x
581+ let elemSize = sizeOf (undefined :: a)
582+ next = cur `plusPtr` elemSize
583+ case pRes of
584+ PR.Partial 0 pst1 ->
585+ gobuf SPEC s (List []) fp next pst1
586+ PR.Partial n pst1 -> do
587+ assert (n <= Prelude.length (x:getList backBuf)) (return ())
588+ let src0 = Prelude.take n (x:getList backBuf)
589+ arr0 = A.fromListN n (Prelude.reverse src0)
590+ !(Ptr curAddr) = next
591+ arr1 = Array (ForeignPtr curAddr contents) (Ptr end)
592+ src = arr0 <> arr1
593+ let !(Array (ForeignPtr start cont1) (Ptr end1)) = src
594+ gobuf SPEC s (List []) (ForeignPtr end1 cont1) (Ptr start) pst1
595+ PR.Continue 0 pst1 ->
596+ gobuf SPEC s (List (x:getList backBuf)) fp next pst1
597+ PR.Continue n pst1 -> do
598+ assert (n <= Prelude.length (x:getList backBuf)) (return ())
599+ let (src0, buf1) = splitAt n (x:getList backBuf)
600+ arr0 = A.fromListN n (Prelude.reverse src0)
601+ !(Ptr curAddr) = next
602+ arr1 = Array (ForeignPtr curAddr contents) (Ptr end)
603+ src = arr0 <> arr1
604+ let !(Array (ForeignPtr start cont1) (Ptr end1)) = src
605+ gobuf SPEC s (List buf1) (ForeignPtr end1 cont1) (Ptr start) pst1
606+ PR.Done 0 b -> do
607+ let !(Ptr curAddr) = next
608+ arr = Array (ForeignPtr curAddr contents) (Ptr end)
609+ return (b, D.cons arr (D.Stream step s))
610+ PR.Done n b -> do
611+ assert (n <= Prelude.length (x:getList backBuf)) (return ())
612+ let src0 = Prelude.take n (x:getList backBuf)
613+ -- XXX create the array in reverse instead
614+ arr0 = A.fromListN n (Prelude.reverse src0)
615+ !(Ptr curAddr) = next
616+ arr1 = Array (ForeignPtr curAddr contents) (Ptr end)
617+ -- XXX Use StreamK to avoid adding arbitrary layers of
618+ -- constructors every time.
619+ str = D.cons arr0 (D.cons arr1 (D.Stream step s))
620+ return (b, str)
621+ PR.Error err -> throwM $ ParseError err
622+
623+-- | Parse an array stream using the supplied 'Parser'. Returns the parse
624+-- result and the unconsumed stream. Throws 'ParseError' if the parse fails.
625+--
626+-- /Internal/
627+--
628+{-# INLINE_NORMAL parse #-}
629+parse ::
630+ (MonadIO m, MonadThrow m, Storable a)
631+ => PRD.Parser m a b
632+ -> SerialT m (A.Array a)
633+ -> m (b, SerialT m (A.Array a))
634+parse p s = fmap D.fromStreamD <$> parseD p (D.toStreamD s)
635+
636+-------------------------------------------------------------------------------
637+-- Elimination - Running Array Folds and parsers
638+-------------------------------------------------------------------------------
639+
470640{-# INLINE_NORMAL parseArrD #-}
471641parseArrD ::
472642 forall m a b. (MonadIO m, MonadThrow m, Storable a)
@@ -540,7 +710,7 @@ parseArrD (PRD.Parser pstep initial extract) stream@(D.Stream step state) = do
540710{-# INLINE parseArr #-}
541711parseArr ::
542712 (MonadIO m, MonadThrow m, Storable a)
543− => PRD.Parser m a b
713+ => ASF.Parser m a b
544714 -> SerialT m (A.Array a)
545715 -> m (b, SerialT m (A.Array a))
546716parseArr p s = fmap D.fromStreamD <$> parseD p (D.toStreamD s)
streamly.cabal+1
@@ -123,6 +123,7 @@ extra-source-files:
123123 test/Streamly/Test/Data/Array/Prim.hs
124124 test/Streamly/Test/Data/Array/Prim/Pinned.hs
125125 test/Streamly/Test/Data/Array/Foreign.hs
126+ test/Streamly/Test/Data/Array/Stream/Foreign.hs
126127 test/Streamly/Test/Data/Parser/ParserD.hs
127128 test/Streamly/Test/FileSystem/Event.hs
128129 test/Streamly/Test/FileSystem/Handle.hs
test/Streamly/Test/Data/Array/Stream/Foreign.hsnew file+72
@@ -0,0 +1,72 @@
1+module Main (main) where
2+
3+import Streamly.Test.Common (listEquals, chooseInt)
4+import Test.Hspec (hspec, describe)
5+import Test.Hspec.QuickCheck
6+import Test.QuickCheck (forAll, Property, vectorOf, Gen)
7+import Test.QuickCheck.Monadic (monadicIO, run)
8+
9+import qualified Streamly.Internal.Data.Array.Foreign as Array
10+import qualified Streamly.Internal.Data.Array.Stream.Foreign as ArrayStream
11+import qualified Streamly.Internal.Data.Fold as Fold
12+import qualified Streamly.Internal.Data.Parser.ParserD as ParserD
13+import qualified Streamly.Internal.Data.Stream.IsStream as Stream
14+import qualified Test.Hspec as Hspec
15+
16+import Prelude hiding (sequence)
17+
18+#if MIN_VERSION_QuickCheck(2,14,0)
19+
20+import Test.QuickCheck (chooseAny)
21+
22+#else
23+
24+import System.Random (Random(random))
25+import Test.QuickCheck.Gen (Gen(MkGen))
26+
27+-- | Generates a random element over the natural range of `a`.
28+chooseAny :: Random a => Gen a
29+chooseAny = MkGen (\r _ -> let (x,_) = random r in x)
30+
31+#endif
32+
33+maxTestCount :: Int
34+maxTestCount = 100
35+
36+parse :: Property
37+parse = do
38+ let len = 200
39+ -- ls = input list (stream)
40+ -- clen = chunk size
41+ -- tlen = parser take size
42+ forAll
43+ ((,,)
44+ <$> vectorOf len (chooseAny :: Gen Int)
45+ <*> chooseInt (1, len)
46+ <*> chooseInt (0, len))
47+ $ \(ls, clen, tlen) ->
48+ monadicIO $ do
49+ (ls1, str) <-
50+ let input =
51+ Stream.chunksOf
52+ clen (Array.writeN clen) (Stream.fromList ls)
53+ parser = ParserD.fromFold (Fold.take tlen Fold.toList)
54+ in run $ ArrayStream.parse parser input
55+ ls2 <- run $ Stream.toList $ ArrayStream.concat str
56+ listEquals (==) (ls1 ++ ls2) ls
57+
58+-------------------------------------------------------------------------------
59+-- Main
60+-------------------------------------------------------------------------------
61+
62+moduleName :: String
63+moduleName = "Data.Array.Stream.Foreign"
64+
65+main :: IO ()
66+main =
67+ hspec $
68+ Hspec.parallel $
69+ modifyMaxSuccess (const maxTestCount) $ do
70+ describe moduleName $ do
71+ describe "Stream parsing" $ do
72+ prop "parse" parse
test/streamly-tests.cabal+5
@@ -222,6 +222,11 @@ test-suite Data.Array.Foreign
222222 main-is: Streamly/Test/Data/Array/Foreign.hs
223223 ghc-options: -main-is Streamly.Test.Data.Array.Foreign.main
224224
225+test-suite Data.Array.Stream.Foreign
226+ import: test-options
227+ type: exitcode-stdio-1.0
228+ main-is: Streamly/Test/Data/Array/Stream/Foreign.hs
229+
225230test-suite Data.Fold
226231 import: test-options
227232 type: exitcode-stdio-1.0