packages feed

fragr-0.1.0.0: src/Fragr/Snapshot.hs

-- | Read-only introspection of the graph, for debug output and testing.
module Fragr.Snapshot
  ( Snapshot (..)
  , PassInfo (..)
  , NodeInfo (..)
  , EntryInfo (..)
  , RetireInfo (..)
  , snapshot
  ) where

import Control.Monad.IO.Class (MonadIO (..))
import Data.Foldable (toList)
import Data.IORef (readIORef)
import Data.IntMap.Strict qualified as IntMap
import Data.Maybe (maybeToList)
import Data.Sequence qualified as Seq
import Data.Text (Text)
import Data.Traversable (for)
import Data.Word (Word64)

import Fragr.Compile (canExecute)
import Fragr.Graph (FrameGraph (..), PassNode (..), ResourceEntry (..), ResourceNode (..), describeEntry, touchedNodes)
import Fragr.Resource (Access)
import Fragr.Sync (Compiled (..), PassSync)
import Fragr.Types (QueueId, SomeHandle)

-- | Read-only view of the whole graph, for debug output and testing.
data Snapshot = Snapshot
  { passes :: [PassInfo]
  , nodes :: [NodeInfo]
  , entries :: [EntryInfo]
  , retires :: [RetireInfo]
  }
  deriving stock (Show)

-- | Read-only view of a 'PassNode' (compile-phase state included).
data PassInfo = PassInfo
  { passId :: Int
  , name :: Text
  , creates :: [SomeHandle]
  , reads :: [Access]
  , writes :: [Access]
  , sideEffect :: Bool
  , queue :: QueueId
  , refCount :: Int
  , canExecute :: Bool
  , sync :: Maybe PassSync
  -- ^ the pass's sync schedule after 'Fragr.Compile.compile' ('Nothing' if culled)
  , level :: Maybe Int
  {- ^ The pass's dependency level: the longest chain of ordering edges
  reaching it, counted in edges ('Nothing' if culled). Meaningful only
  after 'Fragr.Compile.compile'.

  Passes sharing a level have no path between them, so nothing orders them
  and they may overlap whatever their queues. The converse does not hold: a
  level gap is a path's /length/, not a path — it never proves two passes
  cannot overlap. The level count is the critical path, and the widest level
  is the concurrency the graph permits.
  -}
  , antiAfter :: [Int]
  {- ^ The executing passes that rename a resource this one reads, and so
  must run after it, though no data flows to them (write-after-read). Empty
  if culled, and before 'Fragr.Compile.compile'.
  -}
  }
  deriving stock (Show)

{- | Read-only view of a transient entry's deferred-reclamation
requirements: the per-queue timeline value that must be reached before it
may be reclaimed. Imports never appear — the graph does not reclaim them.
-}
data RetireInfo = RetireInfo
  { entryId :: Int
  , requirements :: [(QueueId, Word64)]
  }
  deriving stock (Show)

-- | Read-only view of a 'ResourceNode'.
data NodeInfo = NodeInfo
  { nodeId :: Int
  , name :: Text
  , resourceId :: Int
  , version :: Int
  , refCount :: Int
  }
  deriving stock (Show)

-- | Read-only view of a 'ResourceEntry'.
data EntryInfo = EntryInfo
  { entryId :: Int
  , version :: Int
  , imported :: Bool
  , description :: Text
  , live :: Maybe (Int, Int)
  {- ^ The entry's live range over the executing passes, as inclusive
  positions in execution order ('Nothing' when no executing pass touches
  it). Meaningful only after 'Fragr.Compile.compile'.

  Disjoint ranges are the raw material for plan-time aliasing — placing
  two entries in one allocation, decided before recording, with no
  reclamation bookkeeping at run time. Two conditions ride on the backend,
  because the range alone does not carry them:

  * Only entries the graph owns may be aliased: an import's memory belongs
    to the caller, whatever its range says ('imported' tells them apart).

  * Positions order the executing passes globally, but passes on different
    queues run concurrently, so disjoint positions do not imply disjoint
    lifetimes across queues. Only ranges whose passes are ordered — one
    queue, or a pair the schedule already synchronizes — are safe to alias.

  The backend still owes the aliasing barrier at each handover (the second
  user's contents are undefined until it writes them).
  -}
  }
  deriving stock (Show)

{- |
Capture the current state of the graph. Callable at any time, but the
reference counts and culling state are meaningful only after 'compile'.
-}
{-# INLINEABLE snapshot #-}
snapshot :: (MonadIO m) => FrameGraph ctx alloc -> m Snapshot
snapshot g = liftIO do
  passes <- readIORef g.passesRef
  nodes <- readIORef g.nodesRef
  entries <- readIORef g.entriesRef
  compiled <- readIORef g.compiledRef
  let
    refsMap = maybe IntMap.empty (.passRefs) compiled
    passRef p = IntMap.findWithDefault 0 p.passId refsMap
    nodeRef n = maybe 0 (\c -> IntMap.findWithDefault 0 n.nodeId c.nodeRefs) compiled
    passSyncOf p = compiled >>= \c -> IntMap.lookup p.passId c.passSync
    passLevelOf p = compiled >>= \c -> IntMap.lookup p.passId c.passLevel
    passAntiOf p = maybe [] (\c -> IntMap.findWithDefault [] p.passId c.passAnti) compiled
    retireInfos = maybe [] retiresOf compiled
    retiresOf c = do
      (e, reqs) <- IntMap.toList c.entryRetire
      pure RetireInfo{entryId = e, requirements = reqs}
    -- Live ranges: for each entry, the first and last executing pass that
    -- touches any of its versions, as positions in execution order.
    entryOfNode = IntMap.fromList do
      n <- toList nodes
      pure (n.nodeId, n.resourceId)
    executing = maybe [] (\c -> filter (canExecute c.passRefs) (toList passes)) compiled
    liveRanges = IntMap.fromListWith (\(a, b) (c, d) -> (min a c, max b d)) do
      (pos, p) <- zip [0 ..] executing
      node <- touchedNodes p
      rid <- maybeToList (IntMap.lookup node entryOfNode)
      pure (rid, (pos, pos))
  entryInfos <- for (toList entries) \entry -> do
    v <- readIORef entry.versionRef
    pure
      EntryInfo
        { entryId = entry.entryId
        , version = v
        , imported = entry.imported
        , description = describeEntry entry.payload
        , live = IntMap.lookup entry.entryId liveRanges
        }
  pure
    Snapshot
      { passes = do
          p <- toList passes
          pure
            PassInfo
              { passId = p.passId
              , name = p.name
              , creates = p.creates
              , reads = p.reads
              , writes = p.writes
              , sideEffect = p.sideEffect
              , queue = p.queue
              , refCount = passRef p
              , canExecute = canExecute refsMap p
              , sync = passSyncOf p
              , level = passLevelOf p
              , antiAfter = passAntiOf p
              }
      , nodes = do
          n <- toList nodes
          pure
            NodeInfo
              { nodeId = n.nodeId
              , name = (entries `Seq.index` n.resourceId).name
              , resourceId = n.resourceId
              , version = n.version
              , refCount = nodeRef n
              }
      , entries = entryInfos
      , retires = retireInfos
      }