packages feed

shibuya-metrics-0.11.0.0: bench/SaturationLoad.hs

-- | Deterministic saturation with a real connection and a blocked writer hook.
module Main (main) where

import Control.Concurrent (threadDelay)
import Control.Concurrent.NQE.Supervisor (Strategy (IgnoreAll))
import Control.Concurrent.STM
  ( atomically,
    check,
    newTQueueIO,
    newTVarIO,
    orElse,
    readTQueue,
    readTVar,
    writeTQueue,
    writeTVar,
  )
import Control.Exception (bracket)
import Control.Monad (replicateM, when)
import Data.Aeson (encode, object, (.=))
import Data.ByteString.Lazy qualified as LBS
import Data.Text qualified as Text
import Data.Time.Clock (getCurrentTime)
import Effectful (runEff)
import GHC.Clock (getMonotonicTimeNSec)
import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats)
import Network.Wai.Handler.Warp qualified as Warp
import Network.Wai.Handler.WebSockets qualified as WaiWS
import Network.WebSockets qualified as WS
import Shibuya.Core.Metrics (ProcessorId (..), incrementReceived, newMetricsHandle)
import Shibuya.Internal.Runner.Master (markMasterRunning, registerProcessor, startMaster, stopMaster)
import Shibuya.Metrics.Config (MetricsServerConfig (..), defaultConfig)
import Shibuya.Metrics.Outbound qualified as Out
import Shibuya.Metrics.Server (combinedApp)
import Shibuya.Metrics.WebSocket
  ( WebSocketHooks (..),
    WebSocketState (..),
    defaultWebSocketHooks,
    newWebSocketState,
    websocketAppWithHooks,
  )
import System.Environment (lookupEnv)
import System.Mem (performMajorGC)
import System.Timeout (timeout)

main :: IO ()
main = bracket acquire (runEff . stopMaster) $ \master -> do
  handle <- getCurrentTime >>= newMetricsHandle
  runEff $ registerProcessor master (ProcessorId $ Text.replicate 8192 "p") handle
  gate <- newTVarIO False
  entered <- newTQueueIO
  ticks <- newTQueueIO
  observed <- newTQueueIO
  state <- newWebSocketState 1
  let config = defaultConfig {wsMaxQueuedBytes = 65_536, wsSendTimeoutUs = 1_000_000}
      hooks =
        defaultWebSocketHooks
          { beforeWrite = \_ -> do
              atomically $ readTVar gate >>= \blocked -> when blocked (writeTQueue entered ())
              atomically $ readTVar gate >>= check . not,
            afterProduce = atomically . writeTQueue observed,
            waitForPush = \_ wsState ->
              atomically $
                (readTVar wsState.shutdownRequested >>= \stopped -> check stopped >> pure True)
                  `orElse` (readTQueue ticks >> pure False)
          }
      app =
        WaiWS.websocketsOr
          WS.defaultConnectionOptions
          (websocketAppWithHooks hooks config master state)
          (combinedApp config {enableWebSocket = False} master state [])
      tick =
        incrementReceived handle
          >> atomically (writeTQueue ticks ())
          >> timeout 50_000 (atomically $ readTQueue observed)
      fill n previous = do
        result <- tick
        case result of
          Nothing -> pure (n, previous)
          Just q -> do
            when (Out.queuedFrames q > config.wsMaxQueuedFrames || Out.queuedBytes q > config.wsMaxQueuedBytes) $
              fail "queue exceeded a budget"
            if n >= (500 :: Int) then fail "required saturation never stopped production" else fill (n + 1) q
  report <- Warp.testWithApplication (pure app) $ \port ->
    WS.runClient "127.0.0.1" port "/ws" $ \conn -> do
      _ <- WS.receiveData conn :: IO LBS.ByteString
      atomically $ writeTVar gate True
      _ <- tick
      blocked <- timeout 500_000 $ atomically $ readTQueue entered
      when (blocked /= Just ()) $ fail "writer was not blocked"
      (count, q) <- fill 1 Out.empty
      heap <- replicateM 4 $ do
        performMajorGC
        stats <- getRTSStats
        threadDelay 20_000
        pure stats.gc.gcdetails_live_bytes
      started <- getMonotonicTimeNSec
      released <- timeout 1_500_000 $ atomically $ readTVar state.connectionCount >>= check . (== 0)
      ended <- getMonotonicTimeNSec
      when (released /= Just ()) $ fail "stalled connection slot was not released"
      pure $
        object
          [ "schemaVersion" .= (1 :: Int),
            "producerTicksBeforeSaturation" .= count,
            "frameBudget" .= config.wsMaxQueuedFrames,
            "byteBudget" .= config.wsMaxQueuedBytes,
            "retainedFramesAtSaturation" .= Out.queuedFrames q,
            "retainedEncodedBytesAtSaturation" .= Out.queuedBytes q,
            "liveHeapSamplesAfterSaturation" .= heap,
            "slotRecoveryMicros" .= ((ended - started) `div` 1000),
            "finalWebSocketConnections" .= (0 :: Int)
          ]
  path <- maybe "saturation-load.json" id <$> lookupEnv "OUTPUT_JSON"
  LBS.writeFile path $ encode report <> "\n"
  LBS.putStr $ encode report <> "\n"
  where
    acquire = runEff $ do
      master <- startMaster IgnoreAll
      markMasterRunning master
      pure master