packages feed

crucible-0.7: src/Lang/Crucible/Simulator/Profiling.hs

-----------------------------------------------------------------------
-- |
-- Module           : Lang.Crucible.Simulator.Profiling
-- Description      : Profiling support for the simulator
-- Copyright        : (c) Galois, Inc 2018
-- License          : BSD3
-- Maintainer       : Rob Dockins <rdockins@galois.com>
-- Stability        : provisional
--
------------------------------------------------------------------------
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
module Lang.Crucible.Simulator.Profiling
  ( profilingFeature
  , ProfilingOptions(..)
  , EventFilter(..)
  , profilingEventFilter
  , newProfilingTable
  , recordSolverEvent
  , startRecordingSolverEvents
  , enterEvent
  , exitEvent
  , inProfilingFrame
  , readMetrics
  , CrucibleProfile(..)
  , readProfilingState
  , writeProfileReport

    -- * Profiling data structures
  , CGEvent(..)
  , CGEventType(..)
  , ProfilingTable(..)
  , Lang.Crucible.Simulator.ExecutionTree.Metric(..)
  , Metrics(..)
  , symProUIJSON
  , symProUIString
  ) where

import qualified Control.Exception as Ex
import           Control.Lens
import           Control.Monad ((<=<), when)
import           Data.Foldable (toList)
import           Data.Hashable
import           Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet
import           Data.IORef
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import           Data.Parameterized.TraversableF
import           Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import           Data.Text (Text)
import qualified Data.Text as Text
import           Data.Time.Clock
import           Data.Time.Clock.POSIX
import           Data.Time.Format
import           System.IO (withFile, IOMode(..), hPutStrLn)
import           Text.JSON
import           GHC.Generics (Generic)


import           What4.FunctionName
import           What4.Interface
import           What4.ProgramLoc
import           What4.SatResult

import           Lang.Crucible.Backend
import           Lang.Crucible.CFG.Core
import           Lang.Crucible.Simulator.CallFrame
import           Lang.Crucible.Simulator.EvalStmt
import           Lang.Crucible.Simulator.ExecutionTree
import           Lang.Crucible.Simulator.Operations


data Metrics f =
  Metrics
  { metricSplits   :: f Integer
  , metricMerges   :: f Integer
  , metricAborts   :: f Integer
  , metricSolverStats :: f Statistics
  , metricExtraMetrics :: f (Map Text Integer)
  }

deriving instance Show (Metrics Identity)
deriving instance Generic (Metrics Identity)

traverseF_metrics :: Applicative m =>
  (forall s. e s -> m (f s)) ->
  Metrics e -> m (Metrics f)
traverseF_metrics h (Metrics x1 x2 x3 x4 x5) =
  Metrics <$> h x1 <*> h x2 <*> h x3 <*> h x4 <*> h x5

instance FunctorF Metrics where
  fmapF = fmapFDefault
instance FoldableF Metrics where
  foldMapF = foldMapFDefault
instance TraversableF Metrics where
  traverseF = traverseF_metrics

metricsToJSON :: Metrics Identity -> UTCTime -> JSValue
metricsToJSON m time = JSObject $ toJSObject $
    [ ("time", utcTimeToJSON time)
    , ("allocs", showJSON $ statAllocs $ solverStats )
    , ("paths", showJSON $ runIdentity $ metricSplits m )
    , ("merge-count", showJSON $ runIdentity $ metricMerges m )
    , ("abort-count", showJSON $ runIdentity $ metricAborts m )
    , ("non-linear-count", showJSON $ statNonLinearOps $ solverStats )
    ] ++ [ (Text.unpack k, showJSON v)
         | (k, v) <- Map.toList $ runIdentity $ metricExtraMetrics m ]
    where
      solverStats = runIdentity $ metricSolverStats m


data CGEventType = ENTER | EXIT | BLOCK | BRANCH
 deriving (Show,Eq,Ord,Generic)

data CGEvent =
  CGEvent
  { cgEvent_fnName   :: FunctionName
  , cgEvent_source   :: Maybe Position
  , cgEvent_callsite :: Maybe Position
  , cgEvent_type     :: CGEventType
  , cgEvent_blocks   :: [String]
  , cgEvent_metrics  :: Metrics Identity
  , cgEvent_time     :: UTCTime
  , cgEvent_id       :: Integer
  }
 deriving (Show, Generic)

-- FIXME... figure out why the UI seems to want this in milliseconds...
utcTimeToJSON :: UTCTime -> JSValue
utcTimeToJSON t =
  showJSON (1e3 * (fromRational $ toRational $ utcTimeToPOSIXSeconds t) :: Double)

cgEventTypeToJSON :: CGEventType -> JSValue
cgEventTypeToJSON ENTER = showJSON "ENTER"
cgEventTypeToJSON EXIT  = showJSON "EXIT"
cgEventTypeToJSON BLOCK  = showJSON "BLOCK"
cgEventTypeToJSON BRANCH  = showJSON "BRANCH"

cgEventToJSON :: CGEvent -> JSValue
cgEventToJSON ev = JSObject $ toJSObject $
    [ ("function", showJSON $ functionName $ cgEvent_fnName ev)
    , ("type", cgEventTypeToJSON (cgEvent_type ev))
    , ("metrics", metricsToJSON (cgEvent_metrics ev) (cgEvent_time ev))
    ]
    ++
    (case cgEvent_source ev of
      Nothing -> []
      Just p -> [("source", positionToJSON p)])
    ++
    (case cgEvent_callsite ev of
      Nothing -> []
      Just p -> [("callsite", positionToJSON p)])
    ++
    (case cgEvent_blocks ev of
      [] -> []
      xs -> [("blocks", showJSON xs)])

positionToJSON :: Position -> JSValue
positionToJSON p = showJSON $ show $ p

solverEventToJSON :: (UTCTime, SolverEvent) -> JSValue
solverEventToJSON (time, ev) =
   case ev of
     SolverStartSATQuery ssq -> startSATQueryToJSON time ssq
     SolverEndSATQuery esq -> endSATQueryToJSON time esq

startSATQueryToJSON :: UTCTime -> SolverStartSATQuery -> JSValue
startSATQueryToJSON time ssq = JSObject $ toJSObject
  [ ("type", showJSON "start")
  , ("time", utcTimeToJSON time)
  , ("part", showJSON "solver")
  , ("solver", showJSON $ satQuerySolverName ssq)
  , ("description", showJSON $ satQueryReason ssq)
  ]

endSATQueryToJSON :: UTCTime -> SolverEndSATQuery -> JSValue
endSATQueryToJSON time esq = JSObject $ toJSObject $
  [ ("type", showJSON "finish")
  , ("time", utcTimeToJSON time)
  ] ++
  case (satQueryResult esq) of
    Sat{} -> [("sat", showJSON True)]
    Unsat{} -> [("sat", showJSON False)]
    Unknown{} -> []


callGraphJSON :: UTCTime -> Metrics Identity -> Seq CGEvent -> JSValue
callGraphJSON now m evs = JSObject $ toJSObject
  [ ("type", showJSON "callgraph")
  , ("events", JSArray allEvs)
  ]

 where
 allEvs = map cgEventToJSON (toList evs ++ closingEvents now m evs)


symProUIString :: String -> String -> ProfilingTable -> IO String
symProUIString nm source tbl =
  do js <- symProUIJSON nm source tbl
     return ("data.receiveData("++ encode js ++ ");")


symProUIJSON :: String -> String -> ProfilingTable -> IO JSValue
symProUIJSON nm source tbl =
  do now <- getCurrentTime
     m <- readMetrics tbl
     evs <- readIORef (callGraphEvents tbl)
     solverEvs <- readIORef (solverEvents tbl)
     return $ JSArray $
       [ JSObject $ toJSObject $ metadata now
       , callGraphJSON now m evs
       , JSObject $ toJSObject $ solver_calls solverEvs
       ]
 where
 solver_calls evs  =
   [ ("type", showJSON "solver-calls")
   , ("events", JSArray $ map solverEventToJSON $ toList evs)
   ]

 metadata now =
   [ ("type", showJSON "metadata")
   , ("form", showJSON "")
   , ("name", showJSON nm)
   , ("source", showJSON source)
   , ("time", showJSON $ formatTime defaultTimeLocale rfc822DateFormat now)
   , ("version", showJSON "1")
   ]

data ProfilingTable =
  ProfilingTable
  { callGraphEvents :: IORef (Seq CGEvent)
  , eventDedups :: IORef (HashSet EventDedup)
  , metrics :: Metrics IORef
  , eventIDRef :: IORef Integer
  , solverEvents :: IORef (Seq (UTCTime, SolverEvent))
  }

data EventFilter =
  EventFilter
  { recordProfiling :: Bool
  , recordCoverage :: Bool
  }

-- | An `EventFilter` that enables only Crucible profiling.
profilingEventFilter :: EventFilter
profilingEventFilter = EventFilter
  { recordProfiling = True
  , recordCoverage = False
  }

data CrucibleProfile =
  CrucibleProfile
  { crucibleProfileTime :: UTCTime
  , crucibleProfileCGEvents :: [CGEvent]
  , crucibleProfileSolverEvents :: [SolverEvent]
  } deriving (Show, Generic)

data EventDedup =
    BlockDedup FunctionName String
  | BranchDedup FunctionName [String]
  deriving (Eq, Generic)

instance Hashable EventDedup

readProfilingState :: ProfilingTable -> IO (UTCTime, [CGEvent], [(UTCTime, SolverEvent)])
readProfilingState tbl =
  do now <- getCurrentTime
     m <- readMetrics tbl
     cgevs <- readIORef (callGraphEvents tbl)
     sevs  <- readIORef (solverEvents tbl)
     return (now, toList cgevs ++ closingEvents now m cgevs, toList sevs)

openEventFrames :: Seq CGEvent -> [CGEvent]
openEventFrames = go []
 where
 go :: [CGEvent] -> Seq CGEvent -> [CGEvent]
 go xs Seq.Empty = xs
 go xs (e Seq.:<| es) =
   case cgEvent_type e of
     ENTER -> go (e:xs) es
     EXIT  -> go (tail xs) es
     _     -> go xs es

openToCloseEvent :: UTCTime -> Metrics Identity -> CGEvent -> CGEvent
openToCloseEvent now m cge =
  cge
  { cgEvent_type = EXIT
  , cgEvent_metrics = m
  , cgEvent_time = now
  }

closingEvents :: UTCTime -> Metrics Identity -> Seq CGEvent -> [CGEvent]
closingEvents now m = map (openToCloseEvent now m) . openEventFrames

newProfilingTable :: IO ProfilingTable
newProfilingTable =
  do m <- Metrics <$> newIORef 0
                  <*> newIORef 0
                  <*> newIORef 0
                  <*> newIORef zeroStatistics
                  <*> newIORef Map.empty
                        -- TODO: Find the actual custom metrics and
                        -- initialize them to zero.  Needs a change in
                        -- the Crux API; currently 'newProfilingTable'
                        -- is called before the custom metrics are set
                        -- up.  For now, the extra metrics are missing
                        -- from the very earliest events in the log;
                        -- the JS front end works around this by
                        -- assuming that any missing value is a zero.
     evs <- newIORef mempty
     dedups <- newIORef mempty
     idref <- newIORef 0
     solverevs <- newIORef mempty
     let tbl = ProfilingTable evs dedups m idref solverevs
     return tbl

recordSolverEvent :: ProfilingTable -> SolverEvent -> IO ()
recordSolverEvent tbl ev = do
  do now <- getCurrentTime
     xs <- readIORef (solverEvents tbl)
     writeIORef (solverEvents tbl) (xs Seq.|> (now,ev))

startRecordingSolverEvents ::
  IsSymInterface sym =>
  sym ->
  ProfilingTable ->
  IO ()
startRecordingSolverEvents sym tbl =
  setSolverLogListener sym (Just (recordSolverEvent tbl))

nextEventID :: ProfilingTable -> IO Integer
nextEventID tbl =
  do i <- readIORef (eventIDRef tbl)
     writeIORef (eventIDRef tbl) $! (i+1)
     return i

dedupEvent :: ProfilingTable -> EventDedup -> IO () -> IO ()
dedupEvent tbl evt f =
  do seen <- readIORef (eventDedups tbl)
     when (not $ HashSet.member evt seen) $
       do writeIORef (eventDedups tbl) (HashSet.insert evt seen)
          f

inProfilingFrame ::
  ProfilingTable ->
  FunctionName ->
  Maybe ProgramLoc ->
  IO a ->
  IO a
inProfilingFrame tbl nm mloc action =
  Ex.bracket_
    (enterEvent tbl nm mloc)
    (exitEvent tbl nm)
    action

enterEvent ::
  ProfilingTable ->
  FunctionName ->
  Maybe ProgramLoc ->
  IO ()
enterEvent tbl nm callLoc =
  do now <- getCurrentTime
     m <- readMetrics tbl
     i <- nextEventID tbl
     let p = fmap plSourceLoc callLoc
     modifyIORef' (callGraphEvents tbl) (Seq.|> CGEvent nm Nothing p ENTER [] m now i)

readMetrics :: ProfilingTable -> IO (Metrics Identity)
readMetrics tbl = traverseF (pure . Identity <=< readIORef) (metrics tbl)

exitEvent ::
  ProfilingTable ->
  FunctionName ->
  IO ()
exitEvent tbl nm =
  do now <- getCurrentTime
     m <- traverseF (pure . Identity <=< readIORef) (metrics tbl)
     i <- nextEventID tbl
     modifyIORef' (callGraphEvents tbl) (Seq.|> CGEvent nm Nothing Nothing EXIT [] m now i)

blockEvent ::
  ProfilingTable ->
  FunctionName ->
  Maybe ProgramLoc ->
  Some (BlockID blocks) ->
  IO ()
blockEvent tbl nm callLoc blk =
  dedupEvent tbl (BlockDedup nm (show blk)) $
  do now <- getCurrentTime
     m <- readMetrics tbl
     i <- nextEventID tbl
     let p = fmap plSourceLoc callLoc
     modifyIORef' (callGraphEvents tbl)
       (Seq.|> CGEvent nm Nothing p BLOCK [show blk] m now i)

branchEvent ::
  ProfilingTable ->
  FunctionName ->
  Maybe ProgramLoc ->
  [Some (BlockID blocks)] ->
  IO ()
branchEvent tbl nm callLoc blks =
  dedupEvent tbl (BranchDedup nm (map show blks)) $
  do now <- getCurrentTime
     m <- readMetrics tbl
     i <- nextEventID tbl
     let p = fmap plSourceLoc callLoc
     modifyIORef' (callGraphEvents tbl)
       (Seq.|> CGEvent nm Nothing p BRANCH (map show blks) m now i)


updateProfilingTable ::
  IsExprBuilder sym =>
  ProfilingTable ->
  EventFilter ->
  ExecState p sym ext rtp ->
  IO ()
updateProfilingTable tbl filt exst = do
  when (recordProfiling filt) $ do
    let sym = execStateContext exst ^. ctxSymInterface
    stats <- getStatistics sym
    writeIORef (metricSolverStats (metrics tbl)) stats

    case execStateSimState exst of
      Just (SomeSimState simst) -> do
        let extraMetrics = execStateContext exst ^. profilingMetrics
        extraMetricValues <- traverse (\m -> runMetric m simst) extraMetrics
        writeIORef (metricExtraMetrics (metrics tbl)) extraMetricValues
      Nothing ->
        -- We can't poll custom metrics at the VERY beginning or end of
        -- execution because 'ResultState' and 'InitialState' have no
        -- 'SimState' values. This is probably fine---we still get to
        -- poll them before and after the top-level function being
        -- simulated, since it gets a 'CallState' and 'ReturnState' like
        -- any other function.
        return ()

    case exst of
      InitialState _ _ _ _ _ ->
        enterEvent tbl startFunctionName Nothing
      CallState _rh call st ->
        enterEvent tbl (resolvedCallName call) (st^.stateLocation)
      ReturnState nm _ _ _ ->
        exitEvent tbl nm
      TailCallState _ call st ->
        do exitEvent tbl (st^.stateTree.actFrame.gpValue.frameFunctionName)
           enterEvent tbl (resolvedCallName call) (st^.stateLocation)
      SymbolicBranchState{} ->
        modifyIORef' (metricSplits (metrics tbl)) succ
      AbortState{} ->
        modifyIORef' (metricAborts (metrics tbl)) succ
      UnwindCallState _ _ st ->
        exitEvent tbl (st^.stateTree.actFrame.gpValue.frameFunctionName)
      BranchMergeState tgt st ->
        when (isMergeState tgt st)
             (modifyIORef' (metricMerges (metrics tbl)) succ)
      _ -> return ()

  when (recordCoverage filt) $
    case exst of
      ControlTransferState res st ->
        let funcName = st^.stateTree.actFrame.gpValue.frameFunctionName in
        case res of
          ContinueResumption (ResolvedJump blk _) ->
            blockEvent tbl funcName (st^.stateLocation) (Some blk)
          CheckMergeResumption (ResolvedJump blk _) ->
            blockEvent tbl funcName (st^.stateLocation) (Some blk)
          _ -> return ()
      RunningState (RunBlockEnd _) st ->
        let funcName = st^.stateTree.actFrame.gpValue.frameFunctionName in
        case st^.stateTree.actFrame.gpValue.crucibleSimFrame.frameStmts of
          TermStmt loc term
            | Just blocks <- termStmtNextBlocks term,
              length blocks >= 2 ->
                branchEvent tbl funcName (Just loc) blocks
          _ -> return ()
      _ -> return ()

isMergeState ::
  CrucibleBranchTarget f args ->
  SimState p sym ext root f args ->
  Bool
isMergeState tgt st =
  case st^.stateTree.actContext of
    VFFBranch _ctx _assume_frame _loc _p other_branch tgt'
      | Just Refl <- testEquality tgt tgt' ->
          case other_branch of
            VFFActivePath{} -> False
            VFFCompletePath{} -> True
    VFFPartial _ctx _loc _p _ar NeedsToBeAborted -> True
    _ -> False


data ProfilingOptions =
  ProfilingOptions
  { periodicProfileInterval :: NominalDiffTime
  , periodicProfileAction   :: ProfilingTable -> IO ()
  }


-- | Write a profiling report file in the JS/JSON format expected by tye symProUI front end.
writeProfileReport ::
  FilePath {- ^ File to write -} ->
  String {- ^ "name" for the report -} ->
  String {- ^ "source" for the report -} ->
  ProfilingTable {- ^ profiling data to populate the report -} ->
  IO ()
writeProfileReport fp name source tbl =
   withFile fp WriteMode $ \h -> hPutStrLn h =<< symProUIString name source tbl

-- | This feature will pay attention to function call entry/exit events
--   and track the elapsed time and various other metrics in the given
--   profiling table.  The @ProfilingOptions@ can be used to export
--   intermediate profiling data at regular intervals, if desired.
profilingFeature ::
  ProfilingTable ->
  EventFilter ->
  Maybe ProfilingOptions ->
  IO (GenericExecutionFeature sym)

profilingFeature tbl filt Nothing =
  return $ GenericExecutionFeature $ \exst -> updateProfilingTable tbl filt exst >> return ExecutionFeatureNoChange

profilingFeature tbl filt (Just profOpts) =
  do startTime <- getCurrentTime
     stateRef <- newIORef (computeNextState startTime)
     return (feat stateRef)

 where
 feat stateRef = GenericExecutionFeature $ \exst ->
        do updateProfilingTable tbl filt exst
           deadline <- readIORef stateRef
           now <- getCurrentTime
           if deadline >= now then
             return ExecutionFeatureNoChange
           else
             do periodicProfileAction profOpts tbl
                writeIORef stateRef (computeNextState now)
                return ExecutionFeatureNoChange

 computeNextState :: UTCTime -> UTCTime
 computeNextState lastOutputTime = addUTCTime (periodicProfileInterval profOpts) lastOutputTime