packages feed

moonlight-triangulation-1.4.0.1: src-core/Moonlight/Triangulation/Internal/Paged.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Moonlight.Triangulation.Internal.Paged
  ( Paged
  , MutablePaged
  , FlatMutablePaged
  , emptyPaged
  , fromVector
  , fromLocalVector
  , toVector
  , pagedLength
  , pagedPageCount
  , pagedOverlayPageCount
  , PublicationStats (..)
  , emptyPublicationStats
  , addPublicationStats
  , pagedUnsafeIndex
  , pagedFoldl'
  , newMutablePaged
  , newLocalMutablePaged
  , TransactionShape (..)
  , thawPaged
  , thawPagedDense
  , thawPagedShaped
  , flatMutableSection
  , readFlatMutable
  , writeFlatMutable
  , readPaged
  , writePaged
  , freezePaged
  , readMutablePagedPublicationStats
  ) where

import Control.DeepSeq (NFData (..))
import Control.Monad (when)
import Control.Monad.ST (ST)
import Data.Bits (shiftL, shiftR, (.&.))
import qualified Data.IntMap.Strict as IntMap
import Data.List (unfoldr)
import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as MUV
import Moonlight.Triangulation.Internal.PageDirectory
  ( PageDirectory
  , directoryRestrict
  , directorySize
  , emptyDirectory
  , insertDirectory
  , lookupDirectory
  )

-- | One flat root plus one cumulative page overlay. An extension never wraps
-- a published value in another 'Paged' node: dirty pages are merged into the
-- existing directory, so an untouched read remains one overlay lookup plus
-- one root lookup regardless of extension count.
data Paged a
  = FlatPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(U.Vector a)
  | SharedPaged
      {-# UNPACK #-} !Int
      {-# UNPACK #-} !Int
      !(U.Vector a)
      !(PageDirectory (U.Vector a))

-- | Exact storage work observed by one persistent publication.  The dense
-- page counters describe a whole-base traversal; dirty-page and copied-cell
-- counters describe only pages opened by the local write set.  A zero dense
-- counter is therefore a storage fact from the transaction owner, never a
-- benchmark inference; page work is independent of the resident prefix except
-- for directory-path depth.
data PublicationStats = PublicationStats
  { publicationUnboxedBasePageEnumerations :: {-# UNPACK #-} !Int
  , publicationUnboxedBasePageOpens :: {-# UNPACK #-} !Int
  , publicationUnboxedBasePageFreezes :: {-# UNPACK #-} !Int
  , publicationUnboxedDirtyBasePages :: {-# UNPACK #-} !Int
  , publicationUnboxedDirtyAppendedPages :: {-# UNPACK #-} !Int
  , publicationUnboxedCopiedCells :: {-# UNPACK #-} !Int
  , publicationBoxedBasePageEnumerations :: {-# UNPACK #-} !Int
  , publicationBoxedBasePageOpens :: {-# UNPACK #-} !Int
  , publicationBoxedBasePageFreezes :: {-# UNPACK #-} !Int
  , publicationBoxedDirtyBasePages :: {-# UNPACK #-} !Int
  , publicationBoxedDirtyAppendedPages :: {-# UNPACK #-} !Int
  , publicationBoxedCopiedCells :: {-# UNPACK #-} !Int
  }
  deriving stock (Eq, Show)

instance NFData PublicationStats where
  rnf stats = stats `seq` ()

emptyPublicationStats :: PublicationStats
emptyPublicationStats =
  PublicationStats
    { publicationUnboxedBasePageEnumerations = 0
    , publicationUnboxedBasePageOpens = 0
    , publicationUnboxedBasePageFreezes = 0
    , publicationUnboxedDirtyBasePages = 0
    , publicationUnboxedDirtyAppendedPages = 0
    , publicationUnboxedCopiedCells = 0
    , publicationBoxedBasePageEnumerations = 0
    , publicationBoxedBasePageOpens = 0
    , publicationBoxedBasePageFreezes = 0
    , publicationBoxedDirtyBasePages = 0
    , publicationBoxedDirtyAppendedPages = 0
    , publicationBoxedCopiedCells = 0
    }

addPublicationStats :: PublicationStats -> PublicationStats -> PublicationStats
addPublicationStats left right =
  PublicationStats
    { publicationUnboxedBasePageEnumerations = publicationUnboxedBasePageEnumerations left + publicationUnboxedBasePageEnumerations right
    , publicationUnboxedBasePageOpens = publicationUnboxedBasePageOpens left + publicationUnboxedBasePageOpens right
    , publicationUnboxedBasePageFreezes = publicationUnboxedBasePageFreezes left + publicationUnboxedBasePageFreezes right
    , publicationUnboxedDirtyBasePages = publicationUnboxedDirtyBasePages left + publicationUnboxedDirtyBasePages right
    , publicationUnboxedDirtyAppendedPages = publicationUnboxedDirtyAppendedPages left + publicationUnboxedDirtyAppendedPages right
    , publicationUnboxedCopiedCells = publicationUnboxedCopiedCells left + publicationUnboxedCopiedCells right
    , publicationBoxedBasePageEnumerations = publicationBoxedBasePageEnumerations left + publicationBoxedBasePageEnumerations right
    , publicationBoxedBasePageOpens = publicationBoxedBasePageOpens left + publicationBoxedBasePageOpens right
    , publicationBoxedBasePageFreezes = publicationBoxedBasePageFreezes left + publicationBoxedBasePageFreezes right
    , publicationBoxedDirtyBasePages = publicationBoxedDirtyBasePages left + publicationBoxedDirtyBasePages right
    , publicationBoxedDirtyAppendedPages = publicationBoxedDirtyAppendedPages left + publicationBoxedDirtyAppendedPages right
    , publicationBoxedCopiedCells = publicationBoxedCopiedCells left + publicationBoxedCopiedCells right
    }

-- | A fresh build owns a contiguous arena. A local transaction owns only its
-- dirty pages; clean values remain reachable through the immutable base.
data MutablePaged s a
  = MutableFlatPaged {-# UNPACK #-} !Int !(MUV.MVector s a)
  | MutableSharedPaged
      {-# UNPACK #-} !Int
      !(Paged a)
      !(STRef s (IntMap.IntMap (MUV.MVector s a)))
      !(STRef s PublicationStats)

-- | A proof that one mutable sequence is in its contiguous physical section.
data FlatMutablePaged s a = FlatMutablePaged !(MUV.MVector s a)

localPageBits :: Int
localPageBits = 8
{-# INLINE localPageBits #-}

traversalPageBits :: Int
traversalPageBits = 10
{-# INLINE traversalPageBits #-}

pageSize :: Int -> Int
pageSize bits = 1 `shiftL` bits
{-# INLINE pageSize #-}

pageOf :: Int -> Int -> Int
pageOf bits index = index `shiftR` bits
{-# INLINE pageOf #-}

offsetOf :: Int -> Int -> Int
offsetOf bits index = index .&. (pageSize bits - 1)
{-# INLINE offsetOf #-}

pagesFor :: Int -> Int -> Int
pagesFor bits count
  | count <= 0 = 0
  | otherwise = pageOf bits (count - 1) + 1
{-# INLINE pagesFor #-}

pagedLength :: Paged a -> Int
pagedLength paged = case paged of
  FlatPaged count _ _ -> count
  SharedPaged count _ _ _ -> count
{-# INLINE pagedLength #-}

pagedPageCount :: Paged a -> Int
pagedPageCount paged = pagesFor (pagedBits paged) (pagedLength paged)
{-# INLINE pagedPageCount #-}

-- | Number of pages in the persistent overlay.  A flat root has no copied
-- pages; a shared value records only pages dirtied by its transaction.  This
-- is an observation for publication receipts, not a read-path traversal.
pagedOverlayPageCount :: Paged a -> Int
pagedOverlayPageCount paged = case paged of
  FlatPaged{} -> 0
  SharedPaged _ _ _ directory -> directorySize directory
{-# INLINE pagedOverlayPageCount #-}

pagedBits :: Paged a -> Int
pagedBits paged = case paged of
  FlatPaged _ bits _ -> bits
  SharedPaged _ bits _ _ -> bits
{-# INLINE pagedBits #-}

instance (U.Unbox a, Eq a) => Eq (Paged a) where
  left == right =
    pagedLength left == pagedLength right
      && case (left, right) of
        (FlatPaged _ _ leftValues, FlatPaged _ _ rightValues) -> leftValues == rightValues
        _ -> pagedChunksEqual left right

pagedChunksEqual :: forall a. (U.Unbox a, Eq a) => Paged a -> Paged a -> Bool
pagedChunksEqual left right =
  foldl'
    (\equal (index, width) ->
       equal
         && U.unsafeSlice 0 width (pageSlice left index)
              == U.unsafeSlice 0 width (pageSlice right index))
    True
    chunks
 where
  !count = pagedLength left
  !leftBits = pagedBits left
  !rightBits = pagedBits right
  chunks = unfoldr nextChunk 0

  nextChunk !index
    | index >= count = Nothing
    | otherwise = Just ((index, width), index + width)
    where
      !leftPageEnd = (pageOf leftBits index + 1) * pageSize leftBits
      !rightPageEnd = (pageOf rightBits index + 1) * pageSize rightBits
      !width = min (count - index) (min (leftPageEnd - index) (rightPageEnd - index))
  pageSlice :: Paged a -> Int -> U.Vector a
  pageSlice paged index =
    let !bits = pagedBits paged
        !page = pageOf bits index
        !offset = offsetOf bits index
     in case paged of
          FlatPaged _ _ values -> U.unsafeSlice index (U.length values - index) values
          SharedPaged _ _ root directory ->
            case lookupDirectory page directory of
              Just values -> U.unsafeSlice offset (U.length values - offset) values
              Nothing -> U.unsafeSlice index (U.length root - index) root

instance (U.Unbox a, Show a) => Show (Paged a) where
  showsPrec precedence = showsPrec precedence . U.toList . toVector

instance NFData (Paged a) where
  rnf paged = case paged of
    FlatPaged count bits values -> count `seq` bits `seq` values `seq` ()
    SharedPaged count bits root directory ->
      count `seq` bits `seq` root `seq` directory `seq` ()

emptyPaged :: U.Unbox a => Paged a
emptyPaged = FlatPaged 0 traversalPageBits U.empty

fromVector :: U.Unbox a => a -> U.Vector a -> Paged a
fromVector _padding values = FlatPaged (U.length values) traversalPageBits values

fromLocalVector :: U.Unbox a => a -> U.Vector a -> Paged a
fromLocalVector _padding values = FlatPaged (U.length values) localPageBits values

toVector :: U.Unbox a => Paged a -> U.Vector a
toVector paged = case paged of
  FlatPaged count _ values -> U.unsafeSlice 0 count values
  SharedPaged count _ _ _ -> U.generate count (pagedUnsafeIndex paged)

pagedUnsafeIndex :: U.Unbox a => Paged a -> Int -> a
pagedUnsafeIndex paged index =
  case paged of
    FlatPaged _ _ values -> U.unsafeIndex values index
    SharedPaged _ bits root directory ->
      case lookupDirectory (pageOf bits index) directory of
        Just values -> U.unsafeIndex values (offsetOf bits index)
        Nothing -> U.unsafeIndex root index
{-# INLINE pagedUnsafeIndex #-}

-- | Fold directly over the physical sections. Unlike the old implementation,
-- this does not allocate a complete vector merely to consume it.
pagedFoldl' :: U.Unbox a => (b -> a -> b) -> b -> Paged a -> b
pagedFoldl' step initial paged =
  case paged of
    FlatPaged count _ values -> U.foldl' step initial (U.take count values)
    SharedPaged count bits root directory ->
      foldl' foldPage initial [0 .. pagesFor bits count - 1]
     where
      foldPage !accumulated page =
        let !start = page * pageSize bits
            !width = min (pageSize bits) (count - start)
            !values =
              case lookupDirectory page directory of
                Just pageValues -> U.unsafeSlice 0 width pageValues
                Nothing -> U.unsafeSlice start width root
         in U.foldl' step accumulated values
{-# INLINE pagedFoldl' #-}

newMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
newMutablePaged capacity = do
  values <- MUV.new (max 0 capacity)
  pure (MutableFlatPaged traversalPageBits values)

newLocalMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
newLocalMutablePaged capacity = do
  values <- MUV.new (max 0 capacity)
  pure (MutableFlatPaged localPageBits values)

-- | Open a persistent transaction without constructing a page table over the
-- resident mesh. The mutable dictionary starts empty and acquires storage only
-- when a write first reaches a page.
thawPaged :: Int -> Paged a -> ST s (MutablePaged s a)
thawPaged _requestedCapacity paged = do
  dirty <- newSTRef IntMap.empty
  stats <- newSTRef emptyPublicationStats
  pure (MutableSharedPaged (pagedBits paged) paged dirty stats)

data TransactionShape = DenseTransaction | LocalTransaction

thawPagedShaped :: U.Unbox a => TransactionShape -> Int -> Paged a -> ST s (MutablePaged s a)
thawPagedShaped shape = case shape of
  DenseTransaction -> thawPagedDense
  LocalTransaction -> thawPaged
{-# INLINE thawPagedShaped #-}

thawPagedDense :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a)
thawPagedDense requestedCapacity paged = do
  let !count = pagedLength paged
      !capacity = max requestedCapacity count
  values <- MUV.new capacity
  U.copy (MUV.unsafeSlice 0 count values) (toVector paged)
  pure (MutableFlatPaged (pagedBits paged) values)

flatMutableSection :: MutablePaged s a -> Maybe (FlatMutablePaged s a)
flatMutableSection mutable = case mutable of
  MutableFlatPaged _ values -> Just (FlatMutablePaged values)
  MutableSharedPaged{} -> Nothing
{-# INLINE flatMutableSection #-}

readFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> ST s a
readFlatMutable (FlatMutablePaged values) = MUV.unsafeRead values
{-# INLINE readFlatMutable #-}

writeFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> a -> ST s ()
writeFlatMutable (FlatMutablePaged values) = MUV.unsafeWrite values
{-# INLINE writeFlatMutable #-}

readPaged :: U.Unbox a => MutablePaged s a -> Int -> ST s a
readPaged mutable index = case mutable of
  MutableFlatPaged _ values -> MUV.unsafeRead values index
  MutableSharedPaged bits base dirtyRef _ -> do
    dirty <- readSTRef dirtyRef
    case IntMap.lookup (pageOf bits index) dirty of
      Just values -> MUV.unsafeRead values (offsetOf bits index)
      Nothing -> pure (pagedUnsafeIndex base index)
{-# INLINE readPaged #-}

writePaged :: U.Unbox a => MutablePaged s a -> Int -> a -> ST s ()
writePaged mutable index value = case mutable of
  MutableFlatPaged _ values -> MUV.unsafeWrite values index value
  MutableSharedPaged bits base dirtyRef statsRef -> do
    let !page = pageOf bits index
        !start = page * pageSize bits
        !existingWidth = min (pageSize bits) (max 0 (pagedLength base - start))
    dirty <- readSTRef dirtyRef
    values <- case IntMap.lookup page dirty of
      Just existing -> pure existing
      Nothing -> do
        pageValues <- MUV.new (pageSize bits)
        when (existingWidth > 0) $
            U.copy
              (MUV.unsafeSlice 0 existingWidth pageValues)
              (U.generate existingWidth (\offset -> pagedUnsafeIndex base (start + offset)))
        writeSTRef dirtyRef $! IntMap.insert page pageValues dirty
        modifySTRef'
          statsRef
          (addPublicationStats
             emptyPublicationStats
               { publicationUnboxedBasePageOpens = if existingWidth > 0 then 1 else 0
               , publicationUnboxedDirtyBasePages = if existingWidth > 0 then 1 else 0
               , publicationUnboxedDirtyAppendedPages = if existingWidth > 0 then 0 else 1
               , publicationUnboxedCopiedCells = existingWidth
               })
        pure pageValues
    MUV.unsafeWrite values (offsetOf bits index) value
{-# INLINE writePaged #-}

-- | Publish only dirty pages. A local publication retains one immutable root
-- and merges the changed page paths into its cumulative directory.
freezePaged :: forall s a. U.Unbox a => Int -> MutablePaged s a -> ST s (Paged a)
freezePaged count mutable
  | count <= 0 = pure emptyPaged
  | otherwise = case mutable of
      MutableFlatPaged bits values
        | MUV.length values <= count + (count `quot` 8) ->
            FlatPaged count bits . U.take count <$> U.unsafeFreeze values
        | otherwise ->
            FlatPaged count bits <$> U.freeze (MUV.unsafeSlice 0 count values)
      MutableSharedPaged bits base dirtyRef _statsRef -> do
        dirty <- readSTRef dirtyRef
        if IntMap.null dirty && count == pagedLength base
          then pure base
          else do
            frozen <- traverse freezePage (IntMap.toAscList dirty)
            let !priorDirectory = sharedDirectory base
                !directory =
                  foldl'
                    (\accumulated (page, values) -> insertDirectory page values accumulated)
                    priorDirectory
                    frozen
                !root = sharedRoot base
            pure (SharedPaged count bits root (directoryRestrict (pagesFor bits count) directory))
 where
  freezePage :: (Int, MUV.MVector s a) -> ST s (Int, U.Vector a)
  freezePage (page, values) = do
    frozen <- U.unsafeFreeze values
    pure (page, frozen)

sharedRoot :: Paged a -> U.Vector a
sharedRoot paged = case paged of
  FlatPaged _ _ values -> values
  SharedPaged _ _ root _ -> root
{-# INLINE sharedRoot #-}

sharedDirectory :: Paged a -> PageDirectory (U.Vector a)
sharedDirectory paged = case paged of
  FlatPaged _ _ _ -> emptyDirectory
  SharedPaged _ _ _ directory -> directory
{-# INLINE sharedDirectory #-}

readMutablePagedPublicationStats :: MutablePaged s a -> ST s PublicationStats
readMutablePagedPublicationStats mutable = case mutable of
  MutableFlatPaged _ _ -> pure emptyPublicationStats
  MutableSharedPaged _ _ _ statsRef -> readSTRef statsRef