packages feed

blockio-uring-0.1.0.0: benchmark/Bench.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{- HLINT ignore "Use camelCase" -}

module Main (main) where

import Data.Primitive
import qualified Data.Set as Set
import Control.Monad
import Control.Monad.Primitive (RealWorld)
import Control.Monad.ST (ST, stToIO)
import Control.Exception
import Control.Concurrent (getNumCapabilities)
import Control.Concurrent.Async as Async

import Foreign
import System.Posix.IO
import System.Posix.Files
import System.Posix.Types as Posix
import System.Posix.Fcntl

import System.Random as Random
import System.Environment
import System.Exit
import System.Mem (performMajorGC)
import Data.Time
import qualified GHC.Stats as RTS

import System.IO.BlockIO
import System.IO.BlockIO.URing hiding (submitIO)
import qualified System.IO.BlockIO.URing as URing

import qualified Data.Vector as V
import qualified Data.Vector.Mutable as VM

main :: IO ()
main = do
  args <- getArgs
  case args of
    [level,  cache, filename]
      | Just main_func <- parseLevel level
      , Just useCache <- parseCacheFlag cache
      -> main_func useCache filename
    _   -> do
      putStrLn "Usage: Bench [low|high] [Cache|NoCache] [DataFile]"
      exitFailure
  where
    parseLevel = \case
      "low" -> Just main_lowlevel
      "high" -> Just main_highlevel
      _ -> Nothing
    parseCacheFlag = \case
      "Cache" -> Just True
      "NoCache" -> Just False
      _ -> Nothing

main_lowlevel :: Bool -> FilePath -> IO ()
main_lowlevel useCache filename = do
  putStrLn "Low-level API benchmark"
  fd <- openFd filename ReadOnly defaultFileFlags

  fileSetCaching fd useCache
  putStrLn $ "File caching:    " ++ show useCache

  status <- getFdStatus fd
  let size      = fileSize status
      lastBlock :: Int
      lastBlock = fromIntegral (size `div` 4096 - 1)
      nqueue    = 64
      nbufs     = 64 * 4
  withURing (URingParams nqueue nbufs) $ \uring ->
    allocaBytesAligned (4096 * nbufs) 4096 $ \bufptr -> do
      let submitBatch :: [(Int, Int)] -> IO ()
          submitBatch blocks = do
            sequence_
              [ prepareRead uring fd blockoff bufptr' 4096
                            (IOOpId (fromIntegral i))
              | (i, block) <- blocks
              , let bufptr'  = bufptr `plusPtr` ((i `mod` nbufs) * 4096)
                    blockoff = fromIntegral (block * 4096)
              ]
            URing.submitIO uring

          collectBatch :: Int -> IO ()
          collectBatch n =
            replicateM_ n $ do
              (IOCompletion i count) <- awaitIO uring
              when (count /= IOResult 4096) $
                fail $ "I/O failure: I/O " ++ show i
                    ++ " returned " ++ show count

          go []     = return ()
          go blocks = do
            let (batch, blocks') = splitAt 32 blocks
            submitBatch batch
            let n = case blocks' of
                      [] -> length batch
                      _  -> 32
            collectBatch n
            go blocks'

      rng <- initStdGen
      let blocks = zip [0..] (randomPermute rng [0..lastBlock])
          totalOps = lastBlock + 1
          (leadIn, blocks') = splitAt 64 blocks

      withReport totalOps $ do
        submitBatch leadIn
        go blocks'
        collectBatch 64


{-# NOINLINE main_highlevel #-}
main_highlevel :: Bool -> FilePath -> IO ()
main_highlevel useCache filename = do
  putStrLn "High-level API benchmark"
  fd     <- openFd filename ReadOnly defaultFileFlags

  fileSetCaching fd useCache
  putStrLn $ "File caching:    " ++ show useCache

  status <- getFdStatus fd
  rng    <- initStdGen
  ncaps  <- getNumCapabilities
  let size      = fileSize status
      lastBlock :: Int
      lastBlock = fromIntegral (size `div` 4096 - 1)
      params    = IOCtxParams {
                    ioctxBatchSizeLimit   = 64,
                    ioctxConcurrencyLimit = 64 * 4
                  }
      ntasks    = 4 * ncaps
      batchsz   = 32
      nbatches  = lastBlock `div` (ntasks * batchsz) -- batches per task
      totalOps  = nbatches * batchsz * ntasks

  putStrLn $ "Capabilities:    " ++ show ncaps
  putStrLn $ "Threads     :    " ++ show ntasks

  bracket (initIOCtx params) closeIOCtx $ \ioctx ->
    withReport totalOps $ do
      tasks <-
        forRngSplitM ntasks rng $ \ n !rng_task ->
          Async.asyncOn n $ do
            buf <- newAlignedPinnedByteArray (4096 * batchsz) 4096
            -- Each thread gets its own reusable vector
            (v, unsafeGenerateIOOpsBatch) <- stToIO $ mkGenerateIOOpsBatch fd buf lastBlock batchsz
            forRngSplitM_ nbatches rng_task $ \ _ !rng_batch -> do
              stToIO $ unsafeGenerateIOOpsBatch rng_batch
              submitIO ioctx v
      _ <- Async.waitAnyCancel tasks
      return ()

{-# NOINLINE mkGenerateIOOpsBatch #-}
-- | Create a reusable vector and an /unsafe/ function to fill the vector with
-- random elements.
--
-- The returned vector is immutable, but the filler function mutates the vector
-- in place. The user should make sure not to use (e.g., read) the vector while
-- it's being filled.
mkGenerateIOOpsBatch :: Posix.Fd
                     -> MutableByteArray RealWorld
                     -> Int
                     -> Int
                     -> ST RealWorld ( V.Vector (IOOp RealWorld)
                                     , -- fill the vector with new randomly generated IOOps
                                       Random.StdGen -> ST RealWorld ()
                                     )
mkGenerateIOOpsBatch !fd !buf !lastBlock !size = do
    v <- VM.new size
    v' <- V.unsafeFreeze v
    pure (v', \rng -> go v rng 0)
  where
    go :: V.MVector RealWorld (IOOp RealWorld) -> Random.StdGen -> Int -> ST RealWorld ()
    go !_ !_   !i | i == size = return ()
    go !v !rng !i = do
      let (!block, !rng') = Random.uniformR (0, lastBlock) rng
          !bufOff   = i * 4096
          !blockoff = fromIntegral (block * 4096)
      VM.unsafeWrite v i $! IOOpRead fd blockoff buf bufOff 4096
      go v rng' (i+1)

{-# INLINE forRngSplitM_ #-}
forRngSplitM_ :: Monad m
              => Int
              -> Random.StdGen
              -> (Int -> Random.StdGen -> m a)
              -> m ()
forRngSplitM_ n rng0 action = go 0 rng0
  where
    go !i !_   | i == n = return ()
    go !i !rng = let (!rng', !rng'') = Random.splitGen rng
                  in action i rng' >> go (i+1) rng''

{-# INLINE forRngSplitM #-}
forRngSplitM :: Monad m
             => Int
             -> Random.StdGen
             -> (Int -> Random.StdGen -> m a)
             -> m [a]
forRngSplitM n rng0 action = go [] 0 rng0
  where
    go acc !i !_   | i == n = return (reverse acc)
    go acc !i !rng = let (!rng', !rng'') = Random.splitGen rng
                      in action i rng' >>= \x -> go (x:acc) (i+1) rng''

{-# INLINE withReport #-}
withReport :: Int -> IO () -> IO ()
withReport totalOps action = do
    performMajorGC
    beforeRTS  <- RTS.getRTSStats
    beforeTime <- getCurrentTime
    action
    afterTime <- getCurrentTime
    performMajorGC
    afterRTS  <- RTS.getRTSStats
    report beforeTime afterTime beforeRTS afterRTS totalOps

{-# NOINLINE report #-}
report :: UTCTime -> UTCTime -> RTS.RTSStats -> RTS.RTSStats -> Int -> IO ()
report beforeTime afterTime beforeRTS afterRTS totalOps = do
    putStrLn $ "Total I/O ops:   " ++ show totalOps
    putStrLn $ "Elapsed time:    " ++ show elapsed
    putStrLn $ "IOPS:            " ++ show iops
    putStrLn $ "Allocated total: " ++ show allocated
    putStrLn $ "Allocated per:   " ++ show apio
  where
    elapsed   = afterTime `diffUTCTime` beforeTime
    allocated = RTS.allocated_bytes afterRTS - RTS.allocated_bytes beforeRTS

    iops, apio :: Int
    iops = round (fromIntegral totalOps / realToFrac elapsed :: Double)
    apio = round (fromIntegral allocated / realToFrac totalOps :: Double)

randomPermute :: Ord a => StdGen -> [a] -> [a]
randomPermute rng0 xs0 =
    go (Set.fromList xs0) rng0
  where
    go !xs !_rng | Set.null xs = []

    go !xs !rng = x : go xs' rng'
      where
        (i, rng') = uniformR (0, Set.size xs - 1) rng
        !x   = Set.elemAt i xs
        !xs' = Set.deleteAt i xs