packages feed

mem-info-0.3.1.0: src/System/MemInfo/Print.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeApplications #-}

{- |
Module      : System.MemInfo.Print
Copyright   : (c) 2022 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD3

This module provides functions that format the output of the __printmem__ command
-}
module System.MemInfo.Print (
  AsCmdName (asCmdName),
  fmtMem,
  fmtAsHeader,
  fmtOverall,
  fmtMemUsage,
  styleOutput,
) where

import Data.Proxy (Proxy (..))
import qualified Data.Text as Text
import Fmt (
  build,
  fixedF,
  padBothF,
  padLeftF,
  padRightF,
  (+|),
  (+||),
  (|+),
  (|++|),
  (||+),
 )
import System.MemInfo.Choices (
  Mem (..),
  Power (..),
  Style (..),
 )
import System.MemInfo.Prelude
import System.MemInfo.Proc (MemUsage (..))


-- | Generate the output for a given report using the specified style
styleOutput :: (AsCmdName a) => Bool -> Style -> Bool -> [(a, MemUsage)] -> [Text]
styleOutput showSwap style isAccurate = outputOf isAccurate (printStyle style showSwap)


{- | Generates the text of a row displaying the metrics for a single command in
the memory report
-}
fmtMemUsage :: (AsCmdName a) => Bool -> a -> MemUsage -> Text
fmtMemUsage showSwap name ct =
  let
    padl = padLeftF columnWidth ' ' . fmtMemKb
    private = padl $ muPrivate ct - muShared ct
    shared = padl $ muShared ct
    all' = padl $ muPrivate ct
    swap' = padl $ muSwap ct
    name' = cmdWithCount name $ muCount ct
    ram = private |+ " + " +| shared |+ " = " +| all'
    numbers = if showSwap then ram +| swap' else ram
   in
    numbers |+ "\t" +| name' |+ ""


fmtMemUsageCsv :: (AsCmdName a) => Bool -> a -> MemUsage -> Text
fmtMemUsageCsv showSwap name ct =
  let
    private = build $ muPrivate ct - muShared ct
    shared = build $ muShared ct
    all' = build $ muPrivate ct
    swap' = build $ muSwap ct
    name' = cmdWithCount name $ muCount ct
    ram = private |+ "," +| shared |+ "," +| all' |+ ","
    numbers = if showSwap then ram +| swap' |+ "," else ram
   in
    numbers +| name' |+ ""


-- | Generates the text showing the overall memory in the memory report
fmtOverall :: Bool -> (Int, Int) -> Text
fmtOverall showSwap (private, swap) =
  let
    rimLength = if showSwap then 46 else 36
    gapLength = 26
    top = Text.replicate rimLength "-"
    gap = Text.replicate gapLength " "
    bottom = Text.replicate rimLength "="
    padl = padLeftF columnWidth ' ' . fmtMemKb
    withSwap = "" +| gap |++| padl private |++| padl swap |+ ""
    noSwap = "" +| gap |++| padl private |+ ""
    out = if showSwap then withSwap else noSwap
   in
    Text.unlines [top, out, bottom]


fmtMemKb :: Int -> Text
fmtMemKb = fmtMem . Mem Ki . fromIntegral


columnWidth :: Int
columnWidth = 10


doFmt :: Power -> Double -> Text
doFmt =
  let doFmt' p x = "" +| fixedF 1 x |+ " " +|| p ||+ "B"
      go p x | p == maxBound = doFmt' p x
      go p x | x >= 1024 = doFmt (succ p) (x / 1024.0)
      go p x = doFmt' p x
   in go


fmtMem :: Mem -> Text
fmtMem (Mem p x) = doFmt p $ realToFrac x


hdrPrivate, hdrShared, hdrRamUsed, hdrSwapUsed, hdrProgram, hdrCount, hdrPid :: Text
hdrPrivate = "Private"
hdrShared = "Shared"
hdrRamUsed = "RAM Used"
hdrSwapUsed = "Swap Used"
hdrProgram = "Program"
hdrCount = "(# processes)"
hdrPid = "[pid]"


-- | Generates the text of the printed header of the memory report
fmtAsHeader :: Bool -> Bool -> Text
fmtAsHeader hasPid showSwap =
  let
    padb = padBothF columnWidth ' '
    padr = padRightF columnWidth ' '
    padl = padLeftF columnWidth ' '
    private = padb hdrPrivate
    shared = padb hdrShared
    all' = padl hdrRamUsed
    nameExt = if hasPid then hdrPid else hdrCount
    name' = padr $ hdrProgram <> " " <> nameExt
    swap' = padl hdrSwapUsed
    ram = private |+ " + " +| shared |+ " = " +| all'
    numbers = if showSwap then ram +| swap' else ram
   in
    numbers |+ "\t" +| name' |+ ""


-- | Generates the text of the printed header of the memory report
fmtAsHeaderCsv :: Bool -> Bool -> Text
fmtAsHeaderCsv hasPid showSwap =
  let
    private = build hdrPrivate
    shared = build hdrShared
    all' = build hdrRamUsed
    nameExt = if hasPid then hdrPid else hdrCount
    name' = build $ hdrProgram <> " " <> nameExt
    swap' = build hdrSwapUsed
    ram = private |+ "," +| shared |+ "," +| all' |+ ","
    numbers = if showSwap then ram +| swap' |+ "," else ram
   in
    numbers +| name' |+ ""


{- | Identifies a type as a label to use to index programs in the report
output

The label is also used to group related processes under a single program
-}
class AsCmdName a where
  -- Convert the label to text to print in the report output
  asCmdName :: a -> Text


  -- Add a count of processes using that label to the label
  cmdWithCount :: a -> Int -> Text


  -- Indicate if pid or process count should shown in the hdr
  hdrHasPid :: Proxy a -> Bool


instance AsCmdName Text where
  asCmdName = id
  cmdWithCount cmd count = "" +| asCmdName cmd |+ " (" +| count |+ ")"
  hdrHasPid _ = False


instance AsCmdName (ProcessID, Text) where
  asCmdName (pid, name) = "" +| name |+ " [" +| toInteger pid |+ "]"
  cmdWithCount cmd _count = "" +| asCmdName cmd |+ ""
  hdrHasPid _ = True


overallTotals :: [MemUsage] -> (Int, Int)
overallTotals cts =
  let step (private, swap) ct = (private + muPrivate ct, swap + muSwap ct)
   in foldl' step (0, 0) cts


data Printers a = Printers
  { psUsage :: a -> MemUsage -> Text
  , psHeader :: Text
  , psOverall :: (Int, Int) -> Maybe Text
  }


printStyle :: forall a. (AsCmdName a) => Style -> Bool -> Printers a
printStyle style showSwap =
  let usageFmt Normal = fmtMemUsage
      usageFmt Csv = fmtMemUsageCsv
      headerFmt Normal = fmtAsHeader (hdrHasPid @a Proxy)
      headerFmt Csv = fmtAsHeaderCsv (hdrHasPid @a Proxy)
      overallFmt Normal x = Just $ fmtOverall showSwap x
      overallFmt Csv _ = Nothing
   in Printers
        { psUsage = usageFmt style showSwap
        , psOverall = overallFmt style
        , psHeader = headerFmt style showSwap
        }


outputOf :: (AsCmdName a) => Bool -> Printers a -> [(a, MemUsage)] -> [Text]
outputOf isAccurate style usages =
  let Printers {psUsage, psHeader, psOverall} = style
      overall = psOverall $ overallTotals $ map snd usages
      headerAndRows = [psHeader] <> map (uncurry psUsage) usages
   in case overall of
        Nothing -> headerAndRows
        Just _ | not isAccurate -> headerAndRows
        Just o -> headerAndRows <> [o]