packages feed

ghc-debug-client-0.8.0.0: src/GHC/Debug/Retainers.hs

{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
-- | Functions for computing retainers
module GHC.Debug.Retainers
  ( SearchLimit
  , findRetainers
  , findRetainersIncremental
  , findRetainersOf
  , findRetainersOfConstructor
  , findRetainersOfConstructorExact
  , findRetainersOfInfoTable
  , addLocationToStack
  , displayRetainerStack
  , addLocationToStack'
  , displayRetainerStack'
  , findRetainersOfArrWords
  , EraRange(..)
  , profHeaderInEraRange
  , ClosureFilter(..)
  , profHeaderReferencesCCS
  , findRetainersOfEra) where

import Prelude hiding (filter)
import GHC.Debug.Client
import Control.Monad.State
import GHC.Debug.Trace
import GHC.Debug.Types.Graph
import Control.Monad

import qualified Data.Set as Set
import Control.Monad.RWS
import Data.Word

-- | Optional limit for the number of results of a search.
-- If no search limit is given, the search is unlimited.
type SearchLimit = Maybe Int

-- | Check the search limit, and if it has been reached, then stop the execution.
ifSearchLimitNotReached :: (MonadState s m) => (s -> SearchLimit) -> m () -> m ()
ifSearchLimitNotReached getLimit act = do
  limit <- get
  case getLimit limit of
    Just 0 -> pure ()
    Nothing ->
      act
    Just _ ->
      act

-- | Indicate that a result value has been found and the search limit, if there is any,
-- needs to be updated.
addedOneSearchResult :: (MonadState s m) => ((SearchLimit -> SearchLimit) -> s -> s) -> m ()
addedOneSearchResult updateLimit = do
  st <- get
  put (updateLimit (subtract 1 <$>) st)

data EraRange
  = EraRange { startEra :: Word64, endEra :: Word64} -- inclusive
  deriving (Eq, Ord, Show)

inEraRange :: Word64 -> Maybe EraRange -> Bool
inEraRange _ Nothing = True
inEraRange n (Just (EraRange s e)) = s <= n && n <= e

profHeaderReferencesCCS :: Maybe ProfHeaderWithPtr -> Set.Set CCSPtr -> Bool
profHeaderReferencesCCS Nothing _ = False
profHeaderReferencesCCS (Just profHeader) f = ccs profHeader `Set.member` f

profHeaderInEraRange :: Maybe (ProfHeader a) -> Maybe EraRange -> Bool
profHeaderInEraRange Nothing _ = True
profHeaderInEraRange (Just ph) eras
  = case hp ph of
      EraWord w -> w `inEraRange` eras
      _ -> True -- Don't filter if no era profiling

data ClosureFilter
 = ConstructorDescFilter (ConstrDesc -> Bool)
 | InfoFilter (StgInfoTable -> Bool)
 | InfoPtrFilter (InfoTablePtr -> Bool)
 | InfoSourceFilter (SourceInformation -> Bool)
 | SizeFilter (Size -> Bool)
 | ProfHeaderFilter (Maybe ProfHeaderWithPtr -> Bool)
 | AddressFilter (ClosurePtr -> Bool)
 | AndFilter ClosureFilter ClosureFilter
 | OrFilter ClosureFilter ClosureFilter
 | NotFilter ClosureFilter
 | PureFilter Bool

matchesFilter :: ClosureFilter -> ClosurePtr -> SizedClosure -> [ClosurePtr] -> DebugM Bool
matchesFilter filter ptr sc parents = case filter of
  ConstructorDescFilter p -> case noSize sc of
    ConstrClosure _ _ _ _ cd -> do
      cd' <- dereferenceConDesc cd
      return $ p cd'
    _ -> pure False
  InfoFilter p -> pure $ p (decodedTable (info (noSize sc)))
  InfoPtrFilter p -> pure $ p (tableId (info (noSize sc)))
  InfoSourceFilter p -> do
    loc <- getSourceInfo (tableId (info (noSize sc)))
    case loc of
      Nothing -> return False
      Just cur_loc -> pure $ p cur_loc
  SizeFilter p -> pure $ p (dcSize sc)
  ProfHeaderFilter p -> pure $ p (profHeader $ noSize sc)
  AddressFilter p -> pure $ p ptr
  AndFilter f1 f2 -> do
    r1 <- matchesFilter f1 ptr sc parents
    case r1 of
      False -> pure False
      True -> matchesFilter f2 ptr sc parents
  OrFilter f1 f2 -> do
    r1 <- matchesFilter f1 ptr sc parents
    case r1 of
      True -> pure True
      False -> matchesFilter f2 ptr sc parents
  NotFilter f1  -> do
    r1 <- matchesFilter f1 ptr sc parents
    pure (not r1)
  PureFilter b -> pure b

findRetainersOf :: SearchLimit
                -> [ClosurePtr]
                -> [ClosurePtr]
                -> DebugM [[ClosurePtr]]
findRetainersOf limit cps bads =
  findRetainers limit (AddressFilter (`Set.member` bad_set)) cps
  where
    bad_set = Set.fromList bads

findRetainersOfConstructor :: SearchLimit
                           -> [ClosurePtr] -> String -> DebugM [[ClosurePtr]]
findRetainersOfConstructor limit rroots con_name =
  findRetainers limit (ConstructorDescFilter ((== con_name) . name)) rroots

findRetainersOfConstructorExact
  :: SearchLimit
  -> [ClosurePtr] -> String -> DebugM [[ClosurePtr]]
findRetainersOfConstructorExact limit rroots clos_name =
  findRetainers limit (InfoSourceFilter ((== clos_name) . infoName)) rroots

findRetainersOfEra
  :: SearchLimit
  -> EraRange
  -> [ClosurePtr] -> DebugM [[ClosurePtr]]
findRetainersOfEra limit eras rroots =
  findRetainers limit filter rroots
  where
    filter = ProfHeaderFilter (`profHeaderInEraRange` (Just eras))

findRetainersOfArrWords
  :: SearchLimit
  -> [ClosurePtr] -> Size -> DebugM [[ClosurePtr]]
findRetainersOfArrWords limit rroots lim =
  findRetainers limit filter rroots
  where
    -- TODO : this is the size of the entire closure, not the size of the ArrWords
    filter = AndFilter (InfoFilter ((== ARR_WORDS) . tipe))
                       (SizeFilter (>= lim))

findRetainersOfInfoTable
  :: SearchLimit
  -> [ClosurePtr] -> InfoTablePtr -> DebugM [[ClosurePtr]]
findRetainersOfInfoTable limit rroots info_ptr =
  findRetainers limit (InfoPtrFilter (== info_ptr)) rroots

-- | From the given roots, find any path to one of the given pointers.
-- Note: This function can be quite slow! The first argument is a limit to
-- how many paths to find. You should normally set this to a small number
-- such as 10.
findRetainers :: SearchLimit
  -> ClosureFilter
  -> [ClosurePtr] -> DebugM [[ClosurePtr]]
findRetainers limit filter rroots = (\(_, r, _) -> snd r) <$> runRWST (traceFromM funcs rroots) [] (limit, [])
  where
    funcs = justClosures closAccum
    -- Add clos
    closAccum  :: ClosurePtr
               -> SizedClosure
               -> RWST [ClosurePtr] () (SearchLimit, [[ClosurePtr]]) DebugM ()
               -> RWST [ClosurePtr] () (SearchLimit, [[ClosurePtr]]) DebugM ()
    closAccum _ (noSize -> WeakClosure {}) _ = return ()
    closAccum cp sc k = ifSearchLimitNotReached (\ (searchLimit, _) -> searchLimit) $ do
      ctx <- ask
      b <- lift $ matchesFilter filter cp sc ctx
      when b $ do
        addOne (cp: ctx)
        addedOneSearchResult ( \ upd (searchLimit, r) -> ( upd searchLimit, r))
      local (cp:) k

    addOne :: [ClosurePtr] -> RWST [ClosurePtr] () (SearchLimit, [[ClosurePtr]]) DebugM ()
    addOne cp = do
      modify' (\ (searchLimit, cps) -> ( searchLimit, cp : cps))

-- |  From the given roots, find retainer paths to closures which satisfy the filter.
-- The callback is called immediately when each sample is found.
findRetainersIncremental :: SearchLimit
  -> ClosureFilter
  -> [ClosurePtr]
  -> ([ClosurePtr] -> DebugM ()) -- ^ What to do with each sample when we find it.
  -> DebugM ()
findRetainersIncremental limit filter rroots add_new_sample = () <$ execRWST (traceFromM funcs rroots) [] limit
  where
    funcs = justClosures closAccum
    -- Add clos
    closAccum  :: ClosurePtr
               -> SizedClosure
               -> RWST [ClosurePtr] () SearchLimit DebugM ()
               -> RWST [ClosurePtr] () SearchLimit DebugM ()
    closAccum _ (noSize -> WeakClosure {}) _ = return ()
    closAccum cp sc k = ifSearchLimitNotReached id $ do
      ctx <- ask
      b <- lift $ matchesFilter filter cp sc ctx
      when b $ do
        lift $ add_new_sample (cp : ctx)
        addedOneSearchResult ($)
      local (cp:) k

addLocationToStack :: [ClosurePtr] -> DebugM [(SizedClosureP, Maybe SourceInformation)]
addLocationToStack r = do
  cs <- dereferenceClosures r
  cs' <- mapM dereferenceToClosurePtr cs
  locs <- mapM getSourceLoc cs'
  return $ (zip cs' locs)
  where
    getSourceLoc c = getSourceInfo (tableId (info (noSize c)))

addLocationToStack' :: [ClosurePtr] -> DebugM [(ClosurePtr, SizedClosureP, Maybe SourceInformation)]
addLocationToStack' r = do
  cs <- dereferenceClosures r
  cs' <- mapM dereferenceToClosurePtr cs
  locs <- mapM getSourceLoc cs'
  return $ (zip3 r cs' locs)
  where
    getSourceLoc c = getSourceInfo (tableId (info (noSize c)))

displayRetainerStack :: [(String, [(SizedClosureP, Maybe SourceInformation)])] -> IO ()
displayRetainerStack rs = do
      let disp (d, l) =
            (ppClosure  (\_ -> show) 0 . noSize $ d) ++  " <" ++ maybe "nl" tdisplay l ++ ">"
            where
              tdisplay sl = infoName sl ++ ":" ++ infoType sl ++ ":" ++ infoModule sl ++ ":" ++ infoPosition sl
          do_one k (l, stack) = do
            putStrLn (show k ++ "-------------------------------------")
            print l
            mapM (putStrLn . disp) stack
      zipWithM_ do_one [0 :: Int ..] rs

displayRetainerStack' :: [(String, [(ClosurePtr, SizedClosureP, Maybe SourceInformation)])] -> IO ()
displayRetainerStack' rs = do
      let disp (p, d, l) =
            show p ++ ": " ++ (ppClosure  (\_ -> show) 0 . noSize $ d) ++  " <" ++ maybe "nl" tdisplay l ++ ">"
            where
              tdisplay sl = infoName sl ++ ":" ++ infoType sl ++ ":" ++ infoModule sl ++ ":" ++ infoPosition sl
          do_one k (l, stack) = do
            putStrLn (show k ++ "-------------------------------------")
            print l
            mapM (putStrLn . disp) stack
      zipWithM_ do_one [0 :: Int ..] rs