packages feed

lsm-tree-1.0.0.0: src-core/Database/LSMTree/Internal/Chunk.hs

{-# OPTIONS_HADDOCK not-home #-}
{- HLINT ignore "Avoid restricted alias" -}

-- | Chunks of bytes, typically output during incremental index serialisation.
module Database.LSMTree.Internal.Chunk
(
    -- * Chunks
    Chunk (Chunk),
    toByteVector,
    toByteString,

    -- * Balers
    Baler (Baler),
    createBaler,
    feedBaler,
    unsafeEndBaler
)
where

import           Prelude hiding (length)

import           Control.Exception (assert)
import           Control.Monad.ST.Strict (ST)
import           Data.ByteString (ByteString)
import           Data.List (scanl')
import           Data.Primitive.PrimVar (PrimVar, newPrimVar, readPrimVar,
                     writePrimVar)
import           Data.Vector.Primitive (Vector (Vector), length, unsafeCopy,
                     unsafeFreeze)
import           Data.Vector.Primitive.Mutable (MVector)
import qualified Data.Vector.Primitive.Mutable as Mutable (drop, length, slice,
                     take, unsafeCopy, unsafeNew)
import           Data.Word (Word8)
import           Database.LSMTree.Internal.ByteString (byteArrayToByteString)

-- * Chunks

-- | A chunk of bytes, typically output during incremental index serialisation.
newtype Chunk = Chunk (Vector Word8) deriving stock (Eq, Show)

-- | Yields the contents of a chunk as a byte vector.
toByteVector :: Chunk -> Vector Word8
toByteVector (Chunk byteVector) = byteVector

-- | Yields the contents of a chunk as a (strict) byte string.
toByteString :: Chunk -> ByteString
toByteString (Chunk (Vector vecOffset vecLength byteArray))
    = byteArrayToByteString vecOffset vecLength byteArray

-- * Balers

{-|
    An object that receives blocks of bytes and repackages them into chunks such
    that all chunks except for a possible remnant chunk at the end are of at
    least a given minimum size.
-}
data Baler s = Baler
                   !(MVector s Word8) -- Buffer storing queued bytes
                   !(PrimVar s Int)   -- Reference to the number of queued bytes

-- | Creates a new baler.
createBaler :: Int            -- ^ Minimum chunk size in bytes
            -> ST s (Baler s) -- ^ Creation of the baler
createBaler minChunkSize = assert (minChunkSize > 0)              $
                           Baler                                 <$>
                           Mutable.unsafeNew (pred minChunkSize) <*>
                           newPrimVar 0

{-|
    Feeds a baler blocks of bytes.

    Bytes received by a baler are generally queued for later output, but if
    feeding new bytes makes the accumulated content exceed the minimum chunk
    size then a chunk containing all the accumulated content is output.
-}
feedBaler :: forall s . [Vector Word8] -> Baler s -> ST s (Maybe Chunk)
feedBaler blocks (Baler buffer remnantSizeRef) = do
    remnantSize <- readPrimVar remnantSizeRef
    let

        inputSize :: Int
        !inputSize = sum (map length blocks)

        totalSize :: Int
        !totalSize = remnantSize + inputSize

    if totalSize <= Mutable.length buffer
        then do
                 unsafeCopyBlocks (Mutable.drop remnantSize buffer)
                 writePrimVar remnantSizeRef totalSize
                 pure Nothing
        else do
                 protoChunk <- Mutable.unsafeNew totalSize
                 Mutable.unsafeCopy (Mutable.take remnantSize protoChunk)
                                    (Mutable.take remnantSize buffer)
                 unsafeCopyBlocks (Mutable.drop remnantSize protoChunk)
                 writePrimVar remnantSizeRef 0
                 chunk <- Chunk <$> unsafeFreeze protoChunk
                 pure (Just chunk)
    where

    unsafeCopyBlocks :: MVector s Word8 -> ST s ()
    unsafeCopyBlocks vec
        = sequence_ $
          zipWith3 (\ start size block -> unsafeCopy
                                              (Mutable.slice start size vec)
                                              block)
                   (scanl' (+) 0 blockSizes)
                   blockSizes
                   blocks
        where

        blockSizes :: [Int]
        blockSizes = map length blocks

{-|
    Returns the bytes still queued in a baler, if any, thereby invalidating the
    baler. Executing @unsafeEndBaler baler@ is only safe when @baler@ is not
    used afterwards.
-}
unsafeEndBaler :: forall s . Baler s -> ST s (Maybe Chunk)
unsafeEndBaler (Baler buffer remnantSizeRef) = do
    remnantSize <- readPrimVar remnantSizeRef
    if remnantSize == 0
        then pure Nothing
        else do
                 let

                     protoChunk :: MVector s Word8
                     !protoChunk = Mutable.take remnantSize buffer

                 chunk <- Chunk <$> unsafeFreeze protoChunk
                 pure (Just chunk)