packages feed

apecs-roaring-1.0: bench/Main.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

import Apecs (Cache, Component (..), Destroy, Entity (..), Get, Map, Members, Not (..), Proxy (..), Set, System, cfold, cmap, destroy, exists, makeWorld, runSystem, set)
import Apecs.Stores.Flags (Flags)
import Apecs.Stores.Flags qualified as Flags
import Apecs.Stores.Query (Every (..))
import Apecs.Stores.Query qualified as Query
import Apecs.Stores.Roaring (Roaring)
import Control.DeepSeq (NFData (..), rwhnf)
import Control.Monad (unless)
import Data.List (nub)
import Data.Vector.Unboxed qualified as Unboxed
import GHC.Generics (Generic)
import Test.Tasty.Bench (Benchmark, bcompare, bench, bgroup, defaultMain, env, locateBenchmark, mapLeafBenchmarks, whnfIO)
import Test.Tasty.Patterns.Printer (printAwkExpr)

data MapFlag = MapFlag

instance Component MapFlag where
  type Storage MapFlag = Map MapFlag

data CachedFlag = CachedFlag

instance Component CachedFlag where
  type Storage CachedFlag = Cache 10000 (Map CachedFlag)

data RoaringFlag = RoaringFlag
  deriving (Generic)

instance Component RoaringFlag where
  type Storage RoaringFlag = Flags RoaringFlag

newtype MapHealth = MapHealth Int

instance Component MapHealth where
  type Storage MapHealth = Map MapHealth

newtype CachedHealth = CachedHealth Int

instance Component CachedHealth where
  type Storage CachedHealth = Cache 10000 (Map CachedHealth)

newtype RoaringHealth = RoaringHealth Int

instance Component RoaringHealth where
  type Storage RoaringHealth = Roaring (Cache 10000 (Map RoaringHealth))

makeWorld "MapFlagWorld" [''MapFlag]
makeWorld "CachedFlagWorld" [''CachedFlag]
makeWorld "RoaringFlagWorld" [''RoaringFlag]
makeWorld "MapHealthWorld" [''MapHealth]
makeWorld "CachedHealthWorld" [''CachedHealth]
makeWorld "RoaringHealthWorld" [''RoaringHealth]
makeWorld "MapJoinWorld" [''MapFlag, ''MapHealth]
makeWorld "CachedJoinWorld" [''CachedFlag, ''CachedHealth]
makeWorld "RoaringJoinWorld" [''RoaringFlag, ''RoaringHealth]

instance NFData MapFlagWorld where rnf = rwhnf
instance NFData CachedFlagWorld where rnf = rwhnf
instance NFData RoaringFlagWorld where rnf = rwhnf
instance NFData MapHealthWorld where rnf = rwhnf
instance NFData CachedHealthWorld where rnf = rwhnf
instance NFData RoaringHealthWorld where rnf = rwhnf
instance NFData MapJoinWorld where rnf = rwhnf
instance NFData CachedJoinWorld where rnf = rwhnf
instance NFData RoaringJoinWorld where rnf = rwhnf

type Contender w c = (NFData w, Get w IO c, Set w IO c, Destroy w IO c, Members w IO c)

data Store w c = Store
  { label :: String
  , initWorld :: IO w
  , make :: Int -> c
  , weigh :: c -> Int
  , count :: System w Int
  , clear :: System w ()
  }

data Join w = Join
  { label :: String
  , initWorld :: IO w
  , populate :: Pairs -> System w ()
  , total :: System w Int
  , overlap :: System w Int
  }

data Layout = Layout
  { name :: String
  , present :: Int -> Unboxed.Vector Int
  , absent :: Int -> Unboxed.Vector Int
  }

data Ids = Ids
  { size :: Int
  , present :: Unboxed.Vector Int
  , probes :: Unboxed.Vector Int
  }

instance NFData Ids where
  rnf ids = rnf ids.present `seq` rnf ids.probes

data Pairs = Pairs
  { flagged :: Unboxed.Vector Int
  , healthy :: Unboxed.Vector Int
  , overlap :: Int
  , total :: Int
  }

instance NFData Pairs where
  rnf pairs = rnf pairs.flagged `seq` rnf pairs.healthy

baseline :: String
baseline = "Cache 10000 Map"

sizes :: [Int]
sizes = [1000, 10000, 100000, 1000000]

main :: IO ()
main =
  defaultMain
    . map (mapLeafBenchmarks compareToBaseline)
    $ [ bgroup "flags" $ map (layoutGroup flagsGroup) [dense, sparse]
      , bgroup "payload" $ map (layoutGroup payloadGroup) [dense, sparse]
      , bgroup "join" $ map (layoutGroup joinGroup) [dense, sparse]
      ]

compareToBaseline :: [String] -> Benchmark -> Benchmark
compareToBaseline = \case
  leaf : path | leaf /= baseline -> bcompare . printAwkExpr . locateBenchmark $ baseline : path
  _ -> id

dense :: Layout
dense =
  Layout
    { name = "dense"
    , present = Unboxed.enumFromN 0
    , absent = \count -> Unboxed.enumFromN count count
    }

sparse :: Layout
sparse =
  Layout
    { name = "sparse"
    , present = Unboxed.enumFromStepN 0 37
    , absent = Unboxed.enumFromStepN 1 37
    }

layoutGroup :: (Layout -> Int -> Benchmark) -> Layout -> Benchmark
layoutGroup sizeGroup layout = bgroup layout.name $ map (sizeGroup layout) sizes

idsFor :: Layout -> Int -> Ids
idsFor layout count =
  Ids
    { size = count
    , present = layout.present count
    , probes = layout.present count <> layout.absent count
    }

pairsFor :: Layout -> Int -> Pairs
pairsFor layout count =
  Pairs
    { flagged = every 2
    , healthy = every 3
    , overlap = Unboxed.length common
    , total = Unboxed.sum common
    }
  where
    spread = layout.present count
    every step = Unboxed.ifilter (\position _ -> position `mod` step == 0) spread
    common = every 6

flagsGroup :: Layout -> Int -> Benchmark
flagsGroup layout count =
  env (pure $ idsFor layout count) $ \ids ->
    bgroup (show count) $ byOperation [flagOperations ids mapFlags, flagOperations ids cachedFlags, flagOperations ids roaringFlags]

payloadGroup :: Layout -> Int -> Benchmark
payloadGroup layout count =
  env (pure $ idsFor layout count) $ \ids ->
    bgroup (show count) $ byOperation [operations ids mapHealth, operations ids cachedHealth, operations ids roaringHealth]

joinGroup :: Layout -> Int -> Benchmark
joinGroup layout count =
  env (pure $ pairsFor layout count) $ \pairs ->
    bgroup (show count) $ byOperation [joinOperations pairs mapJoin, joinOperations pairs cachedJoin, joinOperations pairs tupleJoin, joinOperations pairs everyJoin]

byOperation :: [[(String, Benchmark)]] -> [Benchmark]
byOperation perStore = [bgroup operation [benchmark | named <- perStore, (candidate, benchmark) <- named, candidate == operation] | operation <- names]
  where
    names = nub [operation | named <- perStore, (operation, _) <- named]

mapFlags :: Store MapFlagWorld MapFlag
mapFlags = Store{label = "Map", initWorld = initMapFlagWorld, make = const MapFlag, weigh = const 1, count = countAll @MapFlag, clear = clearAll @MapFlag}

cachedFlags :: Store CachedFlagWorld CachedFlag
cachedFlags = Store{label = baseline, initWorld = initCachedFlagWorld, make = const CachedFlag, weigh = const 1, count = countAll @CachedFlag, clear = clearAll @CachedFlag}

roaringFlags :: Store RoaringFlagWorld RoaringFlag
roaringFlags = Store{label = "Flags", initWorld = initRoaringFlagWorld, make = const RoaringFlag, weigh = const 1, count = Query.count @RoaringFlag, clear = Flags.clear @RoaringFlag}

mapHealth :: Store MapHealthWorld MapHealth
mapHealth = Store{label = "Map", initWorld = initMapHealthWorld, make = MapHealth, weigh = \(MapHealth hp) -> hp, count = countAll @MapHealth, clear = clearAll @MapHealth}

cachedHealth :: Store CachedHealthWorld CachedHealth
cachedHealth = Store{label = baseline, initWorld = initCachedHealthWorld, make = CachedHealth, weigh = \(CachedHealth hp) -> hp, count = countAll @CachedHealth, clear = clearAll @CachedHealth}

roaringHealth :: Store RoaringHealthWorld RoaringHealth
roaringHealth = Store{label = "Roaring Cache 10000 Map", initWorld = initRoaringHealthWorld, make = RoaringHealth, weigh = \(RoaringHealth hp) -> hp, count = Query.count @RoaringHealth, clear = clearAll @RoaringHealth}

mapJoin :: Join MapJoinWorld
mapJoin =
  Join
    { label = "Map"
    , initWorld = initMapJoinWorld
    , populate = fill MapFlag MapHealth
    , total = cfold (\running (MapFlag, MapHealth hp) -> running + hp) 0
    , overlap = cfold (\found (MapFlag, MapHealth _) -> found + 1) 0
    }

cachedJoin :: Join CachedJoinWorld
cachedJoin =
  Join
    { label = baseline
    , initWorld = initCachedJoinWorld
    , populate = fill CachedFlag CachedHealth
    , total = cfold (\running (CachedFlag, CachedHealth hp) -> running + hp) 0
    , overlap = cfold (\found (CachedFlag, CachedHealth _) -> found + 1) 0
    }

tupleJoin :: Join RoaringJoinWorld
tupleJoin =
  Join
    { label = "Flags and Roaring tuple"
    , initWorld = initRoaringJoinWorld
    , populate = fill RoaringFlag RoaringHealth
    , total = cfold (\running (RoaringFlag, RoaringHealth hp) -> running + hp) 0
    , overlap = cfold (\found (RoaringFlag, RoaringHealth _) -> found + 1) 0
    }

everyJoin :: Join RoaringJoinWorld
everyJoin =
  Join
    { label = "Flags and Roaring Every"
    , initWorld = initRoaringJoinWorld
    , populate = fill RoaringFlag RoaringHealth
    , total = cfold (\running (Every (RoaringFlag, RoaringHealth hp)) -> running + hp) 0
    , overlap = Query.count @(RoaringFlag, RoaringHealth)
    }

fill :: (Set w IO f, Set w IO h) => f -> (Int -> h) -> Pairs -> System w ()
fill flag health pairs = do
  Unboxed.mapM_ (\ety -> set (Entity ety) flag) pairs.flagged
  Unboxed.mapM_ (\ety -> set (Entity ety) $ health ety) pairs.healthy
{-# INLINE fill #-}

flagOperations :: (Contender w c) => Ids -> Store w c -> [(String, Benchmark)]
flagOperations ids store = operations ids store <> [("reset", withPopulated ids store . runSystem $ store.clear >> raise store ids.present)]
{-# INLINE flagOperations #-}

operations :: forall w c. (Contender w c) => Ids -> Store w c -> [(String, Benchmark)]
operations ids store =
  [ ("set", bench store.label . whnfIO $ store.initWorld >>= runSystem (raise store ids.present))
  , ("exists", withPopulated ids store $ runSystem (countExisting @c ids.probes))
  , ("cfold", withPopulated ids store . runSystem $ cfold (\running component -> running + store.weigh component) 0)
  , ("toggle", withPopulated ids store . runSystem $ lower @c ids.present >> raise store ids.present)
  , ("count", withPopulated ids store $ runSystem store.count)
  ]
{-# INLINE operations #-}

joinOperations :: (NFData w) => Pairs -> Join w -> [(String, Benchmark)]
joinOperations pairs contender =
  [ ("sum", prepared contender.total)
  , ("count", prepared contender.overlap)
  ]
  where
    prepared measure = env prepare $ bench contender.label . whnfIO . runSystem measure
    prepare = do
      world <- contender.initWorld
      (found, summed) <- flip runSystem world $ do
        contender.populate pairs
        (,) <$> contender.overlap <*> contender.total
      unless (found == pairs.overlap && summed == pairs.total) . fail $
        contender.label <> ": joined " <> show found <> " summing " <> show summed <> ", expected " <> show pairs.overlap <> " summing " <> show pairs.total
      pure world
{-# INLINE joinOperations #-}

withPopulated :: forall w c a. (Contender w c) => Ids -> Store w c -> (w -> IO a) -> Benchmark
withPopulated ids store action = env populate $ bench store.label . whnfIO . action
  where
    populate = do
      world <- store.initWorld
      counted <- flip runSystem world $ raise store ids.present >> countAll @c
      unless (counted == ids.size) . fail $ store.label <> ": populated " <> show counted <> " of " <> show ids.size
      pure world
{-# INLINE withPopulated #-}

raise :: (Contender w c) => Store w c -> Unboxed.Vector Int -> System w ()
raise store = Unboxed.mapM_ $ \ety -> set (Entity ety) $ store.make ety
{-# INLINE raise #-}

lower :: forall c w. (Contender w c) => Unboxed.Vector Int -> System w ()
lower = Unboxed.mapM_ $ \ety -> destroy (Entity ety) $ Proxy @c
{-# INLINE lower #-}

countExisting :: forall c w. (Contender w c) => Unboxed.Vector Int -> System w Int
countExisting = Unboxed.foldM' probe 0
  where
    probe found ety = do
      present <- exists (Entity ety) $ Proxy @c
      pure $ if present then found + 1 else found
{-# INLINE countExisting #-}

countAll :: forall c w. (Get w IO c, Members w IO c) => System w Int
countAll = cfold (\found (_ :: c) -> found + 1) 0
{-# INLINE countAll #-}

clearAll :: forall c w. (Get w IO c, Members w IO c, Destroy w IO c) => System w ()
clearAll = cmap $ \(_ :: c) -> Not @c
{-# INLINE clearAll #-}