lsm-tree-1.0.0.0: bench/micro/Bench/Database/LSMTree/Internal/Index/Compact.hs
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE TypeApplications #-}
module Bench.Database.LSMTree.Internal.Index.Compact (
benchmarks
-- * Benchmarked functions
, searches
, constructIndexCompact
) where
import Control.DeepSeq (deepseq)
import Control.Exception (assert)
import Control.Monad.ST.Strict
import Criterion.Main
import qualified Data.Foldable as Fold
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Vector.Unboxed.Mutable as VUM
import Data.Word
import Database.LSMTree.Extras
import Database.LSMTree.Extras.Generators
import Database.LSMTree.Extras.Index
import Database.LSMTree.Extras.Random
import Database.LSMTree.Extras.UTxO
import Database.LSMTree.Internal.Index.Compact
import Database.LSMTree.Internal.Index.CompactAcc
import Database.LSMTree.Internal.Map.Range
import Database.LSMTree.Internal.Serialise (SerialisedKey,
serialiseKey)
import System.Random
import Test.QuickCheck (generate)
benchmarks :: Benchmark
benchmarks = bgroup "Bench.Database.LSMTree.Internal.Index.Compact" [
env (searchEnv 10000 1000) $ \ ~(ic, ks) ->
bench "searches-10-1k" $ whnf (searches ic) ks
, bgroup "construction" [
env (constructionEnv 1000) $ \ pages ->
bench "construction-1k-100" $ whnf (constructIndexCompact 100) pages
, env (VUM.replicate 3000 (7 :: Word32)) $ \ mv ->
bench "unsafeWriteRange-1k" $
whnfAppIO (\x -> stToIO (unsafeWriteRange mv (BoundInclusive 1000) (BoundInclusive 2000) x)) 17
, env (VUM.replicate 30000 (7 :: Word32)) $ \ mv ->
bench "unsafeWriteRange-10k" $
whnfAppIO (\x -> stToIO (unsafeWriteRange mv (BoundInclusive 10000) (BoundInclusive 20000) x)) 17
]
, benchNonUniform
]
-- | Input environment for benchmarking 'searches'.
searchEnv ::
Int -- ^ Number of pages
-> Int -- ^ Number of searches
-> IO (IndexCompact, [SerialisedKey])
searchEnv npages nsearches = do
ic <- constructIndexCompact 100 <$> constructionEnv npages
let stdgen = mkStdGen 17
let ks = serialiseKey <$> uniformWithReplacement @UTxOKey stdgen nsearches
pure (ic, ks)
-- | Used for benchmarking 'search'.
searches ::
IndexCompact
-> [SerialisedKey] -- ^ Keys to search for
-> ()
searches ic ks = Fold.foldl' (\acc k -> search k ic `deepseq` acc) () ks
-- | Input environment for benchmarking 'constructIndexCompact'.
constructionEnv ::
Int -- ^ Number of pages
-> IO [Append]
constructionEnv n = do
let stdgen = mkStdGen 17
let ks = uniformWithoutReplacement @UTxOKey stdgen (2 * n)
ps <- generate (mkPages 0 (error "unused in constructionEnv") 0 ks)
pure (toAppends ps)
-- | Used for benchmarking the incremental construction of a 'IndexCompact'.
constructIndexCompact ::
ChunkSize
-> [Append] -- ^ Pages to add in succession
-> IndexCompact
constructIndexCompact (ChunkSize csize) apps = runST $ do
ica <- new csize
mapM_ (`appendToCompact` ica) apps
(_, index) <- unsafeEnd ica
pure index
{-------------------------------------------------------------------------------
Benchmarks for UTxO keys that are /almost/ uniformly distributed
-------------------------------------------------------------------------------}
-- | UTXO keys are not truly uniformly distributed. The 'txId' is a uniformly
-- distributed hash, but the same 'txId' can appear in multiple UTXO keys, but
-- with a different 'txIx'. In the worst case, this means that we have a clash
-- in the compact index for /every page/. The following benchmarks show
benchNonUniform :: Benchmark
benchNonUniform =
bgroup "non-uniformity" [
-- construction
env (pure $ (appsWithNearDups (mkStdGen 17) 1000)) $ \as ->
bench ("construct appsWithNearDups") $ whnf (constructIndexCompact 1000) as
, env (pure $ (appsWithoutNearDups (mkStdGen 17) 1000)) $ \as ->
bench ("construct appsWithoutNearDups") $ whnf (constructIndexCompact 1000) as
-- search
, env ( let ic = constructIndexCompact 100 (appsWithNearDups (mkStdGen 17) 1000)
g = mkStdGen 42
ks = serialiseKey <$> uniformWithReplacement @UTxOKey g 1000
in pure (ic, ks) ) $ \ ~(ic, ks) ->
bench "search appsWithNearDups" $ whnf (searches ic) ks
, env ( let ic = constructIndexCompact 100 (appsWithoutNearDups (mkStdGen 17) 1000)
g = mkStdGen 42
ks = serialiseKey <$> uniformWithReplacement @UTxOKey g 1000
in pure (ic, ks) ) $ \ ~(ic, ks) ->
bench "search appsWithoutNearDups" $ whnf (searches ic) ks
]
-- | 'Append's with truly uniformly distributed UTXO keys.
appsWithoutNearDups ::
StdGen
-> Int -- ^ Number of pages
-> [Append]
appsWithoutNearDups g n =
let ks = uniformWithoutReplacement @UTxOKey g (n * 2)
ks' = List.sort ks
-- append a dummy UTXO key because appsWithNearDups does so too.
ps = groupsOfN 2 (UTxOKey 0 0 : ks')
in assert (length ps == n + 1) $
fmap fromNE ps
-- | 'Append's with worst-case near-duplicates. Each page boundary splits UTXO
-- keys with the same 'txId' but different a 'txIx'.
appsWithNearDups ::
StdGen
-> Int -- ^ Number of pages
-> [Append]
appsWithNearDups g n =
let ks = uniformWithoutReplacement @UTxOKey g n
ks' = flip concatMap (List.sort ks) $ \k -> [k {txIx = 0}, k {txIx = 1}]
-- append a dummy UTXO key so that each pair of near-duplicate keys is
-- split between pages. That is, the left element of the pair is the
-- maximum key in a page, and the right element of the pair is the
-- minimum key on the next page.
ps = groupsOfN 2 (UTxOKey 0 0 : ks')
in -- check that the number of pages
assert (length ps == n + 1) $
fmap fromNE ps
fromNE :: NonEmpty UTxOKey -> Append
fromNE xs =
assert (NE.sort xs == xs) $
assert (NE.nub xs == xs) $
AppendSinglePage (serialiseKey $ NE.head xs) (serialiseKey $ NE.last xs)