packages feed

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

{-# LANGUAGE BangPatterns   #-}
{-# LANGUAGE DeriveFunctor  #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# OPTIONS_HADDOCK not-home #-}

module Database.LSMTree.Internal.RawOverflowPage (
    RawOverflowPage (..),
    makeRawOverflowPage,
    unsafeMakeRawOverflowPage,
    rawOverflowPageRawBytes,
    rawOverflowPageToByteString,
    rawBytesToOverflowPages,
    pinnedByteArrayToOverflowPages,
    unpinnedByteArrayToOverflowPages,
) where

import           Control.DeepSeq (NFData (rnf))
import           Control.Monad (when)
import           Data.ByteString (ByteString)
import           Data.Primitive.ByteArray (ByteArray (..), copyByteArray,
                     fillByteArray, isByteArrayPinned, newPinnedByteArray,
                     runByteArray)
import qualified Data.Vector.Primitive as VP
import           Database.LSMTree.Internal.Assertions
import           Database.LSMTree.Internal.BitMath (roundUpToPageSize)
import           Database.LSMTree.Internal.RawBytes (RawBytes (..))
import qualified Database.LSMTree.Internal.RawBytes as RB

-------------------------------------------------------------------------------
-- RawOverflowPage type
-------------------------------------------------------------------------------

-- | When a key\/op pair is too large to fit in a single disk page, the
-- representation is split into a normal page, and one or more overflow pages.
-- The normal 'RawPage' follows the run page format, and contains the key,
-- optional blob reference and a prefix of the value, while the overflow pages
-- contain the suffix of a large value that didn't fit in the normal page.
--
-- Each overflow page is the same size as normal pages (currently 4096 only).
--
data RawOverflowPage = RawOverflowPage
                         !Int        -- ^ offset in Word8s.
                         !ByteArray
  deriving stock (Show)

-- | This invariant is the same as for 'RawPage', but there is no alignment
-- constraint. This is for two reasons: 1. we don't need alignment, because
-- the page has no structure that we need to read with aligned memory
-- operations; 2. we don't want alignment because we want to convert the suffix
-- of serialised values (as 'RawBytes') into a 'RawOverflowPage' without
-- copying, and a suffix can start at an arbitrary offset.
--
invariant :: RawOverflowPage -> Bool
invariant (RawOverflowPage off ba) = and
    [ isValidSlice off 4096 ba    -- valid bytearray slice for length 4096
    , isByteArrayPinned ba        -- bytearray must be pinned (for I/O)
    ]

instance NFData RawOverflowPage where
  rnf (RawOverflowPage _ _) = ()

-- | This instance assumes pages are 4096 bytes in size
instance Eq RawOverflowPage where
    r1 == r2 = rawOverflowPageRawBytes r1 == rawOverflowPageRawBytes r2

rawOverflowPageRawBytes :: RawOverflowPage -> RawBytes
rawOverflowPageRawBytes (RawOverflowPage off ba) =
    RB.fromByteArray off 4096 ba

-- | \( O(1) \) since we can avoid copying the pinned byte array.
rawOverflowPageToByteString :: RawOverflowPage -> ByteString
rawOverflowPageToByteString =
    RB.unsafePinnedToByteString . rawOverflowPageRawBytes

-- | Create a 'RawOverflowPage'.
--
-- The length must be 4096 or less.
--
-- This function will copy data if the byte array is not pinned, or the length
-- is strictly less than 4096.
--
makeRawOverflowPage ::
       ByteArray  -- ^ bytearray
    -> Int        -- ^ offset in bytes into the bytearray
    -> Int        -- ^ length in bytes, must be @>= 0 && <= 4096@
    -> RawOverflowPage
makeRawOverflowPage ba off len
    | len == 4096
    , let page = RawOverflowPage off ba
    , invariant page
    = page

    | otherwise
    = makeRawOverflowPageCopy ba off len

makeRawOverflowPageCopy ::
       ByteArray  -- ^ bytearray
    -> Int        -- ^ offset in bytes into the bytearray
    -> Int        -- ^ length in bytes
    -> RawOverflowPage
makeRawOverflowPageCopy ba off len =
    assert (isValidSlice off len ba && len <= 4096) $
    (\page -> assert (invariant page) page) $
    RawOverflowPage 0 $ runByteArray $ do
      mba <- newPinnedByteArray 4096
      let suffixlen = min 4096 len -- would only do anything with assertions off
      copyByteArray mba 0 ba off suffixlen
      when (suffixlen < 4096) $ fillByteArray mba suffixlen (4096-suffixlen) 0
      pure mba

-- | Create a 'RawOverflowPage' without copying. The byte array and offset must
-- satisfy the invariant for 'RawOverflowPage'.
--
unsafeMakeRawOverflowPage ::
       ByteArray  -- ^ bytearray, must be pinned and contain 4096 bytes (after offset)
    -> Int        -- ^ offset in bytes
    -> RawOverflowPage
unsafeMakeRawOverflowPage ba off =
    assert (invariant page) page
  where
    page = RawOverflowPage off ba

-- | Convert 'RawBytes' representing the \"overflow\" part of a value into one
-- or more 'RawOverflowPage's.
--
-- This will avoid copying where possible.
--
rawBytesToOverflowPages :: RawBytes -> [RawOverflowPage]
rawBytesToOverflowPages (RawBytes (VP.Vector off len ba))
  | isByteArrayPinned ba
  = pinnedByteArrayToOverflowPages off len ba
  | otherwise
  = unpinnedByteArrayToOverflowPages off len ba

pinnedByteArrayToOverflowPages :: Int -> Int -> ByteArray -> [RawOverflowPage]
pinnedByteArrayToOverflowPages !off !len !ba
      | len >= 4096
      , let !page = unsafeMakeRawOverflowPage ba off
      = page : pinnedByteArrayToOverflowPages (off+4096) (len-4096) ba

      | len <= 0
      = []

      | otherwise -- > 0 && < 4096
                  -- have to copy the partial last page
      , let !page = makeRawOverflowPageCopy ba off len
      = page : []

-- | Not pinned, in principle shouldn't happen much because if the value
-- is big enough to overflow then it's big enough to be pinned.
-- It is possible however if a page has a huge key and a small value.
--
-- Unfortunately, with GHC versions 9.6.x we also get this because the meaning
-- of pinned has changed. Sigh.
-- See <https://gitlab.haskell.org/ghc/ghc/-/issues/22255>
--
unpinnedByteArrayToOverflowPages :: Int -> Int -> ByteArray -> [RawOverflowPage]
unpinnedByteArrayToOverflowPages !off !len !ba =
    let !lenPages = roundUpToPageSize len
        ba'       = runByteArray $ do
                      mba <- newPinnedByteArray lenPages
                      copyByteArray mba 0 ba off len
                      fillByteArray mba len (lenPages-len) 0
                      pure mba
        pages     = pinnedByteArrayToOverflowPages 0 lenPages ba'
        -- We've arranged to do the conversion without any extra copying,
        -- so assert that we got that right:
     in assert (all (isSliceNotCopy ba') pages)
        pages
  where
    isSliceNotCopy :: ByteArray -> RawOverflowPage -> Bool
    isSliceNotCopy ba1 (RawOverflowPage _ ba2) = sameByteArray ba1 ba2