packages feed

wireform-core-0.2.0.0: src/Wireform/Transport/Capabilities.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-unused-top-binds -Wno-unused-local-binds #-}

module Wireform.Transport.Capabilities
  ( SystemCapabilities (..)
  , IOUringFeatures (..)
  , NumaNodeInfo (..)
  , CoreTopology (..)
  , Placement (..)
  , detectCapabilities
  , recommendPlacement
  ) where

import qualified Control.Exception as CE
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.IORef
import Foreign.C.Types (CLong (..))
import System.IO.Unsafe (unsafePerformIO)

import Wireform.Parser
import Wireform.Parser.Internal (Pure)
import Wireform.Parser.Driver (parseByteString)

data SystemCapabilities = SystemCapabilities
  { capPageSize        :: !Int
  , capHugePageSizes   :: ![Int]
  , capHasIOUring      :: !Bool
  , capIOUringFeatures :: !IOUringFeatures
  , capNumaNodes       :: ![NumaNodeInfo]
  , capIsolatedCores   :: ![Int]
  , capCoreCount       :: !Int
  , capCoreTopology    :: !CoreTopology
  } deriving stock (Show)

data IOUringFeatures = IOUringFeatures
  { ioUringFeatureProvidedBuffers :: !Bool
  , ioUringFeatureMultishotRecv   :: !Bool
  , ioUringFeatureSQPoll          :: !Bool
  } deriving stock (Show)

data NumaNodeInfo = NumaNodeInfo
  { numaNodeId       :: !Int
  , numaNodeCores    :: ![Int]
  , numaNodeMemoryMB :: !Int
  } deriving stock (Show)

data CoreTopology = CoreTopology
  { topoCoreToPackage :: !(IntMap Int)
  , topoCoreToL3      :: !(IntMap Int)
  , topoSiblings      :: !(IntMap [Int])
  } deriving stock (Show)

data Placement = Placement
  { placementNumaNode :: !(Maybe Int)
  , placementCore     :: !(Maybe Int)
  } deriving stock (Show)

{-# NOINLINE cachedCapabilities #-}
cachedCapabilities :: IORef (Maybe SystemCapabilities)
cachedCapabilities = unsafePerformIO (newIORef Nothing)

detectCapabilities :: IO SystemCapabilities
detectCapabilities = do
  cached <- readIORef cachedCapabilities
  case cached of
    Just c  -> pure c
    Nothing -> do
      c <- detectCapabilitiesUncached
      writeIORef cachedCapabilities (Just c)
      pure c

detectCapabilitiesUncached :: IO SystemCapabilities
detectCapabilitiesUncached = do
  ps <- getPageSize
  hugeSizes <- getHugePageSizes
  cores <- getCoreCount
  isolated <- getIsolatedCores
  hasUring <- getHasIOUring
  uringFeats <- getIOUringFeatures
  numa <- getNumaNodes
  pure SystemCapabilities
    { capPageSize        = ps
    , capHugePageSizes   = hugeSizes
    , capHasIOUring      = hasUring
    , capIOUringFeatures = uringFeats
    , capNumaNodes       = numa
    , capIsolatedCores   = isolated
    , capCoreCount       = cores
    , capCoreTopology    = CoreTopology IntMap.empty IntMap.empty IntMap.empty
    }

recommendPlacement :: Maybe Int -> IO Placement
recommendPlacement _fdNumaNode = pure (Placement Nothing Nothing)

------------------------------------------------------------------------
-- Parsers for /proc and /sys files (using our own parser)
------------------------------------------------------------------------

type P = Parser Pure ()

-- | Parse a CPU list like "0-3,5,7-9" into [0,1,2,3,5,7,8,9].
pCpuList :: P [Int]
pCpuList = do
  first <- pRange
  rest  <- many (word8 0x2C *> pRange)  -- ','
  pure (concat (first : rest))
  where
    pRange :: P [Int]
    pRange = do
      lo <- anyAsciiDecimalInt
      withOption (word8 0x2D *> anyAsciiDecimalInt) -- '-'
        (\hi -> pure [lo..hi])
        (pure [lo])

-- | Parse a decimal integer, skipping leading whitespace.
pDecimal :: P Int
pDecimal = skipWs *> anyAsciiDecimalInt

-- | Skip ASCII whitespace.
skipWs :: P ()
skipWs = skipMany (satisfyAscii (\c -> c == ' ' || c == '\t' || c == '\n' || c == '\r'))

-- | Skip to after a keyword, then parse what follows.
pAfterKeyword :: ByteString -> P a -> P a
pAfterKeyword kw p = go
  where
    go = (byteString kw *> p) <|> (anyWord8 *> go)

-- | Count lines starting with a given prefix.
pCountPrefix :: ByteString -> P Int
pCountPrefix pfx = go 0
  where
    go !n = (byteString pfx *> pSkipLine *> go (n + 1))
        <|> (pSkipLine *> go n)  -- pSkipLine fails at EOF, terminating the loop
        <|> pure n

-- | Skip bytes until newline (inclusive). Fails at EOF.
pSkipLine :: P ()
pSkipLine = go
  where
    go = withAnyWord8 \w ->
      if w == 0x0A then pure () else go

-- | Parse "Linux version X.Y.Z ..." from /proc/version.
pKernelVersion :: P (Int, Int)
pKernelVersion = pAfterKeyword "version " $ do
  major <- anyAsciiDecimalInt
  word8 0x2E  -- '.'
  minor <- anyAsciiDecimalInt
  pure (major, minor)

-- | Parse "Hugepagesize:    2048 kB" lines from /proc/meminfo.
pHugePageSize :: P [Int]
pHugePageSize = go []
  where
    go !acc =
      (do byteString "Hugepagesize:"
          skipWs
          n <- anyAsciiDecimalInt
          skipWs
          byteString "kB"
          pSkipLine
          go (n * 1024 : acc))
      <|> (pSkipLine *> go acc)
      <|> pure (reverse acc)

-- | Parse "MemTotal:       12345 kB" from a NUMA node's meminfo.
pNodeMemMB :: P Int
pNodeMemMB = go
  where
    go = (do byteString "MemTotal:"
             skipWs
             n <- anyAsciiDecimalInt
             pure (n `div` 1024))
     <|> (pSkipLine *> go)
     <|> pure 0

------------------------------------------------------------------------
-- File reading + parsing
------------------------------------------------------------------------

tryReadFileBS :: FilePath -> IO (Maybe ByteString)
tryReadFileBS path = do
  r <- CE.try @CE.SomeException (BS.readFile path)
  case r of
    Left _  -> pure Nothing
    Right s -> pure (Just s)

runP :: P a -> ByteString -> Maybe a
runP p bs = case parseByteString p bs of
  Right a -> Just a
  Left _  -> Nothing

------------------------------------------------------------------------
-- Platform-specific detection
------------------------------------------------------------------------

foreign import ccall unsafe "hs_page_size"
  c_page_size :: IO CLong

getPageSize :: IO Int
getPageSize = fromIntegral <$> c_page_size

getCoreCount :: IO Int
getCoreCount = do
#if defined(linux_HOST_OS)
  r <- tryReadFileBS "/proc/cpuinfo"
  case r of
    Nothing -> pure 1
    Just bs -> pure $ max 1 (maybe 1 id (runP (pCountPrefix "processor") bs))
#else
  pure 1
#endif

getHugePageSizes :: IO [Int]
getHugePageSizes = do
#if defined(linux_HOST_OS)
  r <- tryReadFileBS "/proc/meminfo"
  case r of
    Nothing -> pure []
    Just bs -> pure $ maybe [] id (runP pHugePageSize bs)
#else
  pure []
#endif

getIsolatedCores :: IO [Int]
getIsolatedCores = do
#if defined(linux_HOST_OS)
  r <- tryReadFileBS "/sys/devices/system/cpu/isolated"
  case r of
    Nothing -> pure []
    Just bs -> pure $ maybe [] id (runP (skipWs *> pCpuList) (BS.filter (/= 0x0A) bs))
#else
  pure []
#endif

getHasIOUring :: IO Bool
getHasIOUring = do
#if defined(linux_HOST_OS)
  ver <- kernelVersion
  pure (ver >= (5, 1))
#else
  pure False
#endif

getIOUringFeatures :: IO IOUringFeatures
getIOUringFeatures = do
#if defined(linux_HOST_OS)
  hasUring <- getHasIOUring
  if not hasUring
    then pure (IOUringFeatures False False False)
    else do
      ver <- kernelVersion
      pure IOUringFeatures
        { ioUringFeatureProvidedBuffers = ver >= (5, 19)
        , ioUringFeatureMultishotRecv   = ver >= (6, 0)
        , ioUringFeatureSQPoll          = ver >= (5, 1)
        }
#else
  pure (IOUringFeatures False False False)
#endif

getNumaNodes :: IO [NumaNodeInfo]
getNumaNodes = do
#if defined(linux_HOST_OS)
  r <- tryReadFileBS "/sys/devices/system/node/online"
  case r of
    Nothing -> pure []
    Just bs -> do
      let nodeIds = maybe [] id (runP (skipWs *> pCpuList) (BS.filter (/= 0x0A) bs))
      mapM getNodeInfo nodeIds
#else
  pure []
#endif
  where
    getNodeInfo nid = do
      cpuList <- tryReadFileBS ("/sys/devices/system/node/node" <> bsShow nid <> "/cpulist")
      memInfo <- tryReadFileBS ("/sys/devices/system/node/node" <> bsShow nid <> "/meminfo")
      let cores = case cpuList of
            Nothing -> []
            Just bs -> maybe [] id (runP pCpuList (BS.filter (/= 0x0A) bs))
          memMB = case memInfo of
            Nothing -> 0
            Just bs -> maybe 0 id (runP pNodeMemMB bs)
      pure (NumaNodeInfo nid cores memMB)

    bsShow :: Int -> FilePath
    bsShow = show

kernelVersion :: IO (Int, Int)
kernelVersion = do
#if defined(linux_HOST_OS)
  r <- tryReadFileBS "/proc/version"
  pure $ case r of
    Nothing -> (0, 0)
    Just bs -> maybe (0, 0) id (runP pKernelVersion bs)
#else
  pure (0, 0)
#endif