packages feed

sockets-0.3.1.0: bench/Macro.hs

{-# language BangPatterns #-}
{-# language DuplicateRecordFields #-}
{-# language LambdaCase #-}
{-# language MagicHash #-}
{-# language OverloadedStrings #-}
{-# language ScopedTypeVariables #-}
{-# language TypeFamilies #-}
{-# language UnboxedTuples #-}

-- This is a benchmark designed to stress both the sockets library
-- and the GHC event manager. It opens a moderate number of datagram sockets
-- that each belong to one of two teams: A and B. There is one worker
-- thread for each socket. The thread sends a datagram to a pseudorandomly
-- detemined socket on the other team. Then, it waits to receive a datagram
-- from a socket on the other team. All worker threads repeatedly perform
-- this task forever. Once a large number N of total receives have occurred,
-- the lucky worker thread performing the Nth receives fills an MVar that
-- tells the main thread that enough work has been done. The main thread
-- prints the total number of elapsed nanoseconds and then exits. This
-- benchmark does not attempt to close the sockets before exiting.
--
-- Here are some interesting results of running this benchmark on a
-- Dell Precision 7510 (Intel Xeon CPU E3-1505M, 4 physical cores,
-- 8 virtual cores with hyperthreading, 32GB memory):
--
-- * Wall-clock time:
--   * Non-threaded runtime: 37s
--   * Threaded runtime:
--     * N1: 12s
--     * N2: 7.5s
--     * N4: 19s
-- * Memory:
--   * Total Allocations: 4400MB
--   * Copied: 3MB
--   * Maximum Residency: 0.2MB
-- * Productivity: 99.8%
--
-- These were measured with these constants set:
-- 
-- * Participants: 64
-- * Payload Size: 32 words
-- * Total Receives: 3000000
--
-- The memory and productivity numbers do not change much based on
-- the number of capabilities, the nursery size, or the parallel GC
-- settings. In the event-manager-benchmarks repository, I've measured
-- that every call to threadWaitRead causes around 1KB of allocations
-- to happen. This sockets benchmark is certain to call threadWaitRead and
-- unlikely to call threadWaitWrite, so the 3M receives are responsible
-- for 3GB of allocations. It is unclear whether or not improvements
-- to the event manager would result in a tangible gain here.

import Control.Concurrent (forkIO)
import Control.Exception (Exception)
import Control.Exception (throwIO)
import Control.Monad (forever,forM_,when)
import Data.Primitive (PrimArray,MutablePrimArray(..))
import Data.Primitive.MVar (MVar)
import Data.Word (Word16)
import GHC.Clock (getMonotonicTimeNSec)
import GHC.Exts (RealWorld,Int(I#))
import GHC.IO (IO(..))
import Socket.Datagram.IPv4.Undestined (Endpoint(..))
import System.Entropy (getEntropy)

import qualified Data.ByteString as B
import qualified Data.Primitive as PM
import qualified Data.Primitive.MVar as PM
import qualified GHC.Exts as E
import qualified Net.IPv4 as IPv4
import qualified Socket.Datagram.IPv4.Undestined as DIU

main :: IO ()
main = do
  done <- PM.newEmptyMVar
  recvCounter <- PM.newPrimArray 1
  PM.writePrimArray recvCounter 0 0
  socketsCounterA <- PM.newPrimArray 1
  PM.writePrimArray socketsCounterA 0 0
  socketsCounterB <- PM.newPrimArray 1
  PM.writePrimArray socketsCounterB 0 0
  socketsA <- PM.newPrimArray participants
  socketsB <- PM.newPrimArray participants
  socketsMVarA <- PM.newEmptyMVar
  socketsMVarB <- PM.newEmptyMVar
  forM_ (enumFromTo 0 (participants - 1)) $ \ix -> do
    forkIO $ worker ix done recvCounter socketsCounterA socketsA socketsMVarA socketsMVarB
  forM_ (enumFromTo 0 (participants - 1)) $ \ix -> do
    forkIO $ worker ix done recvCounter socketsCounterB socketsB socketsMVarB socketsMVarA
  _ <- PM.readMVar socketsMVarA
  _ <- PM.readMVar socketsMVarB
  start <- getMonotonicTimeNSec
  PM.takeMVar done
  end <- getMonotonicTimeNSec
  print (end - start)

participants :: Int
participants = 64

-- This is in units of machine words
payloadSize :: Int
payloadSize = 32

totalReceives :: Int
totalReceives = 3000000

-- The PrimArray must be of length @participants@.
worker :: 
     Int -- ^ Worker identifier
  -> MVar RealWorld () -- ^ Used to signal that enough receives have happened
  -> MutablePrimArray RealWorld Int -- ^ Counter of total receives, singleton array
  -> MutablePrimArray RealWorld Int -- ^ Counter of opened sockets, singleton array
  -> MutablePrimArray RealWorld Word16 -- ^ Ports used by local team
  -> MVar RealWorld (PrimArray Word16) -- ^ MVar for ports used by local team
  -> MVar RealWorld (PrimArray Word16) -- ^ MVar for ports used by remote team
  -> IO ()
worker !ident !done !recvCounter !counter !locals !mlocals !mremotes = do
  unhandled $ DIU.withSocket (DIU.Endpoint IPv4.loopback 0) $ \sock myPort -> do
    buf@(PM.MutablePrimArray buf#) <- PM.newPrimArray payloadSize
    seedByteString <- getEntropy (payloadSize * PM.sizeOf (undefined :: Int))
    let seedByteArray = E.fromList (B.unpack seedByteString)
    PM.copyByteArray (PM.MutableByteArray buf#) 0 seedByteArray 0 (payloadSize * PM.sizeOf (undefined :: Int))
    PM.writePrimArray locals ident myPort
    incrementWorkerCounter counter >>= \case
      True -> PM.unsafeFreezePrimArray locals >>= PM.putMVar mlocals
      False -> pure ()
    remotes <- PM.readMVar mremotes
    act sock buf remotes recvCounter done

act ::
     DIU.Socket -- Socket
  -> MutablePrimArray RealWorld Int -- Buffer for receives
  -> PrimArray Word16 -- Ports used by remote team
  -> MutablePrimArray RealWorld Int -- Receive counter, singleton array
  -> MVar RealWorld () -- Signal that we are finished
  -> IO ()
act !sock !buf@(MutablePrimArray buf#) !remotes !counter !done = forever $ do
  n <- scramble buf
  let remote = PM.indexPrimArray remotes (mod n participants)
  unhandled $ DIU.sendMutableByteArraySlice sock (Endpoint {port = remote, address = IPv4.loopback})
    (PM.MutableByteArray buf#) 0 (payloadSize * PM.sizeOf (undefined :: Int))
  recvSz <- unhandled $ DIU.receiveMutableByteArraySlice_ sock (PM.MutableByteArray buf#) 0
    (payloadSize * PM.sizeOf (undefined :: Int))
  when (recvSz /= payloadSize * PM.sizeOf (undefined :: Int)) $ do
    fail "bad receive in act"
  incrementReceiveCounter counter >>= \case
    True -> PM.putMVar done ()
    False -> pure ()

scramble :: MutablePrimArray RealWorld Int -> IO Int
scramble arr = go 0 0x36b0b1c47d1ba5e1 0x55109de6a59394b3
  where
  go !ix !acc1 !acc2 = if ix < payloadSize
    then do
      v <- PM.readPrimArray arr ix
      PM.writePrimArray arr ix ((v + acc1) * acc2)
      go (ix + 1) acc2 v
    else pure (acc1 + acc2)

-- Returns true if the value of the counter reached the total
-- number of participants.
incrementWorkerCounter :: MutablePrimArray RealWorld Int -> IO Bool
incrementWorkerCounter (MutablePrimArray arr) = IO $ \s0 -> case E.fetchAddIntArray# arr 0# 1# s0 of
  (# s1, i #) -> (# s1, I# i == participants - 1 #)

incrementReceiveCounter :: MutablePrimArray RealWorld Int -> IO Bool
incrementReceiveCounter (MutablePrimArray arr) = IO $ \s0 -> case E.fetchAddIntArray# arr 0# 1# s0 of
  (# s1, i #) -> (# s1, I# i == totalReceives - 1 #)

unhandled :: Exception e => IO (Either e a) -> IO a
unhandled action = action >>= either throwIO pure