packages feed

disk-bytes-0.1.0.0: src/System/Mem/Disk/Bytes.hs

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}

-- | Implementation of 'DiskBytes' using GHC-specific primitives
-- like 'mkWeak#'. Unfortunately,
-- I have not been able to bring down the RAM size of 'DiskByte'
-- significantly below 100 bytes yet.
module System.Mem.Disk.Bytes
    ( DiskBytes
    , toDiskBytes
    , fromDiskBytes
    ) where

import Data.ByteString
    ( ByteString )
import Data.Int
    ( Int64 )
import System.IO.Unsafe
    ( unsafePerformIO )
import System.Mem.Disk.DiskApi
    ( Disk (..) )

import GHC.Exts (Int#, Int(I#))
import GHC.Base
import GHC.Weak

{-----------------------------------------------------------------------------
    DiskBytes
------------------------------------------------------------------------------}
-- | A sequence of bytes that is stored on disk
-- — if and only if the value is evaluated to WHNF.
--
-- The value is subject to normal garbage collection:
-- When the value is no longer referenced,
-- the disk memory will be freed (eventually).
--
-- For estimating the memory cost:
-- Even though the bulk of the data is kept on disk,
-- each WHNF of 'DiskBytes' occupies roughly @~100@ bytes of RAM;
-- this is due to administrative overhead like weak pointers and finalizers.
data DiskBytes = DiskBytes
    { index :: Int#
    , disk  :: Disk
    }

-- | Make a weak pointer for our purposes.
addFinalizerBytes :: DiskBytes -> IO () -> IO ()
addFinalizerBytes v@(DiskBytes{}) (IO finalizer) = IO $ \s ->
    case mkWeak# v v finalizer s of (# s1, w #) -> (# s1, () #)

-- | Offload a sequence of bytes onto a 'Disk'.
-- 
-- NOTE: The result must be evaluated to WHNF before the data is actually
-- on disk!
-- Also, the original 'ByteString' needs to be garbage collected
-- in for its RAM to become free.
toDiskBytes :: Disk -> ByteString -> DiskBytes
toDiskBytes disk = unsafePerformIO . mkDiskBytes disk

mkDiskBytes :: Disk -> ByteString -> IO DiskBytes
mkDiskBytes disk bytes = do
    I# index <- fromEnum <$> put disk bytes
    let diskbytes = DiskBytes{index,disk}
    diskbytes `seq` (addFinalizerBytes diskbytes $ finalizerBytes diskbytes)
    pure diskbytes

{-# NOINLINE finalizerBytes #-}
finalizerBytes :: DiskBytes -> IO ()
finalizerBytes DiskBytes{index,disk} =
    delete disk $ toEnum $ I# index

-- | Read the sequence of bytes back into RAM.
fromDiskBytes :: DiskBytes -> ByteString
fromDiskBytes = unsafePerformIO . unpackM

unpackM :: DiskBytes -> IO ByteString
unpackM DiskBytes{index,disk} =
    get disk $ toEnum $ I# index