packages feed

ghc-debug-brick-0.8.0.0: src/GHC/Debug/Brick/Model.hs

{-# LANGUAGE OverloadedLabels  #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE LambdaCase #-}

module GHC.Debug.Brick.Model
  ( module GHC.Debug.Brick.Model
  , module GHC.Debug.Brick.Namespace
  , module GHC.Debug.Brick.Common
  ) where

import Brick (EventM, Widget)
import qualified Brick
import Brick.BChan
import Brick.Forms
import Brick.Widgets.IOTree
import Brick.Widgets.List
import Control.Concurrent.Async (Async)
import qualified Control.Exception as Exc
import Control.Monad.IO.Class (liftIO)
import Data.ByteString.Lazy (ByteString)
import Data.Dependent.Map (DMap)
import qualified Data.Dependent.Map as DMap
import Data.GADT.Compare
import Data.GADT.Compare.TH
import Data.Map.Strict (Map)
import Data.Sequence as Seq
import Data.Set (Set)
import Data.Text (Text, pack)
import qualified Data.Text as T
import Data.Time
import GHC.Debug.Brick.Common
import GHC.Debug.Brick.Lib
import GHC.Debug.Brick.Namespace
import qualified Graphics.Vty as Vty
import Lens.Micro.Platform
import System.Directory
import System.FilePath
import Text.Read

import Brick.Widgets.ListPicker
import GHC.Debug.Brick.UI.Filter (UIFilter)
import qualified GHC.Debug.Profile as GD
import qualified GHC.Debug.Profile.Types as GD
import qualified GHC.Debug.Retainers as GD
import qualified GHC.Debug.Strings as GD
import qualified GHC.Debug.Thunks as GD
import qualified GHC.Debug.Types as GD
import qualified GHC.Debug.Types.Version as GD

data Event
  = PollTick  -- Used to perform arbitrary polling based tasks e.g. looking for new debuggees
  | ProgressMessage Text
  | ProgressFinished Text NominalDiffTime
  | AsyncAborted Exc.SomeException
  | AsyncFinished (EventM Name OperationalState ())
  | NewSampleEvent SampleEvent
  -- ^ A sample event is a partial result of a heap traversal.
  | NavigatorEvent NavigatorEvent
  -- ^ Events that navigate the history.
  | DoSearch SomeSearchCmd
  -- ^ Search which doesn't push a new "page" onto the history.
  -- Primarily used for changing the view, for navigation.
  | DoSearchWithHistoryPush SomeSearchCmd
  -- ^ Perform a heap traversal search that pushes a new value
  -- onto the history. The history can be navigated via 'NavigatorEvent'.
  | forall a . CacheSearch (SearchCmd a) a
  -- ^ A final message that is sent when the result of a search query
  -- is finalised. Partial results are not expected here.
  | forall a . CachePartialSearch (SearchCmd a) a
  -- ^ Partial result that we can still use

-- | Wraps a 'SearchCmd' and hides the type variable.
data SomeSearchCmd = forall a . SomeSearchCmd (SearchCmd a)

-- | Messages that switch the view to a new 'TreeMode'.
-- These Messages and their respective result can be cached.
data SearchCmd a where
  -- | Find the garbage collector roots
  DoGcRootsSearch :: SearchCmd [ClosureDetails]
  -- | Perform a retainer search.
  -- We record the search limit in this constructor, because the result is obsolete
  -- when the search limit changes.
  -- This is strictly speaking only correct for when the search limit is increased,
  -- but at the moment we consider it too much work to behave correctly when the
  -- search limit is decreased.
  DoRetainerSearch :: GD.SearchLimit -> [UIFilter] -> SearchCmd RetainerMap
  DoProfileSearch :: ProfileLevel -> SearchCmd GD.CensusByClosureType
  DoStringSearch :: SearchCmd GD.CensusByStrings
  DoArrWordsSearch :: SearchCmd GD.CensusByByteString
  DoThunkSearch :: SearchCmd GD.ThunkCensusBySourceLocation

data SampleEvent
  = GcRootsSample [ClosureDetails]
  | RetainerSample [ClosureDetails]
  -- ^ A new retainer sample is available, and we need to update the retainer tree.
  | ProfileSample ProfileLevel GD.CensusByClosureType
  | StringSample GD.CensusByStrings
  | ArrWordsSample GD.CensusByByteString
  | ThunkSample GD.ThunkCensusBySourceLocation

initialAppState :: BChan Event -> AppState
initialAppState event = AppState
  { _majorState = Setup
      { _setupKind = Socket
      , _knownDebuggees = list Setup_KnownDebuggeesList [] 1
      , _knownSnapshots = list Setup_KnownSnapshotsList [] 1
      },
    _appChan = event
  }

data AppState = AppState
  { _majorState :: MajorState
  , _appChan    :: BChan Event
  }

mkSocketInfo :: FilePath -> IO SocketInfo
mkSocketInfo fp = SocketInfo fp <$> getModificationTime fp

socketName :: SocketInfo -> Text
socketName = pack . takeFileName . _socketLocation

renderSocketTime :: SocketInfo -> Text
renderSocketTime = pack . formatTime defaultTimeLocale "%c" . _socketCreated

data SocketInfo = SocketInfo
                    { _socketLocation :: FilePath -- ^ FilePath to socket, absolute path
                    , _socketCreated :: UTCTime  -- ^ Time of socket creation
                    } deriving Eq

instance Ord SocketInfo where
  compare (SocketInfo s1 t1) (SocketInfo s2 t2) =
    -- Compare time first
    compare t1 t2 <> compare s1 s2

type SnapshotInfo = SocketInfo

data SetupKind = Socket | Snapshot

toggleSetup :: SetupKind -> SetupKind
toggleSetup Socket = Snapshot
toggleSetup Snapshot = Socket

data MajorState
  -- | We have not yet connected to a debuggee.
  = Setup
    { _setupKind :: SetupKind
    , _knownDebuggees :: GenericList Name Seq SocketInfo
    , _knownSnapshots :: GenericList Name Seq SnapshotInfo
    }

  -- | Connected to a debuggee
  | Connected
    { _debuggeeSocket :: SocketInfo
    , _debuggee :: Debuggee
    , _mode     :: ConnectedMode
    }

data InfoInfo = InfoInfo
  {
    _labelInParent :: Text -- ^ A label describing the relationship to the parent
  -- Stuff  that requires IO to calculate
  , _pretty :: Text
  , _sourceLocation :: Maybe SourceInformation
  , _closureType :: Maybe Text
  , _constructor :: Maybe Text
  , _profHeaderInfo :: !(Maybe ProfHeaderWord)
  } deriving Show

data ClosureDetails = ClosureDetails
  { _closure :: DebugClosure CCSPtr SrtCont PayloadCont ConstrDesc StackCont ClosurePtr
  , _excSize :: Size
  , _info :: InfoInfo
  }
  | InfoDetails { _info :: InfoInfo }
  | CCSDetails Text CCSPtr (GenCCSPayload CCSPtr CCPayload)
  | CCDetails Text CCPayload
  | StackFrameDetails
  { _tipe :: ClosureType
  , _frame :: GD.DebugStackFrame (GD.GenSrtPayload ClosurePtr) ClosurePtr
  , _info :: InfoInfo
  }
  | LabelNode { _label :: Text } deriving Show

data RetainerNode = RetainerStackRoot ClosureDetails -- ^ At the root, display the retainer stack as well
                  | RetainerStackChild ClosureDetails -- ^ Not at the root, just display normal closure details

retainerNodeClosure :: RetainerNode -> ClosureDetails
retainerNodeClosure (RetainerStackRoot dc) = dc
retainerNodeClosure (RetainerStackChild dc) = dc

data TreeMode = GcRoots (ClosureDetails -> Widget Name) (IOTree ClosureDetails Name)
              -- ^ Tree corresponding to SavedAndGCRoots mode
              | Retainer (RetainerNode -> Widget Name) (IOTree RetainerNode Name)
              | Profile (ProfileLine -> Widget Name) (IOTree ProfileLine Name)
              | Strings (StringCountLine -> Widget Name) (IOTree StringCountLine Name)
              | ArrWords (ArrWordsLine -> Widget Name) (IOTree ArrWordsLine Name)
              | Thunk (ThunkLine -> Widget Name) (IOTree ThunkLine Name)

data ThunkLine =
            ThunkLine (Maybe SourceInformation) GD.Count GD.Sample
          | ThunkClosureLine ClosureDetails

treeLength :: TreeMode -> Int
treeLength (GcRoots _ tree) = Prelude.length $ getIOTreeRoots tree
treeLength (Retainer _ tree) = Prelude.length $ getIOTreeRoots tree
treeLength (Profile _ tree) = Prelude.length $ getIOTreeRoots tree
treeLength (Strings _ tree) = Prelude.length $ getIOTreeRoots tree
treeLength (ArrWords _ tree) = Prelude.length $ getIOTreeRoots tree
treeLength (Thunk _ tree) = Prelude.length $ getIOTreeRoots tree

-- TODO: This should not be defined here
type RetainerMap = Map Ptr (ClosureDetails, [ClosureDetails])

-- TODO: should maybe not be defined here but rather in `Action.Profile`?
data ProfileLine
  = ProfileLine GD.ProfileKey GD.ProfileKeyArgs CensusStats
  | ClosureLine ClosureDetails

-- TODO: should maybe not be defined here but rather in `Action.ArrWords`?
data ArrWordsLine = ArrWordsCountLine ByteString Int Int (Set ClosurePtr) | ArrWordsFieldLine ClosureDetails

arrWordLineClosureDetails :: ArrWordsLine -> Maybe ClosureDetails
arrWordLineClosureDetails ArrWordsCountLine {} = Nothing
arrWordLineClosureDetails (ArrWordsFieldLine cd) = Just cd

data StringCountLine = StringCountLine String Int Int (Set ClosurePtr) | StringFieldLine ClosureDetails

stringLineClosureDetails :: StringCountLine -> Maybe ClosureDetails
stringLineClosureDetails StringCountLine {} = Nothing
stringLineClosureDetails (StringFieldLine cd) = Just cd

data FooterMode = FooterInfo
                | FooterMessage Text
                | FooterInput FooterInputMode (Form Text () Name)

isFocusedFooter :: FooterMode -> Bool
isFocusedFooter (FooterInput {}) = True
isFocusedFooter _ = False

data FooterInputMode = FClosureAddress {runNow :: Bool, invert :: Bool}
                     | FInfoTableAddress {runNow :: Bool, invert :: Bool}
                     | FConstructorName {runNow :: Bool, invert :: Bool}
                     | FClosureName {runNow :: Bool, invert :: Bool}
                     | FArrWordsSize
                     | FFilterEras {runNow :: Bool, invert :: Bool}
                     | FFilterClosureType {invert :: Bool}
                     | FFilterClosureSize {invert :: Bool}
                     | FFilterCcId {runNow :: Bool, invert :: Bool}
                     | FSnapshot
                     | FDumpArrWords
                     | FSetResultSize
                     | FSaveTree
                     deriving Show

-- | Profiling requirement for a command
data ProfilingReq
  = ReqSomeProfiling
  | ReqErasProfiling
  | NoReq

data Command = Command { commandDescription :: Text
                       , commandKey :: Maybe Vty.Event
                       , dispatchCommand :: EventM Name OperationalState ()
                       , commandRequiresProfMode :: ProfilingReq
                       -- ^ The command requires that the debuggee is in a specific
                       -- profiling mode.
                       -- For example, we can only filter by eras if the program
                       -- has era profiling enabled.
                       }

mkCommand :: Text -> Vty.Event -> EventM Name OperationalState () -> Command
mkCommand desc ev dispatch = Command desc (Just ev) dispatch NoReq

mkCommand' :: Text -> EventM Name OperationalState () -> Command
mkCommand' desc dispatch = Command desc Nothing dispatch NoReq

mkFilterCmd :: Text -> Vty.Event -> EventM Name OperationalState () -> ProfilingReq -> Command
mkFilterCmd desc ev dispatch profMode = Command desc (Just ev) dispatch profMode

mkFilterCmd' :: Text -> EventM Name OperationalState () -> ProfilingReq -> Command
mkFilterCmd' desc dispatch profMode = Command desc Nothing dispatch profMode

isCmdEnabled :: GD.Version -> Command -> Bool
isCmdEnabled debuggeeVersion cmd = case commandRequiresProfMode cmd of
  NoReq -> True
  ReqErasProfiling ->
    Just GD.EraProfiling == GD.v_profiling debuggeeVersion
  ReqSomeProfiling ->
    GD.isProfiledRTS debuggeeVersion

isCmdDisabled :: GD.Version -> Command -> Bool
isCmdDisabled v cmd = not $ isCmdEnabled v cmd

data OverlayMode
  = KeybindingsShown
  | CommandPicker (ListPicker Name OperationalState Event Command)
  | HistoryPicker (ListPicker Name OperationalState Event HistoryCommand)
  | NoOverlay

invertInput :: FooterInputMode -> FooterInputMode
invertInput x@FClosureAddress{invert} = x{invert = not invert}
invertInput x@FInfoTableAddress{invert} = x{invert = not invert}
invertInput x@FConstructorName{invert} = x{invert = not invert}
invertInput x@FClosureName{invert} = x{invert = not invert}
invertInput x@FFilterEras{invert} = x{invert = not invert}
invertInput x@FFilterClosureSize{invert} = x{invert = not invert}
invertInput x@FFilterClosureType{invert} = x{invert = not invert}
invertInput x@FFilterCcId{invert} = x{invert = not invert}
invertInput x = x

formatFooterMode :: FooterInputMode -> Text
formatFooterMode FClosureAddress{invert} = (if invert then "!" else "") <> "address (0x..): "
formatFooterMode FInfoTableAddress{invert} = (if invert then "!" else "") <> "info table pointer (0x..): "
formatFooterMode FConstructorName{invert} = (if invert then "!" else "") <> "constructor name: "
formatFooterMode FClosureName{invert} = (if invert then "!" else "") <> "closure name: "
formatFooterMode FFilterEras{invert} = (if invert then "!" else "") <> "era range (<era>/<start-era>-<end-era>): "
formatFooterMode FFilterClosureSize{invert} = (if invert then "!" else "") <> "closure size (bytes): "
formatFooterMode FFilterClosureType{invert} = (if invert then "!" else "") <> "closure type: "
formatFooterMode FFilterCcId{invert} = (if invert then "!" else "") <> "CC Id: "
formatFooterMode FArrWordsSize = "size (bytes)>= "
formatFooterMode FDumpArrWords = "dump payload to file: "
formatFooterMode FSetResultSize = "search result limit (0 for infinity): "
formatFooterMode FSnapshot = "snapshot name: "
formatFooterMode (FSaveTree {}) = "filename: "

data ConnectedMode
  -- | Debuggee is running
  = RunningMode
  -- | Debuggee is paused and we're exploring the heap
  | PausedMode { _pausedMode :: OperationalState }

data RootsOrigin = DefaultRoots [(Text, Ptr)]
                 | SearchedRoots [(Text, Ptr)]


currentRoots :: RootsOrigin -> [(Text, Ptr)]
currentRoots (DefaultRoots cp) = cp
currentRoots (SearchedRoots cp) = cp

data OperationalState = OperationalState
    { _running_task :: Maybe (Async ())
    , _last_run_time :: Maybe (Text, NominalDiffTime)
    , _treeMode :: TreeMode
    , _keybindingsMode :: OverlayMode
    , _footerMode :: FooterMode
    , _rootsFrom  :: RootsOrigin
    , _viewCache :: RequestCache SearchCmd
    , _navigator :: HistoryNavigator SomeSearchCmd
    , _event_chan :: BChan Event
    , _resultSize :: Maybe Int
    , _filters :: [UIFilter]
    , _asyncSamplingInterval :: !Int
    -- ^ Update period of asynchronous heap traversals in microseconds
    , _version :: GD.Version
    }

clearFilters :: OperationalState -> OperationalState
clearFilters os = os { _filters = [] }

setFilters :: [UIFilter] -> OperationalState -> OperationalState
setFilters fs os = os {_filters = fs}

addFilters :: [UIFilter] -> OperationalState -> OperationalState
addFilters fs os = os {_filters = fs ++ _filters os}

parseEraRange :: Text -> Maybe EraRange
parseEraRange range = case T.splitOn "-" range of
  [nstr] -> case readMaybe (T.unpack nstr) of
    Just n -> Just $ EraRange n n
    Nothing -> Nothing
  [start,end] -> case (T.unpack start, T.unpack end) of
    ("", "") -> Just $ EraRange 0 maxBound
    ("", readMaybe -> Just e) -> Just $ EraRange 0 e
    (readMaybe -> Just s, "") -> Just $ EraRange s maxBound
    (readMaybe -> Just s, readMaybe -> Just e) -> Just $ EraRange s e
    _ -> Nothing
  _ -> Nothing


osSize :: OperationalState -> Int
osSize os = treeLength (_treeMode os)

pauseModeTree :: (forall a . (a -> Widget Name) -> IOTree a Name -> r) -> OperationalState -> r
pauseModeTree k os = case _treeMode os of
  GcRoots render r -> k render r
  Retainer render r -> k render r
  Profile render r -> k render r
  Strings render r -> k render r
  ArrWords render r -> k render r
  Thunk render r -> k render r

sendEvent :: Event -> EventM n OperationalState ()
sendEvent ev = do
  os <- Brick.get
  liftIO $ writeBChan (_event_chan os) ev

-- ----------------------------------------------------------------------------
-- Command History Picker
-- ----------------------------------------------------------------------------

data HistoryCommand = HistoryCommand
  { _historyItem :: SomeSearchCmd
  }

historyCommandDispatch :: HistoryCommand -> EventM Name OperationalState ()
historyCommandDispatch (HistoryCommand searchCmd) =
  sendEvent $ DoSearch searchCmd

-- ----------------------------------------------------------------------------
-- Request Cache Layer
-- ----------------------------------------------------------------------------

data CacheResult a
  = Partial a
  | Complete a

cacheValue :: CacheResult a -> a
cacheValue = \ case
  Partial a -> a
  Complete a -> a

data RequestCache f = RequestCache
  { _requestCache :: DMap f CacheResult
  }

lookupCache :: (GCompare f) => f a -> RequestCache f -> Maybe (CacheResult a)
lookupCache key (RequestCache cache) =
  DMap.lookup key cache

insertCache :: (GCompare f) => f a -> CacheResult a -> RequestCache f -> RequestCache f
insertCache key value (RequestCache cache) =
  RequestCache (DMap.insertWith insertIfNotComplete key value cache)
 where
  -- only insert the new value if the old value isn't already complete
  insertIfNotComplete new old = case old of
    Complete _ -> old
    Partial _ -> new

emptyCache :: RequestCache f
emptyCache = RequestCache
  { _requestCache = DMap.empty
  }

-- ----------------------------------------------------------------------------
-- History Navigation
-- ----------------------------------------------------------------------------

data NavigatorEvent
  = GoOneBackwards
  | GoOneForwards
  deriving (Show, Eq, Ord)

data HistoryNavigator a = HistoryNavigator
  { _history :: [a]
  , _current :: a
  , _future :: [a]
  }

navBackwards :: HistoryNavigator a -> (Maybe a, HistoryNavigator a)
navBackwards orig@(HistoryNavigator history cur future) =
  case history of
    [] -> (Nothing, orig)
    (h:hs) -> (Just h, HistoryNavigator hs h (cur:future))

navForward :: HistoryNavigator a -> (Maybe a, HistoryNavigator a)
navForward orig@(HistoryNavigator history cur future) =
  case future of
    [] -> (Nothing, orig)
    (f:fs) -> (Just f, HistoryNavigator (cur:history) f fs)

navPush :: a -> HistoryNavigator a -> HistoryNavigator a
navPush value orig =
  HistoryNavigator
    { _history = _current orig : _history orig
    , _current = value
    , _future = []
    }

newHistoryNavigator :: a -> HistoryNavigator a
newHistoryNavigator value = HistoryNavigator
  { _history = []
  , _current = value
  , _future = []
  }

navList :: HistoryNavigator a -> [a]
navList nav = Prelude.reverse (_future nav) <> [_current nav] <> _history nav

-- | Get a list of the current history stack with the index pointing
-- to the '_current' element.
navListWithIndex :: HistoryNavigator a -> (Int, [a])
navListWithIndex nav = (Prelude.length (_future nav), navList nav)

-- ----------------------------------------------------------------------------
-- TH derivations
-- ----------------------------------------------------------------------------

makeLenses ''AppState
makeLenses ''MajorState
makeLenses ''ClosureDetails
makeLenses ''ConnectedMode
makeLenses ''OperationalState
makeLenses ''RequestCache
makeLenses ''HistoryNavigator
makeLenses ''SocketInfo
makeLenses ''OverlayMode

deriveGEq ''SearchCmd
deriveGCompare ''SearchCmd

resetFooter :: OperationalState -> OperationalState
resetFooter l = (set footerMode FooterInfo l)

footerMessage :: Text -> OperationalState -> OperationalState
footerMessage t l = (set footerMode (FooterMessage t) l)

-- ----------------------------------------------------------------------------
-- Cache Layer
-- ----------------------------------------------------------------------------

cachedOrRun :: Bool -> SearchCmd a -> (a -> EventM n OperationalState ()) -> EventM n OperationalState () -> EventM n OperationalState ()
cachedOrRun fullSearch query ifFound notFound = do
  lookupCacheForCmd query >>= \ case
    Nothing ->
      notFound
    Just cachedValue
      | fullSearch ->
          case cachedValue of
            Complete value ->
              ifFound value
            Partial _ ->
              notFound
      | otherwise ->
          ifFound (cacheValue cachedValue)

lookupCacheForCmd :: SearchCmd a -> EventM n OperationalState (Maybe (CacheResult a))
lookupCacheForCmd query = do
  os <- Brick.get
  pure $ lookupCache query (os ^. viewCache)

cacheCompleteResult :: SearchCmd a -> a -> EventM n OperationalState ()
cacheCompleteResult key value = do
  os <- Brick.get
  Brick.put (os &
    viewCache %~ insertCache key (Complete value)
    )

cachePartialResult :: SearchCmd a -> a -> EventM n OperationalState ()
cachePartialResult key value = do
  os <- Brick.get
  Brick.put (os &
    viewCache %~ insertCache key (Partial value)
    )