packages feed

memory-pool-0.1.0.0: tests/Test/System/Memory/PoolTests.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Test.System.Memory.PoolTests (poolTests) where

import Common
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.Chan
import Control.Monad
import Control.Monad.Primitive
import Data.Bits
import Data.Function
import Data.Primitive.PVar
import Data.Primitive.Ptr (setPtr)
import Data.Proxy
import Data.Reflection
import Data.Word
import Foreign.ForeignPtr
import Foreign.Ptr
import Foreign.Storable
import GHC.TypeLits
import System.Mem (performGC)
import System.Memory.Pool
import System.Random
import Test.QuickCheck.Monadic

poolTests :: TestTree
poolTests = do
  testGroup
    "Pool"
    [ testProperty "findNextZeroIndex" $ propFindNextZeroIndex
    , poolProps (Block :: Block 32)
    , poolProps (Block :: Block 64)
    , poolPropsArbSizeBlock
    ]

poolProps :: KnownNat n => Block n -> TestTree
poolProps block =
  testGroup
    ("Block " ++ show (blockByteCount block))
    [ testProperty "PoolGarbageCollected" $ propPoolGarbageCollected block
    , testProperty "PoolAllocateAndFinalize" $ propPoolAllocateAndFinalize block
    ]

poolPropsArbSizeBlock :: TestTree
poolPropsArbSizeBlock =
  testGroup
    "Arbitrary sized block"
    [ testProperty "PoolGarbageCollected" $ \(Positive n) ->
        reifyNat n (propPoolGarbageCollected . proxyToBlock)
    , testProperty "PoolAllocateAndFinalize" $ \(Positive n) ->
        reifyNat n (propPoolAllocateAndFinalize . proxyToBlock)
    ]
  where
    proxyToBlock :: Proxy n -> Block n
    proxyToBlock Proxy = Block

propFindNextZeroIndex :: Int -> Property
propFindNextZeroIndex w = monadicIO . run $
  case findNextZeroIndex w of
    Nothing -> w @?= complement 0
    Just ix -> do
      testBit w ix @?= False
      case findNextZeroIndex (setBit w ix) of
        Nothing -> setBit w ix @?= complement 0
        Just ix' -> do
          assertBool
            ("Expected found index to be different, but got same: " ++ show ix)
            (ix' /= ix)
          assertBool
            ("Expected the bit under index: " ++ show ix' ++ " to not be set")
            $ not (testBit w ix')

-- We allow one extra page be allocated due to concurrency false positives in block
-- reservations
checkNumPages :: Pool n RealWorld -> Int -> Int -> Assertion
checkNumPages pool n numBlocks = do
  let estimatedUpperBoundOfPages = 1 + max 1 (numBlocks `div` n `div` 64)
  numPages <- stToPrim $ countPages pool
  assertBool
    ( concat
        [ "Number of pages should not exceed the expected amount: "
        , show estimatedUpperBoundOfPages
        , " but allocated: "
        , show numPages
        ]
    )
    (numPages <= estimatedUpperBoundOfPages)

checkBlockBytes ::
  (KnownNat n, Storable a, Eq a, Show a) =>
  Block n ->
  a ->
  Ptr b ->
  Assertion
checkBlockBytes block byte ptr =
  let checkFillByte i =
        when (i >= 0) $ do
          byte' <- peekByteOff ptr i
          byte' @?= byte
          checkFillByte (i - 1)
   in checkFillByte (blockByteCount block - 1)

mallocPreFilled :: Word8 -> Int -> ST s (ForeignPtr b)
mallocPreFilled preFillByte bc = unsafeIOToPrim $ do
  mfp <- mallocForeignPtrBytes bc
  withForeignPtr mfp $ \ptr -> setPtr (castPtr ptr) bc preFillByte
  pure mfp

-- | @ensureAllGCedWith iterations delay expectedCount registerCounter@ waits
-- for all items to be GCed by triggering garbage collection @iterations@
-- times, once every @delay@ milliseconds. After @iterations@ attempts, if the
-- counter hook was not called exactly @expectedCount@ times, a test failure is
-- raised via 'assertFailure'. Garbage collection is tracked via finalizers
-- (see below).
ensureAllGCedWith ::
  -- | Number of GC attempts to make before failing
  Int ->
  -- | Delay between attempts, in milliseconds
  Int ->
  -- | Expected number of counter hook firings (in practice: individual
  -- garbage collections on 'ForeignPtr's, as per their finalizers).
  Int ->
  -- | Function for registering the counter hook. The argument to this
  -- function should be attached to each 'ForeignPtr' we're interested in
  -- as a finalizer.
  (IO () -> IO a) ->
  IO a
ensureAllGCedWith iterations delay expectedCount registerCounter = do
  countRef <- newPVar (0 :: Int)
  res <- registerCounter (void $ atomicAddIntPVar countRef 1)
  let go i = do
        performGC
        threadDelay (delay * 1000)
        n <- atomicReadIntPVar countRef
        unless (n == expectedCount) $ do
          if i <= 1
            then
              assertFailure $
                "Expected all "
                  ++ show expectedCount
                  ++ " pointers to be GCed in "
                  ++ show (delay * iterations)
                  ++ "ms, but "
                  ++ show n
                  ++ " where GCed instead"
            else go (i - 1)
  res <$ go iterations

-- | 'ensureAllGCedWith' with default values: 100 iterations, 10ms delay.
ensureAllGCed :: Int -> (IO () -> IO a) -> IO a
ensureAllGCed = ensureAllGCedWith 100 10

propPoolGarbageCollected ::
  forall n.
  KnownNat n =>
  Block n ->
  Positive Int ->
  Word16 ->
  Word8 ->
  Word8 ->
  Property
propPoolGarbageCollected block (Positive n) numBlocks16 preFillByte fillByte =
  monadicIO . run $ do
    let numBlocks = 1 + (fromIntegral numBlocks16 `div` 20) -- make it not too big
    (pool, ptrs) <-
      ensureAllGCed numBlocks $ \countOneBlockGCed -> do
        pool <-
          stToPrim $ initPool n (mallocPreFilled preFillByte) $ \ptr -> do
            setPtr (castPtr ptr) (blockByteCount block) fillByte
            countOneBlockGCed
        fmps :: [ForeignPtr (Block n)] <-
          replicateConcurrently numBlocks (stToPrim $ grabNextBlock pool)
        touch fmps
        -- Here we return just the pointers and let the GC collect the ForeignPtrs
        ptrs <-
          forM fmps $ \fma ->
            withForeignPtr fma $ \ptr -> do
              let bytePtr = castPtr ptr
              checkBlockBytes block preFillByte bytePtr
              setPtr bytePtr (blockByteCount block) fillByte
              pure bytePtr
        pure (pool, ptrs)
    forM_ ptrs (checkBlockBytes block fillByte)
    checkNumPages pool n numBlocks
    -- Ensure that memory to that the pointers are referencing to is still alive
    touch pool

propPoolAllocateAndFinalize ::
  forall n.
  KnownNat n =>
  Block n ->
  Positive Int ->
  Word16 ->
  Word8 ->
  Word8 ->
  Property
propPoolAllocateAndFinalize block (Positive n) numBlocks16 emptyByte fullByte =
  monadicIO . run $ do
    let numBlocks = 1 + (fromIntegral numBlocks16 `div` 20)
    pool <-
      ensureAllGCed numBlocks $ \countOneBlockGCed -> do
        chan <- newChan
        pool <-
          stToPrim $ initPool n (mallocPreFilled emptyByte) $ \(ptr :: Ptr (Block n)) -> do
            setPtr (castPtr ptr) (blockByteCount block) emptyByte
            countOneBlockGCed
        -- allocate and finalize blocks concurrently
        pool
          <$ concurrently_
            ( do
                replicateConcurrently_ numBlocks $ do
                  fp <- stToPrim $ grabNextBlock pool
                  withForeignPtr fp (checkBlockBytes block emptyByte)
                  writeChan chan (Just fp)
                -- place Nothing to indicate that we are done allocating blocks
                writeChan chan Nothing
            )
            ( fix $ \loop -> do
                mfp <- readChan chan
                forM_ mfp $ \fp -> do
                  withForeignPtr fp $ \ptr ->
                    -- fill the newly allocated block
                    setPtr (castPtr ptr) (blockByteCount block) fullByte
                  -- manually finalize every other block and let the GC to pick the rest
                  shouldFinalize <- randomIO
                  when shouldFinalize $ finalizeForeignPtr fp
                  loop
            )
    -- verify number of pages
    checkNumPages pool n numBlocks