packages feed

moonlight-triangulation-0.1.0.0: src-core/Moonlight/Triangulation/Internal/PageDirectory.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE NamedFieldPuns #-}

-- | The map from page number to page, for the paged stores.
module Moonlight.Triangulation.Internal.PageDirectory
  ( PageDirectory
  , emptyDirectory
  , lookupDirectory
  , insertDirectory
  , directoryFromAscList
  , directoryToAscList
  , directorySize
  , directoryLookupMax
  , directoryRestrict
  ) where

import Control.DeepSeq (NFData)
import Data.Bits (shiftL, shiftR, (.&.))
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV
import GHC.Generics (Generic)

-- Page numbers are small, dense and non-negative, and the read path resolves
-- one on every field access that misses its owner's current page. An ordered
-- map answers that with a chain of prefix comparisons whose length grows with
-- the mesh; a 32-way radix answers it with two indexed loads at every size the
-- meshes reach, and the arithmetic is shifts and masks rather than branches.
--
-- Absence is representable because the boxed payload store leaves a page out
-- entirely when every slot in it still holds the fill.
data Slot a
  = Absent
  | Page !a
  | Fanout !(V.Vector (Slot a))
  deriving stock (Generic, Functor)
  deriving anyclass (NFData)

-- | @directoryHeight@ counts the fanout levels below the root: at height 0 the
-- root's children are pages, at height 1 they are fanouts of pages, and so on.
data PageDirectory a = PageDirectory
  { directoryHeight :: {-# UNPACK #-} !Int
  , directoryRoot :: !(Slot a)
  , directorySize :: {-# UNPACK #-} !Int
  }
  deriving stock (Generic, Functor)
  deriving anyclass (NFData)

fanout :: Int
fanout = 32
{-# INLINE fanout #-}

slotBits :: Int
slotBits = 5
{-# INLINE slotBits #-}

-- Path copying replaces one slot per level, and the bulk-update operator would
-- route that through a list of one pair. This is the copy the level needs and
-- nothing besides.
updateSlot :: V.Vector (Slot a) -> Int -> Slot a -> V.Vector (Slot a)
updateSlot children index child =
  V.modify (\node -> MV.unsafeWrite node index child) children
{-# INLINE updateSlot #-}

-- | The first page number a directory of this height cannot address.
capacity :: Int -> Int
capacity height = 1 `shiftL` (slotBits * (height + 1))
{-# INLINE capacity #-}

emptyDirectory :: PageDirectory a
emptyDirectory = PageDirectory 0 Absent 0

lookupDirectory :: Int -> PageDirectory a -> Maybe a
lookupDirectory key PageDirectory{directoryHeight, directoryRoot}
  | key < 0 || key >= capacity directoryHeight = Nothing
  | otherwise = go directoryHeight directoryRoot
 where
  go !height slot = case slot of
    Absent -> Nothing
    Page value -> Just value
    Fanout children ->
      go (height - 1) (V.unsafeIndex children ((key `shiftR` (slotBits * height)) .&. (fanout - 1)))
{-# INLINE lookupDirectory #-}

insertDirectory :: Int -> a -> PageDirectory a -> PageDirectory a
insertDirectory key value directory
  | key >= capacity (directoryHeight directory) = insertDirectory key value (grow directory)
  | otherwise =
      let (!root, !added) = write (directoryHeight directory) (directoryRoot directory)
       in directory
            { directoryRoot = root
            , directorySize = directorySize directory + if added then 1 else 0
            }
 where
  write !height slot
    | height < 0 = (Page value, case slot of Page _ -> False; _ -> True)
    | otherwise =
        let !index = (key `shiftR` (slotBits * height)) .&. (fanout - 1)
            !children = case slot of
              Fanout existing -> existing
              _ -> V.replicate fanout Absent
            (!child, !added) = write (height - 1) (V.unsafeIndex children index)
         in (Fanout (updateSlot children index child), added)

-- A directory that has run out of addressable pages gains a level, and its
-- whole former extent becomes child zero of the new root.
grow :: PageDirectory a -> PageDirectory a
grow directory@PageDirectory{directoryHeight, directoryRoot} =
  directory
    { directoryHeight = directoryHeight + 1
    , directoryRoot = case directoryRoot of
        Absent -> Absent
        occupied -> Fanout (updateSlot (V.replicate fanout Absent) 0 occupied)
    }

-- | Build from ascending, distinct page numbers in one descent.
--
-- Every dense transaction ends by publishing a whole directory, so this is on
-- the freeze path of bulk load, constraint recovery and refinement alike.
-- Folding 'insertDirectory' over the list would copy each root path once per
-- page; laying the levels down directly touches each node once.
directoryFromAscList :: [(Int, a)] -> PageDirectory a
directoryFromAscList [] = emptyDirectory
directoryFromAscList entries@((firstKey, _) : remainingEntries) =
  PageDirectory
    { directoryHeight = height
    , directoryRoot = fst (build height 0 entries)
    , directorySize = length entries
    }
 where
  !largest = foldl' (\largestKey (key, _) -> max largestKey key) firstKey remainingEntries
  !height = heightFor 0
  heightFor !candidate
    | largest < capacity candidate = candidate
    | otherwise = heightFor (candidate + 1)

  build :: Int -> Int -> [(Int, b)] -> (Slot b, [(Int, b)])
  build !level !base remaining = case remaining of
    [] -> (Absent, [])
    ((key, value) : rest)
      | key >= base + capacity level -> (Absent, remaining)
      | level < 0 -> (Page value, rest)
      | otherwise ->
          let !childSpan = capacity (level - 1)
              step (children, unconsumed) index =
                let (!child, !beyond) = build (level - 1) (base + index * childSpan) unconsumed
                 in (child : children, beyond)
              (!reversed, !left) = foldl' step ([], remaining) [0 .. fanout - 1]
           in (Fanout (V.fromListN fanout (reverse reversed)), left)

directoryToAscList :: PageDirectory a -> [(Int, a)]
directoryToAscList PageDirectory{directoryHeight, directoryRoot} = go directoryHeight 0 directoryRoot []
 where
  go :: Int -> Int -> Slot b -> [(Int, b)] -> [(Int, b)]
  go !height !prefix slot rest = case slot of
    Absent -> rest
    Page value -> (prefix, value) : rest
    Fanout children ->
      foldr
        (\index accumulated ->
          go
            (height - 1)
            (prefix + (index `shiftL` (slotBits * height)))
            (V.unsafeIndex children index)
            accumulated
        )
        rest
        [0 .. fanout - 1]

directoryLookupMax :: PageDirectory a -> Maybe (Int, a)
directoryLookupMax PageDirectory{directoryHeight, directoryRoot} = go directoryHeight 0 directoryRoot
 where
  go :: Int -> Int -> Slot b -> Maybe (Int, b)
  go !height !prefix slot = case slot of
    Absent -> Nothing
    Page value -> Just (prefix, value)
    Fanout children -> descend (fanout - 1)
     where
      descend !index
        | index < 0 = Nothing
        | otherwise =
            case go (height - 1) (prefix + (index `shiftL` (slotBits * height))) (V.unsafeIndex children index) of
              Nothing -> descend (index - 1)
              found -> found

-- | Keep only the pages a directory of the given page count can hold. The
-- common case is that every page is already within the count, and that is
-- settled by one descent rather than a rebuild.
directoryRestrict :: Int -> PageDirectory a -> PageDirectory a
directoryRestrict pageCount directory
  | pageCount <= 0 = emptyDirectory
  | otherwise = case directoryLookupMax directory of
      Nothing -> directory
      Just (largest, _)
        | largest < pageCount -> directory
        | otherwise ->
            directoryFromAscList (filter ((< pageCount) . fst) (directoryToAscList directory))