packages feed

vcache (empty) → 0.1

raw patch · 28 files changed

+4341/−0 lines, 28 filesdep +basedep +bytestringdep +containerssetup-changed

Dependencies added: base, bytestring, containers, direct-murmur-hash, easy-file, filelock, lmdb, random, stm, transformers

Files

+ LICENSE view
@@ -0,0 +1,24 @@+Copyright (c) 2014, David Barbour+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple+main = defaultMain+
+ hsrc_lib/Database/VCache.hs view
@@ -0,0 +1,23 @@+++module Database.VCache+    ( module Database.VCache.VRef+    , module Database.VCache.PVar+    , module Database.VCache.VTx+    , VCache, openVCache+    , VSpace, vcache_space+    , module Database.VCache.Path+    , module Database.VCache.Stats+    , module Database.VCache.Sync +    , module Database.VCache.VCacheable+    ) where++import Database.VCache.Types +import Database.VCache.VCacheable+import Database.VCache.Open+import Database.VCache.Stats+import Database.VCache.Path+import Database.VCache.Sync+import Database.VCache.VRef+import Database.VCache.PVar+import Database.VCache.VTx
+ hsrc_lib/Database/VCache/Aligned.hs view
@@ -0,0 +1,41 @@+++-- aligned peek/poke variants. These are more expensive, but should+-- be much more portable to different machines. I might later use+-- #ifdef flags to transparently control alignment sensitivity if+-- this becomes a big performance issue.+module Database.VCache.Aligned+    ( peekAligned+    , pokeAligned+    ) where++import Foreign.Storable+import Foreign.Marshal.Alloc+import Foreign.Marshal.Utils+import Foreign.Ptr++-- | An alignment-sensitive peek; will copy data into aligned +-- memory prior to performing 'peek'.+peekAligned :: (Storable a) => Ptr a -> IO a+peekAligned = peekAligned' undefined+{-# INLINE peekAligned #-}++-- | An alignment-sensitive poke. Will poke data into aligned+-- memory then copy it into the destination memory.+pokeAligned :: (Storable a) => Ptr a -> a -> IO ()+pokeAligned = pokeAligned' undefined+{-# INLINE pokeAligned #-}++peekAligned' :: (Storable a) => a -> Ptr a -> IO a+peekAligned' _dummy pSrc = +    allocaBytesAligned (sizeOf _dummy) (alignment _dummy) $ \ pBuff -> do+        copyBytes pBuff pSrc (sizeOf _dummy)+        peek pBuff+{-# INLINE peekAligned' #-}++pokeAligned' :: (Storable a) => a -> Ptr a -> a -> IO ()+pokeAligned' _dummy pDst a =+    allocaBytesAligned (sizeOf _dummy) (alignment _dummy) $ \ pBuff -> do+        poke pBuff a+        copyBytes pDst pBuff (sizeOf _dummy)+{-# INLINE pokeAligned' #-}
+ hsrc_lib/Database/VCache/Alloc.hs view
@@ -0,0 +1,553 @@+{-# LANGUAGE ForeignFunctionInterface, BangPatterns #-}++-- | Constructors and Allocators for VRefs and PVars.+--+-- This module is the nexus of many concurrency concerns. Addresses+-- are GC'd, but structure sharing allows VRef addresses to revive+-- from zero references. Allocations might not be observed in the+-- LMDB layer databases for a couple frames. GC at the Haskell layer+-- must interact with ephemeron maps.+--+-- Because VRefs are frequently constructed by unsafePerformIO, and+-- PVars may be constructed via unsafePerformIO (newPVarIO), none of+-- these operations may use STM.+--+-- For now, I'm essentially using a global lock for managing these+-- in-memory tables. Since the LMDB layer is also limited by a single+-- writer, I think this lock shouldn't become a major bottleneck. But+-- it may require more context switching than desirable.+-- +module Database.VCache.Alloc+    ( addr2vref+    , addr2pvar+    , newVRefIO+    , newVRefIO'+    , initVRefCache+    , newPVar+    , newPVars+    , newPVarIO+    , newPVarsIO+    , loadRootPVar+    , loadRootPVarIO+    ) where++import Control.Exception+import Control.Monad+import Control.Applicative+import Data.Word+import Data.IORef+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.List as L+import qualified Data.Map.Strict as Map+import Data.Typeable+import Data.Maybe++import Foreign.C.Types+import Foreign.Ptr+import Foreign.Storable+import Foreign.Marshal.Alloc++import GHC.Conc (unsafeIOToSTM)+import Control.Concurrent.MVar+import Control.Concurrent.STM.TVar ++import System.Mem.Weak (Weak)+import qualified System.Mem.Weak as Weak+import System.IO.Unsafe+import Unsafe.Coerce++import Database.LMDB.Raw++import Database.VCache.Types+import Database.VCache.Path+import Database.VCache.Aligned+import Database.VCache.VPutFini+import Database.VCache.Hash+import Database.VCache.Read++-- | Obtain a VRef given an address and value. Not initially cached.+-- This operation doesn't touch the persistence layer; it assumes the+-- given address is valid.+--+addr2vref :: (VCacheable a) => VSpace -> Address -> IO (VRef a)+addr2vref !vc !addr = +    assert (isVRefAddr addr) $ +    modifyMVarMasked (vcache_memory vc) $ +    addr2vref' vc addr+{-# INLINE addr2vref #-}++addr2vref' :: (VCacheable a) => VSpace -> Address -> Memory -> IO (Memory, VRef a)+addr2vref' !vc !addr !m = _addr2vref undefined vc addr m+{-# INLINE addr2vref' #-}++-- Since I'm partitioning VRefs based on whether they're cached, I +-- must search two maps to see whether the VRef is already in memory. +_addr2vref :: (VCacheable a) => a -> VSpace -> Address -> Memory -> IO (Memory, VRef a)+_addr2vref _dummy !vc !addr !m = do+    let ty = typeOf _dummy +    mbCacheE <- loadVRefCache addr ty (mem_evrefs m)+    case mbCacheE of+        Just cache -> return (m, VRef addr cache vc ty get)+        Nothing -> do+            mbCacheC <- loadVRefCache addr ty (mem_cvrefs m)+            case mbCacheC of+                Just cache -> return (m, VRef addr cache vc ty get)+                Nothing -> do+                    cache <- newIORef NotCached+                    e <- mkVREph vc addr cache ty+                    let m' = m { mem_evrefs = addVREph e (mem_evrefs m) }+                    m' `seq` return (m', VRef addr cache vc ty get)+{-# NOINLINE _addr2vref #-}++mkVREph :: VSpace -> Address -> IORef (Cache a) -> TypeRep -> IO VREph+mkVREph !vc !addr !cache !ty = +    mkWeakIORef cache (clearVRef vc addr ty) >>= \ wkCache ->+    return $! VREph addr ty wkCache+{-# INLINE mkVREph #-}++loadVRefCache :: Address -> TypeRep -> VREphMap -> IO (Maybe (IORef (Cache a)))+loadVRefCache !addr !ty !em = mbrun _getVREphCache mbf where+    mbf = Map.lookup addr em >>= Map.lookup ty+{-# INLINE loadVRefCache #-}++-- When a VRef is GC'd from the Haskell layer, we need to delete it+-- from the ephemeron table. Of course, while unlikely, another VRef+-- may have since replaced the existing one. +clearVRef :: VSpace -> Address -> TypeRep -> IO ()+clearVRef !vc !addr !ty = modifyMVarMasked_ (vcache_memory vc) $ \ m -> do+    evrefs' <- tryDelVREph addr ty (mem_evrefs m)+    cvrefs' <- tryDelVREph addr ty (mem_cvrefs m)+    let m' = m { mem_evrefs = evrefs', mem_cvrefs = cvrefs' }+    return $! m'++tryDelVREph :: Address -> TypeRep -> VREphMap -> IO VREphMap+tryDelVREph !addr !ty !em =+    case takeVREph addr ty em of+        Nothing -> return em+        Just (VREph { vreph_cache = wk }, em') ->+            Weak.deRefWeak wk >>= \ mbc ->+            if isJust mbc then return em  -- replaced (improbable; race condition)+                          else return em' -- removed++-- This is certainly an unsafe operation in general, but we have+-- already validated the TypeRep matches.+_getVREphCache :: VREph -> IO (Maybe (IORef (Cache a)))+_getVREphCache = Weak.deRefWeak . _u where+    _u :: VREph -> Weak (IORef (Cache a))+    _u (VREph { vreph_cache = w }) = _c w +    _c :: Weak (IORef (Cache b)) -> Weak (IORef (Cache a))+    _c = unsafeCoerce+{-# INLINE _getVREphCache #-}++mbrun :: (Applicative m) => (a -> m (Maybe b)) -> Maybe a -> m (Maybe b)+mbrun = maybe (pure Nothing) +{-# INLINE mbrun #-}+ +-- | Obtain a PVar given an address. PVar will lazily load when read.+-- This operation does not try to read the database. It may fail if+-- the requested address has already been loaded with another type.+addr2pvar :: (VCacheable a) => VSpace -> Address -> IO (PVar a)+addr2pvar !vc !addr = +    assert (isPVarAddr addr) $ +    modifyMVarMasked (vcache_memory vc) $ +    addr2pvar' vc addr+{-# INLINE addr2pvar #-}++addr2pvar' :: (VCacheable a) => VSpace -> Address -> Memory -> IO (Memory, PVar a) +addr2pvar' !vc !addr m = _addr2pvar undefined vc addr m+{-# INLINE addr2pvar' #-}++_addr2pvar :: (VCacheable a) => a -> VSpace -> Address -> Memory -> IO (Memory, PVar a)+_addr2pvar _dummy !vc !addr m = do+    let ty = typeOf _dummy+    mbVar <- loadPVarTVar addr ty (mem_pvars m)+    case mbVar of+        Just var -> return (m, PVar addr var vc ty put)+        Nothing -> do+            lzv <- unsafeInterleaveIO $ RDV . fst <$> readAddrIO vc addr get+            var <- newTVarIO lzv+            e <- mkPVEph vc addr var ty+            let m' = m { mem_pvars = addPVEph e (mem_pvars m) }+            m' `seq` return (m', PVar addr var vc ty put)+{-# NOINLINE _addr2pvar #-}++mkPVEph :: VSpace -> Address -> TVar (RDV a) -> TypeRep -> IO PVEph+mkPVEph !vc !addr !tvar !ty = +    mkWeakTVar tvar (clearPVar vc addr) >>= \ wkTVar ->+    return $! PVEph addr ty wkTVar+{-# INLINE mkPVEph #-}++loadPVarTVar :: Address -> TypeRep -> PVEphMap -> IO (Maybe (TVar (RDV a)))+loadPVarTVar !addr !ty !mpv =+    case Map.lookup addr mpv of+        Nothing -> return Nothing+        Just pve -> do+            let tye = pveph_type pve +            unless (ty == tye) (fail $ eTypeMismatch addr ty tye)+            _getPVEphTVar pve++eTypeMismatch :: Address -> TypeRep -> TypeRep -> String+eTypeMismatch addr tyNew tyOld = +    showString "PVar user error: address " . shows addr .+    showString " type mismatch on load. " .+    showString " Existing: " . shows tyOld .+    showString " Expecting: " . shows tyNew $ ""++-- Clear a PVar from the ephemeron map.+clearPVar :: VSpace -> Address -> IO ()+clearPVar !vc !addr = modifyMVarMasked_ (vcache_memory vc) $ \ m -> do+    pvars' <- tryDelPVEph addr (mem_pvars m)+    let m' = m { mem_pvars = pvars' }+    return $! m'++tryDelPVEph :: Address -> PVEphMap -> IO PVEphMap+tryDelPVEph !addr !mpv =+    case Map.lookup addr mpv of+        Nothing -> return mpv+        Just (PVEph { pveph_data = wk }) ->+            Weak.deRefWeak wk >>= \ mbd ->+            if isJust mbd then return mpv else+            return $! Map.delete addr mpv++-- unsafe: get data, assuming that type already matches.+_getPVEphTVar :: PVEph -> IO (Maybe (TVar (RDV a)))+_getPVEphTVar = Weak.deRefWeak . _u where+    _u :: PVEph -> Weak (TVar (RDV a))+    _u (PVEph { pveph_data = w }) = _c w+    _c :: Weak (TVar (RDV b)) -> Weak (TVar (RDV a))+    _c = unsafeCoerce+{-# INLINE _getPVEphTVar #-}++-- | Construct a new VRef and initialize cache with given value.+-- If cache exists, will touch existing cache as if dereferenced.+newVRefIO :: (VCacheable a) => VSpace -> a -> CacheMode -> IO (VRef a)+newVRefIO vc v cm = +    runVPutIO vc (put v) >>= \ ((), _data, _deps) ->+    allocVRefIO vc _data _deps >>= \ vref ->+    join $ atomicModifyIORef (vref_cache vref) $ \ c -> case c of+        Cached r bf ->+            let bf' = touchCache cm bf in+            let c' = Cached r bf' in+            (c', c' `seq` return vref)+        NotCached ->+            let w = cacheWeight (BS.length _data) (L.length _deps) in+            let c' = mkVRefCache v w cm in+            let op = initVRefCache vref >> return vref in+            (c', c' `seq` op)+{-# NOINLINE newVRefIO #-}++-- I've split the mem_vrefs into two partitions, evrefs and cvrefs.+-- This shifts allows the cache manager to focus on just the cvrefs+-- partition, which will typically be much smaller than evrefs.+-- +-- After a value is first cached, whichever thread was responsible must+-- move the content from the mem_evrefs partition into mem_cvrefs. The+-- cache manager may later move it back and then clear the cache.+--+-- Cached values may be held temporarily by mem_evrefs, i.e. prior to+-- 'init' or just before the cache is cleared. But NotCached VRefs +-- should never be held by mem_cvrefs. +initVRefCache :: VRef a -> IO ()+initVRefCache !vref = +    let vc = vref_space vref in+    let addr = vref_addr vref in+    let ty = vref_type vref in+    modifyMVarMasked_ (vcache_memory vc) $ \ m -> +    case takeVREph addr ty (mem_evrefs m) of+        Nothing -> fail $ show vref ++ " expected in mem_evrefs partition!"+        Just (e, evrefs') ->+            let cvrefs' = addVREph e (mem_cvrefs m) in+            let m' = m { mem_evrefs = evrefs', mem_cvrefs = cvrefs' } in+            return $! m'+{-# NOINLINE initVRefCache #-}++-- | Construct a new VRef without initializing the cache.+newVRefIO' :: (VCacheable a) => VSpace -> a -> IO (VRef a) +newVRefIO' !vc v =+    runVPutIO vc (put v) >>= \ ((), _data, _deps) ->+    allocVRefIO vc _data _deps +{-# INLINE newVRefIO' #-}++-- | Allocate a VRef given data and dependencies.+--+-- We'll try to find an existing match in the database, then from the+-- recent allocations list, skipping addresses that have recently been+-- GC'd (to account for readers running a little behind the writer).+--+-- If a match is discovered, we'll use the existing address. Otherwise,+-- we'll allocate a new address and leave it to the background writer thread.+--+allocVRefIO :: (VCacheable a) => VSpace -> ByteString -> [PutChild] -> IO (VRef a)+allocVRefIO !vc !_data !_deps = +    let _name = hash _data in+    withByteStringVal _name $ \ vName ->+    withByteStringVal _data $ \ vData ->+    withRdOnlyTxn vc $ \ txn ->+    listDups' txn (vcache_db_caddrs vc) vName >>= \ caddrs ->+    seek (candidateVRefInDB vc txn vData) caddrs >>= \ mbVRef ->+    case mbVRef of+        Just !vref -> return vref -- found matching VRef in database+        Nothing -> modifyMVarMasked (vcache_memory vc) $ \ m -> +            let okAddr addr = isVRefAddr addr && not (recentGC (mem_gc m) addr) in+            let match an = okAddr (alloc_addr an) && (_data == (alloc_data an)) in+            let ff frm = Map.lookup _name (alloc_seek frm) >>= L.find match in+            case allocFrameSearch ff (mem_alloc m) of+                Just an -> addr2vref' vc (alloc_addr an) m -- found among recent allocations+                Nothing -> do -- allocate a new VRef address+                    let ac = mem_alloc m+                    let addr = alloc_new_addr ac+                    let an = Allocation { alloc_name = _name, alloc_data = _data+                                        , alloc_deps = _deps, alloc_addr = addr }+                    let frm' = addToFrame an (alloc_frm_next ac) +                    let ac' = ac { alloc_new_addr = 2 + addr, alloc_frm_next = frm' }+                    let m' = m { mem_alloc = ac' }+                    signalAlloc vc+                    addr2vref' vc addr m'+{-# NOINLINE allocVRefIO #-}++-- list all values associated with a given key+listDups' :: MDB_txn -> MDB_dbi' -> MDB_val -> IO [MDB_val]+listDups' txn dbi vKey = +    alloca $ \ pKey ->+    alloca $ \ pVal ->+    withCursor' txn dbi $ \ crs -> do+    let loop l b = +            if not b then return l else+            peek pVal >>= \ v ->+            mdb_cursor_get' MDB_NEXT_DUP crs pKey pVal >>= \ b' ->+            loop (v:l) b'+    poke pKey vKey+    b0 <- mdb_cursor_get' MDB_SET_KEY crs pKey pVal+    loop [] b0++withCursor' :: MDB_txn -> MDB_dbi' -> (MDB_cursor' -> IO a) -> IO a+withCursor' txn dbi = bracket g d where+    g = mdb_cursor_open' txn dbi+    d = mdb_cursor_close'+{-# INLINABLE withCursor' #-}++-- seek an exact match in the database. This will return Nothing in+-- one of these conditions:+--   (a) the candidate is not an exact match for the data+--   (b) the candidate has recently been GC'd from memory+-- Otherwise will return the necessary VRef.+candidateVRefInDB :: (VCacheable a) => VSpace -> MDB_txn -> MDB_val -> MDB_val -> IO (Maybe (VRef a))+candidateVRefInDB vc txn vData vCandidateAddr = do+    mbR <- mdb_get' txn (vcache_db_memory vc) vCandidateAddr+    case mbR of+        Nothing -> -- inconsistent hashmap and memory; this should never happen+            peekAddr vCandidateAddr >>= \ addr ->+            fail $ "VCache bug: undefined address " ++ show addr ++ " in hashmap" +        Just vData' ->+            let bSameSize = mv_size vData == mv_size vData' in+            if not bSameSize then return Nothing else+            c_memcmp (mv_data vData) (mv_data vData') (mv_size vData) >>= \ o ->+            if (0 /= o) then return Nothing else+            peekAddr vCandidateAddr >>= \ addr -> -- exact match! But maybe GC'd.+            modifyMVarMasked (vcache_memory vc) $ \ m ->+                if (recentGC (mem_gc m) addr) then return (m, Nothing) else+                addr2vref' vc addr m >>= \ (m', vref) ->+                return (m', Just vref)+++-- add annotation to frame without seek+addToFrame :: Allocation -> AllocFrame -> AllocFrame+addToFrame an frm + | BS.null (alloc_name an) = -- anonymous PVars +    assert (isPVarAddr (alloc_addr an)) $+    let list' = Map.insert (alloc_addr an) an (alloc_list frm) in+    frm { alloc_list = list' }+ | otherwise = -- root PVars or hashed VRefs +    let list' = Map.insert (alloc_addr an) an (alloc_list frm) in+    let add_an = Just . (an:) . maybe [] id in+    let seek' = Map.alter add_an (alloc_name an) (alloc_seek frm) in+    frm { alloc_list = list', alloc_seek = seek' }++peekAddr :: MDB_val -> IO Address+peekAddr v =+    let expectedSize = fromIntegral (sizeOf (undefined :: Address)) in+    let bBadSize = expectedSize /= mv_size v in+    if bBadSize then fail "VCache bug: badly formed address" else+    peekAligned (castPtr (mv_data v))+{-# INLINABLE peekAddr #-}++   +seek :: (Monad m) => (a -> m (Maybe b)) -> [a] -> m (Maybe b)+seek _ [] = return Nothing+seek f (x:xs) = f x >>= continue where+    continue Nothing = seek f xs+    continue r@(Just _) = return r +++++-- Intermediate data to allocate a new PVar. We don't know the+-- address yet, but we do know every other relevant aspect of+-- this PVar.+data AllocPVar a = AllocPVar+    { alloc_pvar_name :: !ByteString+    , alloc_pvar_data :: !ByteString+    , alloc_pvar_deps :: ![PutChild]+    , alloc_pvar_tvar :: !(TVar (RDV a))+    }++-- | Create a new, anonymous PVar as part of an active transaction.+-- Contents of the new PVar are not serialized unless the transaction+-- commits (though a placeholder is still allocated). +newPVar :: (VCacheable a) => a -> VTx (PVar a)+newPVar x = do+    vc <- getVTxSpace+    pvar <- liftSTM $ unsafeIOToSTM $ +                newTVarIO (RDV x) >>= \ tvar ->+                modifyMVarMasked (vcache_memory vc) $+                allocPVar vc $ allocPlaceHolder BS.empty tvar+    markForWrite pvar x+    return pvar++allocPlaceHolder :: ByteString -> TVar (RDV a) -> AllocPVar a+allocPlaceHolder _name tvar = AllocPVar+    { alloc_pvar_name = _name+    , alloc_pvar_data = BS.singleton 0+    , alloc_pvar_deps = []+    , alloc_pvar_tvar = tvar +    }++-- | Create a new, anonymous PVar via the IO monad. This is similar+-- to `newTVarIO`, but not as well motivated: global PVars should+-- almost certainly be constructed as named, persistent roots. +-- +newPVarIO :: (VCacheable a) => VSpace -> a -> IO (PVar a)+newPVarIO vc x = do+    apv <- preAllocPVarIO vc BS.empty x+    pvar <- modifyMVarMasked (vcache_memory vc) $ allocPVar vc apv+    signalAlloc vc+    return pvar++preAllocPVarIO :: (VCacheable a) => VSpace -> ByteString -> a -> IO (AllocPVar a)+preAllocPVarIO vc _name x = do+    tvar <- newTVarIO (RDV x)+    ((), _data, _deps) <- runVPutIO vc (put x)+    return $! AllocPVar+        { alloc_pvar_name = _name+        , alloc_pvar_data = _data+        , alloc_pvar_deps = _deps+        , alloc_pvar_tvar = tvar+        }++-- | Create an array of PVars with a given set of initial values. This+-- is equivalent to `mapM newPVar`, but guarantees adjacent addresses+-- in the persistence layer. This is mostly useful when working with +-- large arrays, to simplify reasoning about paging performance.+newPVars :: (VCacheable a) => [a] -> VTx [PVar a]+newPVars [] = return []+newPVars xs = do+    vc <- getVTxSpace +    pvars <- liftSTM $ unsafeIOToSTM $ do+        tvars <- mapM (newTVarIO . RDV) xs+        let apvs = fmap (allocPlaceHolder BS.empty) tvars +        allocPVars vc apvs+    sequence_ (L.zipWith markForWrite pvars xs)+    return pvars++-- | Create an array of adjacent PVars via the IO monad. +newPVarsIO :: (VCacheable a) => VSpace -> [a] -> IO [PVar a]+newPVarsIO _ [] = return []+newPVarsIO vc xs = do+    apvs <- mapM (preAllocPVarIO vc BS.empty) xs+    pvars <- allocPVars vc apvs+    signalAlloc vc+    return pvars++-- add a list of PVars to memory, acquiring addresses as we go.+allocPVars :: (VCacheable a) => VSpace -> [AllocPVar a] -> IO [PVar a]+allocPVars vc xs = +    let step (m, vars) apv =+            allocPVar vc apv m >>= \ (m', pvar) ->+            return (m', pvar:vars)+    in+    modifyMVar (vcache_memory vc) $ \ m -> do+    (!m', lReversedPVars) <- foldM step (m,[]) xs+    return (m', L.reverse lReversedPVars)++allocPVar :: (VCacheable a) => VSpace -> AllocPVar a -> Memory -> IO (Memory, PVar a)+allocPVar = _allocPVar undefined+{-# INLINE allocPVar #-}++_allocPVar :: (VCacheable a) => a -> VSpace -> AllocPVar a -> Memory -> IO (Memory, PVar a)+_allocPVar _dummy !vc !apv !m = do+    let ty = typeOf _dummy+    let tvar = alloc_pvar_tvar apv+    let ac = mem_alloc m+    let pv_addr = 1 + (alloc_new_addr ac)+    let pvar = PVar pv_addr tvar vc ty put+    pveph <- mkPVEph vc pv_addr tvar ty+    let an  = Allocation+            { alloc_name = alloc_pvar_name apv+            , alloc_data = alloc_pvar_data apv+            , alloc_addr = pv_addr+            , alloc_deps = alloc_pvar_deps apv+            }+    let frm' = addToFrame an (alloc_frm_next ac)+    let addr' = 2 + alloc_new_addr ac+    let ac' = ac { alloc_new_addr = addr', alloc_frm_next = frm' }+    let pvars' = addPVEph pveph (mem_pvars m)+    let m' = m { mem_pvars = pvars', mem_alloc = ac' }+    return (m', pvar)+{-# NOINLINE _allocPVar #-}++-- | Global, persistent variables may be loaded by name. The name here+-- is prefixed by vcacheSubdir to control namespace collisions between+-- software components. These named variables are roots for GC purposes,+-- and will not be deleted.+--+-- Conceptually, the root PVar has always been there. Loading a root+-- is thus a pure computation. At the very least, it's an idempotent+-- operation. If the PVar exists, its value is lazily read from the+-- persistence layer. Otherwise, the given initial value is stored.+-- To reset a root PVar, simply write before reading.+-- +-- The recommended practice for roots is to use only a few of them for+-- each persistent software component (i.e. each plugin, WAI app, etc.)+-- similarly to how a module might use just a few global variables. If+-- you need a dynamic set of variables, such as one per client, model+-- that explicitly using anonymous PVars. +--+loadRootPVar :: (VCacheable a) => VCache -> ByteString -> a -> PVar a+loadRootPVar vc name ini = unsafePerformIO (loadRootPVarIO vc name ini)+{-# INLINE loadRootPVar #-}++-- | Load a root PVar in the IO monad. This is convenient to control +-- where errors are detected or when initialization is performed.+-- See loadRootPVar.+loadRootPVarIO :: (VCacheable a) => VCache -> ByteString -> a -> IO (PVar a)+loadRootPVarIO vc !name ini =+    case vcacheSubdirM name vc of+        Just (VCache vs path) -> _loadRootPVarIO vs path ini+        Nothing ->+            let path = vcache_path vc `BS.append` name in+            fail $  "VCache: root PVar path too long: " ++ show path++_loadRootPVarIO :: (VCacheable a) => VSpace -> ByteString -> a -> IO (PVar a)+_loadRootPVarIO vc !_name ini = withRdOnlyTxn vc $ \ txn ->+    withByteStringVal _name $ \ rootKey ->+    mdb_get' txn (vcache_db_vroots vc) rootKey >>= \ mbRoot ->+    case mbRoot of+        Just val -> peekAddr val >>= addr2pvar vc -- found root in database+        Nothing -> -- usually allocate new PVar, possibly find in recent allocations+            preAllocPVarIO vc _name ini >>= \ apv -> -- for common case...+            modifyMVarMasked (vcache_memory vc) $ \ m ->+                let match an = isPVarAddr (alloc_addr an) in+                let ff frm = Map.lookup _name (alloc_seek frm) >>= L.find match in+                case allocFrameSearch ff (mem_alloc m) of+                    Just an -> addr2pvar' vc (alloc_addr an) m -- recent allocation+                    Nothing -> signalAlloc vc >> allocPVar vc apv m -- new root PVar+{-# NOINLINE _loadRootPVarIO #-}+ +-- report allocation to the writer thread+signalAlloc :: VSpace -> IO ()+signalAlloc vc = void $ tryPutMVar (vcache_signal vc) () ++foreign import ccall unsafe "string.h memcmp" c_memcmp+    :: Ptr Word8 -> Ptr Word8 -> CSize -> IO CInt+
+ hsrc_lib/Database/VCache/Cache.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE BangPatterns #-}+-- | Limited cache control.+module Database.VCache.Cache+    ( setVRefsCacheLimit+    , clearVRefsCache+    , clearVRefCache+    ) where++import Data.IORef+import Control.Concurrent.MVar+import qualified Data.Map.Strict as Map+import qualified System.Mem.Weak as Weak+import Database.VCache.Types++-- | VCache uses simple heuristics to decide which VRef contents to+-- hold in memory. One heuristic is a target cache size. Developers+-- may tune this to influence how many VRefs are kept in memory. +--+-- The value is specified in bytes, and the default is ten megabytes.+--+-- VCache size estimates are imprecise, converging on approximate +-- size, albeit not accounting for memory amplification (e.g. from a+-- compact UTF-8 string to Haskell's representation for [Char]). The+-- limit given here is soft, influencing how aggressively content is+-- removed from cache - i.e. there is no hard limit on content held+-- by the cache. Estimated cache size is observable via vcacheStats.+--+-- If developers need precise control over caching, they should use+-- normal means to reason about GC of values in Haskell (i.e. VRef is+-- cleared from cache upon GC). Or use vref' and deref' to avoid +-- caching and use VCache as a simple serialization layer.+-- +setVRefsCacheLimit :: VSpace -> Int -> IO ()+setVRefsCacheLimit vc !n = writeIORef (vcache_climit vc) n+{-# INLINE setVRefsCacheLimit #-}++-- | clearVRefsCache will iterate over cached VRefs in Haskell memory +-- at the time of the call, clearing the cache for each of them. This +-- operation isn't recommended for common use. It is rather hostile to+-- independent libraries working with VCache. But this function may +-- find some use for benchmarks or staged applications.+clearVRefsCache :: VSpace -> IO ()+clearVRefsCache vc = do +    -- we must hold lock for long enough to move contents to mem_evrefs+    ephMap <- modifyMVarMasked (vcache_memory vc) $ \ m -> do+        let evrefs' = Map.unionWith (Map.union) (mem_cvrefs m) (mem_evrefs m) +        let m' = m { mem_cvrefs = Map.empty, mem_evrefs = evrefs' }+        m' `seq` return (m', mem_cvrefs m)+    mapM_ (mapM_ clearVREphCache . Map.elems) (Map.elems ephMap)+{-# NOINLINE clearVRefsCache #-}++clearVREphCache :: VREph -> IO ()+clearVREphCache (VREph { vreph_cache = wc }) =   +    Weak.deRefWeak wc >>= \ mbCache ->+    case mbCache of+        Nothing -> return ()+        Just cache -> writeIORef cache NotCached+++-- | Immediately clear the cache associated with a VRef, allowing +-- any contained data to be GC'd. Normally, VRef cached values are+-- cleared either by a background thread or when the VRef itself+-- is garbage collected from Haskell memory. But sometimes the+-- programmer knows best.+clearVRefCache :: VRef a -> IO ()+clearVRefCache v = do+    let vc = vref_space v +    modifyMVarMasked_ (vcache_memory vc) $ \ m -> do+        case takeVREph (vref_addr v) (vref_type v) (mem_cvrefs m) of+            Nothing -> return m -- was not cached+            Just (e, cvrefs') -> do+                let evrefs' = addVREph e (mem_evrefs m)+                let m' = m { mem_cvrefs = cvrefs', mem_evrefs = evrefs' }+                return $! m'+    writeIORef (vref_cache v) NotCached+{-# NOINLINE clearVRefCache #-}++
+ hsrc_lib/Database/VCache/Clean.hs view
@@ -0,0 +1,216 @@+{-# LANGUAGE BangPatterns #-}+-- This module manages the ephemeron tables and VRef caches.+-- +-- In addition, this thread will signal the writer when there seems+-- to be some GC work to perform.  +--+-- DESIGN THOUGHTS:+--+-- The original implementation was inefficient, touching far too many+-- VRefs in each pass. It didn't scale nicely.+--+-- I've redesigned to partition VRefs so I can easily find just those+-- that are cached. And the cleanup function now touches only those in+-- cache. Clearing GC'd content is now handled by the System.Mem.Weak +-- finalizers. Size estimates must be probabilistic, to avoid a global+-- pass to compute sizes.+--+-- At this point, the clean function only touches elements that are +-- certainly cached, and which it plans to remove from cache. The +-- cleanup function is based on exponential decay, i.e. we try to+-- remove X% of the cache in each round. Though X may vary based on+-- whether we are over or under our heuristic cache limit.+--+-- Originally, I had some sophisticated usage tracking. I could move+-- some of this to the touchCache operation, but for now I'm just+-- going to assume that CacheMode alone is sufficient in most cases+-- due to how it resets on each deref.+--+module Database.VCache.Clean+    ( cleanStep+    ) where++import Control.Monad+import Control.Applicative+import Control.Concurrent+import Data.Bits+import qualified Data.Traversable as TR+import qualified Data.Map.Strict as Map+import Data.IORef+import qualified System.Mem.Weak as Weak+import qualified System.Random as Random++import Database.LMDB.Raw+import Database.VCache.Types++-- | Cache cleanup, and signal writer for old content.+cleanStep :: VSpace -> IO ()+cleanStep vc = do+    bsig <- shouldSignalWriter vc+    when bsig (signalWriter vc)++    wtgt <- readIORef (vcache_climit vc)+    w0 <- estCacheSize vc+    let hitRate = +            if ((100 * w0) < ( 80 * wtgt)) then 0.00 else+            if ((100 * w0) < (100 * wtgt)) then 0.01 else+            if ((100 * w0) < (130 * wtgt)) then 0.02 else+            if ((100 * w0) < (170 * wtgt)) then 0.03 else+            if ((100 * w0) < (220 * wtgt)) then 0.04 else +            if ((100 * w0) < (280 * wtgt)) then 0.05 else+            0.06+    xcln vc hitRate+    updateCacheSizeEst vc+    wf <- estCacheSize vc++    let bSatisfied = (max w0 wf) < wtgt+    let dtSleep = if bSatisfied then 295000 else 95000 +    usleep dtSleep -- ~10Hz, slower when steady++-- sleep for a number of microseconds+usleep :: Int -> IO ()+usleep = threadDelay+{-# INLINE usleep #-}++-- For now, I'm choosing to use sqrt( avgSquare ) because it is +-- weighted in favor of larger values, which (conversely) we're+-- less likely to find when randomly sampling a collection with +-- just a few large values and lots of small ones. +--+estCacheSize :: VSpace -> IO Int+estCacheSize vc = do+    csze <- readIORef (vcache_csize vc)+    let avgAddr = sqrt (csze_addr_sqsz csze) +    ctAddrs <- fromIntegral <$> readCacheAddrCt vc+    return $! ceiling $ avgAddr * ctAddrs++readCacheAddrCt :: VSpace -> IO Int+readCacheAddrCt vc = do+    m <- readMVar (vcache_memory vc)+    return $! Map.size (mem_cvrefs m)++-- sample the cache at a few random addresses, use this to update the+-- cache size by a small factor. Over the course of many seconds, the+-- estimated average size per address should approach the actual size+-- assuming the average itself is stable. Even if average size isn't+-- stable, this is good enough to help guide the cache manager.+--+-- The assumption here is that the cvrefs map is usually large. If it+-- is small, we'll still use the same algorithm, even if it's a bit +-- redundant, to simplify reasoning and testing. A constant number of+-- samples are taken in each round. Probabilistically+updateCacheSizeEst :: VSpace -> IO ()+updateCacheSizeEst vc =+    readMVar (vcache_memory vc) >>= \ m ->+    let cvrefs = mem_cvrefs m in+    if Map.null cvrefs then return () else+    let nextIx = Random.randomR (0, Map.size cvrefs - 1) in+    let loop !n !r !sz !sqsz = +            if (0 == n) then return (sz,sqsz) else+            let (ix,r') = nextIx r in+            let (_, tym) = Map.elemAt ix cvrefs in+            let (_, e) = Map.findMin tym in -- safe; address elements non-empty+            readVREphSize e >>= \ esz ->+            let addrsz = fromIntegral $ esz * Map.size tym in+            let sz' = sz + addrsz in+            let sqsz' = sqsz + (addrsz * addrsz) in+            loop (n-1) r' sz' sqsz'+    in+    let nSamples = 15 :: Int in+    Random.newStdGen >>= \ r ->+    loop nSamples r 0 0 >>= \ (totalSize, totalSqSize) ->+    let sampleAvg = totalSize / fromIntegral nSamples in+    let sampleAvgSq = totalSqSize / fromIntegral nSamples in+    readIORef (vcache_csize vc) >>= \ (CacheSizeEst oldAvg oldAvgSq) ->+    let alpha = 0.015 :: Double in+    let newAvg = alpha * sampleAvg + ((1.0 - alpha) * oldAvg) in+    let newAvgSq = alpha * sampleAvgSq + ((1.0 - alpha) * oldAvgSq) in+    writeIORef (vcache_csize vc) $! (CacheSizeEst newAvg newAvgSq)++readVREphSize :: VREph -> IO Int+readVREphSize (VREph { vreph_cache = wk }) =+    Weak.deRefWeak wk >>= \ mbc -> case mbc of +        Nothing -> return 2048 -- GC'd recently; high estimate+        Just cache -> readIORef cache >>= \ c -> case c of+            NotCached -> +                let eMsg = "VCache bug: NotCached element found in mem_cvrefs" in+                fail eMsg+            Cached _ bf ->+                let lgSz = 6 + fromIntegral (0x1f .&. bf) in+                return $! 1 `shiftL` lgSz++-- | exponential decay based cleanup. In this case, we attack a+-- random fraction of the cached addresses. Each attack reduces+-- the CacheMode of cached elements. If the CacheMode is zero, the+-- element is removed from the database. Active contents have their+-- CacheMode reset on each use, and cleanup stops when estimated +-- size is good. +xcln :: VSpace -> Double -> IO ()+xcln !vc !hr = do+    ct <- readCacheAddrCt vc+    let hct = ceiling $ hr * fromIntegral ct +    r <- Random.newStdGen+    xclnLoop vc hct r++xclnLoop :: VSpace -> Int -> Random.StdGen -> IO ()+xclnLoop !vc !n !r =+    if (n < 1) then return () else+    xclnStrike vc r >>= xclnLoop vc (n-1)++xclnStrike :: VSpace -> Random.StdGen -> IO Random.StdGen+xclnStrike !vc !r = modifyMVarMasked (vcache_memory vc) $ \ m ->+    if Map.null (mem_cvrefs m) then return (m,r) else do+    let cvrefs = mem_cvrefs m+    let evrefs = mem_evrefs m+    let (ix,r') = Random.randomR (0, Map.size cvrefs - 1) r+    let (addr, tym) = Map.elemAt ix cvrefs +    (tymc, tyme) <- Map.mapEither id <$> TR.traverse strikeVREph tym+    let cvrefs' = if Map.null tymc then Map.delete addr cvrefs else+                  if Map.null tyme then cvrefs else+                  Map.insert addr tymc cvrefs+    let evrefs' = if Map.null tyme then evrefs else+                  Map.insertWith (Map.union) addr tyme evrefs+    let m' = m { mem_cvrefs = cvrefs', mem_evrefs = evrefs' }+    return (m', m' `seq` r')++-- strikeVREph will reduce the CacheMode for a cached element or+-- remove it from the cache (in right) for CacheMode0.+strikeVREph :: VREph -> IO (Either VREph VREph)+strikeVREph vreph@(VREph { vreph_cache = wk }) =+    Weak.deRefWeak wk >>= \ mbCache -> case mbCache of+        Nothing -> return (Right vreph) -- +        Just cache -> atomicModifyIORef cache $ \ c -> case c of+            Cached r bf | (0 /= bf .&. 0x60) -> +                let bf' = (0x80 .|. (bf - 0x20)) in+                let c' = Cached r bf' in+                (c', c' `seq` (Left vreph))+            _ -> (NotCached, Right vreph)++-- If the writer has obvious work it could be doing, signal it. This+-- won't significantly affect a busy writer, but an idle writer may+-- require a kick in the pants to remove content from the allocations+-- list or clear old zeroes.+shouldSignalWriter :: VSpace -> IO Bool+shouldSignalWriter vc = +    readMVar (vcache_memory vc) >>= \ m ->+    let bHoldingAllocs = not (emptyAllocation (mem_alloc m)) in+    if bHoldingAllocs then return True else+    readZeroesCt vc >>= \ ctZeroes ->+    let ctEphAddrs = Map.size (mem_cvrefs m) + Map.size (mem_evrefs m) + Map.size (mem_pvars m) in+    if (ctEphAddrs < ctZeroes) then return True else+    return False++readZeroesCt :: VSpace -> IO Int+readZeroesCt vc = withRdOnlyTxn vc $ \ txn ->+    mdb_stat' txn (vcache_db_refct0 vc) >>= \ stat ->+    return $! fromIntegral (ms_entries stat)++emptyAllocation :: Allocator -> Bool+emptyAllocation ac = fn n && fn c && fn p where+    n = alloc_frm_next ac+    c = alloc_frm_curr ac+    p = alloc_frm_prev ac+    fn = Map.null . alloc_list++signalWriter :: VSpace -> IO ()+signalWriter vc = void $ tryPutMVar (vcache_signal vc) ()
+ hsrc_lib/Database/VCache/Hash.hs view
@@ -0,0 +1,23 @@++-- This is the hash function for content addressing.+module Database.VCache.Hash+    ( hash+    , hashVal+    ) where++import qualified Data.Digest.Murmur3 as M3+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as BSI+import Foreign.ForeignPtr++import Database.LMDB.Raw (MDB_val(..))++hash :: ByteString -> ByteString+hash = BS.take 8 . M3.asByteString . M3.hash++hashVal :: MDB_val -> IO ByteString+hashVal mv = do+    fp <- newForeignPtr_ (mv_data mv) -- no finalizer+    let bs = BSI.PS fp 0 (fromIntegral (mv_size mv))+    return $! hash bs
+ hsrc_lib/Database/VCache/Open.hs view
@@ -0,0 +1,246 @@+module Database.VCache.Open+    ( openVCache+    ) where++import Control.Monad+import Control.Exception+import System.FileLock (FileLock)+import qualified System.FileLock as FileLock+import qualified System.EasyFile as EasyFile+import qualified System.IO.Error as IOE+import Foreign.Storable+import Foreign.Marshal.Alloc+import Foreign.Ptr++import Data.Bits+import Data.IORef+import Data.Maybe+import qualified Data.Map.Strict as Map+import qualified Data.ByteString as BS+import qualified Data.List as L+import Control.Concurrent.MVar+import Control.Concurrent.STM.TVar+import Control.Concurrent++import qualified System.IO as Sys+import qualified System.Exit as Sys++import Database.LMDB.Raw+import Database.VCache.Types +import Database.VCache.RWLock+import Database.VCache.Aligned+import Database.VCache.Write+import Database.VCache.Clean ++++-- | Open a VCache with a given database file. +--+-- In most cases, a Haskell process should open VCache in the Main+-- module then pass it as an argument to the different libraries,+-- frameworks, plugins, and other software components that require+-- persistent storage. Use vcacheSubdir to progect against namespace+-- collisions. +--+-- When opening VCache, developers decide the maximum size and the file+-- name. For example:+--+-- > vc <- openVCache 100 "db"+--+-- This would open a VCache whose file-size limit is 100 megabytes, +-- with the name "db", plus an additional "db-lock" lockfile. An +-- exception will be raised if these files cannot be created, locked,+-- or opened. The size limit is passed to LMDB and is separate from+-- setVRefsCacheSize. +--+-- Once opened, VCache typically remains open until process halt. +-- If errors are detected, e.g. due to writing an undefined value+-- to a PVar or running out of space, VCache will attempt to halt+-- the process.+--+openVCache :: Int -> FilePath -> IO VCache+openVCache nMB fp = do+    let (fdir,fn) = EasyFile.splitFileName fp+    let eBadFile = fp ++ " not recognized as a file name"+    when (L.null fn) (fail $ "openVCache: " ++ eBadFile)+    EasyFile.createDirectoryIfMissing True fdir+    let fpLock = fp ++ "-lock"+    let nBytes = (max 1 nMB) * 1024 * 1024+    mbLock <- FileLock.tryLockFile fpLock FileLock.Exclusive +    case mbLock of+        Nothing -> ioError $ IOE.mkIOError+            IOE.alreadyInUseErrorType+            "openVCache lockfile"+            Nothing (Just fpLock)+        Just fl -> openVC' nBytes fl fp +                    `onException` FileLock.unlockFile fl++vcFlags :: [MDB_EnvFlag] +vcFlags = [MDB_NOSUBDIR     -- open file name, not directory name+          ,MDB_NOLOCK       -- leave lock management to VCache+          ]++--+-- I'm providing a non-empty root bytestring. There are a few reasons+-- for this. LMDB doesn't support zero-sized keys. And the empty+-- bytestring will indicate anonymous PVars in the allocator. And if+-- I ever want PVar roots within VCache, I can use a different prefix.+--+-- The maximum path, including the PVar name, is 511 bytes. That is+-- enough for almost any use case, especially since roots should not+-- depend on domain data. Too large a path results in runtime error.+vcRootPath :: BS.ByteString+vcRootPath = BS.singleton 47++-- Default address for allocation. We start this high to help +-- regulate serialization sizes and simplify debugging.+vcAllocStart :: Address +vcAllocStart = 999999999++-- Default cache size is somewhat arbitrary. I've chosen to set it+-- to about ten megabytes (as documented in the Cache module). +vcDefaultCacheLimit :: Int+vcDefaultCacheLimit = 10 * 1000 * 1000 ++-- initial cache size+vcInitCacheSizeEst :: CacheSizeEst+vcInitCacheSizeEst = CacheSizeEst+    { csze_addr_size = sz -- err likely on high side to start+    , csze_addr_sqsz = (sz * sz)+    }+    where sz = 2048 -- err likely on high side to start++-- Checking for a `-threaded` runtime+threaded :: Bool+threaded = rtsSupportsBoundThreads++openVC' :: Int -> FileLock -> FilePath -> IO VCache+openVC' nBytes fl fp = do+    +    unless threaded (fail "VCache needs -threaded runtime")++    dbEnv <- mdb_env_create+    mdb_env_set_mapsize dbEnv nBytes+    mdb_env_set_maxdbs dbEnv 5+    mdb_env_open dbEnv fp vcFlags+    flip onException (mdb_env_close dbEnv) $ do++        -- initial transaction to grab database handles and init allocator+        txnInit <- mdb_txn_begin dbEnv Nothing False+        dbiMemory <- mdb_dbi_open' txnInit (Just "@") [MDB_CREATE, MDB_INTEGERKEY]+        dbiRoots  <- mdb_dbi_open' txnInit (Just "/") [MDB_CREATE]+        dbiHashes <- mdb_dbi_open' txnInit (Just "#") [MDB_CREATE, MDB_INTEGERKEY, MDB_DUPSORT, MDB_DUPFIXED, MDB_INTEGERDUP]+        dbiRefct  <- mdb_dbi_open' txnInit (Just "^") [MDB_CREATE, MDB_INTEGERKEY]+        dbiRefct0 <- mdb_dbi_open' txnInit (Just "%") [MDB_CREATE, MDB_INTEGERKEY]+        allocEnd <- findLastAddrAllocated txnInit dbiMemory+        mdb_txn_commit txnInit++        -- ephemeral resources+        let allocStart = nextAllocAddress allocEnd+        memory <- newMVar (initMemory allocStart)+        tvWrites <- newTVarIO (Writes Map.empty [])+        mvSignal <- newMVar ()+        cLimit <- newIORef vcDefaultCacheLimit+        cSize <- newIORef vcInitCacheSizeEst+        ctWrites <- newIORef $ WriteCt 0 0 0+        gcStart <- newIORef Nothing+        gcCount <- newIORef 0+        rwLock <- newRWLock++        -- finalizer, in unlikely event of closure+        _ <- mkWeakMVar mvSignal $ do+                mdb_env_close dbEnv+                FileLock.unlockFile fl++        let vc = VCache +                { vcache_path = vcRootPath+                , vcache_space = VSpace +                    { vcache_lockfile = fl+                    , vcache_db_env = dbEnv+                    , vcache_db_memory = dbiMemory+                    , vcache_db_vroots = dbiRoots+                    , vcache_db_caddrs = dbiHashes+                    , vcache_db_refcts = dbiRefct+                    , vcache_db_refct0 = dbiRefct0+                    , vcache_memory = memory+                    , vcache_signal = mvSignal+                    , vcache_writes = tvWrites+                    , vcache_rwlock = rwLock+                    , vcache_climit = cLimit+                    , vcache_csize = cSize+                    , vcache_signal_writes = updWriteCt ctWrites+                    , vcache_ct_writes = ctWrites+                    , vcache_alloc_init = allocStart+                    , vcache_gc_start = gcStart+                    , vcache_gc_count = gcCount+                    }+                }++        initVCacheThreads (vcache_space vc)+        return $! vc++-- our allocator should be set for the next *even* address.+nextAllocAddress :: Address -> Address+nextAllocAddress addr | (0 == (addr .&. 1)) = 2 + addr+                      | otherwise           = 1 + addr++-- Determine the last VCache VRef address allocated, based on the+-- actual database contents. If nothing is+findLastAddrAllocated :: MDB_txn -> MDB_dbi' -> IO Address+findLastAddrAllocated txn dbiMemory = alloca $ \ pKey ->+    mdb_cursor_open' txn dbiMemory >>= \ crs ->+    mdb_cursor_get' MDB_LAST crs pKey nullPtr >>= \ bFound ->+    mdb_cursor_close' crs >>+    if (not bFound) then return vcAllocStart else +    peek pKey >>= \ key -> +    let bBadSize = fromIntegral (sizeOf vcAllocStart) /= mv_size key in +    if bBadSize then fail "VCache memory table corrupted" else+    peekAligned (castPtr (mv_data key)) ++-- initialize memory based on initial allocation position+initMemory :: Address -> Memory+initMemory addr = m0 where+    af = AllocFrame Map.empty Map.empty addr+    ac = Allocator addr af af af+    gcf = GCFrame Map.empty+    gc = GC gcf gcf+    m0 = Memory Map.empty Map.empty Map.empty gc ac++-- Update write counts.+updWriteCt :: IORef WriteCt -> Writes -> IO ()+updWriteCt var w = modifyIORef' var $ \ wct ->+    let frmCt = 1 + wct_frames wct in+    let pvCt = wct_pvars wct + Map.size (write_data w) in+    let synCt = wct_sync wct + L.length (write_sync w) in+    WriteCt { wct_frames = frmCt, wct_pvars = pvCt, wct_sync = synCt }++-- | Create background threads needed by VCache.+initVCacheThreads :: VSpace -> IO ()+initVCacheThreads vc = begin where+    begin = do+        task (writeStep vc)+        task (cleanStep vc)+        return ()+    task step = void (forkIO (forever step `catch` onE))+    onE :: SomeException -> IO ()+    onE e | isBlockedOnMVar e = return () -- full GC of VCache+    onE e = do+        putErrLn "VCache background thread has failed."+        putErrLn (indent "  " (show e))+        putErrLn "Halting program."+        Sys.exitFailure++isBlockedOnMVar :: (Exception e) => e -> Bool+isBlockedOnMVar = isJust . test . toException where+    test :: SomeException -> Maybe BlockedIndefinitelyOnMVar+    test = fromException++putErrLn :: String -> IO ()+putErrLn = Sys.hPutStrLn Sys.stderr++indent :: String -> String -> String+indent ws = (ws ++) . indent' where+    indent' ('\n':s) = '\n' : ws ++ indent' s+    indent' (c:s) = c : indent' s+    indent' [] = []+
+ hsrc_lib/Database/VCache/PVar.hs view
@@ -0,0 +1,112 @@++module Database.VCache.PVar+    ( PVar+    , newPVar+    , newPVars+    , newPVarIO+    , newPVarsIO+    , loadRootPVar+    , loadRootPVarIO+    , readPVar+    , readPVarIO+    , writePVar+    , modifyPVar+    , modifyPVar'+    , swapPVar+    , pvar_space+    , unsafePVarAddr+    , unsafePVarRefct+    ) where++import Control.Concurrent.STM++import Database.VCache.Types+import Database.VCache.Alloc ( newPVar, newPVars, newPVarIO, newPVarsIO+                             , loadRootPVar, loadRootPVarIO)+import Database.VCache.Read (readRefctIO)++-- | Read a PVar as part of a transaction.+readPVar :: PVar a -> VTx a+readPVar pvar = +    getVTxSpace >>= \ space ->+    if (space /= pvar_space pvar) then fail eBadSpace else+    liftSTM $ readTVar (pvar_data pvar) >>= \ rdv ->+              case rdv of { (RDV v) -> return v }+{-# INLINABLE readPVar #-}++-- Note that readPVar and readPVarIO must be strict in RDV in order to force+-- the initial, lazy read from the database. This is the only reason for RDV.+-- Without forcing here, a lazy read might return a value from an update.++-- | Read a PVar in the IO monad. +--+-- This is more efficient than a full transaction. It simply peeks at+-- the underlying TVar with readTVarIO. Durability of the value read+-- is not guaranteed. +readPVarIO :: PVar a -> IO a+readPVarIO pv = +    readTVarIO (pvar_data pv) >>= \ rdv ->+    case rdv of { (RDV v) -> return v }+{-# INLINE readPVarIO #-}++eBadSpace :: String+eBadSpace = "VTx: mismatch between VTx VSpace and PVar VSpace"++-- | Write a PVar as part of a transaction.+writePVar :: PVar a -> a -> VTx ()+writePVar pvar v = +    getVTxSpace >>= \ space ->+    if (space /= pvar_space pvar) then fail eBadSpace else+    markForWrite pvar v >>+    liftSTM (writeTVar (pvar_data pvar) (RDV v))+{-# INLINABLE writePVar #-}+++-- | Modify a PVar. +modifyPVar :: PVar a -> (a -> a) -> VTx ()+modifyPVar var f = do+    x <- readPVar var+    writePVar var (f x)+{-# INLINE modifyPVar #-}++-- | Modify a PVar, strictly.+modifyPVar' :: PVar a -> (a -> a) -> VTx ()+modifyPVar' var f = do+    x <- readPVar var+    writePVar var $! f x+{-# INLINE modifyPVar' #-}++-- | Swap contents of a PVar for a new value.+swapPVar :: PVar a -> a -> VTx a+swapPVar var new = do+    old <- readPVar var +    writePVar var new+    return old+{-# INLINE swapPVar #-}++-- | Each PVar has a stable address in the VCache. This address will+-- be very stable, but is not deterministic and isn't really something+-- you should treat as meaningful information about the PVar. Mostly, +-- this function exists to support hashtables or memoization with+-- PVar keys.+--+-- The Show instance for PVars will also show the address.+unsafePVarAddr :: PVar a -> Address+unsafePVarAddr = pvar_addr+{-# INLINE unsafePVarAddr #-}++-- | This function allows developers to access the reference count +-- for the PVar that is currently recorded in the database. This may+-- be useful for heuristic purposes. However, caveats are needed:+--+-- First, because the VCache writer operates in a background thread,+-- the reference count returned here may be slightly out of date.+--+-- Second, it is possible that VCache will eventually use some other+-- form of garbage collection than reference counting. This function+-- should be considered an unstable element of the API.+--+-- Root PVars start with one root reference.+unsafePVarRefct :: PVar a -> IO Int+unsafePVarRefct var = readRefctIO (pvar_space var) (pvar_addr var)+
+ hsrc_lib/Database/VCache/Path.hs view
@@ -0,0 +1,54 @@++module Database.VCache.Path+    ( vcacheSubdir+    , vcacheSubdirM+    ) where++import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Database.VCache.Types++maxPathLen :: Int+maxPathLen = 511  -- key size limit from LMDB 0.9.10++-- | VCache implements a simplified filesystem metaphor. Developers +-- can assign a distinct prefix for all PVars created by the VCache,+-- thus modeling namespaces or subdirectories. Assuming the normal +-- advice of opening the VCache only once in the main module, these+-- prefixes enable transparent, modular decomposition of a VCache +-- application without risk of name collisions.+--+-- VCache is simplistic about this: a prefix is appended directly.+-- If developers use subdir "foo" followed by "bar", the result is +-- the same as "foobar". Separators are left to local conventions.+-- Consider "foo/" and "bar/" to model filesystem subdirectories. +--                 +-- Paths have a limited maximum size of ~500 bytes, including the+-- final PVar name. A runtime error may be generated for oversized+-- paths. In practice, this should not be an issue. +--+-- Usage Note: Subdirectories allow developers to control risk of+-- namespace collisions between modules or plugins. But they are not+-- intended for domain data! Avoid dynamically creating directories+-- or named PVars based on runtime data. It's better to push most+-- domain logic and schema into the PVar layer, which is subject to+-- rich type safety, GC, potential versioning, and other benefits.+--+vcacheSubdir :: ByteString -> VCache -> VCache+vcacheSubdir p (VCache vs d) = +    let d' = subdir d p in+    if (BS.length d' > maxPathLen) +        then error ("VCache path too long: " ++ show d')+        else (VCache vs d')++-- | as vcacheSubdir, but returns Nothing if the path is too large.+vcacheSubdirM :: ByteString -> VCache -> Maybe VCache+vcacheSubdirM p (VCache vs d) =+    let d' = subdir d p in+    if (BS.length d' > maxPathLen)+        then Nothing+        else Just (VCache vs d')++subdir :: ByteString -> ByteString -> ByteString+subdir = BS.append+{-# INLINE subdir #-}
+ hsrc_lib/Database/VCache/RWLock.hs view
@@ -0,0 +1,146 @@++-- | read-write lock specialized for using LMDB with MDB_NOLOCK option+--+module Database.VCache.RWLock+    ( RWLock+    , newRWLock+    , withRWLock+    , withRdOnlyLock+    ) where++import Control.Monad+import Control.Exception+import Control.Concurrent.MVar+import Data.IORef+import Data.IntSet (IntSet)+import qualified Data.IntSet as IntSet++-- | RWLock+--+-- VCache uses LMDB with the MDB_NOLOCK option, mostly because I don't+-- want to deal with the whole issue of OS bound threads or a limit on+-- number of concurrent readers. Without locks, we essentially have one+-- valid snapshot. The writer can begin dismantling earlier snapshots+-- as needed to allocate pages. +--+-- RWLock essentially enforces this sort of frame-buffer concurrency.+data RWLock = RWLock +    { rwlock_frames :: !(MVar FB)+    , rwlock_writer :: !(MVar ())  -- enforce single writer+    }++data FB = FB !F !F+type F = IORef Frame+data Frame = Frame +    { frame_reader_next :: {-# UNPACK #-} !Int+    , frame_readers     :: !IntSet+    , frame_onClear     :: ![IO ()] -- actions to perform +    }+frame0 :: Frame+frame0 = Frame 1 IntSet.empty []++newRWLock :: IO RWLock+newRWLock = liftM2 RWLock (newMVar =<< newF2) newEmptyMVar where++newF2 :: IO FB+newF2 = liftM2 FB newF newF ++newF :: IO F+newF = newIORef frame0++withWriterMutex :: RWLock -> IO a -> IO a+withWriterMutex l = bracket_ getLock dropLock where+    getLock = putMVar (rwlock_writer l) ()+    dropLock = takeMVar (rwlock_writer l)+{-# INLINE withWriterMutex #-}++-- | Grab the current read-write lock for the duration of+-- an underlying action. This may wait on older readers. +withRWLock :: RWLock -> IO a -> IO a+withRWLock l action = withWriterMutex l $ do+    oldFrame <- rotateReaderFrames l +    mvWait <- newEmptyMVar+    onFrameCleared oldFrame (putMVar mvWait ())+    takeMVar mvWait+    action++-- rotate a fresh reader frame, and grab the oldest.+-- Thus should only be performed while holding the writer lock.+rotateReaderFrames :: RWLock -> IO F+rotateReaderFrames l = mask_ $ do+    let var = rwlock_frames l+    f0 <- newF+    (FB f1 f2) <- takeMVar var+    putMVar var (FB f0 f1)+    return f2++--+-- NOTE: Each of these 'frames' actually contains readers of two+-- transactions. Alignment between LMDB transactions and VCache+-- RWLock isn't exact. +--+-- Each write lock will rotate reader frames just once: +--+--     (f1,f2) → (f0,f1) returning f2+--+-- Writer is working on LMDB frame N.+--+-- f0 will have readers for frame N-1 and (after commit) for N.+-- f1 will have readers for frame N-2 and some for N-1.+-- f2 will have readers for frame N-3 and some for N-2.+--+-- LMDB guarantees that the data pages for frames N-1 and N-2 are +-- intact. However, frame N-3 will be dismantled while building+-- frame N. Thus, we must wait for f2 readers to finish before we +-- begin the writer N transaction.+--+-- If we assume short-running readers and long-running writers, it+-- is rare that the writer ever needs to wait on readers. Readers +-- never need to wait on the writer. This assumption is achieved by+-- batching writes  in VCache.+--+++-- perform some action when a frame is cleared +-- performs immediately, if possible.+onFrameCleared :: F -> IO () -> IO ()+onFrameCleared f action = atomicModifyIORef f addAction >>= id where+    addAction frame =+        let bAlreadyClear = IntSet.null (frame_readers frame) in+        if bAlreadyClear then (frame0,action) else+        let onClear' = action : frame_onClear frame in+        let frame' = frame { frame_onClear = onClear' } in+        (frame', return ())++-- | Grab a read-only lock for the duration of some IO action. +--+-- Readers never need to wait on the writer.+withRdOnlyLock :: RWLock -> IO a -> IO a+withRdOnlyLock l = bracket (newReader l) releaseReader . const++newtype Reader = Reader { releaseReader :: IO () }++-- obtains a reader handle; returns function to release reader.+newReader :: RWLock -> IO Reader+newReader l = mask_ $ do+    let var = rwlock_frames l+    fb@(FB f _) <- takeMVar var+    r <- atomicModifyIORef f addReader+    putMVar var fb+    return (Reader (delReader f r))++addReader :: Frame -> (Frame, Int)+addReader f =+    let r     = frame_reader_next f in+    let rdrs' = IntSet.insert r (frame_readers f) in+    let f'    = f { frame_reader_next = (r + 1)+                  , frame_readers = rdrs' } in+    (f', r)++delReader :: F -> Int -> IO ()+delReader f r = atomicModifyIORef f del >>= sequence_ where+    del frm =+        let rdrs' = IntSet.delete r (frame_readers frm) in+        if IntSet.null rdrs' then (frame0, frame_onClear frm) else+        let frm' = frm { frame_readers = rdrs' } in+        (frm', []) 
+ hsrc_lib/Database/VCache/Read.hs view
@@ -0,0 +1,92 @@++module Database.VCache.Read+    ( readAddrIO+    , readRefctIO+    ) where++import Control.Monad+import qualified Data.Map.Strict as Map+import qualified Data.List as L+import Control.Concurrent.MVar+import Foreign.Ptr+import Foreign.Storable+import Foreign.Marshal.Alloc++import Database.LMDB.Raw++import Database.VCache.Types+import Database.VCache.VGetInit+import Database.VCache.VGetAux+import Database.VCache.Refct++-- | Parse contents at a given address. Returns both the value and the+-- cache weight, or fails. This first tries reading the database, then+-- falls back to reading from recent allocation frames. +readAddrIO :: VSpace -> Address -> VGet a -> IO (a, Int)+readAddrIO vc addr parser = +    alloca $ \ pAddr ->+    poke pAddr addr >>+    let vAddr = MDB_val { mv_data = castPtr pAddr+                        , mv_size = fromIntegral (sizeOf addr)+                        }+    in+    withRdOnlyTxn vc $ \ txn -> +    let db = vcache_db_memory vc in+    let rd = readVal vc parser in+    mdb_get' txn db vAddr >>= \ mbData ->+    case mbData of+        Just vData -> rd vData -- found data in database (ideal)+        Nothing -> -- since not in the database, try the allocator+            let ff = Map.lookup addr . alloc_list in+            readMVar (vcache_memory vc) >>= \ memory ->+            let ac = mem_alloc memory in+            case allocFrameSearch ff ac of+                Just an -> withByteStringVal (alloc_data an) rd -- found data in allocator+                Nothing -> fail $ "VCache: address " ++ show addr ++ " is undefined!"++readVal :: VSpace -> VGet a -> MDB_val -> IO (a, Int)+readVal vc p v = _vget (vgetFull p) s0 >>= retv where+    s0 = VGetS { vget_children = []+               , vget_target = mv_data v+               , vget_limit = mv_data v `plusPtr` fromIntegral (mv_size v)+               , vget_space = vc+               }+    retv (VGetR result _) = return result+    retv (VGetE eMsg) = fail eMsg++-- get the full value and weight+vgetFull :: VGet a -> VGet (a, Int)+vgetFull parser = do+    vgetInit +    w <- vgetWeight+    r <- parser+    assertDone+    return (r,w)++assertDone :: VGet ()+assertDone = isEmpty >>= \ b -> unless b (fail emsg) where+    emsg = "VCache: failed to read full input" +{-# INLINE assertDone #-}++vgetWeight :: VGet Int+vgetWeight = VGet $ \ s ->+    let nBytes = vget_limit s `minusPtr` vget_target s in+    let nRefs = L.length (vget_children s) in+    let w = cacheWeight nBytes nRefs in+    w `seq` return (VGetR w s)+{-# INLINE vgetWeight #-}+++-- | Read a reference count for a given address. +readRefctIO :: VSpace -> Address -> IO Int+readRefctIO vc addr = +    alloca $ \ pAddr ->+    withRdOnlyTxn vc $ \ txn -> +    poke pAddr addr >>+    let vAddr = MDB_val { mv_data = castPtr pAddr+                        , mv_size = fromIntegral (sizeOf addr) }+    in+    mdb_get' txn (vcache_db_refcts vc) vAddr >>= \ mbData ->+    maybe (return 0) readRefctBytes mbData++
+ hsrc_lib/Database/VCache/Refct.hs view
@@ -0,0 +1,45 @@++-- Code for reading and writing reference counts.+module Database.VCache.Refct +    ( readRefctBytes+    , toRefctBytes+    , writeRefctBytes+    , Refct+    ) where++import Control.Exception+import Data.Word+import Data.Bits+import Foreign.Ptr+import Foreign.Storable+import Database.LMDB.Raw++type Refct = Int++-- simple variable-width encoding for reference counts. Encoding is +-- base256 then adding 1. (The lead byte should always be non-zero,+-- but that isn't checked here.)+readRefctBytes :: MDB_val -> IO Refct+readRefctBytes v = rd (mv_data v) (mv_size v) 0 where+    rd _ 0 rc = return (rc + 1)+    rd p n rc = do+        w8 <- peek p+        let rc' = (rc `shiftL` 8) .|. (fromIntegral w8) +        rd (p `plusPtr` 1) (n - 1) rc'++-- compute list of bytes for a big-endian encoding. +-- Reference count must be positive!+toRefctBytes :: Refct -> [Word8]+toRefctBytes = rcb [] . subtract 1 . assertPositive where+    rcb l 0 = l+    rcb l rc = rcb (fromIntegral rc : l) (rc `shiftR` 8)+    assertPositive n = assert (n > 0) n++-- write a reference variable-width count into an MDB_val.+--+-- Note: given buffer should be large enough for any reference count. +writeRefctBytes :: Ptr Word8 -> Refct -> IO MDB_val+writeRefctBytes p0 = wrcb p0 0 . toRefctBytes where+    wrcb _ n [] = return $! MDB_val { mv_data = p0, mv_size = n }+    wrcb p n (b:bs) = do { poke p b ; wrcb (p `plusPtr` 1) (n + 1) bs }+
+ hsrc_lib/Database/VCache/Stats.hs view
@@ -0,0 +1,93 @@++module Database.VCache.Stats+    ( VCacheStats(..)+    , vcacheStats+    ) where++import Data.IORef+import Control.Concurrent.MVar+import qualified Data.Map.Strict as Map++import Database.LMDB.Raw+import Database.VCache.Types+++-- | Miscellaneous statistics for a VCache instance. These are not+-- necessarily consistent, current, or useful. But they can say a+-- a bit about the liveliness and health of a VCache system.+data VCacheStats = VCacheStats+        { vcstat_file_size      :: {-# UNPACK #-} !Int  -- ^ estimated database file size (in bytes)+        , vcstat_vref_count     :: {-# UNPACK #-} !Int  -- ^ number of immutable values in the database+        , vcstat_pvar_count     :: {-# UNPACK #-} !Int  -- ^ number of mutable PVars in the database+        , vcstat_root_count     :: {-# UNPACK #-} !Int  -- ^ number of named roots (a subset of PVars)+        , vcstat_mem_vrefs      :: {-# UNPACK #-} !Int  -- ^ number of VRefs in Haskell process memory (some may share address)+        , vcstat_mem_pvars      :: {-# UNPACK #-} !Int  -- ^ number of PVars in Haskell process memory+        , vcstat_mem_addrs      :: {-# UNPACK #-} !Int  -- ^ number of addresses held by Haskell process memory+        , vcstat_eph_count      :: {-# UNPACK #-} !Int  -- ^ number of addresses with zero references+        , vcstat_alloc_pos      :: {-# UNPACK #-} !Address -- ^ address to next be used by allocator+        , vcstat_alloc_count    :: {-# UNPACK #-} !Int  -- ^ number of allocations by this process +        , vcstat_cache_count    :: {-# UNPACK #-} !Int  -- ^ number of VRefs with cached values+        , vcstat_cache_limit    :: {-# UNPACK #-} !Int  -- ^ target cache size +        , vcstat_cache_size     :: {-# UNPACK #-} !Int  -- ^ estimated cache size in bytes+        , vcstat_gc_count       :: {-# UNPACK #-} !Int  -- ^ number of addresses GC'd by this process+        , vcstat_write_pvars    :: {-# UNPACK #-} !Int  -- ^ number of PVar updates to disk (after batching)+        , vcstat_write_sync     :: {-# UNPACK #-} !Int  -- ^ number of sync requests (~ durable transactions)+        , vcstat_write_frames   :: {-# UNPACK #-} !Int  -- ^ number of LMDB-layer transactions by this process+        } deriving (Show, Ord, Eq)++-- | Compute some miscellaneous statistics for a VCache instance at+-- runtime. These aren't really useful for anything, except to gain+-- some confidence about activity or comprehension of performance. +vcacheStats :: VSpace -> IO VCacheStats+vcacheStats vc = withRdOnlyTxn vc $ \ txnStat -> do+    let db = vcache_db_env vc+    envInfo <- mdb_env_info db+    envStat <- mdb_env_stat db+    dbMemStat <- mdb_stat' txnStat (vcache_db_memory vc)+    rootStat <- mdb_stat' txnStat (vcache_db_vroots vc)+    hashStat <- mdb_stat' txnStat (vcache_db_caddrs vc)+    ephStat <- mdb_stat' txnStat (vcache_db_refct0 vc)+    memory <- readMVar (vcache_memory vc)+    gcCount <- readIORef (vcache_gc_count vc)+    wct <- readIORef (vcache_ct_writes vc)+    cLimit <- readIORef (vcache_climit vc)+    cSizeEst <- readIORef (vcache_csize vc)+    +    let fileSize = (1 + (fromIntegral $ me_last_pgno envInfo)) +                 * (fromIntegral $ ms_psize envStat)+    let vrefCount = (fromIntegral $ ms_entries hashStat) +    let pvarCount = (fromIntegral $ ms_entries dbMemStat) - vrefCount+    let ephCount = (fromIntegral $ ms_entries ephStat)+    let rootCount = (fromIntegral $ ms_entries rootStat)+    let cvrefsCount = Map.foldl' (\ a b -> a + Map.size b) 0 (mem_cvrefs memory)+    let evrefsCount = Map.foldl' (\ a b -> a + Map.size b) 0 (mem_evrefs memory)+    let cacheSizeBytes = ceiling $ fromIntegral (Map.size (mem_cvrefs memory))+                                 * csze_addr_size cSizeEst+    let memVRefsCount = cvrefsCount + evrefsCount+    let memPVarsCount = Map.size (mem_pvars memory)+    let memAddrsCount = Map.size (mem_pvars memory) +                      + Map.size (mem_cvrefs memory) +                      + Map.size (mem_evrefs memory)+    let allocPos = alloc_new_addr (mem_alloc memory)+    let allocDiff = allocPos - vcache_alloc_init vc+    let allocCount = fromIntegral $ allocDiff `div` 2 +    return $ VCacheStats+        { vcstat_file_size = fileSize+        , vcstat_vref_count = vrefCount+        , vcstat_pvar_count = pvarCount+        , vcstat_root_count = rootCount+        , vcstat_mem_vrefs = memVRefsCount+        , vcstat_mem_pvars = memPVarsCount+        , vcstat_mem_addrs = memAddrsCount+        , vcstat_eph_count = ephCount+        , vcstat_alloc_pos = allocPos+        , vcstat_alloc_count = allocCount+        , vcstat_cache_count = cvrefsCount+        , vcstat_cache_limit = cLimit+        , vcstat_cache_size = cacheSizeBytes+        , vcstat_write_sync = wct_sync wct+        , vcstat_write_pvars = wct_pvars wct+        , vcstat_write_frames = wct_frames wct+        , vcstat_gc_count = gcCount+        }+
+ hsrc_lib/Database/VCache/Sync.hs view
@@ -0,0 +1,19 @@+++module Database.VCache.Sync+    ( vcacheSync+    ) where++import Database.VCache.Types+import Database.VCache.VTx++-- | If you use a lot of non-durable transactions, you may wish to+-- ensure they are synchronized to disk at various times. vcacheSync+-- will simply wait for all transactions committed up to this point.+-- This is equivalent to running a durable, read-only transaction.+--+-- It is recommended you perform a vcacheSync as part of graceful+-- shutdown of any application that uses VCache.+--+vcacheSync :: VSpace -> IO ()+vcacheSync vc = runVTx vc markDurable
+ hsrc_lib/Database/VCache/Types.hs view
@@ -0,0 +1,663 @@+{-# LANGUAGE DeriveDataTypeable, ExistentialQuantification, GeneralizedNewtypeDeriving #-}+{-# LANGUAGE BangPatterns #-}++-- Internal file. Lots of types. Lots of coupling.+module Database.VCache.Types+    ( Address, isVRefAddr, isPVarAddr+    , VRef(..), Cache(..), CacheMode(..)+    , VREph(..), VREphMap, addVREph, takeVREph+    , PVar(..), RDV(..)+    , PVEph(..), PVEphMap, addPVEph+    , VCache(..), VSpace(..)+    , VPut(..), VPutS(..), VPutR(..), PutChild(..), putChildAddr+    , VGet(..), VGetS(..), VGetR(..)+    , VCacheable(..)+    , Allocator(..), AllocFrame(..), Allocation(..)+    , GC(..), GCFrame(..)+    , Memory(..)+    , VTx(..), VTxState(..), TxW(..), VTxBatch(..)+    , Writes(..), WriteLog, WriteCt(..)+    , CacheSizeEst(..)++    -- misc. utilities+    , allocFrameSearch+    , recentGC++    , withRdOnlyTxn+    , withByteStringVal++    , getVTxSpace, markForWrite, liftSTM+    , mkVRefCache, cacheWeight, cacheModeBits, touchCache+    ) where++import Data.Bits+import Data.Word+import Data.Function (on)+import Data.Typeable+import Data.IORef+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.ByteString (ByteString)+import qualified Data.ByteString.Internal as BSI+import Control.Monad+import Control.Monad.Trans.Class (lift)+import Control.Monad.STM (STM)+import Control.Applicative+import Control.Concurrent.MVar+import Control.Concurrent.STM.TVar+import Control.Monad.Trans.State.Strict+import Control.Exception (bracket)+import System.Mem.Weak (Weak)+import System.FileLock (FileLock)+import Database.LMDB.Raw+import Foreign.Ptr+import Foreign.ForeignPtr (withForeignPtr)++import Database.VCache.RWLock++-- | An address in the VCache address space+type Address = Word64 -- with '0' as special case++-- | VRefs and PVars are divided among odds and evens.+isVRefAddr, isPVarAddr :: Address -> Bool+isVRefAddr addr = (0 == (1 .&. addr))+isPVarAddr = not . isVRefAddr+{-# INLINE isVRefAddr #-}+{-# INLINE isPVarAddr #-}++-- | A VRef is an opaque reference to an immutable value backed by a+-- file, specifically via LMDB. The primary motivation for VRefs is+-- to support memory-cached values, i.e. very large data structures+-- that should not be stored in all at once in RAM.+--+-- The API involving VRefs is conceptually pure.+--+-- > vref  :: (VCacheable a) => VSpace -> a -> VRef a+-- > deref :: VRef a -> a+--+-- Under the hood, each VRef has a 64-bit address and a local cache.+-- When dereferenced, the cache is checked or the value is read from+-- the database then cached. Variants of vref and deref control cache+-- behavior.+-- +-- VCacheable values may themselves contain VRefs and PVars, storing+-- just the address. Very large structured data is readily modeled+-- by using VRefs to load just the pieces you need. However, there is+-- one major constraint:+--+-- VRefs may only represent acyclic structures. +--+-- If developers want cyclic structure, they need a PVar in the chain.+-- Alternatively, cycles may be modeled indirectly using explicit IDs.+-- +-- Besides memory caching, VRefs also utilize structure sharing: all+-- VRefs sharing the same serialized representation will share the +-- same address. Structure sharing enables VRefs to be compared for+-- equality without violating conceptual purity. It also simplifies+-- reasoning about idempotence, storage costs, memoization, etc..+--+data VRef a = VRef +    { vref_addr   :: {-# UNPACK #-} !Address            -- ^ address within the cache+    , vref_cache  :: {-# UNPACK #-} !(IORef (Cache a))  -- ^ cached value & weak refs +    , vref_space  :: !VSpace                            -- ^ virtual address space for VRef+    , vref_type   :: !TypeRep                           -- ^ type of value held by VRef+    , vref_parse  :: !(VGet a)                          -- ^ parser for this VRef+    } deriving (Typeable)+instance Eq (VRef a) where (==) = (==) `on` vref_cache+instance Show (VRef a) where +    showsPrec _ v = showString "VRef#" . shows (vref_addr v)+                  . showString "::" . shows (vref_type v)++-- For every VRef we have in memory, we need an ephemeron in a table.+-- This ephemeron table supports structure sharing, caching, and GC.+-- I model this ephemeron by use of `mkWeakMVar`.+data VREph = forall a . VREph +    { vreph_addr  :: {-# UNPACK #-} !Address+    , vreph_type  :: !TypeRep+    , vreph_cache :: {-# UNPACK #-} !(Weak (IORef (Cache a)))+    } +type VREphMap = Map Address (Map TypeRep VREph)+    -- Address is at the top layer of the map mostly to simplify GC.+++addVREph :: VREph -> VREphMap -> VREphMap+addVREph e = Map.alter (Just . maybe i0 ins) (vreph_addr e) where+    ty = vreph_type e+    i0 = Map.singleton ty e+    ins = Map.insert ty e+{-# INLINABLE addVREph #-}+                    +takeVREph :: Address -> TypeRep -> VREphMap -> Maybe (VREph, VREphMap)+takeVREph !addr !ty !em = +    case Map.lookup addr em of+         Nothing -> Nothing+         Just tym -> case Map.lookup ty tym of+            Nothing -> Nothing+            Just e -> +                let em' = if (1 == Map.size tym)+                        then Map.delete addr em +                        else Map.insert addr (Map.delete ty tym) em+                in+                Just (e, em') +{-# INLINABLE takeVREph #-}+++-- TODO: I may need a way to "reserve" VRef addresses for destruction, +-- i.e. such that I can guard against ++-- Every VRef contains its own cache. Thus, there is no extra lookup+-- overhead to test the cache. The cache includes an integer as a+-- bitfield to describe mode.+data Cache a +        = NotCached +        | Cached a {-# UNPACK #-} !Word16++--+-- cache bitfield for mode:+--   bit 0..4: heuristic weight, log scale+--     weight = bytes + 80 * (deps + 1)+--     log scale: 2^(N+6), max N=31+--   bits 5..6: cache mode 0..3+--   bit 7: toggle; set 1 by manager, 0 by derefc+--+-- The weight is used for estimates of cache size, and at the moment+-- cache mode is the primary factor in deciding survival of an object+-- after the cache manager touches it. Higher bits might be used to +-- estimate use and extend survival, but for now don't do anything.+--+++-- | Cache modes are used when deciding, heuristically, whether to+-- clear a value from cache. These modes don't have precise meaning,+-- but there is a general intention: higher numbered modes indicate+-- that VCache should hold onto a value for longer or with greater+-- priority. In the current implementation, CacheMode is used as a+-- pool of 'hitpoints' in a gaming metaphor: if an entry would be+-- removed, but its mode is greater than zero, the mode is reduced+-- instead.+--+-- The default for vref and deref is CacheMode1. Use of vrefc or +-- derefc may specify other modes. Cache mode is monotonic: if+-- the same VRef is deref'd with two different modes, the higher+-- mode will be favored.+--+-- Note: Regardless of mode, a VRef that is fully GC'd from the+-- Haskell layer will ensure any cached content is also GC'd.+-- +data CacheMode+        = CacheMode0+        | CacheMode1+        | CacheMode2+        | CacheMode3+        deriving (Eq, Ord, Show)++cacheModeBits :: CacheMode -> Word16+cacheModeBits CacheMode0 = 0+cacheModeBits CacheMode1 = 1 `shiftL` 5+cacheModeBits CacheMode2 = 2 `shiftL` 5+cacheModeBits CacheMode3 = 3 `shiftL` 5+++-- | clear bit 7; adjust cache mode monotonically.+touchCache :: CacheMode -> Word16 -> Word16+touchCache !cm !w =+    let cb' = (w .&. 0x60) `max` cacheModeBits cm in+    (w .&. 0xff1f) .|. cb'+{-# INLINE touchCache #-}++mkVRefCache :: a -> Int -> CacheMode -> Cache a+mkVRefCache val !w !cm = Cached val cw where+    cw = m .|. cs 0 64+    cs r k = if ((k > w) || (r == 0x1f)) then r else cs (r+1) (k*2)+    m = cacheModeBits cm++cacheWeight :: Int -> Int -> Int+cacheWeight !nBytes !nDeps = nBytes + (80 * nDeps)+{-# INLINE cacheWeight #-}++-- | A PVar is a mutable variable backed by VCache. PVars can be read+-- or updated transactionally (see VTx), and may store by reference+-- as part of domain data (see VCacheable). +--+-- A PVar is not cached. If you want memory cached contents, you'll +-- need a PVar that contains one or more VRefs. However, the first +-- read from a PVar is lazy, so merely referencing a PVar does not +-- require loading its contents into memory.+--+-- Due to how updates are batched, high frequency or bursty updates +-- on a PVar should perform acceptably. Not every intermediate value +-- is written to disk.+--+-- Anonymous PVars will be garbage collected if not in use. Persistence+-- requires ultimately tying contents to named roots (cf. loadRootPVar).+-- Garbage collection is based on reference counting, so developers must+-- be cautious when working with cyclic data, i.e. break cycles before+-- disconnecting them from root.+--+-- Note: PVars must never contain undefined or error values, nor any+-- value that cannot be serialized by a VCacheable instance. +--+data PVar a = PVar+    { pvar_addr  :: {-# UNPACK #-} !Address+    , pvar_data  :: {-# UNPACK #-} !(TVar (RDV a))+    , pvar_space :: !VSpace -- ^ virtual address space for PVar+    , pvar_type  :: !TypeRep+    , pvar_write :: !(a -> VPut ())+    } deriving (Typeable)+instance Eq (PVar a) where (==) = (==) `on` pvar_data+instance Show (PVar a) where +    showsPrec _ pv = showString "PVar#" . shows (pvar_addr pv)+                   . showString "::" . shows (pvar_type pv)++-- ephemeron table for PVars.+data PVEph = forall a . PVEph +    { pveph_addr :: {-# UNPACK #-} !Address+    , pveph_type :: !TypeRep+    , pveph_data :: {-# UNPACK #-} !(Weak (TVar (RDV a)))+    }+type PVEphMap = Map Address PVEph++addPVEph :: PVEph -> PVEphMap -> PVEphMap+addPVEph pve = Map.insert (pveph_addr pve) pve+{-# INLINE addPVEph #-}++-- I need some way to force an evaluation when a PVar is first+-- read, i.e. in order to load the initial value, without forcing+-- on every read. For the moment, I'm using a simple type wrapper.+data RDV a = RDV a++-- | VCache supports a filesystem-backed address space plus a set of+-- persistent, named root variables that can be loaded from one run +-- of the application to another. VCache supports a simple filesystem+-- like model to resist namespace collisions between named roots.+--+-- > openVCache   :: Int -> FilePath -> IO VCache+-- > vcacheSubdir :: ByteString -> VCache -> VCache+-- > loadRootPVar :: (VCacheable a) => VCache -> ByteString -> a -> PVar a+--+-- The normal use of VCache is to open VCache in the main function, +-- use vcacheSubdir for each major framework, plugin, or independent+-- component that might need persistent storage, then load at most a+-- few root PVars per component. Most domain modeling should be at +-- the data layer, i.e. the type held by the PVar.+--+-- See VSpace, VRef, and PVar for more information.+data VCache = VCache+    { vcache_space :: !VSpace -- ^ virtual address space for VCache+    , vcache_path  :: !ByteString+    } deriving (Eq)++-- | VSpace represents the virtual address space used by VCache. Except+-- for loadRootPVar, most operations use VSpace rather than the VCache.+-- VSpace is accessed by vcache_space, vref_space, or pvar_space.+--+-- Addresses from this space are allocated incrementally, odds to PVars+-- and evens to VRefs, independent of object size. The space is elastic:+-- it isn't a problem to change the size of PVars (even drastically) from+-- one update to another.+--+-- In theory, VSpace could run out of 64-bit addresses. In practice, this+-- isn't a real concern - a quarter million years at a sustained million +-- allocations per second. +--+data VSpace = VSpace+    { vcache_lockfile   :: !FileLock -- block concurrent use of VCache file++    -- LMDB contents. +    , vcache_db_env     :: !MDB_env+    , vcache_db_memory  :: {-# UNPACK #-} !MDB_dbi' -- address → value+    , vcache_db_vroots  :: {-# UNPACK #-} !MDB_dbi' -- path → address +    , vcache_db_caddrs  :: {-# UNPACK #-} !MDB_dbi' -- hashval → [address]+    , vcache_db_refcts  :: {-# UNPACK #-} !MDB_dbi' -- address → Word64+    , vcache_db_refct0  :: {-# UNPACK #-} !MDB_dbi' -- address → ()++    , vcache_memory     :: !(MVar Memory) -- Haskell-layer memory management+    , vcache_signal     :: !(MVar ()) -- signal writer that work is available+    , vcache_writes     :: !(TVar Writes) -- STM layer PVar writes+    , vcache_rwlock     :: !RWLock -- replace gap left by MDB_NOLOCK+++    -- Signal writes mostly exists to prevent GC of PVars until after +    -- any updated PVars are durable. I also use it to maintain stats. :)+    , vcache_signal_writes :: !(Writes -> IO ()) -- signal durable writes+    , vcache_ct_writes  :: !(IORef WriteCt) -- (stat) information about writes++    , vcache_alloc_init :: {-# UNPACK #-} !Address -- (for stats) initial allocator on open++    , vcache_gc_start   :: !(IORef (Maybe Address)) -- supports incremental GC+    , vcache_gc_count   :: !(IORef Int) -- (stat) number of addresses GC'd++    , vcache_climit     :: !(IORef Int) -- targeted max cache size in bytes+    , vcache_csize      :: !(IORef CacheSizeEst) -- estimated cache sizes+++    -- share persistent variables for safe STM++    -- Further, I need...+    --   log or queue of 'new' vrefs and pvars, +    --     including those still being written+    --   a thread performing writes and incremental GC+    --   a channel to talk to that thread+    --   queue of MVars waiting on synchronization/flush.++    }++instance Eq VSpace where (==) = (==) `on` vcache_signal++-- needed: a transactional queue of updates to PVars++-- | The Allocator both tracks the 'bump-pointer' address for the+-- next allocation, plus in-memory logs for recent and near future +-- allocations.+--+-- The log has three frames, based on the following observations:+--+-- * frames are rotated when the writer lock is held+-- * when the writer lock is held, readers exist for two prior frames+-- * readers from two frames earlier use log to find allocations from:+--   * the previous write frame+--   * the current write frame+--   * the next write frame (allocated during write)+-- +-- Each write frame includes content for both the primary (db_memory)+-- and secondary (db_caddrs or db_vroots) indices. +--+-- Normal Data.Map is favored because I want the keys in sorted order+-- when writing into the LMDB layer anyway.+--+data Allocator = Allocator+    { alloc_new_addr :: {-# UNPACK #-} !Address -- next address+    , alloc_frm_next :: !AllocFrame -- frame N+1 (next step)+    , alloc_frm_curr :: !AllocFrame -- frame N   (curr step)+    , alloc_frm_prev :: !AllocFrame -- frame N-1 (prev step)+    }++data AllocFrame = AllocFrame +    { alloc_list :: !(Map Address Allocation)       -- allocated addresses+    , alloc_seek :: !(Map ByteString [Allocation])  -- named addresses+    , alloc_init :: {-# UNPACK #-} !Address         -- alloc_new_addr at frame init.+    }++data Allocation = Allocation+    { alloc_name :: {-# UNPACK #-} !ByteString -- VRef hash or PVar path, or empty for anon PVar+    , alloc_data :: {-# UNPACK #-} !ByteString -- initial content+    , alloc_addr :: {-# UNPACK #-} !Address    -- where to save content+    , alloc_deps :: [PutChild]                 -- keepalive for allocation+    }++allocFrameSearch :: (AllocFrame -> Maybe a) -> Allocator -> Maybe a+allocFrameSearch f a = f n <|> f c <|> f p where+    n = alloc_frm_next a+    c = alloc_frm_curr a+    p = alloc_frm_prev a++-- | In addition to recent allocations, we track garbage collection.+-- The goal here is to prevent revival of VRefs after we decide to+-- delete them. So, when we try to allocate a VRef, we'll check to+-- see if it's address has been targeted for deletion.+--+-- To keep this simple, GC is performed by the writer thread. Other+-- threads must worry about reading outdated reference counts. This+-- also means we only need the two frames: a reader of frame N-2  +-- only needs to prevent revival of VRefs GC'd at N-1 or N.+--+data GC = GC +    { gc_frm_curr :: !GCFrame+    , gc_frm_prev :: !GCFrame+    } +data GCFrame = forall a . GCFrame !(Map Address a)+    -- The concrete map type depends on the writer++recentGC :: GC -> Address -> Bool+recentGC gc addr = ff c || ff p where+    ff (GCFrame m) = Map.member addr m+    c = gc_frm_curr gc+    p = gc_frm_prev gc+{-# INLINE recentGC #-}++-- | The Memory datatype tracks allocations, GC, and ephemeron+-- tables for tracking both PVars and VRefs in Haskell memory.+-- These are combined into one type mostly because typical +-- operations on them are atomic... and STM isn't permitted +-- because vref constructors are used with unsafePerformIO.+data Memory = Memory+    { mem_evrefs :: !VREphMap   -- ^ VRefs with empty cache.+    , mem_cvrefs :: !VREphMap   -- ^ VRefs with full cache.+    , mem_pvars  :: !PVEphMap   -- ^ In-memory PVars+    , mem_gc     :: !GC         -- ^ recently GC'd addresses (two frames)+    , mem_alloc  :: !Allocator  -- ^ recent or pending allocations (three frames)+    }++++-- simple read-only operations +--  LMDB transaction is aborted when finished, so cannot open DBIs+withRdOnlyTxn :: VSpace -> (MDB_txn -> IO a) -> IO a+withRdOnlyTxn vc = withLock . bracket newTX endTX where+    withLock = withRdOnlyLock (vcache_rwlock vc)+    newTX = mdb_txn_begin (vcache_db_env vc) Nothing True+    endTX = mdb_txn_abort+{-# INLINE withRdOnlyTxn #-}++withByteStringVal :: ByteString -> (MDB_val -> IO a) -> IO a+withByteStringVal (BSI.PS fp off len) action = withForeignPtr fp $ \ p ->+    action $ MDB_val { mv_size = fromIntegral len, mv_data = p `plusPtr` off }+{-# INLINE withByteStringVal #-}++-- | The VTx transactions allow developers to atomically manipulate+-- PVars and STM resources (TVars, TArrays, etc..). VTx is a thin+-- layer above STM, additionally tracking which PVars are written so+-- it can push the batch to a background writer thread upon commit.+-- +-- VTx supports full ACID semantics (atomic, consistent, isolated,+-- durable), but durability is optional (see markDurable). +-- +newtype VTx a = VTx { _vtx :: StateT VTxState STM a }+    deriving (Monad, Functor, Applicative, Alternative, MonadPlus)++-- | In addition to the STM transaction, I need to track whether+-- the transaction is durable (such that developers may choose +-- based on internal domain-model concerns) and which variables+-- have been read or written. All PVars involved must be part of+-- the same VSpace.+data VTxState = VTxState+    { vtx_space     :: !VSpace+    , vtx_writes    :: !WriteLog+    , vtx_durable   :: !Bool+    }++-- | run an arbitrary STM operation as part of a VTx transaction.+liftSTM :: STM a -> VTx a+liftSTM = VTx . lift+{-# INLINE liftSTM #-}++getVTxSpace :: VTx VSpace+getVTxSpace = VTx (gets vtx_space)+{-# INLINE getVTxSpace #-}++-- | add a PVar write to the VTxState.+-- (Does not modify the underlying TVar.)+markForWrite :: PVar a -> a -> VTx ()+markForWrite pv a = VTx $ modify $ \ vtx ->+    let txw = TxW pv a in+    let addr = pvar_addr pv in+    let writes' = Map.insert addr txw (vtx_writes vtx) in+    vtx { vtx_writes = writes' }+{-# INLINE markForWrite #-}+++-- | Estimate for cache size is based on random samples with an+-- exponential moving average. It isn't very precise, but it is+-- good enough for the purpose of guiding aggressiveness of the+-- exponential decay model.+data CacheSizeEst = CacheSizeEst+    { csze_addr_size  :: {-# UNPACK #-} !Double -- average of sizes+    , csze_addr_sqsz  :: {-# UNPACK #-} !Double -- average of squares+    }++++type WriteLog  = Map Address TxW+data TxW = forall a . TxW !(PVar a) a+    -- Note: I can either record just the PVar, or the PVar and its value.+    -- The latter is favorable because it avoids risk of creating very large+    -- transactions in the writer thread (i.e. to read the updated PVars).++data Writes = Writes +    { write_data :: !WriteLog+    , write_sync :: ![MVar ()]+    }+    -- Design Thoughts: It might be worthwhile to separate the writelog+    -- and synchronization, i.e. to potentially reduce conflicts between+    -- transactions. But I'll leave this to later.++data WriteCt = WriteCt+    { wct_frames :: {-# UNPACK #-} !Int -- how many write frames+    , wct_pvars  :: {-# UNPACK #-} !Int -- how many PVars written+    , wct_sync   :: {-# UNPACK #-} !Int -- how many sync requests+    }++data VTxBatch = VTxBatch SyncOp WriteLog +type SyncOp = IO () -- called after write is synchronized.++type Ptr8 = Ptr Word8+type PtrIni = Ptr8+type PtrEnd = Ptr8+type PtrLoc = Ptr8++-- | VPut is a serialization monad akin to Data.Binary or Data.Cereal.+-- However, VPut is not restricted to pure binaries: developers may+-- include VRefs and PVars in the output.+--+-- Content emitted by VPut will generally be read only by VCache. So+-- it may be worth optimizing some cases, such as lists are written +-- in reverse such that readers won't need to reverse the list.+newtype VPut a = VPut { _vput :: VPutS -> IO (VPutR a) }+data VPutS = VPutS +    { vput_space    :: !VSpace +    , vput_children :: ![PutChild] -- ^ addresses written+    , vput_buffer   :: !(IORef PtrIni) -- ^ current buffer for easy free+    , vput_target   :: {-# UNPACK #-} !PtrLoc -- ^ location within buffer+    , vput_limit    :: {-# UNPACK #-} !PtrEnd -- ^ current limit for input+    }+    -- note: vput_buffer is an IORef mostly to simplify error handling.+    --  On error, we'll need to free the buffer. However, it may be +    --  reallocated many times during serialization of a large value,+    --  so we need easy access to the final value.+data PutChild = forall a . PutChild (Either (PVar a) (VRef a))++putChildAddr :: PutChild -> Address+putChildAddr (PutChild (Left (PVar { pvar_addr = x }))) = x+putChildAddr (PutChild (Right (VRef { vref_addr = x }))) = x+++data VPutR r = VPutR+    { vput_result :: r+    , vput_state  :: !VPutS+    }++instance Functor VPut where +    fmap f m = VPut $ \ s -> +        _vput m s >>= \ (VPutR r s') ->+        return (VPutR (f r) s')+    {-# INLINE fmap #-}+instance Applicative VPut where+    pure = return+    (<*>) = ap+    {-# INLINE pure #-}+    {-# INLINE (<*>) #-}+instance Monad VPut where+    fail msg = VPut (\ _ -> fail ("VCache.VPut.fail " ++ msg))+    return r = VPut (\ s -> return (VPutR r s))+    m >>= k = VPut $ \ s ->+        _vput m s >>= \ (VPutR r s') ->+        _vput (k r) s'+    m >> k = VPut $ \ s ->+        _vput m s >>= \ (VPutR _ s') ->+        _vput k s'+    {-# INLINE return #-}+    {-# INLINE (>>=) #-}+    {-# INLINE (>>) #-}+++-- | VGet is a parser combinator monad for VCache. Unlike pure binary+-- parsers, VGet supports reads from a stack of VRefs and PVars to +-- directly model structured data.+--+newtype VGet a = VGet { _vget :: VGetS -> IO (VGetR a) }+data VGetS = VGetS +    { vget_children :: ![Address]+    , vget_target   :: {-# UNPACK #-} !PtrLoc+    , vget_limit    :: {-# UNPACK #-} !PtrEnd+    , vget_space    :: !VSpace+    }+data VGetR r +    = VGetR r !VGetS+    | VGetE String+++instance Functor VGet where+    fmap f m = VGet $ \ s ->+        _vget m s >>= \ c ->+        return $ case c of+            VGetR r s' -> VGetR (f r) s'+            VGetE msg -> VGetE msg +    {-# INLINE fmap #-}+instance Applicative VGet where+    pure = return+    (<*>) = ap+    {-# INLINE pure #-}+    {-# INLINE (<*>) #-}+instance Monad VGet where+    fail msg = VGet (\ _ -> return (VGetE msg))+    return r = VGet (\ s -> return (VGetR r s))+    m >>= k = VGet $ \ s ->+        _vget m s >>= \ c ->+        case c of+            VGetE msg -> return (VGetE msg)+            VGetR r s' -> _vget (k r) s'+    m >> k = VGet $ \ s ->+        _vget m s >>= \ c ->+        case c of+            VGetE msg -> return (VGetE msg)+            VGetR _ s' -> _vget k s'+    {-# INLINE fail #-}+    {-# INLINE return #-}+    {-# INLINE (>>=) #-}+    {-# INLINE (>>) #-}+instance Alternative VGet where+    empty = mzero+    (<|>) = mplus+    {-# INLINE empty #-}+    {-# INLINE (<|>) #-}+instance MonadPlus VGet where+    mzero = fail "mzero"+    mplus f g = VGet $ \ s ->+        _vget f s >>= \ c ->+        case c of+            VGetE _ -> _vget g s+            r -> return r+    {-# INLINE mzero #-}+    {-# INLINE mplus #-}+++-- | To be utilized with VCache, a value must be serializable as a +-- simple sequence of binary data and child VRefs. Also, to put then+-- get a value must result in equivalent values. Further, values are+-- Typeable to support memory caching of values loaded.+-- +-- Under the hood, structured data is serialized as the pair:+--+--    (ByteString,[Either VRef PVar])+--+-- Developers must ensure that `get` on the serialization from `put` +-- returns the same value. And `get` must be backwards compatible.+-- Developers should consider version wrappers, cf. SafeCopy package.+-- +class (Typeable a) => VCacheable a where +    -- | Serialize a value as a stream of bytes and value references. +    put :: a -> VPut ()++    -- | Parse a value from its serialized representation into memory.+    get :: VGet a+
+ hsrc_lib/Database/VCache/VCacheable.hs view
@@ -0,0 +1,178 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE BangPatterns #-}++module Database.VCache.VCacheable+    ( VCacheable(..)+    , module Database.VCache.VGet+    , module Database.VCache.VPut+    ) where++import Control.Applicative+import Control.Monad++import Data.Word+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS++import Database.VCache.VGet+import Database.VCache.VPut+import Database.VCache.Types++-- VCacheable defined in Database.VCache.Types++instance VCacheable Int where+    get = fromIntegral <$> getVarInt+    put = putVarInt . fromIntegral+    {-# INLINE get #-}+    {-# INLINE put #-}++instance VCacheable Integer where+    get = getVarInt+    put = putVarInt+    {-# INLINE get #-}+    {-# INLINE put #-}++instance VCacheable Bool where+    get = getWord8 >>= \ n -> case n of+        0 -> return False+        1 -> return True+        _ -> fail "Boolean expects a 0 or 1 byte"+    put False = putWord8 0+    put True  = putWord8 1++instance VCacheable Char where +    get = getc+    put = putc+    {-# INLINE get #-}+    {-# INLINE put #-}++instance VCacheable Word8 where+    get = getWord8+    put = putWord8+    {-# INLINE get #-}+    {-# INLINE put #-}++instance VCacheable BS.ByteString where+    get = getVarNat >>= getByteString . fromIntegral+    put s = putVarNat (fromIntegral $ BS.length s) >> putByteString s +    {-# INLINE get #-}+    {-# INLINE put #-}++instance VCacheable LBS.ByteString where+    get = getVarNat >>= getByteStringLazy . fromIntegral+    put s = putVarNat (fromIntegral $ LBS.length s) >> putByteStringLazy s+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a) => VCacheable (VRef a) where+    get = getVRef+    put = putVRef+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a) => VCacheable (PVar a) where+    get = getPVar+    put = putPVar+    {-# INLINE get #-}+    {-# INLINE put #-}++-- unit is not actually serialized.+instance VCacheable () where+    get = return ()+    put () = return ()+    {-# INLINE get #-}+    {-# INLINE put #-}++-- `Maybe a` may be upgraded transparently to [a], and may share+-- structure with single element lists.+instance (VCacheable a) => VCacheable (Maybe a) where+    get = getWord8 >>= \ n -> case n of +        0 -> return Nothing+        1 -> Just <$> get+        _ -> fail "Type `Maybe a` expects prefix byte 0 or 1"+    put Nothing  = putWord8 0+    put (Just a) = putWord8 1 >> put a++instance (VCacheable a, VCacheable b) => VCacheable (Either a b) where+    get = getWord8 >>= \ lr -> case lr of+        0 -> Left <$> get+        1 -> Right <$> get+        _ -> fail "Type `Either a b` expects prefix byte 0 or 1"+    put (Left a) = putWord8 0 >> put a+    put (Right b) = putWord8 1 >> put b++-- NOTE: lists are stored in *reverse* order, such that when read+-- the nodes can be directly constructed into normal order without+-- reversing the list, i.e. thus optimizing for read.+instance (VCacheable a) => VCacheable [a] where+    get = do+        nCount <- liftM fromIntegral getVarNat+        replicateReversed [] nCount get+    put ls = do+        let (nCount, lsr) = countAndReverse ls+        putVarNat (fromIntegral nCount)+        mapM_ put lsr+    {-# INLINE get #-}+    {-# INLINE put #-}++-- replicate an operation and build a reversed list of results.+replicateReversed :: (Monad m) => [a] -> Int -> m a -> m [a]+replicateReversed xs 0 _ = return xs+replicateReversed xs n op = op >>= \ x -> replicateReversed (x:xs) (n-1) op++-- single pass to count and reverse list+countAndReverse :: [a] -> (Int, [a])+countAndReverse = cr [] 0 where+    cr l !n (x:xs) = cr (x:l) (n+1) xs+    cr l !n [] = (n, l)+++-- note that ((a,b),c) and (a,(b,c)) share serialized structure.+-- So does ((a,b),(c,d)) and (a,(b,c),d), etc.+instance (VCacheable a, VCacheable b) => VCacheable (a,b) where+    get = liftM2 (,) get get+    put (a,b) = do { put a; put b }+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a, VCacheable b, VCacheable c) => VCacheable (a,b,c) where+    get = liftM3 (,,) get get get+    put (a,b,c) = do { put a; put b; put c }+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a, VCacheable b, VCacheable c, VCacheable d) +    => VCacheable (a,b,c,d) where+    get = liftM4 (,,,) get get get get+    put (a,b,c,d) = do { put a; put b; put c; put d }+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a, VCacheable b, VCacheable c, VCacheable d, VCacheable e) +    => VCacheable (a,b,c,d,e) where+    get = liftM5 (,,,,) get get get get get+    put (a,b,c,d,e) = do { put a; put b; put c; put d; put e }+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a, VCacheable b, VCacheable c, VCacheable d, VCacheable e+         , VCacheable f) => VCacheable (a,b,c,d,e,f) where+    get = +        do a <- get; b <- get; c <- get+           d <- get; e <- get; f <- get+           return (a,b,c,d,e,f)+    put (a,b,c,d,e,f) = do { put a; put b; put c; put d; put e; put f }+    {-# INLINE get #-}+    {-# INLINE put #-}++instance (VCacheable a, VCacheable b, VCacheable c, VCacheable d, VCacheable e+         , VCacheable f, VCacheable g) => VCacheable (a,b,c,d,e,f,g) where+    get = +        do a <- get; b <- get; c <- get+           d <- get; e <- get; f <- get; g <- get+           return (a,b,c,d,e,f,g)+    put (a,b,c,d,e,f,g) = do { put a; put b; put c; put d; put e; put f; put g }+    {-# INLINE get #-}+    {-# INLINE put #-}++
+ hsrc_lib/Database/VCache/VGet.hs view
@@ -0,0 +1,329 @@+{-# LANGUAGE BangPatterns #-}++module Database.VCache.VGet +    ( VGet++    -- * Prim Readers+    , getVRef, getPVar+    , getWord8+    , getWord16le, getWord16be+    , getWord32le, getWord32be+    , getWord64le, getWord64be+    , getStorable+    , getVarNat, getVarInt+    , getByteString, getByteStringLazy+    , getc++    -- * Parser Combinators+    , isolate+    , label+    , lookAhead, lookAheadM, lookAheadE+    , isEmpty++    ) where++import Control.Applicative++import Data.Bits+import Data.Char+import Data.Word+import Foreign.Ptr+import Foreign.Storable (Storable(..))+import Foreign.Marshal.Alloc (mallocBytes,finalizerFree)+import Foreign.Marshal.Utils (copyBytes)+import Foreign.ForeignPtr (newForeignPtr)++import qualified Data.List as L+import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as BSI+import qualified Data.ByteString.Lazy as LBS++import Database.VCache.Types+import Database.VCache.Aligned+import Database.VCache.Alloc+import Database.VCache.VGetAux ++-- | isolate a parser to a subset of bytes and value references. The+-- child parser must process its entire input (all bytes and values) +-- or will fail. If there is not enough available input to isolate, +-- this operation will fail. +--+--      isolate nBytes nVRefs operation+--+isolate :: Int -> Int -> VGet a -> VGet a+isolate nBytes nRefs op = VGet $ \ s ->+    let pF = vget_target s `plusPtr` nBytes in+    if (pF > vget_limit s) then return (VGetE "isolate: not enough data") else+    case takeExact nRefs (vget_children s) of+        Nothing -> return (VGetE "isolate: not enough children")+        Just (cs,cs') -> +            let s_isolated = s { vget_children = cs+                               , vget_limit  = pF+                               }+            in+            let s_postIsolate = s { vget_children = cs'+                                  , vget_target = pF+                                  }+            in+            _vget op s_isolated >>= \ r_isolated -> case r_isolated of+                VGetE emsg -> return (VGetE emsg)+                VGetR r s' ->+                    let bDone = vgetStateEmpty s' in+                    if bDone then return (VGetR r s_postIsolate) else+                    return (VGetE "isolate: did not parse all input")++-- take exactly the requested amount from a list, or return Nothing.+takeExact :: Int -> [a] -> Maybe ([a],[a])+takeExact = takeExact' [] +{-# INLINE takeExact #-}++takeExact' :: [a] -> Int -> [a] -> Maybe ([a],[a])+takeExact' l 0 r = Just (L.reverse l, r)+takeExact' l n (r:rs) = takeExact' (r:l) (n-1) rs+takeExact' _ _ _ = Nothing++-- | Load a VRef, just the reference rather than the content. User must+-- know the type of the value, since getVRef is essentially a typecast.+-- VRef content is not read until deref. +--+-- All instances of a VRef with the same type and address will share the+-- same cache.+getVRef :: (VCacheable a) => VGet (VRef a)+getVRef = VGet $ \ s -> +    case (vget_children s) of+        (c:cs) | isVRefAddr c -> do+            let s' = s { vget_children = cs }+            vref <- addr2vref (vget_space s) c+            return (VGetR vref s')+        _ -> return (VGetE "getVRef")+{-# INLINABLE getVRef #-}++-- | Load a PVar, just the variable. Content is loaded lazily on first+-- read, then kept in memory until the PVar is GC'd. Unlike other Haskell+-- variables, PVars can be serialized to the VCache address space. All +-- PVars for a specific address are collapsed, using the same TVar.+--+-- Developers must know the type of the PVar, since getPVar will cast to+-- any cacheable type. A runtime error is raised only if you attempt to+-- load the same PVar address with two different types.+--+getPVar :: (VCacheable a) => VGet (PVar a) +getPVar = VGet $ \ s ->+    case (vget_children s) of+        (c:cs) | isPVarAddr c -> do+            let s' = s { vget_children = cs }+            pvar <- addr2pvar (vget_space s) c+            return (VGetR pvar s')+        _ -> return (VGetE "getPVar")+{-# INLINABLE getPVar #-}++-- | Read words of size 16, 32, or 64 in little-endian or big-endian.+getWord16le, getWord16be :: VGet Word16+getWord32le, getWord32be :: VGet Word32+getWord64le, getWord64be :: VGet Word64++getWord16le = consuming 2 $ VGet $ \ s -> do+    let p = vget_target s+    b0 <- peekByte p+    b1 <- peekByte (p `plusPtr` 1)+    let r = (fromIntegral b1 `shiftL`  8) .|.+            (fromIntegral b0            )+    let s' = s { vget_target = p `plusPtr` 2 }+    return (VGetR r s')+{-# INLINE getWord16le #-}++getWord32le = consuming 4 $ VGet $ \ s -> do+    let p = vget_target s+    b0 <- peekByte p+    b1 <- peekByte (p `plusPtr` 1)+    b2 <- peekByte (p `plusPtr` 2)+    b3 <- peekByte (p `plusPtr` 3)+    let r = (fromIntegral b3 `shiftL` 24) .|.+            (fromIntegral b2 `shiftL` 16) .|.+            (fromIntegral b1 `shiftL`  8) .|.+            (fromIntegral b0            )+    let s' = s { vget_target = p `plusPtr` 4 }+    return (VGetR r s')+{-# INLINE getWord32le #-}++getWord64le = consuming 8 $ VGet $ \ s -> do+    let p = vget_target s+    b0 <- peekByte p+    b1 <- peekByte (p `plusPtr` 1)+    b2 <- peekByte (p `plusPtr` 2)+    b3 <- peekByte (p `plusPtr` 3)+    b4 <- peekByte (p `plusPtr` 4)+    b5 <- peekByte (p `plusPtr` 5)+    b6 <- peekByte (p `plusPtr` 6)+    b7 <- peekByte (p `plusPtr` 7)+    let r = (fromIntegral b7 `shiftL` 56) .|.+            (fromIntegral b6 `shiftL` 48) .|.+            (fromIntegral b5 `shiftL` 40) .|.+            (fromIntegral b4 `shiftL` 32) .|.+            (fromIntegral b3 `shiftL` 24) .|.+            (fromIntegral b2 `shiftL` 16) .|.+            (fromIntegral b1 `shiftL`  8) .|.+            (fromIntegral b0            )    +    let s' = s { vget_target = p `plusPtr` 8 }+    return (VGetR r s')+{-# INLINE getWord64le #-}++getWord16be = consuming 2 $ VGet $ \ s -> do+    let p = vget_target s+    b0 <- peekByte p+    b1 <- peekByte (p `plusPtr` 1)+    let r = (fromIntegral b0 `shiftL`  8) .|.+            (fromIntegral b1            )+    let s' = s { vget_target = p `plusPtr` 2 }+    return (VGetR r s')+{-# INLINE getWord16be #-}++getWord32be = consuming 4 $ VGet $ \ s -> do+    let p = vget_target s+    b0 <- peekByte p+    b1 <- peekByte (p `plusPtr` 1)+    b2 <- peekByte (p `plusPtr` 2)+    b3 <- peekByte (p `plusPtr` 3)+    let r = (fromIntegral b0 `shiftL` 24) .|.+            (fromIntegral b1 `shiftL` 16) .|.+            (fromIntegral b2 `shiftL`  8) .|.+            (fromIntegral b3            )+    let s' = s { vget_target = p `plusPtr` 4 }+    return (VGetR r s')+{-# INLINE getWord32be #-}++getWord64be = consuming 8 $ VGet $ \ s -> do+    let p = vget_target s+    b0 <- peekByte p+    b1 <- peekByte (p `plusPtr` 1)+    b2 <- peekByte (p `plusPtr` 2)+    b3 <- peekByte (p `plusPtr` 3)+    b4 <- peekByte (p `plusPtr` 4)+    b5 <- peekByte (p `plusPtr` 5)+    b6 <- peekByte (p `plusPtr` 6)+    b7 <- peekByte (p `plusPtr` 7)+    let r = (fromIntegral b0 `shiftL` 56) .|.+            (fromIntegral b1 `shiftL` 48) .|.+            (fromIntegral b2 `shiftL` 40) .|.+            (fromIntegral b3 `shiftL` 32) .|.+            (fromIntegral b4 `shiftL` 24) .|.+            (fromIntegral b5 `shiftL` 16) .|.+            (fromIntegral b6 `shiftL`  8) .|.+            (fromIntegral b7            )    +    let s' = s { vget_target = p `plusPtr` 8 }+    return (VGetR r s')+{-# INLINE getWord64be #-}++-- | Read a Storable value. In this case, the content should be+-- bytes only, since pointers aren't really meaningful when persisted.+-- Data is copied to an intermediate structure via alloca to avoid+-- alignment issues.+getStorable :: (Storable a) => VGet a+getStorable = _getStorable undefined+{-# INLINE getStorable #-}++_getStorable :: (Storable a) => a -> VGet a+_getStorable _dummy = +    let n = sizeOf _dummy in+    consuming n $ VGet $ \ s -> do+        let pTgt = vget_target s +        let s' = s { vget_target = pTgt `plusPtr` n }+        a <- peekAligned (castPtr pTgt)+        return (VGetR a s')+{-# INLINE _getStorable #-}++-- | Load a number of bytes from the underlying object. A copy is+-- performed in this case (typically no copy is performed by VGet,+-- but the underlying pointer is ephemeral, becoming invalid after+-- the current read transaction). Fails if not enough data. O(N)+getByteString :: Int -> VGet BS.ByteString+getByteString n | (n > 0)   = _getByteString n+                | otherwise = return (BS.empty)+{-# INLINE getByteString #-}++_getByteString :: Int -> VGet BS.ByteString+_getByteString n = consuming n $ VGet $ \ s -> do+    let pSrc = vget_target s+    pDst <- mallocBytes n+    copyBytes pDst pSrc n+    fp <- newForeignPtr finalizerFree pDst+    let r = BSI.fromForeignPtr fp 0 n+    let s' = s { vget_target = (pSrc `plusPtr` n) }+    return (VGetR r s')++-- | Get a lazy bytestring. (Simple wrapper on strict bytestring.)+getByteStringLazy :: Int -> VGet LBS.ByteString+getByteStringLazy n = LBS.fromStrict <$> getByteString n+{-# INLINE getByteStringLazy #-}+++-- | Get a character from UTF-8 format. Assumes a valid encoding.+-- (In case of invalid encoding, arbitrary characters may be returned.)+getc :: VGet Char+getc = +    _c0 >>= \ b0 ->+    if (b0 < 0x80) then return $! chr b0 else+    if (b0 < 0xe0) then _getc2 (b0 `xor` 0xc0) else+    if (b0 < 0xf0) then _getc3 (b0 `xor` 0xe0) else+    _getc4 (b0 `xor` 0xf0)++-- get UTF-8 of size 2,3,4 bytes+_getc2, _getc3, _getc4 :: Int -> VGet Char+_getc2 b0 =+    _cc >>= \ b1 ->+    return $! chr ((b0 `shiftL` 6) .|. b1)+_getc3 b0 =+    _cc >>= \ b1 ->+    _cc >>= \ b2 ->+    return $! chr ((b0 `shiftL` 12) .|. (b1 `shiftL` 6) .|. b2)+_getc4 b0 =+    _cc >>= \ b1 ->+    _cc >>= \ b2 ->+    _cc >>= \ b3 ->+    return $! chr ((b0 `shiftL` 18) .|. (b1 `shiftL` 12) .|. (b2 `shiftL` 6) .|. b3)++_c0,_cc :: VGet Int+_c0 = fromIntegral <$> getWord8+_cc = (fromIntegral . xor 0x80) <$> getWord8+{-# INLINE _c0 #-}+{-# INLINE _cc #-}++++-- | label will modify the error message returned from the+-- argument operation; it can help contextualize parse errors.+label :: ShowS -> VGet a -> VGet a+label sf op = VGet $ \ s ->+    _vget op s >>= \ r ->+    return $+    case r of+        VGetE emsg -> VGetE (sf emsg)+        ok@(VGetR _ _) -> ok++-- | lookAhead will parse a value, but not consume any input.+lookAhead :: VGet a -> VGet a+lookAhead op = VGet $ \ s ->+    _vget op s >>= \ result -> +    return $+    case result of+        VGetR r _ -> VGetR r s+        other -> other++-- | lookAheadM will consume input only if it returns `Just a`.+lookAheadM :: VGet (Maybe a) -> VGet (Maybe a)+lookAheadM op = VGet $ \ s ->+    _vget op s >>= \ result -> +    return $+    case result of+        VGetR Nothing _ -> VGetR Nothing s+        other -> other++-- | lookAheadE will consume input only if it returns `Right b`.+lookAheadE :: VGet (Either a b) -> VGet (Either a b)+lookAheadE op = VGet $ \ s ->+    _vget op s >>= \ result ->+    return $+    case result of+        VGetR l@(Left _) _ -> VGetR l s+        other -> other+
+ hsrc_lib/Database/VCache/VGetAux.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE BangPatterns #-}+-- This module mostly exists to avoid cyclic dependencies+module Database.VCache.VGetAux +    ( getWord8FromEnd+    , getWord8+    , isEmpty, vgetStateEmpty+    , getVarNat+    , getVarInt+    , consuming+    , peekByte+    ) where++import Control.Applicative+import Data.Word+import Data.Bits+import qualified Data.List as L+import Foreign.Ptr+import Foreign.Storable+import Database.VCache.Types++-- | Read one byte of data, or fail if not enough data.+getWord8 :: VGet Word8 +getWord8 = consuming 1 $ VGet $ \ s -> do+    let p = vget_target s+    r <- peekByte p+    let s' = s { vget_target = p `plusPtr` 1 }+    return (VGetR r s')+{-# INLINE getWord8 #-}++getWord8FromEnd :: VGet Word8+getWord8FromEnd = consuming 1 $ VGet $ \ s -> do+    let p = vget_limit s `plusPtr` (-1)+    r <- peekByte p+    let s' = s { vget_limit = p }+    return (VGetR r s')+{-# INLINE getWord8FromEnd #-}++-- to simplify type inference+peekByte :: Ptr Word8 -> IO Word8+peekByte = peek+{-# INLINE peekByte #-}++-- | isEmpty will return True iff there is no available input (neither+-- references nor values).+isEmpty :: VGet Bool+isEmpty = VGet $ \ s ->+    let bEOF = vgetStateEmpty s in+    bEOF `seq` return (VGetR bEOF s)+{-# INLINE isEmpty #-}++vgetStateEmpty :: VGetS -> Bool+vgetStateEmpty s = (vget_target s == vget_limit s)+                && (L.null (vget_children s))+{-# INLINE vgetStateEmpty #-}++-- | Get an integer represented in the Google protocol buffers zigzag+-- 'varint' encoding, e.g. as produced by 'putVarInt'. +getVarInt :: VGet Integer+getVarInt = unZigZag <$> getVarNat+{-# INLINE getVarInt #-}++-- undo protocol buffers zigzag encoding+unZigZag :: Integer -> Integer+unZigZag !n =+    let (q,r) = n `divMod` 2 in+    if (1 == r) then negate q - 1+                else q+{-# INLINE unZigZag #-}++-- | Get a non-negative number represented in the Google protocol+-- buffers 'varint' encoding, e.g. as produced by 'putVarNat'.+getVarNat :: VGet Integer+getVarNat = getVarNat' 0+{-# INLINE getVarNat #-}++-- getVarNat' uses accumulator+getVarNat' :: Integer -> VGet Integer+getVarNat' !n =+    getWord8 >>= \ w ->+    let n' = (128 * n) + fromIntegral (w .&. 0x7f) in+    if (w < 128) then return $! n'+                 else getVarNat' n'+++-- consuming a number of bytes (for unsafe VGet operations)+--  does not perform a full isolation+consuming :: Int -> VGet a -> VGet a+consuming n op = VGet $ \ s ->+    let pConsuming = vget_target s `plusPtr` n in+    if (pConsuming > vget_limit s) then return (VGetE "not enough data") else +    _vget op s +{-# RULES+"consuming.consuming"   forall n1 n2 op . consuming n1 (consuming n2 op) = consuming (max n1 n2) op+"consuming>>consuming"  forall n1 n2 f g . consuming n1 f >> consuming n2 g = consuming (n1+n2) (f>>g)+"consuming>>=consuming" forall n1 n2 f g . consuming n1 f >>= consuming n2 . g = consuming (n1+n2) (f>>=g)+ #-}+{-# INLINABLE consuming #-}+
+ hsrc_lib/Database/VCache/VGetInit.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE BangPatterns #-}++module Database.VCache.VGetInit+    ( vgetInit+    ) where++import Data.Bits+import Foreign.Ptr+import Database.VCache.Types+import Database.VCache.VGetAux++-- | For VGet from the database, we start with just a pointer and a+-- size. To process the VGet data, we also need to read addresses +-- from a dedicated region. This is encoded from the end, as follows:+--+--     (normal data) addressN offset offset offset offset ... bytes+--                                                           +-- Here 'bytes' is basically a varNat encoded backwards for the+-- number of bytes (not counting 'bytes') back to the start of the+-- first address. This address is then encoded as a varNat, and any+-- offset is encoded as a varInt with the idea of reducing overhead+-- for encoding addresses near to each other in memory.+--+-- Addresses are encoded such that the first address to parse is last+-- in the sequence (thereby avoiding a list reverse operation).+--+-- To read addresses, we simply read the number of bytes from the end,+-- step back that far, then read the initial address and offsets until+-- we get back to the end. This must be performed before we apply the+-- normal read operation for the VGet state. It must be applied exactly+-- once for a given input.+--+vgetInit :: VGet ()+vgetInit =+    readAddrBytes >>= \ nAddrBytes ->+    if (0 == nAddrBytes) then return () else+    VGet $ \ s -> +        let bUnderflow = nAddrBytes > (vget_limit s `minusPtr` vget_target s) in +        if bUnderflow then return eBadAddressRegion else +        let pAddrs = vget_limit s `plusPtr` negate nAddrBytes in+        let sAddrs = s { vget_target = pAddrs } in+        _vget readAddrs sAddrs >>= \ mbAddrs ->+        case mbAddrs of+            VGetR addrs _ ->+                let s' = s { vget_children = addrs, vget_limit = pAddrs } in+                return (VGetR () s')+            VGetE eMsg -> return (VGetE eMsg)+{-# INLINABLE vgetInit #-}++eBadAddressRegion :: VGetR a+eBadAddressRegion = VGetE "VGet: failed to read address region"++readAddrBytes :: VGet Int+readAddrBytes = readAddrBytes' 0+{-# INLINE readAddrBytes #-}++readAddrBytes' :: Int -> VGet Int+readAddrBytes' !nAccum = +    getWord8FromEnd >>= \ w8 ->+    let nAccum' = (nAccum `shiftL` 7) .|. (fromIntegral (0x7f .&. w8)) in+    if (w8 < 0x80) then return $! nAccum' else+    readAddrBytes' nAccum'++-- read a variable list of at least one address+readAddrs :: VGet [Address]+readAddrs = +    getVarNat >>= \ nFirst ->+    let addr0 = fromIntegral nFirst in+    addr0 `seq` readAddrs' [addr0] nFirst++-- read address offsets until end of input+readAddrs' :: [Address] -> Integer -> VGet [Address]+readAddrs' addrs !nLast =+    isEmpty >>= \ bEmpty ->+    if bEmpty then return addrs else+    getVarInt >>= \ nOff ->+    let nCurr = nLast + nOff in+    let addr = fromIntegral nCurr in+    addr `seq` readAddrs' (addr:addrs) nCurr+
+ hsrc_lib/Database/VCache/VPut.hs view
@@ -0,0 +1,224 @@++++module Database.VCache.VPut+    ( VPut++    -- * Prim Writers+    , putVRef, putPVar+    , putWord8+    , putWord16le, putWord16be+    , putWord32le, putWord32be+    , putWord64le, putWord64be+    , putStorable+    , putVarNat, putVarInt+    , reserve, reserving, unsafePutWord8+    , putByteString, putByteStringLazy+    , putc+    ) where++import Data.Bits+import Data.Char+import Data.Word+import Foreign.Ptr (plusPtr,castPtr)+import Foreign.Storable (Storable(..))+import Foreign.Marshal.Utils (copyBytes)+import Foreign.ForeignPtr (withForeignPtr)++import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as BSI+import qualified Data.ByteString.Lazy as LBS++import Database.VCache.Types+import Database.VCache.Aligned+import Database.VCache.VPutAux+-- import Database.VCache.Impl++-- | Store a reference to a value. The value reference must already+-- use the same VCache and addres space as where you're putting it.+putVRef :: VRef a -> VPut ()+putVRef ref = VPut $ \ s ->+    if (vput_space s == vref_space ref) then _putVRef s ref else+    fail $ "putVRef argument is not from destination VCache" +{-# INLINABLE putVRef #-}++-- assuming destination and ref have same address space+_putVRef :: VPutS -> VRef a -> IO (VPutR ())+_putVRef s ref = +    let cs = vput_children s in+    let c  = PutChild (Right ref) in+    let s' = s { vput_children = (c:cs) } in+    return (VPutR () s')+{-# INLINE _putVRef #-}++-- | Store an identifier for a persistent variable in the same VCache+-- and address space.+putPVar :: PVar a -> VPut ()+putPVar pvar = VPut $ \ s ->+    if (vput_space s == pvar_space pvar) then _putPVar s pvar else +    fail $ "putPVar argument is not from destination VCache"+{-# INLINABLE putPVar #-}++-- assuming destination and var have same address space+_putPVar :: VPutS -> PVar a -> IO (VPutR ())+_putPVar s pvar = +    let cs = vput_children s in+    let c  = PutChild (Left pvar) in+    let s' = s { vput_children = (c:cs) } in+    return (VPutR () s')+{-# INLINE _putPVar #-}++-- | Put a Word in little-endian or big-endian form.+--+-- Note: These are mostly included because they're part of the +-- Data.Binary and Data.Cereal APIs. They may be useful in some+-- cases, but putVarInt will frequently be preferable.+putWord16le, putWord16be :: Word16 -> VPut ()+putWord32le, putWord32be :: Word32 -> VPut ()+putWord64le, putWord64be :: Word64 -> VPut ()++-- THOUGHTS: I could probably optimize these further by using+-- an intermediate type and some rewriting to combine reserve+-- operations. However, I doubt I'll actually use putWord* all+-- that much... mostly just including to match the Data.Cereal+-- and Data.Binary APIs. I expect to use variable-sized integers+-- and such much more frequently.+++putWord16le w = reserving 2 $ VPut $ \ s -> do+    let p = vput_target s+    let s' = s { vput_target = (p `plusPtr` 2) }+    poke (p            ) (fromIntegral (w           ) :: Word8)+    poke (p `plusPtr` 1) (fromIntegral (w `shiftR` 8) :: Word8)+    return (VPutR () s')+{-# INLINE putWord16le #-}++putWord32le w = reserving 4 $ VPut $ \ s -> do+    let p = vput_target s+    let s' = s { vput_target = (p `plusPtr` 4) }+    poke (p            ) (fromIntegral (w            ) :: Word8)+    poke (p `plusPtr` 1) (fromIntegral (w `shiftR`  8) :: Word8)+    poke (p `plusPtr` 2) (fromIntegral (w `shiftR` 16) :: Word8)+    poke (p `plusPtr` 3) (fromIntegral (w `shiftR` 24) :: Word8)+    return (VPutR () s')+{-# INLINE putWord32le #-}++putWord64le w = reserving 8 $ VPut $ \ s -> do+    let p = vput_target s+    let s' = s { vput_target = (p `plusPtr` 8) }+    poke (p            ) (fromIntegral (w            ) :: Word8)+    poke (p `plusPtr` 1) (fromIntegral (w `shiftR`  8) :: Word8)+    poke (p `plusPtr` 2) (fromIntegral (w `shiftR` 16) :: Word8)+    poke (p `plusPtr` 3) (fromIntegral (w `shiftR` 24) :: Word8)+    poke (p `plusPtr` 4) (fromIntegral (w `shiftR` 32) :: Word8)+    poke (p `plusPtr` 5) (fromIntegral (w `shiftR` 40) :: Word8)+    poke (p `plusPtr` 6) (fromIntegral (w `shiftR` 48) :: Word8)+    poke (p `plusPtr` 7) (fromIntegral (w `shiftR` 56) :: Word8)+    return (VPutR () s')+{-# INLINE putWord64le #-}++putWord16be w = reserving 2 $ VPut $ \ s -> do+    let p = vput_target s+    let s' = s { vput_target = (p `plusPtr` 2) }+    poke (p            ) (fromIntegral (w `shiftR` 8) :: Word8)+    poke (p `plusPtr` 1) (fromIntegral (w           ) :: Word8)+    return (VPutR () s')+{-# INLINE putWord16be #-}++putWord32be w = reserving 4 $ VPut $ \ s -> do+    let p = vput_target s+    let s' = s { vput_target = (p `plusPtr` 4) }+    poke (p            ) (fromIntegral (w `shiftR` 24) :: Word8)+    poke (p `plusPtr` 1) (fromIntegral (w `shiftR` 16) :: Word8)+    poke (p `plusPtr` 2) (fromIntegral (w `shiftR`  8) :: Word8)+    poke (p `plusPtr` 3) (fromIntegral (w            ) :: Word8)+    return (VPutR () s')+{-# INLINE putWord32be #-}++putWord64be w = reserving 8 $ VPut $ \ s -> do+    let p = vput_target s+    let s' = s { vput_target = (p `plusPtr` 8) }+    poke (p            ) (fromIntegral (w `shiftR` 56) :: Word8)+    poke (p `plusPtr` 1) (fromIntegral (w `shiftR` 48) :: Word8)+    poke (p `plusPtr` 2) (fromIntegral (w `shiftR` 40) :: Word8)+    poke (p `plusPtr` 3) (fromIntegral (w `shiftR` 32) :: Word8)+    poke (p `plusPtr` 4) (fromIntegral (w `shiftR` 24) :: Word8)+    poke (p `plusPtr` 5) (fromIntegral (w `shiftR` 16) :: Word8)+    poke (p `plusPtr` 6) (fromIntegral (w `shiftR`  8) :: Word8)+    poke (p `plusPtr` 7) (fromIntegral (w            ) :: Word8)+    return (VPutR () s')+{-# INLINE putWord64be #-}++-- | Put a Data.Storable value, using intermediate storage to+-- ensure alignment when serializing argument. Note that this+-- shouldn't have any pointers, since serialized pointers won't+-- usually be valid when loaded later. Also, the storable type+-- shouldn't have any gaps (unassigned bytes); uninitialized+-- bytes may interfere with structure sharing in VCache.+putStorable :: (Storable a) => a -> VPut () +putStorable a = +    let n = sizeOf a in+    reserving n $ VPut $ \ s -> do+        let pTgt = vput_target s +        let s' = s { vput_target = (pTgt `plusPtr` n) } +        pokeAligned (castPtr pTgt) a+        return (VPutR () s')+{-# INLINABLE putStorable #-}++-- | Put the contents of a bytestring directly. Unlike the 'put' method for+-- bytestrings, this does not include size information; just raw bytes.+putByteString :: BS.ByteString -> VPut ()+putByteString s = reserving (BS.length s) (_putByteString s)+{-# INLINE putByteString #-}++-- | Put contents of a lazy bytestring directly. Unlike the 'put' method for+-- bytestrings, this does not include size information; just raw bytes.+putByteStringLazy :: LBS.ByteString -> VPut ()+putByteStringLazy s = reserving (fromIntegral $ LBS.length s) (mapM_ _putByteString (LBS.toChunks s))+{-# INLINE putByteStringLazy #-}++-- put a byte string, assuming enough space has been reserved already.+-- this uses a simple memcpy to the target space.+_putByteString :: BS.ByteString -> VPut ()+_putByteString (BSI.PS fpSrc p_off p_len) = +    VPut $ \ s -> withForeignPtr fpSrc $ \ pSrc -> do+        let pDst = vput_target s+        copyBytes pDst (pSrc `plusPtr` p_off) p_len+        let s' = s { vput_target = (pDst `plusPtr` p_len) }+        return (VPutR () s')+{-# INLINABLE _putByteString #-}++-- | Put a character in UTF-8 format.+putc :: Char -> VPut ()+putc a | c <= 0x7f      = putWord8 (fromIntegral c)+       | c <= 0x7ff     = reserving 2 $ VPut $ \ s -> do+                            let p  = vput_target s+                            let s' = s { vput_target = (p `plusPtr` 2) } +                            poke (p            ) (0xc0 .|. y)+                            poke (p `plusPtr` 1) (0x80 .|. z)+                            return (VPutR () s')+       | c <= 0xffff    = reserving 3 $ VPut $ \ s -> do+                            let p  = vput_target s+                            let s' = s { vput_target = (p `plusPtr` 3) }+                            poke (p            ) (0xe0 .|. x)+                            poke (p `plusPtr` 1) (0x80 .|. y)+                            poke (p `plusPtr` 2) (0x80 .|. z)+                            return (VPutR () s')+       | c <= 0x10ffff  = reserving 4 $ VPut $ \ s -> do+                            let p = vput_target s+                            let s' = s { vput_target = (p `plusPtr` 4) }+                            poke (p            ) (0xf0 .|. w)+                            poke (p `plusPtr` 1) (0x80 .|. x)+                            poke (p `plusPtr` 2) (0x80 .|. y)+                            poke (p `plusPtr` 3) (0x80 .|. z)+                            return (VPutR () s')+        | otherwise     = fail "not a valid character" -- shouldn't happen+    where +        c = ord a+        z, y, x, w :: Word8+        z = fromIntegral (c           .&. 0x3f)+        y = fromIntegral (shiftR c 6  .&. 0x3f)+        x = fromIntegral (shiftR c 12 .&. 0x3f)+        w = fromIntegral (shiftR c 18 .&. 0x7)++
+ hsrc_lib/Database/VCache/VPutAux.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE BangPatterns #-}++-- dependencies of both VPutFini and VPut+module Database.VCache.VPutAux+    ( reserving, reserve+    , unsafePutWord8+    , putWord8+    , putVarNat+    , putVarInt+    , putVarNatR+    ) where++import Control.Applicative+import Data.Bits+import Data.Word+import Data.IORef+import Foreign.Storable+import Foreign.Ptr+import Foreign.Marshal.Alloc++import Database.VCache.Types+++reserving :: Int -> VPut a -> VPut a+reserving n op = reserve n >> op+{-# RULES+"reserving >> reserving" forall n1 n2 f g . reserving n1 f >> reserving n2 g = reserving (n1+n2) (f>>g)+ #-}+{-# INLINABLE reserving #-}++-- | Ensure that at least N bytes are available for storage without+-- growing the underlying buffer. Use this before unsafePutWord8 +-- and similar operations. If the buffer must grow, it will grow+-- exponentially to ensure amortized constant allocation costs.+reserve :: Int -> VPut ()+reserve n = VPut $ \ s ->+    let avail = vput_limit s `minusPtr` vput_target s in+    if (avail >= n) then return (VPutR () s) +                    else VPutR () <$> grow n s +{-# INLINE reserve #-}++grow :: Int -> VPutS -> IO VPutS+grow n s =+    readIORef (vput_buffer s) >>= \ pBuff ->+    let currSize = vput_limit s `minusPtr` pBuff in+    let bytesUsed = vput_target s `minusPtr` pBuff in+    -- heuristic exponential growth+    let bytesNeeded = (2 * currSize) + n + 1000 in +    reallocBytes pBuff bytesNeeded >>= \ pBuff' ->+    -- (realloc will throw if it fails)+    writeIORef (vput_buffer s) pBuff' >>+    let target' = pBuff' `plusPtr` bytesUsed in+    let limit' = pBuff' `plusPtr` bytesNeeded in+    return $ s+        { vput_target = target'+        , vput_limit = limit'+        }+{-# NOINLINE grow #-}++-- | Store an 8 bit word *assuming* enough space has been reserved.+-- This can be used safely together with 'reserve'.+unsafePutWord8 :: Word8 -> VPut ()+unsafePutWord8 w8 = VPut $ \ s -> +    let pTgt = vput_target s in+    let s' = s { vput_target = (pTgt `plusPtr` 1) } in+    poke pTgt w8 >>+    return (VPutR () s')+{-# INLINE unsafePutWord8 #-}++-- | Store an 8 bit word.+putWord8 :: Word8 -> VPut ()+putWord8 w8 = reserving 1 $ unsafePutWord8 w8+{-# INLINE putWord8 #-}++-- | Put an arbitrary non-negative integer in 'varint' format associated+-- with Google protocol buffers. This takes one byte for values 0..127,+-- two bytes for 128..16k, etc.. Will fail if given a negative argument.+putVarNat :: Integer -> VPut ()+putVarNat n | (n < 0) = fail $ "putVarNat with " ++ show n+            | otherwise = _putVarNat q >> putWord8 bLo +  where q   = n `shiftR` 7+        bLo = 0x7f .&. fromIntegral n++_putVarNat :: Integer -> VPut ()+_putVarNat 0 = return ()+_putVarNat n = _putVarNat q >> putWord8 b where+    q = n `shiftR` 7+    b = 0x80 .|. (0x7f .&. fromIntegral n)++-- | Put an arbitrary integer in a 'varint' format associated with+-- Google protocol buffers with zigzag encoding of negative numbers.+-- This takes one byte for values -64..63, two bytes for -8k..8k, +-- three bytes for -1M..1M, etc.. Very useful if most numbers are+-- near 0.+putVarInt :: Integer -> VPut ()+putVarInt = putVarNat . zigZag+{-# INLINE putVarInt #-}++zigZag :: Integer -> Integer+zigZag n | (n < 0)   = (negate n * 2) - 1+         | otherwise = (n * 2)+{-# INLINE zigZag #-}+++-- | write a varNat, but reversed (i.e. little-endian)+--+-- This is only used by VPutFini: the last entry is the size (in bytes)+-- of the children list. But we write backwards so we can later read it+-- from the end of the buffer.+putVarNatR :: Int -> VPut ()+putVarNatR n | (n < 0) = fail $ "putVarNatR with " ++ show n +             | otherwise = putWord8 bLo >> _putVarNatR q +  where bLo  = 0x7f .&. fromIntegral n+        q    = n `shiftR` 7++_putVarNatR :: Int -> VPut () +_putVarNatR 0 = return ()+_putVarNatR n = putWord8 b >> _putVarNatR q where+    b = 0x80 .|. (0x7f .&. fromIntegral n)+    q = n `shiftR` 7
+ hsrc_lib/Database/VCache/VPutFini.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE BangPatterns #-}++module Database.VCache.VPutFini+    ( vputFini+    , runVPutIO+    , runVPut+    ) where++import Control.Exception (onException)+import Data.IORef+import Data.ByteString (ByteString)+import Foreign.Ptr+import Foreign.ForeignPtr+import Foreign.Marshal.Alloc+import System.IO.Unsafe (unsafePerformIO)+import qualified Data.ByteString.Internal as BSI++import Database.VCache.Types+import Database.VCache.VPutAux++-- | When we're just about done with VPut, we really have one more+-- task to perform: to output the address list for any contained +-- PVars and VRefs. These addresses are simply concatenated onto the+-- normal byte output, with a final size value (not including itself)+-- to indicate how far to jump back.+--+-- Actually, we output the first address followed by relative offsets+-- for every following address. This behavior allows us to reduce the+-- serialization costs when addresses are near each other in memory.+--+-- The address list is output in the reverse order of serialization.+-- (This simplifies reading in the same order as serialization without+-- a list reversal operation.)+--+-- It's important that we finalize exactly once for every serialization,+-- and that this be applied before any hash functions.+vputFini :: VPut ()+vputFini = do+    szStart <- getBufferSize+    lChildren <- listChildren+    putChildren lChildren+    szFini <- getBufferSize+    putVarNatR (szFini - szStart)+    -- shrinkBuffer++getBufferSize :: VPut Int+getBufferSize = VPut $ \ s ->+    readIORef (vput_buffer s) >>= \ pStart ->+    let size = (vput_target s) `minusPtr` pStart in+    size `seq`+    return (VPutR size s)+{-# INLINE getBufferSize #-}++listChildren :: VPut [PutChild]+listChildren = VPut $ \ s ->+    let r = vput_children s in+    return (VPutR r s)+{-# INLINE listChildren #-}++putChildren :: [PutChild] -> VPut ()+putChildren [] = return ()+putChildren (x:xs) = +    let addr0 = putChildAddr x in+    putVarNat (fromIntegral addr0) >>+    putChildren' addr0 xs++-- putChildren after the first, using offsets.+putChildren' :: Address -> [PutChild] -> VPut ()+putChildren' _ [] = return ()+putChildren' !prev (x:xs) = +    let addrX = putChildAddr x in+    let offset = (fromIntegral addrX) - (fromIntegral prev) in+    putVarInt offset >>+    putChildren' addrX xs++runVPutIO :: VSpace -> VPut a -> IO (a, ByteString, [PutChild])+runVPutIO vs action = do+    let initialSize = 1000 -- avoid reallocs for small data+    pBuff <- mallocBytes initialSize+    vBuff <- newIORef pBuff+    let s0 = VPutS { vput_space = vs+                   , vput_children = []+                   , vput_buffer = vBuff+                   , vput_target = pBuff+                   , vput_limit = pBuff `plusPtr` initialSize+                   }+    let freeBuff = readIORef vBuff >>= free+    let fullWrite = do { result <- action; vputFini; return result }+    let runPut = _vput fullWrite s0+    (VPutR r sf) <- runPut `onException` freeBuff+    pBuff' <- readIORef vBuff+    let len = vput_target sf `minusPtr` pBuff'+    pBuffR <- reallocBytes pBuff len -- reclaim unused space+    fpBuff' <- newForeignPtr finalizerFree pBuffR+    let bytes = BSI.fromForeignPtr fpBuff' 0 len+    return (r, bytes, vput_children sf)+{-# NOINLINE runVPutIO #-}++runVPut :: VSpace -> VPut a -> (a, ByteString, [PutChild])+runVPut vs action = unsafePerformIO (runVPutIO vs action)+
+ hsrc_lib/Database/VCache/VRef.hs view
@@ -0,0 +1,123 @@+++module Database.VCache.VRef+    ( VRef+    , vref, deref+    , vref', deref'+    , unsafeVRefAddr+    , unsafeVRefRefct+    , vref_space+    , CacheMode(..)+    , vrefc, derefc+    ) where++import Control.Monad+import Data.IORef+import System.IO.Unsafe +import Database.VCache.Types+import Database.VCache.Alloc+import Database.VCache.Read++-- | Construct a reference with the cache initially active, i.e.+-- such that immediate deref can access the value without reading+-- from the database. The given value will be placed in the cache+-- unless the same vref has already been constructed.+vref :: (VCacheable a) => VSpace -> a -> VRef a+vref = vrefc CacheMode1 +{-# INLINE vref #-}++-- | Construct a VRef with an alternative cache control mode. +vrefc :: (VCacheable a) => CacheMode -> VSpace -> a -> VRef a+vrefc cm vc v = unsafePerformIO (newVRefIO vc v cm)+{-# INLINABLE vrefc #-}++-- | In some cases, developers can reasonably assume they won't need a +-- value in the near future. In these cases, use the vref' constructor+-- to allocate a VRef without caching the content. +vref' :: (VCacheable a) => VSpace -> a -> VRef a+vref' vc v = unsafePerformIO (newVRefIO' vc v)+{-# INLINABLE vref' #-}++readVRef :: VRef a -> IO (a, Int)+readVRef v = readAddrIO (vref_space v) (vref_addr v) (vref_parse v)+{-# INLINE readVRef #-}++-- | Dereference a VRef, obtaining its value. If the value is not in+-- cache, it will be read into the database then cached. Otherwise, +-- the value is read from cache and the cache is touched to restart+-- any expiration.+--+-- Assuming a valid VCacheable instance, this operation should return+-- an equivalent value as was used to construct the VRef.+deref :: VRef a -> a+deref = derefc CacheMode1+{-# INLINE deref #-}++-- | Dereference a VRef with an alternative cache control mode.+derefc :: CacheMode -> VRef a -> a+derefc cm v = unsafeDupablePerformIO $ +    unsafeInterleaveIO (readVRef v) >>= \ lazy_read_rw ->+    join $ atomicModifyIORef (vref_cache v) $ \ c -> case c of+        Cached r bf ->+            let bf' = touchCache cm bf in+            let c' = Cached r bf' in+            (c', c' `seq` return r)+        NotCached ->+            let (r,w) = lazy_read_rw in+            let c' = mkVRefCache r w cm in+            let op = initVRefCache v >> return r in+            (c', c' `seq` op)+{-# NOINLINE derefc #-}+++-- I've modified how VRefs are recorded +++-- | Dereference a VRef. This will read from the cache if the value+-- is available, but will not update the cache. If the value is not+-- cached, it will be read instead from the persistence layer.+--+-- This can be useful if you know you'll only dereference a value +-- once for a given task, or if the datatype involved is cheap to+-- parse (e.g. simple bytestrings) such that there isn't a strong+-- need to cache the parse result.+deref' :: VRef a -> a+deref' v = unsafePerformIO $ +    readIORef (vref_cache v) >>= \ c -> case c of+        Cached r _ -> return r+        NotCached -> liftM fst (readVRef v)+{-# INLINABLE deref' #-}++-- | Each VRef has an numeric address in the VSpace. This address is+-- non-deterministic, and essentially independent of the arguments to+-- the vref constructor. This function is 'unsafe' in the sense that+-- it violates the illusion of purity. However, the VRef address will+-- be stable so long as the developer can guarantee it is reachable.+--+-- This function may be useful for memoization tables and similar.+--+-- The 'Show' instance for VRef will also show the address.+unsafeVRefAddr :: VRef a -> Address+unsafeVRefAddr = vref_addr+{-# INLINE unsafeVRefAddr #-}++-- | This function allows developers to access the reference count +-- for the VRef that is currently recorded in the database. This may+-- be useful for heuristic purposes. However, caveats are needed:+--+-- First, due to structure sharing, a VRef may share an address with+-- VRefs of other types having the same serialized form. Reference +-- counts are at the address level.+--+-- Second, because the VCache writer operates in a background thread,+-- the reference count returned here may be slightly out of date.+--+-- Third, it is possible that VCache will eventually use some other+-- form of garbage collection than reference counting. This function+-- should be considered an unstable element of the API.+unsafeVRefRefct :: VRef a -> IO Int+unsafeVRefRefct v = readRefctIO (vref_space v) (vref_addr v) +{-# INLINE unsafeVRefRefct #-}+++
+ hsrc_lib/Database/VCache/VTx.hs view
@@ -0,0 +1,86 @@++module Database.VCache.VTx+    ( VTx+    , runVTx+    , liftSTM+    , markDurable+    , markDurableIf+    , getVTxSpace+    ) where++import Control.Monad +import Control.Monad.Trans.State.Strict+import Control.Concurrent.STM+import Control.Concurrent.MVar+import qualified Data.Map.Strict as Map+import Database.VCache.Types++-- | runVTx executes a transaction that may involve both STM TVars+-- (via liftSTM) and VCache PVars (via readPVar, writePVar). +runVTx :: VSpace -> VTx a -> IO a+runVTx vc action = do+    mvWait <- newEmptyMVar+    join (atomically (runVTx' vc mvWait action))+{-# INLINABLE runVTx #-}++runVTx' :: VSpace -> MVar () -> VTx a -> STM (IO a)+runVTx' vc mvWait action = +    let s0 = VTxState vc Map.empty False in+    runStateT (_vtx action) s0 >>= \ (r,s) ->+    -- fast path for read-only, non-durable actions+    let bWrite = not (Map.null (vtx_writes s)) in+    let bSync = vtx_durable s in+    let bDone = not (bWrite || bSync) in+    if bDone then return (return r) else+    -- otherwise, we update shared queue w/ potential conflicts+    readTVar (vcache_writes vc) >>= \ w ->+    let wdata' = updateLog (vtx_writes s) (write_data w) in+    let wsync' = updateSync bSync mvWait (write_sync w) in+    let w' = Writes { write_data = wdata', write_sync = wsync' } in+    writeTVar (vcache_writes vc) w' >>= \ () ->+    return $ w' `seq` do+        signalWriter vc +        when bSync (takeMVar mvWait)+        return r++-- Signal the writer of work to do.+signalWriter :: VSpace  -> IO ()+signalWriter vc = void (tryPutMVar (vcache_signal vc) ())+{-# INLINE signalWriter #-}++-- Record recent writes for each PVar.+updateLog :: WriteLog -> WriteLog -> WriteLog+updateLog updates writeLog = Map.union updates writeLog +{-# INLINE updateLog #-}++-- Track which threads are waiting on a commit signal.+updateSync :: Bool -> MVar () -> [MVar ()] -> [MVar ()]+updateSync bSync v = if bSync then (v:) else id+{-# INLINE updateSync #-}++-- | Durability for a VTx transaction is optional: it requires an+-- additional wait for the background thread to signal that it has+-- committed content to the persistence layer. Due to how writes +-- are batched, a durable transaction may share its wait with many+-- other transactions that occur at more or less the same time.+-- +-- Developers should mark a transaction durable only if necessary+-- based on domain layer policies. E.g. for a shopping service, +-- normal updates and views of the virtual shopping cart might not+-- be durable while committing to a purchase is durable. +--+markDurable :: VTx ()+markDurable = VTx $ modify $ \ vtx -> +    vtx { vtx_durable = True }+{-# INLINE markDurable #-}++-- | This variation of markDurable makes it easier to short-circuit+-- complex computations to decide durability when the transaction is+-- already durable. If durability is already marked, the boolean is+-- not evaluated.+markDurableIf :: Bool -> VTx ()+markDurableIf b = VTx $ modify $ \ vtx -> +    let bDurable = vtx_durable vtx || b in+    vtx { vtx_durable = bDurable }+{-# INLINE markDurableIf #-}+
+ hsrc_lib/Database/VCache/Write.hs view
@@ -0,0 +1,494 @@+{-# LANGUAGE BangPatterns #-}++-- Implementation of the Writer threads.+--+-- Some general design goals here:+--+--   Favor sequential processing (not random access)+--   Single read/write pass per database per frame+--   Append newly written content when possible+--+-- An exception to the single pass is the db_refct0 table, for which+-- I'll make a few passes. However, assuming GC is working, db_refct0+-- should be smaller than the in-memory ephemeron tables. So this +-- should not create any significant paging burden.+--+-- The writer handles GC to simplify reasoning about concurrency, in+-- particular the arbitration between reviving VRef addresses via the+-- structure sharing feature and deleting VRef addresses with zero+-- references. GC is incremental to avoid latency spikes with durable+-- transactions.+-- +module Database.VCache.Write+    ( writeStep+    ) where++import Control.Monad+import Control.Exception+import Control.Concurrent+import Control.Concurrent.STM+import Data.IORef+import Data.Map (Map)+import qualified Data.Map as Map+import qualified Data.List as L+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Foreign.Ptr+import Foreign.Storable+import Foreign.Marshal.Alloc++import Database.LMDB.Raw+import Database.VCache.Types +import Database.VCache.VPutFini -- serialize updated PVars+import Database.VCache.VGetInit -- read dependencies to manage refcts+import Database.VCache.RWLock -- need a writer lock+import Database.VCache.Refct  -- to update reference counts+import Database.VCache.Hash   -- for GC of VRefs+import Database.VCache.Aligned++-- | when processing write batches, we'll need to track+-- differences in reference counts for each address.+--+-- For deletions, I'll use minBound as a simple sentinel.+type RefctDiff = Map Address Refct++-- A batch of updates to perform on memory, including dependencies+-- to incref. +--+-- Note: if a WriteCell has a null ByteString, this means we'll delete+-- the content. Empty bytestring is impossible as output from VPut+-- because we have at least a size for the child list.+type WriteBatch = Map Address WriteCell+type WriteCell = (ByteString, [PutChild])+type GCBatch = WriteBatch++-- for updating secondary indices, track names to addresses +type UpdSeek = Map ByteString [Address]++addrSize :: Int+addrSize = sizeOf (undefined :: Address)++-- Single step for VCache writer.+writeStep :: VSpace -> IO ()+writeStep vc = withRWLock (vcache_rwlock vc) $ do+    takeMVar (vcache_signal vc) ++    -- acquire writes, allocations, GC for this step+    ws <- atomically (takeWrites (vcache_writes vc))+    wb <- seralizeWrites (write_data ws)+    afrm <- allocFrameStep vc+    let allocInit = alloc_init afrm +    let ab = fmap fnWriteAlloc (alloc_list afrm) -- alloc batch+    let ub = Map.union wb ab                     -- update batch (favor writes)++    -- LMDB-layer read-write transaction.+    txn <- mdb_txn_begin (vcache_db_env vc) Nothing False++    -- select addresses for garbage collection +    let gcLimit = 1000 + 2 * Map.size ub -- adaptive GC rate+    gcb <- runGarbageCollector vc txn gcLimit++    -- update the db_memory. allocs, writes, deletes.+    let fb = Map.union gcb ub -- full batch+    let bUpdGCSep = Map.size fb == (Map.size ub + Map.size gcb) +    unless bUpdGCSep (fail "VCache bug: overlapping GC and update targets")+    (UpdateNotes rcDiff hsDel) <- updateVirtualMemory vc txn allocInit fb++    -- Update reference counts, +1 for new roots.+    let rcAlloc = fmap (\ an -> if isNewRoot an then 1 else 0) (alloc_list afrm)+    let rcUpd = Map.unionWith (\ a b -> (a + b)) rcDiff rcAlloc +    updateReferenceCounts vc txn allocInit rcUpd++    -- Update secondary indices: PVar roots, VRef hashes.+    let hsAlloc = fmap (fmap alloc_addr) (alloc_seek afrm)+    let hsUpd = Map.unionWith (++) hsDel hsAlloc +    writeSecondaryIndexes vc txn allocInit hsUpd++    -- Finish writeStep: commit, synch, stats, signals.+    mdb_txn_commit txn -- LMDB commit & synch+    modifyIORef' (vcache_gc_count vc) (+ (Map.size gcb)) -- update GC count+    vcache_signal_writes vc ws -- report write stats+    mapM_ syncSignal (write_sync ws) -- signal waiting threads+{-# NOINLINE writeStep #-}++-- Interact with GC and Allocation. Only safe once per writeStep.+allocFrameStep :: VSpace -> IO AllocFrame+allocFrameStep vc = modifyMVarMasked (vcache_memory vc) $ \ m -> do+    let ac = mem_alloc m+    let addr = alloc_new_addr ac+    let ac' = Allocator+            { alloc_new_addr = addr+            , alloc_frm_next = AllocFrame Map.empty Map.empty addr+            , alloc_frm_curr = alloc_frm_next ac+            , alloc_frm_prev = alloc_frm_curr ac+            }+    let m' = m { mem_alloc = ac' }+    return (m', alloc_frm_curr ac')++isNewRoot :: Allocation -> Bool+isNewRoot an = isPVarAddr (alloc_addr an) && not (BS.null (alloc_name an))++takeWrites :: TVar Writes -> STM Writes+takeWrites tv = do+    wb <- readTVar tv+    writeTVar tv (Writes Map.empty [])+    return wb++seralizeWrites :: WriteLog -> IO WriteBatch+seralizeWrites = Map.traverseWithKey (const writeTxW)++writeTxW :: TxW -> IO WriteCell+writeTxW (TxW pv v) =+    runVPutIO (pvar_space pv) (pvar_write pv v) >>= \ ((), _data, _deps) ->+    return (_data,_deps)++fnWriteAlloc :: Allocation -> WriteCell+fnWriteAlloc an = (alloc_data an, alloc_deps an)++syncSignal :: MVar () -> IO ()+syncSignal mv = void (tryPutMVar mv ())++-- Write the PVar roots and VRef hashmap. In these cases, the address+-- is the data, and a bytestring (a path or hash) is the key. I'm using+-- bytestring-sorted input, in this case, so we can easily insert these+-- in a sequential order (though they may be widely scattered).+--+-- In this case, I haven't encoded whether an entry in the UpdSeek map+-- is a deletion vs. an insertion. But, since I know where allocations+-- start, I can infer this information.+writeSecondaryIndexes :: VSpace -> MDB_txn -> Address -> UpdSeek -> IO ()+writeSecondaryIndexes vc txn allocInit updSeek =+    if (Map.null updSeek) then return () else+    alloca $ \ pAddr -> do+    let vAddr = MDB_val { mv_data = castPtr pAddr, mv_size = fromIntegral addrSize }+    croot <- mdb_cursor_open' txn (vcache_db_vroots vc) +    chash <- mdb_cursor_open' txn (vcache_db_caddrs vc)++    -- logic inlined for easy access to cursors and buffers+    let recordRoot vKey addr = do+            when (addr < allocInit) (fail "VCache bug: attempt to delete named root")+            let flags = compileWriteFlags [MDB_NOOVERWRITE]+            bOK <- mdb_cursor_put' flags croot vKey vAddr+            unless bOK (fail "VCache bug: attempt to overwrite named root")+    let insertHash vKey addr = do+            let flags = compileWriteFlags [MDB_NODUPDATA]+            bOK <- mdb_cursor_put' flags chash vKey vAddr+            unless bOK (addrBug addr "VRef hash recorded twice")+    let deleteHash vKey addr =+            alloca $ \ pvKey ->+            alloca $ \ pvAddr -> do+            poke pvKey vKey+            poke pvAddr vAddr+            bExist <- mdb_cursor_get' MDB_GET_BOTH chash pvKey pvAddr -- position cursor+            unless bExist (addrBug addr "VRef hash not found for deletion")+            let flags = compileWriteFlags []+            mdb_cursor_del' flags chash+    let processName (name, addrs) =+            withByteStringVal name $ \ vKey ->+            forM_ addrs $ \ addr ->+            poke pAddr addr >> -- prepares vAddr+            if isPVarAddr addr then recordRoot vKey addr else+            if addr < allocInit then deleteHash vKey addr else+            insertHash vKey addr++    -- process all (key, [address]) pairs+    mapM_ processName (Map.toAscList updSeek) ++    mdb_cursor_close' chash+    mdb_cursor_close' croot+    return ()+{-# NOINLINE writeSecondaryIndexes #-}++-- | Update reference counts in the database. This requires, for each+-- older address, reading the old reference count, updating it, then+-- writing the new value. Newer addresses may simply be appended. +--+-- VCache uses two tables for reference counts. One table just contains+-- zeroes. The other table includes positive counts. This separation +-- makes it easy for the garbage collector to find its targets. Zeroes+-- are also recorded to guarantee that GC can continue after a process+-- crashes.+--+-- Currently, I assume that all entries older than allocInit should+-- be recorded in the database, i.e. it's an error for both db_refct+-- and db_refct0 to be undefined unless I'm allocating a new address.+-- (Thus newPVar does need a placeholder.)+--+-- Ephemerons in the Haskell layer are not reference counted.+--+-- This operation should never fail. Failure indicates there is a bug+-- in VCache or some external source of database corruption. +--+updateReferenceCounts :: VSpace -> MDB_txn -> Address -> RefctDiff -> IO ()+updateReferenceCounts vc txn allocInit rcDiffMap =+    if Map.null rcDiffMap then return () else+    alloca $ \ pAddr ->+    allocaBytes 16 $ \ pRefctBuff -> -- overkill, but that's okay+    alloca $ \ pvAddr ->+    alloca $ \ pvData -> do+    let vAddr = MDB_val { mv_data = castPtr pAddr, mv_size = fromIntegral addrSize }+    let vZero = MDB_val { mv_data = nullPtr, mv_size = 0 }+    poke pvAddr vAddr -- the MDB_SET cursor operations will not update this+    wrc <- mdb_cursor_open' txn (vcache_db_refcts vc) -- write new reference counts+    wc0 <- mdb_cursor_open' txn (vcache_db_refct0 vc) -- write zeroes for ephemeral values++    -- the logic is inlined here for easy access to buffers and cursors+    let newEphemeron addr = do -- just write a zero+            let flags = compileWriteFlags [MDB_APPEND]+            bOK <- mdb_cursor_put' flags wc0 vAddr vZero+            unless bOK (addrBug addr "refct0 could not be appended")+    let newAllocation addr rc =+            if (0 == rc) then newEphemeron addr else do +            unless (rc > 0) (addrBug addr "allocation with negative refct")+            vRefct <- writeRefctBytes pRefctBuff rc+            let flags = compileWriteFlags [MDB_APPEND]+            bOK <- mdb_cursor_put' flags wrc vAddr vRefct+            unless bOK (addrBug addr "refct could not be appended")+    let updateFromZero addr rc = do+            bZeroFound <- mdb_cursor_get' MDB_SET wc0 pvAddr pvData+            unless bZeroFound (addrBug addr "has undefined refct")+            unless (rc > 0) (addrBug addr "update refct0 to negative refct")+            let df = compileWriteFlags []+            mdb_cursor_del' df wc0+            vRefct <- writeRefctBytes pRefctBuff rc+            let wf = compileWriteFlags [MDB_NOOVERWRITE]+            bOK <- mdb_cursor_put' wf wrc vAddr vRefct+            unless bOK (addrBug addr "could not update refct from zero")+    let deleteZero addr = do+            bFoundZero <- mdb_cursor_get' MDB_SET wc0 pvAddr pvData+            unless bFoundZero (addrBug addr "refct0 not found for deletion")+            let df = compileWriteFlags []+            mdb_cursor_del' df wc0+    let updateRefct (addr,rcDiff) = +            poke pAddr addr >> -- prepares vAddr, pvAddr+            if (addr >= allocInit) then newAllocation addr rcDiff else+            if (minBound == rcDiff) then deleteZero addr else -- sentinel for GC+            if (0 == rcDiff) then return () else -- zero delta, may skip+            mdb_cursor_get' MDB_SET wrc pvAddr pvData >>= \ bHasRefct ->+            if (not bHasRefct) then updateFromZero addr rcDiff else+            peek pvData >>= readRefctBytes >>= \ rcOld ->+            assert (rcOld > 0) $ +            let rc = rcOld + rcDiff in+            if (rc < 0) then addrBug addr "positive to negative refct" else+            if (0 == rc) +                then do let df = compileWriteFlags []+                        mdb_cursor_del' df wrc+                        let wf0 = compileWriteFlags [MDB_NOOVERWRITE]+                        bOK <- mdb_cursor_put' wf0 wc0 vAddr vZero+                        unless bOK (addrBug addr "has both refct0 and refct")+                else do vRefct <- writeRefctBytes pRefctBuff rc+                        let ucf = compileWriteFlags [MDB_CURRENT]+                        bOK <- mdb_cursor_put' ucf wrc vAddr vRefct+                        unless bOK (addrBug addr "could not update refct")++    -- process every reference count update+    mapM_ updateRefct (Map.toAscList rcDiffMap)+    mdb_cursor_close' wc0+    mdb_cursor_close' wrc+    return ()+{-# NOINLINE updateReferenceCounts #-}++-- paranoid checks for bugs that should be impossible+addrBug :: Address -> String -> IO a+addrBug addr msg = fail $ "VCache bug: address " ++ show addr ++ " " ++ msg++-- Since we only make one pass through memory, we need to maintain notes+-- about the changes in content:+--+--  * changes in reference counts from content+--  * hash values for deleted VRefs+-- +data UpdateNotes = UpdateNotes !RefctDiff !UpdSeek++emptyNotes :: UpdateNotes+emptyNotes = UpdateNotes Map.empty Map.empty++-- Typical CRUD, performed in a sorted-order pass, aggregating notes+-- useful for further processing.+updateVirtualMemory :: VSpace -> MDB_txn -> Address -> WriteBatch -> IO UpdateNotes+updateVirtualMemory vc txn allocStart fb = +    if Map.null fb then return emptyNotes else  +    alloca $ \ pAddr ->+    alloca $ \ pvAddr ->+    alloca $ \ pvOldData -> do+    let vAddr = MDB_val { mv_data = castPtr pAddr, mv_size = fromIntegral addrSize }+    poke pvAddr vAddr -- used with MDB_SET so should not be modified+    cmem <- mdb_cursor_open' txn (vcache_db_memory vc) ++    -- logic inlined here for easy access to cursors and buffers+    let create udn addr bytes =   +            withByteStringVal bytes $ \ vData -> do+            let cf = compileWriteFlags [MDB_APPEND]+            bOK <- mdb_cursor_put' cf cmem vAddr vData+            unless bOK (addrBug addr "created out of order")+            return udn -- no notes for allocations+    let update (UpdateNotes rcs hs) addr bytes = +            withByteStringVal bytes $ \ vData -> do+            unless (isPVarAddr addr) (addrBug addr "VRef cannot be updated")+            bExists <- mdb_cursor_get' MDB_SET cmem pvAddr pvOldData+            unless bExists (addrBug addr "undefined on update")+            oldDeps <- readDataDeps vc addr =<< peek pvOldData+            let rcs' = addRefcts oldDeps rcs+            let uf = compileWriteFlags [MDB_CURRENT]+            bOK <- mdb_cursor_put' uf cmem vAddr vData+            unless bOK (addrBug addr "could not updated")+            return (UpdateNotes rcs' hs)+    let delete (UpdateNotes rcs hs) addr = do+            bExists <- mdb_cursor_get' MDB_SET cmem pvAddr pvOldData+            unless bExists (addrBug addr "undefined on delete")+            vOldData <- peek pvOldData+            hs' <- if not (isVRefAddr addr) then return hs else+                   hashVal vOldData >>= \ h ->+                   return (addHash h addr hs)+            oldDeps <- readDataDeps vc addr vOldData+            let rcs' = addRefcts oldDeps rcs+            let df = compileWriteFlags []+            mdb_cursor_del' df cmem+            return (UpdateNotes rcs' hs')+    let processCell rcs (addr, (bytes, deps')) =+            poke pAddr addr >> -- +            if (BS.null bytes) then assert (L.null deps') $ delete rcs addr else+            if (addr >= allocStart) then create rcs addr bytes else+            update rcs addr bytes++    (UpdateNotes rcOld delSeek) <- foldM processCell emptyNotes (Map.toAscList fb)+    mdb_cursor_close' cmem++    assertValidOldDeps allocStart rcOld -- sanity check+    let rcNew = Map.foldr' (addRefcts . fmap putChildAddr . snd) Map.empty fb+    let rcDiff = Map.unionWith (-) rcNew rcOld+    return (UpdateNotes rcDiff delSeek)+{-# NOINLINE updateVirtualMemory #-}++-- here we might have one bytestring to many addresses... but this is +-- extremely unlikely.+addHash :: ByteString -> Address -> UpdSeek -> UpdSeek+addHash h addr = Map.alter f h where+    f = Just . (addr:) . maybe [] id ++addRefcts :: [Address] -> RefctDiff -> RefctDiff+addRefcts = flip (L.foldl' altr) where+    altr rc addr = Map.alter (Just . maybe 1 (+ 1)) addr rc ++-- sanity check: we should never have dependencies from+-- old content into the newly allocated space.   +assertValidOldDeps :: Address -> RefctDiff -> IO ()+assertValidOldDeps allocStart rcDepsOld = +    case Map.maxViewWithKey rcDepsOld of+        Nothing -> return ()+        Just ((maxOldDep,_), _) -> +            unless (maxOldDep < allocStart) (fail "VCache bug: time traveling allocator")++-- Read just enough of an MDB_val to obtain the address list.+readDataDeps :: VSpace -> Address -> MDB_val -> IO [Address]+readDataDeps vc addr vData = _vget vgetInit state0 >>= toDeps where+    toDeps (VGetR () sf) = return (vget_children sf)+    toDeps (VGetE eMsg) = addrBug addr $ "contains malformed data: " ++ eMsg+    state0 = VGetS+        { vget_children = []+        , vget_target = mv_data vData+        , vget_limit = mv_data vData `plusPtr` fromIntegral (mv_size vData)+        , vget_space = vc+        }+++-- | Garbage collection in VCache involves selecting addresses with+-- zero references, filtering objects that are held by VRefs and +-- PVars in Haskell memory, then deleting the remainders. +--+-- GC is incremental. We limiting the amount of work performed in+-- each write step to avoid creating too much latency for writers.+-- To keep up with heavy sustained work loads, GC rate will adapt +-- based on the write rates via the gcLimit argument. +--+runGarbageCollector :: VSpace -> MDB_txn -> Int -> IO GCBatch+runGarbageCollector vc txn gcLimit = do+    gcb0 <- gcCandidates vc txn gcLimit+    gcb <- gcSelectFrame vc gcb0+    gcClearFrame vc txn gcb+    return gcb+{-# NOINLINE runGarbageCollector #-}+++gcCandidates :: VSpace -> MDB_txn -> Int -> IO GCBatch+gcCandidates vc txn gcLimit =+    alloca $ \ pvAddr -> do+    c0 <- mdb_cursor_open' txn (vcache_db_refct0 vc)++    let loop !n !b !gcb = -- select candidates+            if (not b) then restartGC vc >> return gcb else+            (peek pvAddr >>= peekAddr) >>= \ addr ->+            let gcb' = Map.insert addr gcCell gcb in+            if (0 == n) then continueGC vc addr >> return gcb' else+            mdb_cursor_get' MDB_NEXT c0 pvAddr nullPtr >>= \ b' ->+            loop (n-1) b' gcb'++    let initC0 = -- continue GC or start from beginning of map+            readIORef (vcache_gc_start vc) >>= \ mbContinue ->+            case mbContinue of+                Nothing -> mdb_cursor_get' MDB_FIRST c0 pvAddr nullPtr+                Just addr -> alloca $ \ pAddr -> do+                    let vAddr = MDB_val { mv_data = castPtr pAddr+                                        , mv_size = fromIntegral $ sizeOf addr }+                    poke pAddr (1 + addr)+                    poke pvAddr vAddr+                    mdb_cursor_get' MDB_SET_RANGE c0 pvAddr nullPtr++    b0 <- initC0+    gcb <- loop (gcLimit - 1) b0 Map.empty+    mdb_cursor_close' c0+    return gcb++-- filter candidates for ephemeral addresses then record the GC frame.+gcSelectFrame :: VSpace -> GCBatch -> IO GCBatch+gcSelectFrame vc gcb = +    modifyMVarMasked (vcache_memory vc) $ \ m -> do+    let gcb' = (((gcb `Map.difference` mem_evrefs m) +                      `Map.difference` mem_cvrefs m) +                      `Map.difference` mem_pvars  m) +    let gc' = GC { gc_frm_curr = GCFrame gcb'+                 , gc_frm_prev = gc_frm_curr (mem_gc m) }+    let m' = m { mem_gc = gc' }+    return (m', gcb')++-- delete GC'd addresses from the db_refct0 table. Returns +-- number of addresses in +gcClearFrame :: VSpace -> MDB_txn -> GCBatch -> IO ()+gcClearFrame vc txn gcb = +    alloca $ \ pAddr -> +    alloca $ \ pvAddr -> do+    let vAddr = MDB_val { mv_data = castPtr pAddr, mv_size = fromIntegral addrSize }+    poke pvAddr vAddr+    c0 <- mdb_cursor_open' txn (vcache_db_refct0 vc)+    +    let clearAddr addr = do+            poke pAddr addr+            bFound <- mdb_cursor_get' MDB_SET c0 pvAddr nullPtr+            unless bFound (addrBug addr "not found for GC")+            let flags = compileWriteFlags []+            mdb_cursor_del' flags c0++    mapM_ clearAddr (Map.keys gcb)+    mdb_cursor_close' c0+    return ()+   + +-- GC from first address (affects next frame)+restartGC :: VSpace -> IO ()+restartGC vc = writeIORef (vcache_gc_start vc) Nothing++-- GC from given address (affects next frame)+continueGC :: VSpace -> Address -> IO ()+continueGC vc !addr = writeIORef (vcache_gc_start vc) (Just addr)++gcCell :: WriteCell+gcCell = (BS.empty, [])    ++peekAddr :: MDB_val -> IO Address+peekAddr v =+    let expectedSize = fromIntegral addrSize in+    let bBadSize = expectedSize /= mv_size v in+    if bBadSize then fail "VCache bug: badly formed address" else+    peekAligned (castPtr (mv_data v))+{-# INLINABLE peekAddr #-}+
+ vcache.cabal view
@@ -0,0 +1,77 @@+Name: vcache+Version: 0.1+Synopsis: large, persistent, memcached values and structure sharing for Haskell +Category: Database+Description:+  VCache provides a nearly-transparent persistent memory for Haskell+  with transactional variables, persistent roots, and large structured+  values. The virtual space is a memory-mapped file via LMDB, with +  structure sharing and incremental GC. +  .+  VCache is very similar to packages acid-state, perdure, and TCache.+  VCache is intended as an acid-state alternative, offering flexibility+  to model fine-grained variables or extremely large values.+  +Author: David Barbour+Maintainer: dmbarbour@gmail.com+Homepage: http://github.com/dmbarbour/haskell-vcache++Package-Url: +Copyright: (c) 2014 by David Barbour+License: BSD3+license-file: LICENSE+Stability: experimental+build-type: Simple+cabal-version: >= 1.16.0.3++Source-repository head+  type: git+  location: http://github.com/dmbarbour/haskell-vcache.git++Library+  hs-Source-Dirs: hsrc_lib+  default-language: Haskell2010+  Build-Depends: base (>= 4.6 && < 5)+    , direct-murmur-hash+    , bytestring+    , transformers+    , containers (>= 0.5)+    , stm (>= 2.4.3)+    , lmdb (>= 0.2.5)+    , filelock+    , easy-file+    , random (>= 1.0)++  Exposed-Modules:+    Database.VCache+    Database.VCache.VRef+    Database.VCache.PVar+    Database.VCache.VTx+    Database.VCache.VCacheable+    Database.VCache.VPut+    Database.VCache.VGet++    Database.VCache.Path+    Database.VCache.Sync+    Database.VCache.Stats+    Database.VCache.Cache++  Other-Modules:+    Database.VCache.Types+    Database.VCache.Open+    Database.VCache.Aligned+    Database.VCache.RWLock+    Database.VCache.Hash+    Database.VCache.VGetAux+    Database.VCache.VGetInit+    Database.VCache.VPutAux+    Database.VCache.VPutFini+    -- Database.VCache.Adjacency+    Database.VCache.Alloc+    Database.VCache.Read+    Database.VCache.Write+    Database.VCache.Clean+    Database.VCache.Refct+   +  ghc-options: -Wall -auto-all+