packages feed

monad-par-0.1: Control/Monad/Par/Stream.hs

{-# LANGUAGE BangPatterns, CPP #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing -fwarn-unused-imports #-}
-- -Wall 

-- A module for stream processing built on top of Control.Monad.Par

-- (In the future may want to look into the stream interface used by
--  the stream fusion framework.)

#define DEBUGSTREAMS

module Control.Monad.Par.Stream 
 ( 
   streamMap, streamScan, streamFold
 , countupWin, generate
 , runParList, toListSpin
 , measureRate, measureRateList
 , browseStream
 , Stream, Window, WStream

 -- TEMP:
 , one_second, commaint
 )
where
import Control.Monad
import Control.Monad.Par as P
import Control.Monad.Par.IList
import Control.DeepSeq

--import qualified Data.Array.Unboxed as U
import Data.Array.Unboxed as U
import Data.Array.CArray as C
import Data.Int
import Data.Word
import Data.List (intersperse)
import Data.List.Split (chunk)

import Foreign.Storable

import System.CPUTime
import System.CPUTime.Rdtsc
import GHC.Conc as Conc
import System.IO
import GHC.IO (unsafePerformIO, unsafeDupablePerformIO, unsafeInterleaveIO)


import Debug.Trace
import Control.Monad.Par.Logging

debugflag = True

--------------------------------------------------------------------------------
-- Types

type Stream a = IVar (IList a)

-- A windowed stream passes chunks of stream elements.
type WStream a = Stream (Window a)

#define CARRAY
#ifdef CARRAY
type Window a = CArray Int a
#else
type Window a = U.UArray Int a
#endif


--------------------------------------------------------------------------------
-- Stream Operators

-- | This version applies a function to every element in a stream,
--   exposing data parallelism and pipeline parallelism.
streamMapDP :: NFData b => (a -> b) -> Stream a -> Par (Stream b)
streamMapDP fn instrm = 
    do outstrm <- new
       fork$ loop instrm outstrm
       return outstrm
 where
  loop instrm outstrm = 
   do 
      ilst <- get instrm
      case ilst of 
	Null -> put outstrm Null -- End of stream.
	Cons h t -> 
	  do newtl <- new
	     h' <- pval (fn h)	     

-- WARNING: This only makes sense with continuation-stealing..  With child stealing this will go crazy.
	     fork$ loop t newtl
	     h'' <- get h'
	     put outstrm (Cons h'' newtl)
	     

-- This version exposes pipeline parallelism but no data parallelism.
-- It shouldn't be necessary if fork is sufficiently efficient and if
-- work stealing is done right.
streamMap :: NFData b => (a -> b) -> Stream a -> Par (Stream b)
streamMap fn instrm = 
    do outstrm <- new
       fork$ loop instrm outstrm
       return outstrm
 where
  loop instrm outstrm = 
   do 
      ilst <- get instrm
      case ilst of 
	Null -> put outstrm Null -- End of stream.
	Cons h t -> 
	  do newtl <- new
	     put outstrm (Cons (fn h) newtl)
	     loop t newtl


-- | Applies a stateful kernel to the stream.  Output stream elements match input one-to-one.
-- streamScan :: (NFData b, NFData c) => 
streamScan :: (NFData a, NFData b, NFData c) =>  -- <- TEMP, don't need NFData a in general.
	      (a -> b -> (a,c)) -> a -> Stream b -> Par (Stream c)
streamScan fn initstate instrm = 
    do outstrm <- new
       fork$ loop initstate instrm outstrm
       return outstrm
 where
#ifdef DEBUGSTREAMS
  -- Create a task log for each unique input stream fed to this function:
  tasklog = unsafeNewTaskSeries (nameFromValue instrm)
#endif

  loop state instrm outstrm = 
   do 
      ilst <- get instrm
      case ilst of 
	Null -> put outstrm Null -- End of stream.
	Cons h t -> 
	  do newtl <- new
	     let (newstate, outp) = 
#ifdef DEBUGSTREAMS
		                    timePure tasklog$ fn state h
#else
		                    fn state h
#endif
	     put outstrm (Cons outp newtl)
	     loop newstate t newtl

-- TODO: streamMapM -- monadic version.  Define the non-monadic one in
-- terms of it and watch for performance regression.


-- TODO: More flexible version that passes an "emit" function to the
-- kernel so that it may produce zero output elements or more than one.
-- This also enables nested parallelism within the kernel.
-- 
-- streamKernel :: ((c -> Par ()) -> a -> b -> Par ()) -> a -> Stream b -> Par (Stream c)
--
-- ALSO: Can have a "concat" operator for streams of lists where
-- streamScan . concat rewrites to streamKernel perhaps...



-- | Reduce a stream to a single value.  This function will not return
--   until it reaches the end-of-stream.
streamFold :: (a -> b -> a) -> a -> Stream b -> Par a
streamFold fn acc instrm = 
   do ilst <- get instrm
      case ilst of 
	Null     -> return acc 
	Cons h t -> streamFold fn (fn acc h) t 

-- | Generate a stream of the given length by applying the function to each index (starting at zero).
-- 
-- WARNING, this source calls yield, letting other par computations
-- run, but there is no backpressure.  Thus if the source runs at a
-- higher rate than its consumer, buffered stream elements accumulate.
generate :: NFData a => Int -> (Int -> a) -> Par (Stream a)
-- NOTE: I don't currently know of a good way to do backpressure
-- directly in this system... but here are some other options: 
--
--   (1) we can use timers and look at maximum sustained rate.  

--   (2) Also, we can use coarse grained global barriers.  That is, we
--       can produce some number of output elements, wait until quiescence
--       of all Par computations, and then produce more.

--   (3) We can register computations that should only execute when a
--       worker goes idle.  This is a simple form of priority scheduling.
generate size fn = 
   do outstrm <- new
      fork$ loop (0::Int) outstrm
      return outstrm
 where 
  loop n strm | n == size = 
	  do when debugflag (print_$ " [generate] Done.  Produced "++ show size++" elements.\n")
	     put strm Null
             return ()
  loop n strm = 
    do 
       newtl <- new
       put strm (Cons (fn n) newtl)
       P.yield  -- This is necessary to avoid starving others when there
		-- aren't enough worker threads to go around.
       loop (n+1) newtl


-- | Create a [windowed] stream of consecutive integers.  Generates at
--   least the target number of elements windowed into segments of a
--   specified size.
countupWin :: (Storable a, NFData a, Num a) => 
              Int -> Int -> Par (WStream a)
countupWin bufsize target = 
   generate num fn
 where 
  num = case r of 0 -> q
		  _ -> q+1
  (q,r) = quotRem target bufsize
  fn n = 
   let start = n * bufsize in
   array (start,start + bufsize-1)
         [(i, fromIntegral (n + fromIntegral i)) 
	  | i <- [start .. start + bufsize-1]]



measureRate :: Stream a -> IO ()
measureRate strm = 
  do lazyls <- toListSpin strm
     measureRateList lazyls

measureRateList :: [a] -> IO ()
measureRateList lazyls = 
  do 
     t0 <- getTime
     print_$ " [measureRate] Counting stream rate starting at time: "++ show t0
     loop t0 t0 (0::Int64) (0::Int64) lazyls
     
 where 
  loop _     _    _     n [] = 
    do print_$ " [measureRate] Hit end of stream after "++show n++" elements."
       return ()

  loop start time lastN n (h:t) = 
       do 
	  time2 <- getTime
	  if time2 - time > one_second then do
	    (print_$ " [measureRate] current rate: "++show (n+1-lastN) ++ 
	             "  Total elems&time "++ commaint (n+1)++ "  " ++commaint (time2-start))
--               print_ (show (n+1))
	    loop start time2 (n+1) (n+1) t
	   else do
	    loop start time  lastN (n+1) t


-- | Use the keyboard to interactively browse through stream elements.
browseStream :: Show a => Stream a -> IO ()
browseStream strm = 
  do putStrLn$ "[browseStream] Beginning interactive stream browser, press enter for more elements:"
     ls <- toListSpin strm
     loop 0 ls
 where 
  loop n ls = 
   do putStr$ show n ++ "# "
      hFlush stdout
      c <- getChar      
      if c == '\EOT' -- User presses ctrl D to exit.
       then putStrLn$ "[browseStream] Ctrl-D pressed, exiting."
       else case ls of 
             []    -> putStrLn$ "[browseStream] Reached end of stream after "++show n++" elements."
             (h:t) -> do print h
		         loop (n+1) t


--------------------------------------------------------------------------------
-- Conversion:

-- Convert a stream to a lazy list.  Spin wait (with yield) until stream elements are available.
toListSpin :: Stream a -> IO [a]
toListSpin strm = 
   do x <- pollIVar strm
      case x of
        Nothing  -> do Conc.yield       -- run other GHC threads
                       toListSpin strm  -- spin wait
	Just (ils) -> 
	    case ils of 
	      Null     -> return []
	      Cons h t -> return (h : unsafePerformIO (unsafeInterleaveIO (toListSpin t)))


-- TODO: If it is unavailable we should help run the computation and then try again.
-- This version will do runParAsync itself:

-- Run a Par computation to produce a stream.  Convert that stream to a lazy list.
runParList :: Par (Stream a) -> [a]
runParList = 
  undefined
-- runForList parcomp = loop (runParAsync parcomp) 
--  where 
--   loop strm = 
--    do x <- pollIVar strm
--       case x of
--         Nothing  -> 
-- 	    -- For now we just spin:
-- 	    do yield -- run other GHC threads
-- 	       loop strm
-- 	Just (ils) -> 
-- 	    case ils of 
-- 	      Null     -> return []
-- 	      Cons h t -> return (h : unsafePerformIO (unsafeInterleaveIO (loop t)))




--------------------------------------------------------------------------------
-- Helpers and Scrap:

print_ msg = trace msg $ return ()

_unsafe_io :: IO a -> Par a
_unsafe_io io =  let x = unsafePerformIO io in
		 x `seq` return x

_unsafe_dupable :: IO a -> Par a
_unsafe_dupable io = 
  let x = unsafeDupablePerformIO io in 
  x `seq` return x

-- This version simply busy-waits to stay on the same core:
measure_freq2 :: IO Word64
measure_freq2 = do 
--  let second = 1000 * 1000 * 1000 * 1000 -- picoseconds are annoying
  let tenth = 100 * 1000 * 1000 * 1000 -- picoseconds are annoying      
      coef = 10
  t1 <- rdtsc 
  start <- getCPUTime
  let loop !n !last = 
       do t2 <- rdtsc 
	  when (t2 < last) $
	       putStrLn$ "WARNING, measure_freq2: COUNTERS WRAPPED "++ show (last,t2) 
	  cput <- getCPUTime		
--	  if (cput - start < second) 
	  if (cput - start < tenth)
	   then loop (n+1) t2
	   else return (n,t2)
  (n,t2) <- loop 0 t1
  putStrLn$ "  Approx getCPUTime calls per second: "++ commaint (coef * n)
  when (t2 < t1) $ 
    putStrLn$ "WARNING: rdtsc not monotonically increasing, first "++show t1++" then "++show t2++" on the same OS thread"

  return$ coef * fromIntegral (t2 - t1)

commaint :: Integral a => a -> String
commaint n = 
   reverse $
   concat $
   intersperse "," $ 
   chunk 3 $ 
   reverse (show n)

-- Having trouble with this:
-- getTime = getCPUTime
-- one_second = 1000000000000 -- picoseconds
getTime = rdtsc
one_second = unsafePerformIO$ measure_freq2


instance NFData (U.UArray a b) where 
  rnf !arr = ()

instance NFData (C.CArray a b) where 
  rnf !arr = ()