mem-info-0.4.0.0: src/System/MemInfo/SysInfo.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : System.MemInfo.SysInfo
Copyright : (c) 2022 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3
This module provides data types that
- define memory reports (cf @'ReportBud'@) and
- provide info about the system where the report will run (cf @'KernelVersion'@,
@'SwapFlaw'@ and @'RamFlaw'@).
along with functions that use these types.
-}
module System.MemInfo.SysInfo (
-- * define reports
ReportBud (..),
mkReportBud,
-- * indicate calculation flaws
RamFlaw (..),
SwapFlaw (..),
checkForFlaws,
fmtRamFlaws,
fmtSwapFlaws,
-- * system kernel version
KernelVersion,
parseKernelVersion,
readKernelVersion,
fickleSharing,
) where
import Control.Monad ((>=>))
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.Text.Read as Text
import Fmt ((+|), (|+))
import System.MemInfo.Prelude
-- | Represents a version of @Linux@ kernel
type KernelVersion = (Natural, Natural, Natural)
{- | On linux kernels before smaps became available, there was no reliable way to
determine how much of a processes memory was shared
http://lkml.org/lkml/2005/7/6/250
-}
fickleSharing :: KernelVersion -> Bool
fickleSharing k = k >= (2, 6, 1) && k <= (2, 6, 9)
-- | Determines the version of the Linux kernel on the current system.
readKernelVersion :: FilePath -> IO (Maybe KernelVersion)
readKernelVersion = fmap parseKernelVersion . Text.readFile . kernelVersionPath
kernelVersionPath :: FilePath -> FilePath
kernelVersionPath root = "" +| root |+ "/sys/kernel/osrelease"
-- | Parses @Text@ into a @'KernelVersion'@
parseKernelVersion :: Text -> Maybe KernelVersion
parseKernelVersion =
let unrecognized = Nothing
dec' (Right (x, extra)) | Text.null extra = Just x
dec' _ = unrecognized
dec1st' (Right (x, _)) = Just x
dec1st' _ = unrecognized
dec = dec' . Text.decimal
dec1st = dec1st' . Text.decimal
fromSplit [x] = (,,) <$> dec x <*> pure 0 <*> pure 0
fromSplit [x, y] = (,,) <$> dec x <*> dec1st y <*> pure 0
fromSplit [x, y, z] = (,,) <$> dec x <*> dec y <*> dec1st z
fromSplit _ = unrecognized
in fromSplit . Text.split (== '.')
-- | Gathers the inputs needed to generate a memory usage report
data ReportBud = ReportBud
{ rbPids :: !(NonEmpty ProcessID)
, rbKernel :: !KernelVersion
, rbHasPss :: !Bool
, rbHasSwapPss :: !Bool
, rbHasSmaps :: !Bool
, rbRamFlaws :: !(Maybe RamFlaw)
, rbSwapFlaws :: !(Maybe SwapFlaw)
, rbProcRoot :: !FilePath
}
deriving (Eq, Show)
-- | Describes inaccuracies in the RAM calculation
data RamFlaw
= -- | no shared mem is reported
NoSharedMem
| -- | some shared mem not reported
SomeSharedMem
| -- | accurate only considering each process in isolation
ExactForIsolatedMem
deriving (Eq, Show, Ord)
-- | Provide @Text@ that explains the 'RamFlaw'
fmtRamFlaws :: RamFlaw -> Text
fmtRamFlaws NoSharedMem =
Text.unlines
[ "shared memory is not reported by this system."
, "Values reported will be too large, and totals are not reported"
]
fmtRamFlaws SomeSharedMem =
Text.unlines
[ "shared memory is not reported accurately by this system."
, "Values reported could be too large, and totals are not reported"
]
fmtRamFlaws ExactForIsolatedMem =
Text.unlines
[ "shared memory is slightly over-estimated by this system"
, "for each program, so totals are not reported."
]
-- | Describes inaccuracies in the swap measurement
data SwapFlaw
= -- | not available
NoSwap
| -- | accurate only considering each process in isolation
ExactForIsolatedSwap
deriving (Eq, Show, Ord)
-- | Provide @Text@ that explains the 'SwapFlaw'
fmtSwapFlaws :: SwapFlaw -> Text
fmtSwapFlaws NoSwap = "swap is not reported by this system."
fmtSwapFlaws ExactForIsolatedSwap =
Text.unlines
[ "swap is over-estimated by this system"
, "for each program, so totals are not reported."
]
{- | Examine the target system for @'RamFlaw's@ and @'SwapFlaw's@, and update
@bud@ reflect the findings.
-}
checkForFlaws :: ReportBud -> IO ReportBud
checkForFlaws bud = do
let memInfoPath = meminfoPathOf (rbProcRoot bud)
ReportBud
{ rbHasPss = hasPss
, rbHasSmaps = hasSmaps
, rbHasSwapPss = hasSwapPss
, rbKernel = version
} = bud
(rbRamFlaws, rbSwapFlaws) <- case version of
(2, 4, _patch) -> do
let hasInact = Text.isInfixOf "Inact_" <$> readUtf8Text memInfoPath
andHasInact x = if x then hasInact else pure False
memInfoMem x = if x then SomeSharedMem else ExactForIsolatedMem
mkFlawPair x = (Just $ memInfoMem x, Just NoSwap)
isInactPresent = doesFileExist >=> andHasInact
mkFlawPair <$> isInactPresent memInfoPath
(2, 6, _patch) -> do
pure $
if
| hasSmaps && hasPss -> (Nothing, Just ExactForIsolatedSwap)
| hasSmaps -> (Just ExactForIsolatedMem, Just ExactForIsolatedSwap)
| fickleSharing version -> (Just NoSharedMem, Just NoSwap)
| otherwise -> (Just SomeSharedMem, Just NoSwap)
(major, _minor, _patch) | major > 2 && hasSmaps -> do
pure (Nothing, if hasSwapPss then Nothing else Just ExactForIsolatedSwap)
_other -> pure (Just ExactForIsolatedMem, Just NoSwap)
pure $ bud {rbRamFlaws, rbSwapFlaws}
{- | Construct a @ReportBud@ from some @ProcessIDs@
Generates values for the other fields by inspecting the system
The result is @Nothing@ when either
- the process root is not accessible
- the @KernelVersion@ cannot be determined
-}
mkReportBud :: FilePath -> NonEmpty ProcessID -> IO (Maybe ReportBud)
mkReportBud rbProcRoot rbPids = do
let firstPid = NE.head rbPids
smapsPath = pidPath rbProcRoot "smaps" firstPid
hasPss = Text.isInfixOf "Pss:"
hasSwapPss = Text.isInfixOf "SwapPss:"
memtypes x = (hasPss x, hasSwapPss x)
rootAccessible <- canAccessRoot rbProcRoot
if not rootAccessible
then pure Nothing
else do
rbHasSmaps <- doesFileExist smapsPath
(rbHasPss, rbHasSwapPss) <-
if rbHasSmaps
then memtypes <$> readUtf8Text smapsPath
else pure (False, False)
readKernelVersion rbProcRoot >>= \case
Nothing -> pure Nothing
Just rbKernel ->
fmap Just $
checkForFlaws $
ReportBud
{ rbPids
, rbKernel
, rbHasPss
, rbHasSwapPss
, rbHasSmaps
, rbRamFlaws = Nothing
, rbSwapFlaws = Nothing
, rbProcRoot
}
pidPath :: FilePath -> FilePath -> ProcessID -> FilePath
pidPath root base pid = "" +| root |+ "/" +| toInteger pid |+ "/" +| base |+ ""
meminfoPathOf :: FilePath -> FilePath
meminfoPathOf root = "" +| root |+ "/meminfo"
canAccessRoot :: FilePath -> IO Bool
canAccessRoot root = do
doesRootExist <- doesDirectoryExist root
if not doesRootExist
then pure False
else do
p <- getPermissions root
pure $ readable p && searchable p