packages feed

haskoin-store-1.15.1: src/Haskoin/Store/Runtime.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Haskoin.Store.Runtime
-- Copyright   : (c) Haskoin Developers
-- License     : MIT
-- Maintainer  : jprupp@protonmail.ch
-- Stability   : experimental
-- Portability : POSIX
--
-- The garbage collector of GHC, as StatsD metrics.
--
-- Every collection of GHC stops every core, the small ones included, and a
-- busy store collects hundreds of times a second. While it is stopped, no
-- request moves: not even @\/health@, which only reads memory. So how long
-- the store spends stopped, and how the heap grows, say as much about how
-- it answers as the timings of its requests do. The statistics that GHC
-- keeps are read once a second and sent as metrics, under @rts.*@.
--
-- GHC keeps them only when the program runs with @+RTS -T@, which the
-- executable of the store turns on by default: it costs a few counters at
-- each collection. A program that uses the library without it gets a line
-- in the log that says so, and no metrics.
--
-- The statistics come as totals since the start, and are sent as what
-- changed in the second: counters, which StatsD sums and turns into rates.
-- The last collection's details would have given a pause time for each
-- collection; read once a second, they would have shown one collection in
-- hundreds, and not the longest. The mean pause of the second is sent
-- instead.
module Haskoin.Store.Runtime
  ( runtimeMetrics,
  )
where

import Control.Monad (forever, unless)
import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled)
import Haskoin.Node (Logger, logInfo)
import System.Metrics.StatsD
  ( Stats,
    StatCounter,
    StatGauge,
    incrementCounter,
    newStatCounter,
    newStatGauge,
    setGauge,
  )
import UnliftIO.Concurrent (threadDelay)

-- | The metrics of the collector.
data Metrics = Metrics
  { gcs :: !StatCounter,
    majorGcs :: !StatCounter,
    paused :: !StatCounter,
    pauseMean :: !StatGauge,
    pausePercent :: !StatGauge,
    gcCpu :: !StatCounter,
    syncs :: !StatCounter,
    marking :: !StatCounter,
    allocated :: !StatCounter,
    live :: !StatGauge,
    heap :: !StatGauge
  }

-- | Send the statistics of the collector to StatsD once a second, forever:
-- run it in a thread of its own. If the program does not keep them, it
-- says so in the log and returns.
runtimeMetrics :: Logger -> Stats -> IO ()
runtimeMetrics logger stats = do
  enabled <- getRTSStatsEnabled
  if not enabled
    then logInfo logger "No garbage collector metrics: run with +RTS -T for them"
    else do
      m <- metrics
      start <- getRTSStats
      let loop before = do
            threadDelay 1000000
            now <- getRTSStats
            report m before now
            loop now
      forever (loop start)
  where
    -- one sample a second each: sent whole, unless only aggregates are wanted
    counter k = newStatCounter stats ("rts." <> k) 1
    gauge k = newStatGauge stats ("rts." <> k) 0
    metrics =
      Metrics
        <$> counter "gcs"
        <*> counter "major_gcs"
        <*> counter "gc_pause_us"
        <*> gauge "gc_pause_mean_us"
        <*> gauge "gc_pause_percent"
        <*> counter "gc_cpu_us"
        <*> counter "nonmoving_sync_us"
        <*> counter "nonmoving_mark_us"
        <*> counter "allocated_bytes"
        <*> gauge "live_bytes"
        <*> gauge "heap_bytes"

-- | Send what changed between two readings of the statistics.
report :: Metrics -> RTSStats -> RTSStats -> IO ()
report m before now = do
  add m.gcs collections
  add m.majorGcs (delta (.major_gcs))
  add m.paused (micros (.gc_elapsed_ns))
  setGauge m.pauseMean (if collections > 0 then micros (.gc_elapsed_ns) `div` collections else 0)
  setGauge m.pausePercent (if wall > 0 then 100 * delta (.gc_elapsed_ns) `div` wall else 0)
  add m.gcCpu (micros (.gc_cpu_ns))
  add m.syncs (micros (.nonmoving_gc_sync_elapsed_ns))
  add m.marking (micros (.nonmoving_gc_elapsed_ns))
  add m.allocated (delta (.allocated_bytes))
  setGauge m.live (fromIntegral now.gc.gcdetails_live_bytes)
  setGauge m.heap (fromIntegral now.gc.gcdetails_mem_in_use_bytes)
  where
    delta :: (Integral a) => (RTSStats -> a) -> Int
    delta f = fromIntegral (f now) - fromIntegral (f before)
    micros :: (Integral a) => (RTSStats -> a) -> Int
    micros f = delta f `div` 1000
    collections = delta (.gcs)
    wall = delta (.elapsed_ns)
    -- a counter that did not move is not sent
    add c n = unless (n <= 0) (incrementCounter c n)