module Utils.ARC ( ARCTick , ARC, initARC , arcAlterF, lookupARC, insertARC , ARCHandle, initARCHandle, cachedARC, cachedARC' , lookupARCHandle , readARCHandle , arcRecentSize, arcFrequentSize, arcGhostRecentSize, arcGhostFrequentSize , getARCRecentWeight, getARCFrequentWeight , describeARC ) where import ClassyPrelude import Data.OrdPSQ (OrdPSQ) import qualified Data.OrdPSQ as OrdPSQ import Control.Lens -- https://web.archive.org/web/20210115184012/https://dbs.uni-leipzig.de/file/ARC.pdf -- https://jaspervdj.be/posts/2015-02-24-lru-cache.html newtype ARCTick = ARCTick { _getARCTick :: Word64 } deriving (Eq, Ord, Show, Typeable) deriving newtype (NFData) makeLenses ''ARCTick data ARC k w v = ARC { arcRecent, arcFrequent :: !(OrdPSQ k ARCTick (v, w)) , arcGhostRecent, arcGhostFrequent :: !(OrdPSQ k ARCTick ()) , arcRecentWeight, arcFrequentWeight :: !w , arcTargetRecent, arcMaximumWeight :: !w , arcMaximumGhost :: !Int } instance (NFData k, NFData w, NFData v) => NFData (ARC k w v) where rnf ARC{..} = rnf arcRecent `seq` rnf arcFrequent `seq` rnf arcGhostRecent `seq` rnf arcGhostFrequent `seq` rnf arcRecentWeight `seq` rnf arcFrequentWeight `seq` rnf arcTargetRecent `seq` rnf arcMaximumWeight `seq` rnf arcMaximumGhost describeARC :: Show w => ARC k w v -> String describeARC ARC{..} = intercalate ", " [ "arcRecent: " <> show (OrdPSQ.size arcRecent) , "arcFrequent: " <> show (OrdPSQ.size arcFrequent) , "arcGhostRecent: " <> show (OrdPSQ.size arcGhostRecent) , "arcGhostFrequent: " <> show (OrdPSQ.size arcGhostFrequent) , "arcRecentWeight: " <> show arcRecentWeight , "arcFrequentWeight: " <> show arcFrequentWeight , "arcTargetRecent: " <> show arcTargetRecent , "arcMaximumWeight: " <> show arcMaximumWeight , "arcMaximumGhost: " <> show arcMaximumGhost ] arcRecentSize, arcFrequentSize, arcGhostRecentSize, arcGhostFrequentSize :: ARC k w v -> Int arcRecentSize = OrdPSQ.size . arcRecent arcFrequentSize = OrdPSQ.size . arcFrequent arcGhostRecentSize = OrdPSQ.size . arcGhostRecent arcGhostFrequentSize = OrdPSQ.size . arcGhostFrequent getARCRecentWeight, getARCFrequentWeight :: ARC k w v -> w getARCRecentWeight = arcRecentWeight getARCFrequentWeight = arcFrequentWeight initialARCTick :: ARCTick initialARCTick = ARCTick 0 initARC :: forall k w v. Integral w => Int -- ^ @arcMaximumGhost@ -> w -- ^ @arcMaximumWeight@ -> (ARC k w v, ARCTick) initARC arcMaximumGhost arcMaximumWeight | arcMaximumWeight < 0 = error "initARC given negative maximum weight" | arcMaximumGhost < 0 = error "initARC given negative maximum ghost size" | otherwise = (, initialARCTick) ARC { arcRecent = OrdPSQ.empty , arcFrequent = OrdPSQ.empty , arcGhostRecent = OrdPSQ.empty , arcGhostFrequent = OrdPSQ.empty , arcRecentWeight = 0 , arcFrequentWeight = 0 , arcMaximumWeight , arcTargetRecent = 0 , arcMaximumGhost } infixl 6 |- (|-) :: (Num a, Ord a) => a -> a -> a (|-) m s | s >= m = 0 | otherwise = m - s arcAlterF :: forall f k w v. ( Ord k , Functor f , Integral w ) => k -> (Maybe (v, w) -> f (Maybe (v, w))) -> ARC k w v -> ARCTick -> f (ARC k w v, ARCTick) -- | Unchecked precondition: item weights are always less than `arcMaximumWeight` arcAlterF k f oldARC@ARC{..} now | later <= initialARCTick = uncurry (arcAlterF k f) $ initARC arcMaximumGhost arcMaximumWeight | otherwise = (, later) <$> if | Just (_p, x@(_, w), arcFrequent') <- OrdPSQ.deleteView k arcFrequent -> f (Just x) <&> \(fromMaybe x -> x'@(_, w')) -> let (arcFrequent'', arcFrequentWeight'', arcGhostFrequent') = evictToSize (arcMaximumWeight |- arcTargetRecent |- w') arcFrequent' (arcFrequentWeight - w) arcGhostFrequent in oldARC { arcFrequent = OrdPSQ.insert k now x' arcFrequent'' , arcFrequentWeight = arcFrequentWeight'' + w' , arcGhostFrequent = arcGhostFrequent' } | Just (_p, x@(_, w), arcRecent') <- OrdPSQ.deleteView k arcRecent -> f (Just x) <&> \(fromMaybe x -> x'@(_, w')) -> let (arcFrequent', arcFrequentWeight', arcGhostFrequent') = evictToSize (arcMaximumWeight |- arcTargetRecent |- w') arcFrequent arcFrequentWeight arcGhostFrequent in oldARC { arcRecent = arcRecent' , arcRecentWeight = arcRecentWeight - w , arcFrequent = OrdPSQ.insert k now x' arcFrequent' , arcFrequentWeight = arcFrequentWeight' + w' , arcGhostFrequent = arcGhostFrequent' } | Just (_p, (), arcGhostRecent') <- OrdPSQ.deleteView k arcGhostRecent -> f Nothing <&> \case Nothing -> oldARC { arcGhostRecent = OrdPSQ.insert k now () arcGhostRecent' } Just x@(_, w) -> let arcTargetRecent' = min arcMaximumWeight $ arcTargetRecent + max avgWeight (round $ toRational (OrdPSQ.size arcGhostFrequent) / toRational (OrdPSQ.size arcGhostRecent) * toRational avgWeight) (arcFrequent', arcFrequentWeight', arcGhostFrequent') = evictToSize (arcMaximumWeight |- arcTargetRecent' |- w) arcFrequent arcFrequentWeight arcGhostFrequent (arcRecent', arcRecentWeight', arcGhostRecent'') = evictToSize (max arcTargetRecent' $ arcMaximumWeight |- arcFrequentWeight' |- w) arcRecent arcRecentWeight arcGhostRecent' in oldARC { arcRecent = arcRecent' , arcFrequent = OrdPSQ.insert k now x arcFrequent' , arcGhostRecent = arcGhostRecent'' , arcGhostFrequent = arcGhostFrequent' , arcRecentWeight = arcRecentWeight' , arcFrequentWeight = arcFrequentWeight' + w , arcTargetRecent = arcTargetRecent' } | Just (_p, (), arcGhostFrequent') <- OrdPSQ.deleteView k arcGhostFrequent -> f Nothing <&> \case Nothing -> oldARC { arcGhostFrequent = OrdPSQ.insert k now () arcGhostFrequent' } Just x@(_, w) -> let arcTargetRecent' = arcTargetRecent |- max avgWeight (round $ toRational (OrdPSQ.size arcGhostRecent) / toRational (OrdPSQ.size arcGhostFrequent) * toRational avgWeight) (arcFrequent', arcFrequentWeight', arcGhostFrequent'') = evictToSize (arcMaximumWeight |- arcTargetRecent' |- w) arcFrequent arcFrequentWeight arcGhostFrequent' (arcRecent', arcRecentWeight', arcGhostRecent') = evictToSize (max arcTargetRecent' $ arcMaximumWeight |- arcFrequentWeight' |- w) arcRecent arcRecentWeight arcGhostRecent in oldARC { arcRecent = arcRecent' , arcFrequent = OrdPSQ.insert k now x arcFrequent' , arcGhostRecent = arcGhostRecent' , arcGhostFrequent = arcGhostFrequent'' , arcRecentWeight = arcRecentWeight' , arcFrequentWeight = arcFrequentWeight' + w , arcTargetRecent = arcTargetRecent' } | otherwise -> f Nothing <&> \case Nothing -> oldARC { arcGhostRecent = OrdPSQ.insert k now () $ evictGhostToCount arcGhostRecent } Just x@(_, w) -> let (arcRecent', arcRecentWeight', arcGhostRecent') = evictToSize (max arcTargetRecent (arcMaximumWeight |- arcFrequentWeight) |- w) arcRecent arcRecentWeight arcGhostRecent in oldARC { arcRecent = OrdPSQ.insert k now x arcRecent' , arcRecentWeight = arcRecentWeight' + w , arcGhostRecent = arcGhostRecent' } where avgWeight = round $ toRational (arcRecentWeight + arcFrequentWeight) / toRational (OrdPSQ.size arcFrequent + OrdPSQ.size arcRecent) later :: ARCTick later = over getARCTick succ now evictToSize :: w -> OrdPSQ k ARCTick (v, w) -> w -> OrdPSQ k ARCTick () -> (OrdPSQ k ARCTick (v, w), w, OrdPSQ k ARCTick ()) evictToSize tSize c cSize ghostC | cSize <= tSize = (c, cSize, ghostC) | Just (k', p', (_, w'), c') <- OrdPSQ.minView c = evictToSize tSize c' (cSize - w') . evictGhostToCount $ OrdPSQ.insert k' p' () ghostC | otherwise = error "evictToSize: cannot reach required size through eviction" evictGhostToCount :: OrdPSQ k ARCTick () -> OrdPSQ k ARCTick () evictGhostToCount c | OrdPSQ.size c <= arcMaximumGhost = c | Just (_, _, _, c') <- OrdPSQ.minView c = evictGhostToCount c' | otherwise = error "evictGhostToCount: cannot reach required count through eviction" lookupARC :: forall k w v. ( Ord k , Integral w ) => k -> (ARC k w v, ARCTick) -> Maybe (v, w) lookupARC k = getConst . uncurry (arcAlterF k Const) insertARC :: forall k w v. ( Ord k , Integral w ) => k -> Maybe (v, w) -> ARC k w v -> ARCTick -> (ARC k w v, ARCTick) insertARC k newVal = (runIdentity .) . arcAlterF k (const $ pure newVal) newtype ARCHandle k w v = ARCHandle { _getARCHandle :: IORef (ARC k w v, ARCTick) } deriving (Eq, Typeable) initARCHandle :: forall k w v m. ( MonadIO m , Integral w ) => Int -- ^ @arcMaximumGhost@ -> w -- ^ @arcMaximumWeight@ -> m (ARCHandle k w v) initARCHandle maxGhost maxWeight = fmap ARCHandle . newIORef $ initARC maxGhost maxWeight cachedARC' :: forall k w v m. ( MonadIO m , Ord k , Integral w , NFData k, NFData w, NFData v ) => ARCHandle k w v -> k -> (Maybe (v, w) -> m (Maybe (v, w))) -> m (Maybe v) cachedARC' (ARCHandle arcVar) k f = do oldVal <- lookupARC k <$> readIORef arcVar newVal <- f oldVal modifyIORef' arcVar $ force . uncurry (insertARC k newVal) -- Using `modifyIORef'` instead of `atomicModifyIORef'` might very -- well drop newer values computed during the update. -- -- Currently we accept that to reduce lock contention. -- -- Another alternative would be to use "optimistic locking", -- i.e. read the current value of `arcVar`, compute an updated -- version, and write it back atomically iff the `ARCTick` hasn't -- changed. -- -- This was not implemented to avoid the overhead and contention -- likely associated with the atomic transaction required for the -- "compare and swap" return $ view _1 <$> newVal cachedARC :: forall k w v m. ( MonadIO m , Ord k , Integral w , NFData k, NFData w, NFData v ) => ARCHandle k w v -> k -> (Maybe (v, w) -> m (v, w)) -> m v cachedARC h k f = fromMaybe (error "cachedARC: cachedARC' returned Nothing") <$> cachedARC' h k (fmap Just . f) lookupARCHandle :: forall k w v m. ( MonadIO m , Ord k , Integral w ) => ARCHandle k w v -> k -> m (Maybe (v, w)) lookupARCHandle (ARCHandle arcVar) k = lookupARC k <$> readIORef arcVar readARCHandle :: MonadIO m => ARCHandle k w v -> m (ARC k w v, ARCTick) readARCHandle (ARCHandle arcVar) = readIORef arcVar