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))