packages feed

easy-logger-0.1.0.0: src/EasyLogger/LoggerSet.hs

{-# LANGUAGE BangPatterns #-}
module EasyLogger.LoggerSet
  ( Logger(..)
  , LoggerSet(..)
  , BufSize
  , newFileLoggerSet
  , newFileLoggerSetSameFile
  , newStdoutLoggerSet
  , newStderrLoggerSet
  , newFDLoggerSet
  , toBufIOWith
  , write
  , writeLogStr
  , flushLog
  , rmLoggerSet
  , flushLoggerSet
  ) where

import           Control.Concurrent            (getNumCapabilities)
import           Control.Concurrent.MVar
import           Control.Debounce              (debounceAction, defaultDebounceSettings,
                                                mkDebounce)
import           Control.Monad                 (replicateM, when)
import           Data.Array                    (Array, bounds, listArray, (!))
import           Data.ByteString.Builder
import           Data.ByteString.Builder.Extra (Next (..))
import qualified Data.ByteString.Builder.Extra as BBE
import           Data.ByteString.Internal
import           Data.IORef
import           Data.Maybe                    (isJust)
import           Data.Word
import           Foreign.ForeignPtr            (withForeignPtr)
import           Foreign.Marshal.Alloc         (free, mallocBytes)
import           Foreign.Ptr                   (Ptr, plusPtr)
import           GHC.IO.Device                 (close)
import           GHC.IO.FD                     (FD, openFile, stderr, stdout,
                                                writeRawBufferPtr)
import           GHC.IO.IOMode                 (IOMode (..))

import           EasyLogger.LogStr

-- | The type for buffer size of each core.
type BufSize = Int
type Buffer = Ptr Word8

data Logger = Logger !BufSize (MVar Buffer) (IORef LogStr)

newLogger :: BufSize -> IO Logger
newLogger size = Logger size <$> (mallocBytes size >>= newMVar) <*> newIORef mempty

-- | A set of loggers.
--   The number of loggers is the capabilities of GHC RTS.
--   You can specify it with \"+RTS -N\<x\>\".
--   A buffer is prepared for each capability.
data LoggerSet = LoggerSet (Maybe FilePath) (IORef FD) (Array Int Logger) (IO ())

-- | Creating a new 'LoggerSet' using a file.
newFileLoggerSet :: BufSize -> FilePath -> IO LoggerSet
newFileLoggerSet size file = openFileFD >>= newFDLoggerSet size (Just file)
  where
    openFileFD = fst <$> openFile file AppendMode False

-- | Creating a new 'LoggerSet' using a file.
newFileLoggerSetSameFile :: BufSize -> LoggerSet -> IO LoggerSet
newFileLoggerSetSameFile size (LoggerSet mFp ioRefFD _ _) = readIORef ioRefFD >>= newFDLoggerSet size mFp


-- | Creating a new 'LoggerSet' using stdout.
newStdoutLoggerSet :: BufSize -> IO LoggerSet
newStdoutLoggerSet size = newFDLoggerSet size Nothing stdout

-- | Creating a new 'LoggerSet' using stderr.
newStderrLoggerSet :: BufSize -> IO LoggerSet
newStderrLoggerSet size = newFDLoggerSet size Nothing stderr

-- | Creating a new 'LoggerSet' using a FD.
newFDLoggerSet :: BufSize -> Maybe FilePath -> FD -> IO LoggerSet
newFDLoggerSet size mfile fd = do
    n <- getNumCapabilities
    loggers <- replicateM n $ newLogger (max 1 size)
    let arr = listArray (0,n-1) loggers
    fref <- newIORef fd
    flush <- mkDebounce defaultDebounceSettings
        { debounceAction = flushLogStrRaw fref arr
        }
    return $ LoggerSet mfile fref arr flush


flushLogStrRaw :: IORef FD -> Array Int Logger -> IO ()
flushLogStrRaw fdref arr = do
    let (l,u) = bounds arr
    mapM_ flushIt [l .. u]
  where
    flushIt i = flushLog fdref (arr ! i)


flushLog :: IORef FD -> Logger -> IO ()
flushLog fdref (Logger size mbuf lref) = do
    logmsg <- atomicModifyIORef' lref (\old -> (mempty, old))
    -- If a special buffer is prepared for flusher, this MVar could
    -- be removed. But such a code does not contribute logging speed
    -- according to experiment. And even with the special buffer,
    -- there is no grantee that this function is exclusively called
    -- for a buffer. So, we use MVar here.
    -- This is safe and speed penalty can be ignored.
    withMVar mbuf $ \buf -> writeLogStr fdref buf size logmsg


-- | Writting 'LogStr' using a buffer in blocking mode.
--   The size of 'LogStr' must be smaller or equal to
--   the size of buffer.
writeLogStr :: IORef FD
            -> Buffer
            -> BufSize
            -> LogStr
            -> IO ()
writeLogStr fdref buf size (LogStr len builder)
  | size < len = error "writeLogStr"
  | otherwise  = toBufIOWith buf size (write fdref) builder

write :: IORef FD -> Buffer -> Int -> IO ()
write fdref buf len' = loop buf (fromIntegral len')
  where
    loop bf !len = do
        written <- writeRawBufferPtr2FD fdref bf len
        when (written < len) $
            loop (bf `plusPtr` fromIntegral written) (len - written)

writeRawBufferPtr2FD :: IORef FD -> Ptr Word8 -> Int -> IO Int
writeRawBufferPtr2FD fdref bf len = do
    fd <- readIORef fdref
    fromIntegral <$> writeRawBufferPtr "write" fd bf 0 (fromIntegral len)


toBufIOWith :: Buffer -> BufSize -> (Buffer -> Int -> IO ()) -> Builder -> IO ()
toBufIOWith buf !size io builder = loop $ BBE.runBuilder builder
  where
    loop writer = do
        (len, next) <- writer buf size
        io buf len
        case next of
             Done -> return ()
             More minSize writer'
               | size < minSize -> error "toBufIOWith: More: minSize"
               | otherwise      -> loop writer'
             Chunk (PS fptr off siz) writer' ->
               withForeignPtr fptr $ \ptr -> io (ptr `plusPtr` off) siz >> loop writer'

-- | Flushing the buffers.
flushLoggerSet :: LoggerSet -> IO ()
flushLoggerSet (LoggerSet _ fdref arr _) = do
    let (l,u) = bounds arr
    let nums = [l .. u]
    mapM_ flushIt nums
  where
    flushIt i = flushLog fdref (arr ! i)


-- | Flushing the buffers, closing the internal file information
--   and freeing the buffers.
rmLoggerSet :: LoggerSet -> IO ()
rmLoggerSet (LoggerSet mfile fdref arr _) = do
    let (l,u) = bounds arr
    let nums = [l .. u]
    mapM_ flushIt nums
    mapM_ freeIt nums
    fd <- readIORef fdref
    when (isJust mfile) $ close fd
  where
    flushIt i = flushLog fdref (arr ! i)
    freeIt i = do
        let (Logger _ mbuf _) = arr ! i
        takeMVar mbuf >>= free