lsm-tree-1.0.0.0: src-core/Database/LSMTree/Internal/Index/CompactAcc.hs
{-# LANGUAGE CPP #-}
{-# OPTIONS_HADDOCK not-home #-}
-- |
-- Incremental construction of a compact index yields chunks of the primary array
-- that can be serialised incrementally.
--
-- Incremental construction is an 'ST' computation that can be started using
-- 'new', returning an 'IndexCompactAcc' structure that accumulates internal
-- state. 'append'ing new pages to the 'IndexCompactAcc' /might/ yield 'Chunk's.
-- Incremental construction can be finalised with 'unsafeEnd', which yields both
-- a 'Chunk' (possibly) and the `IndexCompact'.
--
module Database.LSMTree.Internal.Index.CompactAcc (
-- * Construction
IndexCompactAcc (..)
, new
, newWithDefaults
, appendSingle
, appendMulti
, unsafeEnd
-- * Utility
, SMaybe (..)
, smaybe
-- * Internal: exported for testing and benchmarking
, unsafeWriteRange
, vectorLowerBound
, mvectorUpperBound
) where
#ifdef NO_IGNORE_ASSERTS
import Control.Exception (assert)
#endif
import Control.Monad (when)
import Control.Monad.ST.Strict
import Data.Bit hiding (flipBit)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Primitive.ByteArray (newPinnedByteArray, setByteArray)
import Data.STRef.Strict
import qualified Data.Vector.Generic.Mutable as VGM
import qualified Data.Vector.Primitive.Mutable as VPM
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Data.Word
import Database.LSMTree.Internal.BitMath
import Database.LSMTree.Internal.Chunk (Chunk)
import Database.LSMTree.Internal.Index.Compact
import Database.LSMTree.Internal.Map.Range (Bound (..))
import Database.LSMTree.Internal.Page
import Database.LSMTree.Internal.Serialise
import Database.LSMTree.Internal.Unsliced
{-------------------------------------------------------------------------------
Construction
-------------------------------------------------------------------------------}
-- | A mutable version of 'IndexCompact'. See [incremental
-- construction](#incremental).
data IndexCompactAcc s = IndexCompactAcc {
-- * Core index structure
-- | Accumulates pinned chunks of 'ciPrimary'.
icaPrimary :: !(STRef s (NonEmpty (VU.MVector s Word64)))
-- | Accumulates chunks of 'ciClashes'.
, icaClashes :: !(STRef s (NonEmpty (VU.MVector s Bit)))
-- | Accumulates the 'ciTieBreaker'.
, icaTieBreaker :: !(STRef s (Map (Unsliced SerialisedKey) PageNo))
-- | Accumulates chunks of 'ciLargerThanPage'.
, icaLargerThanPage :: !(STRef s (NonEmpty (VU.MVector s Bit)))
-- * Aux information required for incremental construction
-- | Maximum size of a chunk
, icaMaxChunkSize :: !Int
-- | The number of the current disk page we are constructing the index for.
, icaCurrentPageNumber :: !(STRef s Int)
-- | The primary bits of the page-maximum key that we saw last.
--
-- This should be 'SNothing' if we haven't seen any keys/pages yet.
, icaLastMaxPrimbits :: !(STRef s (SMaybe Word64))
-- | The ful minimum key of the page that we saw last.
--
-- This should be 'SNothing' if we haven't seen any keys/pages yet.
, icaLastMinKey :: !(STRef s (SMaybe SerialisedKey))
}
-- | @'new' maxcsize@ creates a new mutable index with a maximum chunk size of
-- @maxcsize@.
--
-- PRECONDITION: maxcsize > 0
--
-- Note: after initialisation, @maxcsize@ can no longer be changed.
new ::Int -> ST s (IndexCompactAcc s)
new maxcsize = IndexCompactAcc
-- Core index structure
<$> (newSTRef . pure =<< newPinnedMVec64 maxcsize)
<*> (newSTRef . pure =<< VUM.new maxcsize)
<*> newSTRef Map.empty
<*> (newSTRef . pure =<< VUM.new maxcsize)
-- Aux information required for incremental construction
<*> pure maxcsize
<*> newSTRef 0
<*> newSTRef SNothing
<*> newSTRef SNothing
-- | We explicitly pin the byte arrays, since that allows for more efficient
-- serialisation, as the definition of 'isByteArrayPinned' changed in GHC 9.6,
-- see <https://gitlab.haskell.org/ghc/ghc/-/issues/22255>.
--
-- TODO: remove this workaround once a solution exists, e.g. a new primop that
-- allows checking for implicit pinning.
newPinnedMVec64 :: Int -> ST s (VUM.MVector s Word64)
newPinnedMVec64 lenWords = do
mba <- newPinnedByteArray (mul8 lenWords)
setByteArray mba 0 lenWords (0 :: Word64)
pure (VUM.MV_Word64 (VPM.MVector 0 lenWords mba))
{-|
For a specification of this operation, see the documentation of [its
type-agnostic version]('Database.LSMTree.Internal.Index.newWithDefaults').
-}
newWithDefaults :: ST s (IndexCompactAcc s)
newWithDefaults = new 1024
{-|
For a specification of this operation, see the documentation of [its
type-agnostic version]('Database.LSMTree.Internal.Index.appendSingle').
-}
appendSingle :: forall s. (SerialisedKey, SerialisedKey) -> IndexCompactAcc s -> ST s (Maybe Chunk)
appendSingle (minKey, maxKey) ica@IndexCompactAcc{..} = do
#ifdef NO_IGNORE_ASSERTS
lastMinKey <- readSTRef icaLastMinKey
assert (minKey <= maxKey && smaybe True (<= minKey) lastMinKey) $ pure () -- sorted
#endif
pageNo <- readSTRef icaCurrentPageNumber
let ix = pageNo `mod` icaMaxChunkSize
goAppend pageNo ix
writeSTRef icaCurrentPageNumber $! pageNo + 1
yield ica
where
minPrimbits, maxPrimbits :: Word64
minPrimbits = keyTopBits64 minKey
maxPrimbits = keyTopBits64 maxKey
-- | Meat of the function
goAppend ::
Int -- ^ Current /global/ page number
-> Int -- ^ Current /local/ page number (inside the current chunk)
-> ST s ()
goAppend pageNo ix = do
writePrimary
writeClashesAndLTP
where
-- | Set value in primary vector
writePrimary :: ST s ()
writePrimary =
readSTRef icaPrimary >>= \cs -> VUM.write (NE.head cs) ix minPrimbits
-- | Set value in clash vector, tie-breaker map and larger-than-page
-- vector
writeClashesAndLTP :: ST s ()
writeClashesAndLTP = do
lastMaxPrimbits <- readSTRef icaLastMaxPrimbits
let clash = lastMaxPrimbits == SJust minPrimbits
writeSTRef icaLastMaxPrimbits $! SJust maxPrimbits
lastMinKey <- readSTRef icaLastMinKey
let ltp = SJust minKey == lastMinKey
writeSTRef icaLastMinKey $! SJust minKey
readSTRef icaClashes >>= \cs -> VUM.write (NE.head cs) ix (Bit clash)
readSTRef icaLargerThanPage >>= \cs -> VUM.write (NE.head cs) ix (Bit ltp)
when (clash && not ltp) $
modifySTRef' icaTieBreaker (Map.insert (makeUnslicedKey minKey) (PageNo pageNo))
{-|
For a specification of this operation, see the documentation of [its
type-agnostic version]('Database.LSMTree.Internal.Index.appendMulti').
-}
appendMulti :: forall s. (SerialisedKey, Word32) -> IndexCompactAcc s -> ST s [Chunk]
appendMulti (k, n0) ica@IndexCompactAcc{..} =
maybe id (:) <$> appendSingle (k, k) ica <*> overflows (fromIntegral n0)
where
minPrimbits :: Word64
minPrimbits = keyTopBits64 k
-- | Fill primary, clash and LTP vectors for a larger-than-page value. Yields
-- chunks if necessary
overflows :: Int -> ST s [Chunk]
overflows n
| n <= 0 = pure []
| otherwise = do
pageNo <- readSTRef icaCurrentPageNumber
let ix = pageNo `mod` icaMaxChunkSize -- will be 0 in recursive calls
remInChunk = min n (icaMaxChunkSize - ix)
readSTRef icaPrimary >>= \cs ->
unsafeWriteRange (NE.head cs) (BoundInclusive ix) (BoundExclusive $ ix + remInChunk) minPrimbits
readSTRef icaClashes >>= \cs ->
unsafeWriteRange (NE.head cs) (BoundInclusive ix) (BoundExclusive $ ix + remInChunk) (Bit True)
readSTRef icaLargerThanPage >>= \cs ->
unsafeWriteRange (NE.head cs) (BoundInclusive ix) (BoundExclusive $ ix + remInChunk) (Bit True)
writeSTRef icaCurrentPageNumber $! pageNo + remInChunk
res <- yield ica
maybe id (:) res <$> overflows (n - remInChunk)
-- | Yield a chunk and start a new one if the current chunk is already full.
--
-- TODO(optimisation): yield will eagerly allocate new mutable vectors, but
-- maybe that should be done lazily.
--
-- INVARIANTS: see [construction invariants](#construction-invariants).
yield :: IndexCompactAcc s -> ST s (Maybe Chunk)
yield IndexCompactAcc{..} = do
pageNo <- readSTRef icaCurrentPageNumber
if pageNo `mod` icaMaxChunkSize == 0 then do -- The current chunk is full
primaryChunk <- VU.unsafeFreeze . NE.head =<< readSTRef icaPrimary
modifySTRef' icaPrimary . NE.cons =<< newPinnedMVec64 icaMaxChunkSize
modifySTRef' icaClashes . NE.cons =<< VUM.new icaMaxChunkSize
modifySTRef' icaLargerThanPage . NE.cons =<< VUM.new icaMaxChunkSize
pure $ Just (word64VectorToChunk primaryChunk)
else -- the current chunk is not yet full
pure Nothing
{-|
For a specification of this operation, see the documentation of [its
type-agnostic version]('Database.LSMTree.Internal.Index.unsafeEnd').
-}
unsafeEnd :: IndexCompactAcc s -> ST s (Maybe Chunk, IndexCompact)
unsafeEnd IndexCompactAcc{..} = do
pageNo <- readSTRef icaCurrentPageNumber
let ix = pageNo `mod` icaMaxChunkSize
chunksPrimary <-
traverse VU.unsafeFreeze . sliceCurrent ix =<< readSTRef icaPrimary
chunksClashes <-
traverse VU.unsafeFreeze . sliceCurrent ix =<< readSTRef icaClashes
chunksLargerThanPage <-
traverse VU.unsafeFreeze . sliceCurrent ix =<< readSTRef icaLargerThanPage
-- Only slice out a chunk if there are entries in the chunk
let mchunk = if ix == 0
then Nothing
else Just (word64VectorToChunk (head chunksPrimary))
let icPrimary = VU.concat . reverse $ chunksPrimary
let icClashes = VU.concat . reverse $ chunksClashes
let icLargerThanPage = VU.concat . reverse $ chunksLargerThanPage
icTieBreaker <- readSTRef icaTieBreaker
pure (mchunk, IndexCompact {..})
where
-- The current (most recent) chunk of the bitvectors is only partially
-- constructed, so we need to only use the part that is already filled.
sliceCurrent ix (c NE.:| cs)
| ix == 0 = cs -- current chunk is completely empty, just ignore it
| otherwise = VUM.slice 0 ix c : cs
{-------------------------------------------------------------------------------
Strict 'Maybe'
-------------------------------------------------------------------------------}
data SMaybe a = SNothing | SJust !a
deriving stock (Eq, Show)
smaybe :: b -> (a -> b) -> SMaybe a -> b
smaybe snothing sjust = \case
SNothing -> snothing
SJust x -> sjust x
{-------------------------------------------------------------------------------
Vector extras
-------------------------------------------------------------------------------}
unsafeWriteRange :: VU.Unbox a => VU.MVector s a -> Bound Int -> Bound Int -> a -> ST s ()
unsafeWriteRange !v !lb !ub !x = VUM.set (VUM.unsafeSlice lb' len v) x
where
!lb' = vectorLowerBound lb
!ub' = mvectorUpperBound v ub
!len = ub' - lb' + 1
-- | Map a 'Bound' to the equivalent inclusive lower bound.
vectorLowerBound :: Bound Int -> Int
vectorLowerBound = \case
NoBound -> 0
BoundExclusive i -> i + 1
BoundInclusive i -> i
-- | Map a 'Bound' to the equivalent inclusive upper bound.
mvectorUpperBound :: VGM.MVector v a => v s a -> Bound Int -> Int
mvectorUpperBound v = \case
NoBound -> VGM.length v - 1
BoundExclusive i -> i - 1
BoundInclusive i -> i