packages feed

nri-kafka-0.4.0.1: scripts/sync-write-benchmark/Main.hs

{-# LANGUAGE DisambiguateRecordFields #-}

-- | Benchmark for `Kafka.sendSync` per-message latency.
--
-- Measures wall time of N sequential synchronous produces against the broker
-- configured by 'KAFKA_BROKER_ADDRESSES'. Run with the same env on master
-- (baseline) and on the fix branch (after) to compare the polling-induced
-- latency floor.
--
-- > cabal run sync-write-benchmark -fsync-write-benchmark
--
-- Tunables:
--   BENCHMARK_TOPIC          (default: nri-kafka-sync-benchmark)
--   BENCHMARK_MESSAGE_COUNT  (default: 1000)
--   BENCHMARK_WARMUP         (default: 50)  -- excluded from stats
module Main where

import qualified Conduit
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.List
import qualified Data.Word
import qualified Environment
import GHC.Clock (getMonotonicTimeNSec)
import qualified Kafka
import qualified System.Environment
import Text.Printf (printf)
import Prelude (IO, String, error, fromIntegral, pure, putStrLn)
import qualified Prelude

newtype Sample = Sample {idx :: Int}
  deriving (Generic)

instance FromJSON Sample

instance ToJSON Sample

main :: IO ()
main = do
  topic <- envText "BENCHMARK_TOPIC" "nri-kafka-sync-benchmark"
  count <- envInt "BENCHMARK_MESSAGE_COUNT" 1000
  warmup <- envInt "BENCHMARK_WARMUP" 50

  settings <- Environment.decode Kafka.decoder
  logHandler <- Platform.silentHandler

  putStrLn
    ( printf
        "Benchmarking sync writes: warmup=%d count=%d topic=%s"
        warmup
        count
        (Text.toList topic)
    )

  Conduit.withAcquire (Kafka.handler settings) <| \handler -> do
    putStrLn "Warming up..."
    Prelude.mapM_ (sendOne logHandler handler topic) [1 .. warmup]

    putStrLn (printf "Measuring %d sends..." count)
    samples <-
      Prelude.traverse
        (timeOne logHandler handler topic)
        [warmup + 1 .. warmup + count]

    putStrLn (formatStats samples)

envText :: String -> Text -> IO Text
envText name def = do
  v <- System.Environment.lookupEnv name
  pure
    ( case v of
        Prelude.Just s -> Text.fromList s
        Prelude.Nothing -> def
    )

envInt :: String -> Int -> IO Int
envInt name def = do
  v <- System.Environment.lookupEnv name
  pure
    ( case v of
        Prelude.Just s -> case Prelude.reads s of
          [(i, "")] -> i
          _ -> def
        Prelude.Nothing -> def
    )

mkMsg :: Text -> Int -> Kafka.Msg
mkMsg topic i =
  Kafka.emptyMsg topic
    |> Kafka.addPayload (Sample {idx = i})
    -- Pin all messages to one partition so partition variability doesn't
    -- pollute the latency distribution.
    |> Kafka.addKey "benchmark"

sendOne :: Platform.LogHandler -> Kafka.Handler -> Text -> Int -> IO ()
sendOne logHandler handler topic i = do
  result <- Task.attempt logHandler (Kafka.sendSync handler (mkMsg topic i))
  case result of
    Ok _ -> pure ()
    Err err -> error (Text.toList err)

timeOne :: Platform.LogHandler -> Kafka.Handler -> Text -> Int -> IO Data.Word.Word64
timeOne logHandler handler topic i = do
  let msg = mkMsg topic i
  t0 <- getMonotonicTimeNSec
  result <- Task.attempt logHandler (Kafka.sendSync handler msg)
  t1 <- getMonotonicTimeNSec
  case result of
    Ok _ -> pure (t1 - t0)
    Err err -> error (Text.toList err)

formatStats :: [Data.Word.Word64] -> String
formatStats samples =
  let sorted = Data.List.sort samples
      n = Prelude.length sorted
      total = Prelude.sum sorted
      avg = nsToMs (total `Prelude.div` fromIntegral n)
      pct :: Prelude.Double -> Prelude.Double
      pct p =
        let i = Prelude.min (n - 1) (Prelude.floor (Prelude.fromIntegral n Prelude.* p / 100))
         in nsToMs (sorted Prelude.!! i)
   in printf
        "count=%d  min=%.1fms  avg=%.1fms  p50=%.1fms  p95=%.1fms  p99=%.1fms  max=%.1fms"
        n
        (nsToMs (Prelude.head sorted))
        avg
        (pct 50)
        (pct 95)
        (pct 99)
        (nsToMs (Prelude.last sorted))

nsToMs :: Data.Word.Word64 -> Prelude.Double
nsToMs ns = fromIntegral ns / 1_000_000