packages feed

keiro-0.22.0.0: bench/Main.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedRecordDot #-}

module Main
  ( main,
  )
where

import BenchSetup (memoSetup, nextBoundedIteration, nextIteration, selected, selectedCounterWithCleanup, selectedWithCleanup, setupTrace)
import CommandProbe (commandPlanProbe, maintenanceProbe, rebuildCountProbe, tableResetProbe, workCountProbe)
import Control.Concurrent (threadDelay)
import Control.Monad (replicateM_)
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.IORef (IORef, newIORef)
import Data.Int (Int32)
import Data.Text qualified as Text
import Data.Time (UTCTime (..), secondsToDiffTime)
import Data.Time.Calendar (Day (ModifiedJulianDay))
import Data.UUID qualified as UUID
import Data.Vector qualified as Vector
import Effectful (Eff, IOE, (:>))
import Effectful.Error.Static (Error)
import GHC.Clock (getMonotonicTimeNSec)
import GuardedCatalogBench (runGuardedCatalogEvidence)
import InboxDelegatedBench (inboxDelegatedBenchmarks, runInboxDelegatedExplainIfRequested)
import Keiki.Core
  ( Edge (..),
    HsPred,
    InCtor,
    RegFile (..),
    SymTransducer (..),
    Update (..),
    WireCtor,
    inpCtor,
    lit,
    matchInCtor,
    oNil,
    pack,
    unavailableInCtor,
    unavailableWireCtor,
    (*:),
  )
import Keiki.Core qualified as Keiki
import Keiro
import Keiro.Inbox
  ( InboxDedupePolicy (..),
    InboxPersistence (..),
    InboxResult (..),
    KafkaDeliveryRef (..),
    runInboxTransactionBatch,
    runInboxTransactionWith,
  )
import Keiro.Integration.Event (IntegrationContentType (..), IntegrationEvent (..))
import Keiro.Outbox
  ( OutboxId (..),
    OutboxRow,
    PublishOutcome (..),
    countOutboxBacklog,
    defaultPublishOptions,
    enqueueIntegrationEventTx,
    publishClaimedOutbox,
  )
import Keiro.Prelude
import Keiro.ProcessManager
  ( DomainProcessManager (..),
    PMCommand (..),
    ProcessManagerAction (..),
    runDomainProcessManagerWorker,
  )
import Keiro.Projection (InlineProjection (..))
import Keiro.ReadModel.Rebuild
  ( RebuildOptions (..),
    RebuildRequest (..),
    RebuildRunId,
    RebuildRunStatus (..),
    defaultRebuildOptions,
    mkRebuildRunId,
    registerProjectionCatalog,
    startCatalogRebuild,
  )
import Keiro.Telemetry qualified as Telemetry
import Keiro.Test.Postgres (StoreRunner (..), checkpointBenchmarkStore, withFreshResourceStore, withMigratedBenchmarkSuite)
import Kiroku.Store qualified as Store
import Kiroku.Store.Effect (Store)
import Kiroku.Store.Effect.Resource (KirokuStoreResource)
import Kiroku.Store.Types
  ( CategoryName (..),
    EventData (..),
    EventId (..),
    ExpectedVersion (..),
    GlobalPosition (..),
    RecordedEvent (..),
    StreamId (..),
    StreamName (..),
    StreamVersion (..),
  )
import OpenTelemetry.MeterProvider (createMeterProvider, defaultSdkMeterProviderOptions)
import OpenTelemetry.Metric.Core (getMeter)
import OpenTelemetry.Resource (emptyMaterializedResources)
import ProducerIdentityBench (producerIdentityBenchmarks, runProducerIdentityProbe)
import ReadModelBench
  ( probeReadModelCounts,
    readModelBenchmarks,
    runReadModelExplainEvidenceIfRequested,
    runReadModelLatencyEvidenceIfRequested,
    setupReadModelBench,
  )
import Shibuya.Adapter (Adapter (..))
import Shibuya.Core.AckHandle (AckHandle (..))
import Shibuya.Core.Ingested (DeliveryStatus (..), Ingested (..))
import Shibuya.Core.Types (Envelope (..))
import Streamly.Data.Stream qualified as Streamly
import System.Environment (lookupEnv)
import System.IO (hFlush, stdout)
import Test.Tasty.Bench (Benchmark, bcompareWithin, bench, bgroup, defaultMain, nfIO)
import "hasql-transaction" Hasql.Transaction qualified as Tx
import Prelude

workloadSize :: Int
workloadSize = 2000

seedChunkSize :: Int
seedChunkSize = 500

payloadSize :: Int
payloadSize = 1024

maxDrainPasses :: Int
maxDrainPasses = 2000

fixedOccurredAt :: UTCTime
fixedOccurredAt = UTCTime (ModifiedJulianDay 61000) (secondsToDiffTime 0)

data BrokerModel = BrokerModel
  { invocationMicros :: !Int,
    perRecordMicros :: !Int
  }
  deriving stock (Eq, Show)

data OutboxScenario = OutboxScenario
  { scenarioName :: !Text,
    brokerModel :: !BrokerModel,
    messages :: ![(OutboxId, IntegrationEvent)]
  }

data InboxScenario = InboxScenario
  { inboxScenarioName :: !Text,
    inboxMetrics :: !(Maybe Telemetry.KeiroMetrics),
    inboxPersistence :: !InboxPersistence,
    inboxBatchSize :: !(Maybe Int),
    inboxMessages :: ![(IntegrationEvent, KafkaDeliveryRef)]
  }

main :: IO ()
main = do
  evidence <- runGuardedCatalogEvidence
  unless evidence ordinaryMain

ordinaryMain :: IO ()
ordinaryMain =
  withMigratedBenchmarkSuite \fixture ->
    withFreshResourceStore fixture \(store, runner) ->
      withFreshResourceStore fixture \(outboxStore, _) ->
        withFreshResourceStore fixture \(inboxStore, _) ->
          withFreshResourceStore fixture \(delegatedStore, _) ->
            withFreshResourceStore fixture \(producerStore, _) ->
              withFreshResourceStore fixture \(rebuildStore, _) ->
                withFreshResourceStore fixture \(readModelStore, readModelRunner) -> do
                  (provider, _env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions
                  meter <- getMeter provider Telemetry.keiroInstrumentationLibrary
                  metrics <- Telemetry.newKeiroMetrics meter
                  producerCounter <- newIORef 0
                  readModelSetup <- memoSetup do
                    setupTrace "read-model" "shared" "schema history=25000"
                    model <- setupReadModelBench readModelStore readModelRunner
                    runReadModelExplainEvidenceIfRequested model
                    runReadModelLatencyEvidenceIfRequested model
                    pure model
                  runInboxDelegatedExplainIfRequested delegatedStore
                  producerProbe <- lookupEnv "KEIRO_BENCH_PRODUCER_PROBE"
                  resetProbe <- lookupEnv "KEIRO_BENCH_RESET_PROBE"
                  probe <- lookupEnv "KEIRO_BENCH_COMMAND_PROBE"
                  if producerProbe == Just "1"
                    then runProducerIdentityProbe producerStore
                    else
                      if probe == Just "1"
                        then runCommandGrowthProbe store
                        else
                          if resetProbe == Just "1"
                            then runResetProbes outboxStore inboxStore producerStore
                            else
                              defaultMain
                                ( benchmarks store runner outboxStore inboxStore rebuildStore metrics (inboxDelegatedBenchmarks delegatedStore metrics)
                                    <> [selectedWithCleanup readModelSetup probeReadModelCounts (\model -> readModelBenchmarks model !! groupIndex) | groupIndex <- [0 .. 2]]
                                    <> producerIdentityBenchmarks producerStore producerCounter
                                )

benchmarks :: Store.KirokuStore -> StoreRunner -> Store.KirokuStore -> Store.KirokuStore -> Store.KirokuStore -> Telemetry.KeiroMetrics -> [Benchmark] -> [Benchmark]
benchmarks store runner outboxStore inboxStore rebuildStore metrics delegatedInboxBenchmarks =
  [ bgroup
      "outbox"
      [ scenarioBench outboxStore hotKey,
        scenarioBench outboxStore hotKeyNoLatency,
        scenarioBench outboxStore multiKey
      ],
    bgroup
      "inbox"
      [ inboxScenarioBench inboxStore (singleFull metrics),
        inboxScenarioBench inboxStore singleNoMetrics,
        inboxScenarioBench inboxStore batch100,
        inboxScenarioBench inboxStore singleSlim,
        bgroup "downstream" delegatedInboxBenchmarks
      ],
    selected (prepareCommandStore store) $ \_ ->
      bgroup
        "command"
        [ bgroup
            "legacy"
            [commandScenarioBench store "accepted-1" legacyAcceptedOneStream legacyAcceptedOneTarget EmitOne],
          bgroup
            "control"
            [commandScenarioBench store "accepted-1" legacyAcceptedOneStream (stream "bench-command-control") EmitOne],
          bgroup
            "domain-warmup"
            [domainCommandScenarioBench store "accepted-1" domainAcceptedOneHandler (stream "bench-command-domain-warmup") EmitOne],
          bgroup
            "domain"
            [ bcompareWithin 0 1.25 acceptedOneControlPattern $
                domainCommandScenarioBench store "accepted-1" domainAcceptedOneHandler domainAcceptedOneTarget EmitOne
            ],
          bgroup
            "legacy"
            [commandScenarioBench store "accepted-large" legacyAcceptedLargeStream legacyAcceptedLargeTarget EmitLarge],
          bgroup
            "domain"
            [ bcompareWithin 0 1.25 legacyAcceptedLargePattern $
                domainCommandScenarioBench store "accepted-large" domainAcceptedLargeHandler domainAcceptedLargeTarget EmitLarge
            ],
          bgroup
            "legacy"
            [commandScenarioBench store "no-op" legacyNoOpStream legacyNoOpTarget SelectNoOp],
          bgroup
            "domain"
            [ bcompareWithin 0 1.25 legacyNoOpPattern $
                domainCommandScenarioBench store "rejected" domainRejectedHandler domainRejectedTarget SelectNoOp,
              bcompareWithin 0 1.25 legacyNoOpPattern $
                domainCommandScenarioBench store "no-op" domainNoOpHandler domainNoOpTarget SelectNoOp,
              bgroup
                "router-fanout"
                [ domainRouterFanoutBench store runner fanout
                | fanout <- coordinatorFanouts
                ],
              bgroup
                "process-manager-fanout"
                [ domainProcessManagerFanoutBench store runner fanout
                | fanout <- coordinatorFanouts
                ]
            ]
        ],
    bgroup
      "rebuild"
      [rebuildScenarioBench rebuildStore]
  ]
  where
    hotKey =
      OutboxScenario
        { scenarioName = "hot-key",
          brokerModel = BrokerModel {invocationMicros = 1000, perRecordMicros = 10},
          messages = scenarioMessages \_ -> Just "aggregate-hot"
        }
    hotKeyNoLatency =
      OutboxScenario
        { scenarioName = "hot-key-nolatency",
          brokerModel = BrokerModel {invocationMicros = 0, perRecordMicros = 0},
          messages = scenarioMessages \_ -> Just "aggregate-hot"
        }
    multiKey =
      OutboxScenario
        { scenarioName = "multi-key",
          brokerModel = BrokerModel {invocationMicros = 1000, perRecordMicros = 10},
          messages = scenarioMessages \i -> Just ("aggregate-" <> Text.pack (show (i `mod` 200)))
        }
    singleFull metrics' =
      InboxScenario
        { inboxScenarioName = "single-full",
          inboxMetrics = Just metrics',
          inboxPersistence = PersistFullEnvelope,
          inboxBatchSize = Nothing,
          inboxMessages = inboxScenarioMessages
        }
    singleNoMetrics =
      InboxScenario
        { inboxScenarioName = "single-nometrics",
          inboxMetrics = Nothing,
          inboxPersistence = PersistFullEnvelope,
          inboxBatchSize = Nothing,
          inboxMessages = inboxScenarioMessages
        }
    batch100 =
      InboxScenario
        { inboxScenarioName = "batch-100",
          inboxMetrics = Nothing,
          inboxPersistence = PersistFullEnvelope,
          inboxBatchSize = Just 100,
          inboxMessages = inboxScenarioMessages
        }
    singleSlim =
      InboxScenario
        { inboxScenarioName = "single-slim",
          inboxMetrics = Nothing,
          inboxPersistence = PersistDedupeOnly,
          inboxBatchSize = Nothing,
          inboxMessages = inboxScenarioMessages
        }

scenarioBench :: Store.KirokuStore -> OutboxScenario -> Benchmark
scenarioBench store scenario =
  selected (warmAction store "outbox" (Text.unpack scenario.scenarioName) (runScenario store scenario)) $ \_ ->
    bench (Text.unpack scenario.scenarioName) $ nfIO (runScenario store scenario)

warmAction :: Store.KirokuStore -> String -> String -> IO () -> IO ()
warmAction store group name action = do
  setupTrace group name "checkpoint=before-warm-up"
  checkpointBenchmarkStore store
  setupTrace group name "warm-up invocations=3 timed_reset=TRUNCATE"
  replicateM_ 3 action

runScenario :: Store.KirokuStore -> OutboxScenario -> IO ()
runScenario store scenario = do
  runStoreChecked store do
    Store.runTransaction (Tx.sql "TRUNCATE keiro.keiro_outbox")
  seedOutbox store scenario.messages
  runStoreChecked store (drainOutbox scenario.brokerModel 0)

inboxScenarioBench :: Store.KirokuStore -> InboxScenario -> Benchmark
inboxScenarioBench store scenario =
  selected (warmAction store "inbox" (Text.unpack scenario.inboxScenarioName) (runInboxScenario store scenario)) $ \_ ->
    bench (Text.unpack scenario.inboxScenarioName) $ nfIO (runInboxScenario store scenario)

runInboxScenario :: Store.KirokuStore -> InboxScenario -> IO ()
runInboxScenario store scenario = do
  runStoreChecked store do
    Store.runTransaction (Tx.sql "TRUNCATE keiro.keiro_inbox")
  runStoreChecked store $
    case scenario.inboxBatchSize of
      Nothing ->
        traverse_
          (processInboxDelivery scenario.inboxMetrics scenario.inboxPersistence)
          scenario.inboxMessages
      Just batchSize ->
        traverse_
          (processInboxBatch scenario.inboxMetrics scenario.inboxPersistence)
          (chunksOf batchSize scenario.inboxMessages)

processInboxDelivery ::
  (IOE :> es, Store :> es) =>
  Maybe Telemetry.KeiroMetrics ->
  InboxPersistence ->
  (IntegrationEvent, KafkaDeliveryRef) ->
  Eff es ()
processInboxDelivery mMetrics persistence (event, kafkaRef) = do
  result <- runInboxTransactionWith mMetrics persistence PreferIntegrationMessageId event (Just kafkaRef) (\_ -> pure ())
  case result of
    Right (InboxProcessed ()) -> pure ()
    other -> liftIO (fail ("unexpected inbox benchmark result: " <> show other))

processInboxBatch ::
  (IOE :> es, Store :> es) =>
  Maybe Telemetry.KeiroMetrics ->
  InboxPersistence ->
  [(IntegrationEvent, KafkaDeliveryRef)] ->
  Eff es ()
processInboxBatch mMetrics persistence chunk = do
  results <-
    runInboxTransactionBatch
      mMetrics
      3
      PreferIntegrationMessageId
      persistence
      [(event, Just kafkaRef) | (event, kafkaRef) <- chunk]
      (\_ -> pure ())
  for_ results \case
    Right (InboxProcessed ()) -> pure ()
    other -> liftIO (fail ("unexpected inbox batch benchmark result: " <> show other))

seedOutbox :: Store.KirokuStore -> [(OutboxId, IntegrationEvent)] -> IO ()
seedOutbox store messages =
  traverse_ seedChunk (chunksOf seedChunkSize messages)
  where
    seedChunk chunk =
      runStoreChecked store $
        Store.runTransaction $
          traverse_ (uncurry enqueueIntegrationEventTx) chunk

drainOutbox :: (IOE :> es, Store :> es) => BrokerModel -> Int -> Eff es ()
drainOutbox broker passes = do
  backlog <- countOutboxBacklog
  if backlog == 0
    then pure ()
    else do
      when (passes >= maxDrainPasses) $
        liftIO (fail ("outbox benchmark exceeded safety cap of " <> show maxDrainPasses <> " passes"))
      void (publishClaimedOutbox (simulatedPublish broker) defaultPublishOptions Nothing)
      drainOutbox broker (passes + 1)

simulatedPublish :: (IOE :> es) => BrokerModel -> [OutboxRow] -> Eff es [(OutboxId, PublishOutcome)]
simulatedPublish broker rows = do
  let totalMicros = broker.invocationMicros + broker.perRecordMicros * length rows
  when (totalMicros > 0) $
    liftIO (threadDelay totalMicros)
  pure [(row ^. #outboxId, PublishSucceeded) | row <- rows]

scenarioMessages :: (Int -> Maybe Text) -> [(OutboxId, IntegrationEvent)]
scenarioMessages keyFor =
  [ (OutboxId (UUID.fromWords64 0x018f0f1800007000 (0x8000000000000000 + fromIntegral i)), integrationEvent i (keyFor i))
  | i <- [1 .. workloadSize]
  ]

integrationEvent :: Int -> Maybe Text -> IntegrationEvent
integrationEvent i key =
  IntegrationEvent
    { messageId = "bench-msg-" <> Text.pack (show i),
      source = "bench.outbox",
      destination = "bench.outbox.events.v1",
      key,
      eventType = "BenchEvent",
      schemaVersion = 1,
      contentType = ApplicationJson,
      schemaReference = Nothing,
      sourceEventId = Nothing,
      sourceGlobalPosition = Nothing,
      payloadBytes = BS.replicate payloadSize 65,
      occurredAt = fixedOccurredAt,
      causationId = Nothing,
      correlationId = Nothing,
      traceContext = Nothing,
      attributes = Nothing
    }

inboxScenarioMessages :: [(IntegrationEvent, KafkaDeliveryRef)]
inboxScenarioMessages =
  [ ( integrationEvent i (Just ("inbox-key-" <> Text.pack (show i)))
        & #messageId
        .~ ("bench-inbox-msg-" <> Text.pack (show i))
        & #source
        .~ "bench.inbox"
        & #destination
        .~ "bench.inbox.events.v1",
      KafkaDeliveryRef "bench.inbox.events.v1" 0 (fromIntegral i)
    )
  | i <- [1 .. workloadSize]
  ]

chunksOf :: Int -> [a] -> [[a]]
chunksOf n xs
  | n <= 0 = error "chunksOf: non-positive chunk size"
  | otherwise =
      case splitAt n xs of
        ([], _) -> []
        (chunk, rest) -> chunk : chunksOf n rest

-- * Command runner benchmark fixture ----------------------------------------

data BenchCommand
  = EmitOne
  | EmitLarge
  | SelectNoOp
  deriving stock (Eq, Show)

data BenchEvent
  = BenchOneEmitted !Text
  | BenchLargeEmitted !Text
  deriving stock (Eq, Show)

data BenchState = BenchReady
  deriving stock (Bounded, Enum, Eq, Ord, Show)

type BenchEventStream = EventStream (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent

type ValidatedBenchEventStream = ValidatedEventStream (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent

largeCommandBatchSize :: Int
largeCommandBatchSize = 100

fixedCommandPayload :: Text
fixedCommandPayload = Text.replicate payloadSize "a"

legacyAcceptedOneTarget :: Stream BenchEventStream
legacyAcceptedOneTarget = stream "bench-command-legacy-accepted-1"

legacyAcceptedLargeTarget :: Stream BenchEventStream
legacyAcceptedLargeTarget = stream "bench-command-legacy-accepted-large"

legacyNoOpTarget :: Stream BenchEventStream
legacyNoOpTarget = stream "bench-command-legacy-no-op"

domainAcceptedOneTarget :: Stream BenchEventStream
domainAcceptedOneTarget = legacyAcceptedOneTarget

domainAcceptedLargeTarget :: Stream BenchEventStream
domainAcceptedLargeTarget = legacyAcceptedLargeTarget

domainRejectedTarget :: Stream BenchEventStream
domainRejectedTarget = legacyNoOpTarget

domainNoOpTarget :: Stream BenchEventStream
domainNoOpTarget = legacyNoOpTarget

legacyAcceptedOneStream :: ValidatedBenchEventStream
legacyAcceptedOneStream = mkEventStreamOrThrow "bench-command-legacy-accepted-1" (benchEventStream oneTransducer)

legacyAcceptedLargeStream :: ValidatedBenchEventStream
legacyAcceptedLargeStream = mkEventStreamOrThrow "bench-command-legacy-accepted-large" (benchEventStream largeTransducer)

legacyNoOpStream :: ValidatedBenchEventStream
legacyNoOpStream = mkEventStreamOrThrow "bench-command-legacy-no-op" (benchEventStream noOpTransducer)

domainAcceptedOneHandler :: DomainCommandHandler (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent Text Text
domainAcceptedOneHandler =
  DomainCommandHandler
    { eventStream = legacyAcceptedOneStream,
      classifySilent = \_ -> error "domainAcceptedOneHandler: eventful edge classified as silent"
    }

domainAcceptedLargeHandler :: DomainCommandHandler (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent Text Text
domainAcceptedLargeHandler =
  DomainCommandHandler
    { eventStream = legacyAcceptedLargeStream,
      classifySilent = \_ -> error "domainAcceptedLargeHandler: eventful edge classified as silent"
    }

domainRejectedHandler :: DomainCommandHandler (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent Text Text
domainRejectedHandler =
  DomainCommandHandler
    { eventStream = legacyNoOpStream,
      classifySilent = \_ -> SilentRejected "benchmark rejection"
    }

domainNoOpHandler :: DomainCommandHandler (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent Text Text
domainNoOpHandler =
  DomainCommandHandler
    { eventStream = legacyNoOpStream,
      classifySilent = \_ -> SilentNoOp "benchmark no-op"
    }

acceptedOneControlPattern :: String
acceptedOneControlPattern = "$NF == \"accepted-1\" && $(NF-1) == \"control\" && $(NF-2) == \"command\""

legacyAcceptedLargePattern :: String
legacyAcceptedLargePattern = "$NF == \"accepted-large\" && $(NF-1) == \"legacy\" && $(NF-2) == \"command\""

legacyNoOpPattern :: String
legacyNoOpPattern = "$NF == \"no-op\" && $(NF-1) == \"legacy\" && $(NF-2) == \"command\""

benchEventStream :: SymTransducer (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent -> BenchEventStream
benchEventStream transducer =
  EventStream
    { transducer,
      initialState = BenchReady,
      initialRegisters = RNil,
      eventCodec = benchEventCodec,
      resolveStreamName = streamName,
      snapshotPolicy = Never,
      stateCodec = Nothing
    }

oneTransducer :: SymTransducer (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent
oneTransducer = singleEdgeTransducer emitOneCtor [pack emitOneCtor oneEventCtor (lit fixedCommandPayload *: oNil)]

largeTransducer :: SymTransducer (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent
largeTransducer =
  singleEdgeTransducer
    emitLargeCtor
    (Prelude.replicate largeCommandBatchSize (pack emitLargeCtor largeEventCtor (lit fixedCommandPayload *: oNil)))

noOpTransducer :: SymTransducer (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent
noOpTransducer = singleEdgeTransducer selectNoOpCtor []

singleEdgeTransducer :: InCtor BenchCommand '[] -> [Keiki.OutTerm '[] BenchCommand BenchEvent] -> SymTransducer (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent
singleEdgeTransducer commandCtor emitted =
  SymTransducer
    { edgesOut = \BenchReady ->
        [ Edge
            { guard = matchInCtor commandCtor,
              update = UKeep,
              output = emitted,
              target = BenchReady,
              mode = Keiki.Live
            }
        ],
      initial = BenchReady,
      initialRegs = RNil,
      isFinal = \_ -> False
    }

emitOneCtor :: InCtor BenchCommand '[]
emitOneCtor =
  unavailableInCtor
    "EmitOne"
    (\case EmitOne -> Just RNil; _ -> Nothing)
    (\RNil -> EmitOne)

emitLargeCtor :: InCtor BenchCommand '[]
emitLargeCtor =
  unavailableInCtor
    "EmitLarge"
    (\case EmitLarge -> Just RNil; _ -> Nothing)
    (\RNil -> EmitLarge)

selectNoOpCtor :: InCtor BenchCommand '[]
selectNoOpCtor =
  unavailableInCtor
    "SelectNoOp"
    (\case SelectNoOp -> Just RNil; _ -> Nothing)
    (\RNil -> SelectNoOp)

oneEventCtor :: WireCtor BenchEvent (Text, ())
oneEventCtor =
  unavailableWireCtor
    "BenchOneEmitted"
    (\case BenchOneEmitted value -> Just (value, ()); _ -> Nothing)
    (\(value, ()) -> BenchOneEmitted value)

largeEventCtor :: WireCtor BenchEvent (Text, ())
largeEventCtor =
  unavailableWireCtor
    "BenchLargeEmitted"
    (\case BenchLargeEmitted value -> Just (value, ()); _ -> Nothing)
    (\(value, ()) -> BenchLargeEmitted value)

benchEventCodec :: Codec BenchEvent
benchEventCodec =
  Codec
    { eventTypes = EventType "BenchOneEmitted" :| [EventType "BenchLargeEmitted"],
      eventType = \case
        BenchOneEmitted {} -> EventType "BenchOneEmitted"
        BenchLargeEmitted {} -> EventType "BenchLargeEmitted",
      schemaVersion = 1,
      encode = \case
        BenchOneEmitted value -> toJSON value
        BenchLargeEmitted value -> toJSON value,
      decode = \(EventType eventTypeName) _ ->
        case eventTypeName of
          "BenchOneEmitted" -> Right (BenchOneEmitted fixedCommandPayload)
          "BenchLargeEmitted" -> Right (BenchLargeEmitted fixedCommandPayload)
          other -> Left ("unknown command benchmark event type: " <> other),
      upcasters = []
    }

-- Seed a representative stream population before any prepared lookup is planned.
-- Empty-table statistics otherwise keep sequential scans as fresh samples grow.
prepareCommandStore :: Store.KirokuStore -> IO ()
prepareCommandStore store = do
  setupTrace "command" "shared" "schema seed_streams=5000 seed_events=5000 analyze=once-before-warm-up"
  for_ [1 .. 5000 :: Int] $ \n -> runStoreChecked store $ do
    void
      ( Store.appendToStream
          (StreamName ("benchseed-" <> Text.pack (show n)))
          NoStream
          [EventData Nothing (EventType "BenchSeed") Aeson.Null Nothing Nothing Nothing]
      )
  runStoreChecked store (Store.runTransaction (Tx.sql "ANALYZE streams; ANALYZE stream_events; ANALYZE events"))

commandScenarioBench :: Store.KirokuStore -> String -> ValidatedBenchEventStream -> Stream BenchEventStream -> BenchCommand -> Benchmark
commandScenarioBench store benchmarkName validatedStream target command =
  selectedCounterWithCleanup label units (prepareCommand store label units (commandBudget command) action) checkCounts $ \counter ->
    bench benchmarkName $ nfIO (action counter)
  where
    label = show (streamName target) <> "/legacy/" <> benchmarkName
    checkCounts n = do
      maintenanceProbe store (Text.pack label <> "-finished")
      let StreamName prefix = streamName (tagTarget target "legacy")
      workCountProbe store (Text.pack label) (prefix <> "-iteration-") (if units == 0 then 0 else n) (n * units)
    units = commandUnits command
    action counter = do
      n <- nextBoundedIteration label (commandBudget command) counter
      let fresh = iterationTarget (tagTarget target "legacy") n
      runStoreChecked store do
        result <- runCommand defaultRunCommandOptions validatedStream fresh command
        case result of
          Right value | value ^. #eventsAppended == eventCount command -> pure ()
          other -> liftIO (fail ("unexpected command benchmark result: " <> show other))

domainCommandScenarioBench :: Store.KirokuStore -> String -> DomainCommandHandler (HsPred '[] BenchCommand) '[] BenchState BenchCommand BenchEvent Text Text -> Stream BenchEventStream -> BenchCommand -> Benchmark
domainCommandScenarioBench store benchmarkName handler target command =
  selectedCounterWithCleanup label units (prepareCommand store label units (commandBudget command) action) checkCounts $ \counter ->
    bench benchmarkName $ nfIO (action counter)
  where
    label = show (streamName target) <> "/domain/" <> benchmarkName
    checkCounts n = do
      maintenanceProbe store (Text.pack label <> "-finished")
      let StreamName prefix = streamName (tagTarget target ("domain-" <> Text.pack benchmarkName))
      workCountProbe store (Text.pack label) (prefix <> "-iteration-") (if units == 0 then 0 else n) (n * units)
    units = commandUnits command
    action counter = do
      n <- nextBoundedIteration label (commandBudget command) counter
      let fresh = iterationTarget (tagTarget target ("domain-" <> Text.pack benchmarkName)) n
      runStoreChecked store do
        result <- runDomainCommand defaultRunCommandOptions handler fresh command
        case result of
          Right value | value ^. #result . #eventsAppended == eventCount command -> pure ()
          other -> liftIO (fail ("unexpected typed command benchmark result: " <> show other))

commandUnits :: BenchCommand -> Int
commandUnits = eventCount

commandBudget :: BenchCommand -> Int
commandBudget EmitOne = 100000
commandBudget EmitLarge = 10000
commandBudget SelectNoOp = 1000000

eventCount :: BenchCommand -> Int
eventCount EmitOne = 1
eventCount EmitLarge = largeCommandBatchSize
eventCount SelectNoOp = 0

tagTarget :: Stream eventStream -> Text -> Stream eventStream
tagTarget target tag =
  let StreamName name = streamName target
   in stream (name <> "-" <> tag)

iterationTarget :: Stream eventStream -> Int -> Stream eventStream
iterationTarget target n =
  let StreamName name = streamName target
   in stream (name <> "-iteration-" <> Text.pack (show n))

prepareCommand :: Store.KirokuStore -> String -> Int -> Int -> (IORef Int -> IO ()) -> IO (IORef Int)
prepareCommand store label units budget action = do
  setupTrace "command" label ("counter budget_iterations=" <> show budget <> " events_per_iteration=" <> show units)
  counter <- newIORef 0
  setupTrace "command" label "checkpoint=before-warm-up"
  checkpointBenchmarkStore store
  setupTrace "command" label "warm-up invocations=3"
  replicateM_ 3 (action counter)
  maintenanceProbe store (Text.pack label <> "-ready")
  pure counter

-- * Projection rebuild benchmark fixtures ---------------------------------

rebuildEventsPerCategory :: Int
rebuildEventsPerCategory = 200

rebuildPageSize :: Int32
rebuildPageSize = 16

rebuildScenarioBench :: Store.KirokuStore -> Benchmark
rebuildScenarioBench store =
  selectedCounterWithCleanup "rebuild" 600 setup (rebuildCountProbe store) $ \runCounter -> bench "three-categories-200" $ nfIO (runRebuild False runCounter)
  where
    setup = do
      setupTrace "rebuild" "three-categories-200" "schema source_events=600 bookkeeping_run_budget=10000"
      runStoreChecked store do
        Store.runTransaction (Tx.sql rebuildBenchSql)
        traverse_ seedRebuildStream rebuildStreams
        registerProjectionCatalog rebuildCatalog >>= \case
          Left err -> liftIO (fail (show err))
          Right _ -> pure ()
      counter <- newIORef 0
      checkpointBenchmarkStore store
      setupTrace "rebuild" "three-categories-200" "checkpoint=before-warm-up warm-up invocations=3 source_events_per_invocation=600"
      replicateM_ 3 (runRebuild True counter)
      probe <- lookupEnv "KEIRO_BENCH_REBUILD_PROBE"
      when (probe == Just "1") (replicateM_ 3 (runRebuild True counter))
      pure counter
    runRebuild probe runCounter = do
      runNumber <- nextBoundedIteration "rebuild" 10000 runCounter
      runStoreChecked store do
        let request =
              RebuildRequest
                { rebuildRunId = benchmarkRebuildRunId runNumber,
                  requestedBy = "keiro-bench",
                  requestReason = "measure buffered projection replay",
                  replayFrom = GlobalPosition 0
                }
            options = (defaultRebuildOptions request) {replayPageSize = rebuildPageSize}
        startCatalogRebuild rebuildCatalog benchmarkRebuildGroupId options >>= \case
          Left err -> liftIO (fail ("unexpected rebuild benchmark result: " <> show err))
          Right report
            | report ^. #runStatus == RebuildRunPromoted -> do
                let sourceEvents = sum [sourceProgress ^. #eventCount | sourceProgress <- report ^. #sources]
                unless (sourceEvents == 600) (liftIO (fail ("rebuild source count changed: " <> show sourceEvents)))
                when probe (liftIO (setupTrace "rebuild" (show runNumber) ("source_events=" <> show sourceEvents)))
            | otherwise -> liftIO (fail ("rebuild benchmark did not promote: " <> show (report ^. #runStatus)))

seedRebuildStream :: (Store :> es) => StreamName -> Eff es ()
seedRebuildStream streamName' =
  void $
    Store.appendToStream
      streamName'
      NoStream
      [ EventData
          { eventId = Nothing,
            eventType = EventType "BenchRebuildEvent",
            payload = Aeson.toJSON eventNumber,
            metadata = Nothing,
            causationId = Nothing,
            correlationId = Nothing
          }
      | eventNumber <- [1 .. rebuildEventsPerCategory]
      ]

rebuildStreams :: [StreamName]
rebuildStreams =
  [ StreamName "orders-bench-rebuild",
    StreamName "customers-bench-rebuild",
    StreamName "billing-bench-rebuild"
  ]

rebuildCatalog :: ValidatedProjectionCatalog
rebuildCatalog =
  case validateProjectionCatalog rebuildCatalogDeclaration of
    Success catalog -> catalog
    Failure diagnostics -> error ("invalid rebuild benchmark catalog: " <> show diagnostics)

rebuildCatalogDeclaration :: ProjectionCatalog
rebuildCatalogDeclaration =
  ProjectionCatalog
    { sources =
        [ source "orders" ordersRebuildSourceId,
          source "customers" customersRebuildSourceId,
          source "billing" billingRebuildSourceId
        ],
      targets =
        [ target ordersRebuildTargetId "bench_rebuild_orders",
          target customersRebuildTargetId "bench_rebuild_customers",
          target billingRebuildTargetId "bench_rebuild_billing"
        ],
      rebuildGroups =
        [ RebuildGroupDeclaration
            { rebuildGroupId = benchmarkRebuildGroupId,
              orderedTargets = [ordersRebuildTargetId, customersRebuildTargetId, billingRebuildTargetId],
              verificationHooks = [],
              claimSite = rebuildSite "bench:rebuild-group"
            }
        ],
      projectionRevisions = [],
      externalReadContracts = [],
      subscriptions = [],
      dedupKeys = [],
      queryModels = [],
      projectionSets =
        [ projectionSet "orders" ordersRebuildSourceId ordersRebuildProjectionId ordersRebuildTargetId,
          projectionSet "customers" customersRebuildSourceId customersRebuildProjectionId customersRebuildTargetId,
          projectionSet "billing" billingRebuildSourceId billingRebuildProjectionId billingRebuildTargetId
        ]
    }
  where
    source sourceCategory sourceId =
      SourceDeclaration
        { sourceId,
          sourceScope = CategorySource (CategoryName sourceCategory),
          codecFingerprint = "bench-rebuild-v1",
          claimSite = rebuildSite ("bench:rebuild-source:" <> sourceCategory)
        }
    target targetId tableName =
      TargetDeclaration
        { targetId,
          qualifiedTable = QualifiedTable "app" tableName,
          resetPolicy = ClearBeforeReplay,
          dependsOn = [],
          claimSite = rebuildSite ("bench:rebuild-target:" <> tableName)
        }
    projectionSet label sourceId projectionId targetId =
      SomeProjectionSet
        ProjectionSet
          { projectionSource = sourceId,
            projectionDefinitions =
              ProjectionDefinition
                { projectionId,
                  rebuildGroup = benchmarkRebuildGroupId,
                  ownedTargets = targetId :| [],
                  replayPolicy =
                    Replayable
                      ReplayAdapter
                        { decodeForReplay = const ReplayIrrelevant,
                          applyForReplay = \() _ -> pure ()
                        },
                  handlers =
                    InlineHandler
                      InlineProjection
                        { name = "bench-rebuild-live-" <> label,
                          apply = \_ _ -> pure ()
                        }
                      (rebuildSite ("bench:rebuild-handler:" <> label))
                      :| [],
                  claimSite = rebuildSite ("bench:rebuild-projection:" <> label)
                }
                :| [],
            claimSite = rebuildSite ("bench:rebuild-set:" <> label)
          }

benchmarkRebuildRunId :: Int -> RebuildRunId
benchmarkRebuildRunId runNumber =
  either (error . Text.unpack) id (mkRebuildRunId ("bench-rebuild-" <> Text.pack (show runNumber)))

rebuildSite :: Text -> ClaimSite
rebuildSite raw = either (error . show) id (mkClaimSite raw)

rebuildIdentity :: (Text -> Either CatalogIdentityError value) -> Text -> value
rebuildIdentity constructor raw = either (error . show) id (constructor raw)

ordersRebuildSourceId, customersRebuildSourceId, billingRebuildSourceId :: SourceId
ordersRebuildSourceId = rebuildIdentity mkSourceId "bench-orders-source"
customersRebuildSourceId = rebuildIdentity mkSourceId "bench-customers-source"
billingRebuildSourceId = rebuildIdentity mkSourceId "bench-billing-source"

ordersRebuildTargetId, customersRebuildTargetId, billingRebuildTargetId :: TargetId
ordersRebuildTargetId = rebuildIdentity mkTargetId "bench-orders-target"
customersRebuildTargetId = rebuildIdentity mkTargetId "bench-customers-target"
billingRebuildTargetId = rebuildIdentity mkTargetId "bench-billing-target"

ordersRebuildProjectionId, customersRebuildProjectionId, billingRebuildProjectionId :: ProjectionId
ordersRebuildProjectionId = rebuildIdentity mkProjectionId "bench-orders-projection"
customersRebuildProjectionId = rebuildIdentity mkProjectionId "bench-customers-projection"
billingRebuildProjectionId = rebuildIdentity mkProjectionId "bench-billing-projection"

benchmarkRebuildGroupId :: RebuildGroupId
benchmarkRebuildGroupId = rebuildIdentity mkRebuildGroupId "bench-rebuild-group"

rebuildBenchSql :: BS.ByteString
rebuildBenchSql =
  "CREATE SCHEMA IF NOT EXISTS app; \
  \CREATE TABLE IF NOT EXISTS app.bench_rebuild_orders (marker bigint); \
  \CREATE TABLE IF NOT EXISTS app.bench_rebuild_customers (marker bigint); \
  \CREATE TABLE IF NOT EXISTS app.bench_rebuild_billing (marker bigint)"

-- * Domain coordinator worker benchmark fixtures ---------------------------

data FanoutInput = FanoutInput !Text !Int

data FanoutCommand = EmitFanout !Int
  deriving stock (Eq, Show)

data FanoutEvent = FanoutEmitted !Int !Text
  deriving stock (Eq, Show)

data FanoutState = FanoutReady
  deriving stock (Bounded, Enum, Eq, Ord, Show)

type ValidatedFanoutEventStream = ValidatedEventStream (HsPred '[] FanoutCommand) '[] FanoutState FanoutCommand FanoutEvent

type FanoutCommandFields = '[ '("targetIndex", Int)]

fanoutCommandCtor :: InCtor FanoutCommand FanoutCommandFields
fanoutCommandCtor =
  unavailableInCtor
    "EmitFanout"
    (\case EmitFanout targetIndex -> Just (RCons Proxy targetIndex RNil))
    (\(RCons _ targetIndex RNil) -> EmitFanout targetIndex)

fanoutEventCtor :: WireCtor FanoutEvent (Int, ())
fanoutEventCtor =
  unavailableWireCtor
    "FanoutEmitted"
    ( \case
        FanoutEmitted targetIndex payload
          | payload == fanoutPayload targetIndex -> Just (targetIndex, ())
        _ -> Nothing
    )
    (\(targetIndex, ()) -> FanoutEmitted targetIndex (fanoutPayload targetIndex))

fanoutTransducer :: SymTransducer (HsPred '[] FanoutCommand) '[] FanoutState FanoutCommand FanoutEvent
fanoutTransducer =
  SymTransducer
    { edgesOut = \FanoutReady ->
        [ Edge
            { guard = matchInCtor fanoutCommandCtor,
              update = UKeep,
              output = [pack fanoutCommandCtor fanoutEventCtor (inpCtor fanoutCommandCtor #targetIndex *: oNil)],
              target = FanoutReady,
              mode = Keiki.Live
            }
        ],
      initial = FanoutReady,
      initialRegs = RNil,
      isFinal = const False
    }

fanoutEventCodec :: Codec FanoutEvent
fanoutEventCodec =
  Codec
    { eventTypes = EventType "FanoutEmitted" :| [],
      eventType = const (EventType "FanoutEmitted"),
      schemaVersion = 1,
      encode = \(FanoutEmitted targetIndex payload) -> toJSON (targetIndex, payload),
      decode = \(EventType eventTypeName) payload ->
        case eventTypeName of
          "FanoutEmitted" ->
            case Aeson.fromJSON payload of
              Aeson.Success (targetIndex, value) -> Right (FanoutEmitted targetIndex value)
              Aeson.Error err -> Left (Text.pack err)
          other -> Left ("unknown fan-out benchmark event type: " <> other),
      upcasters = []
    }

fanoutEventStream :: ValidatedFanoutEventStream
fanoutEventStream =
  mkEventStreamOrThrow
    "bench-command-domain-fanout"
    EventStream
      { transducer = fanoutTransducer,
        initialState = FanoutReady,
        initialRegisters = RNil,
        eventCodec = fanoutEventCodec,
        resolveStreamName = streamName,
        snapshotPolicy = Never,
        stateCodec = Nothing
      }

fanoutDomainHandler :: DomainCommandHandler (HsPred '[] FanoutCommand) '[] FanoutState FanoutCommand FanoutEvent Text Text
fanoutDomainHandler =
  DomainCommandHandler
    { eventStream = fanoutEventStream,
      classifySilent = \_ -> error "fanoutDomainHandler: eventful edge classified as silent"
    }

fanoutPayload :: Int -> Text
fanoutPayload targetIndex =
  Text.take payloadSize (Text.replicate repetitions seed)
  where
    seed = Text.pack (show targetIndex) <> ":"
    repetitions = payloadSize `div` Text.length seed + 1

coordinatorFanouts :: [Int]
coordinatorFanouts = [10, 100, 1000]

domainRouterFanoutBench :: Store.KirokuStore -> StoreRunner -> Int -> Benchmark
domainRouterFanoutBench store runner fanout =
  selectedCounterWithCleanup label fanout (prepareCommand store label fanout (100000 `div` fanout) action) checkCounts $ \counter -> bench (show fanout) $ nfIO (action counter)
  where
    label = "router-" <> show fanout
    checkCounts n = do
      maintenanceProbe store (Text.pack label <> "-finished")
      workCountProbe store (Text.pack label) ("bench-command-domain-router:" <> Text.pack label <> "-") (n * fanout) (n * fanout)
    action counter = do
      n <- nextIteration label fanout counter
      let correlationId = Text.pack label <> "-" <> Text.pack (show n)
          input = FanoutInput correlationId fanout
      runResourceStoreChecked runner do
        runDomainRouterWorker
          defaultRunCommandOptions
          fanoutDomainRouter
          (fanoutAdapter input)
          (\message -> Just (iterationSourceEvent n, message))
        when (n <= 3) (verifyFanout "router" correlationId fanout)

domainProcessManagerFanoutBench :: Store.KirokuStore -> StoreRunner -> Int -> Benchmark
domainProcessManagerFanoutBench store runner fanout =
  selectedCounterWithCleanup label fanout (prepareCommand store label fanout (100000 `div` fanout) action) checkCounts $ \counter -> bench (show fanout) $ nfIO (action counter)
  where
    label = "process-manager-" <> show fanout
    checkCounts n = do
      maintenanceProbe store (Text.pack label <> "-finished")
      workCountProbe store (Text.pack label) ("bench-command-domain-process-manager:" <> Text.pack label <> "-") (n * fanout) (n * fanout)
    action counter = do
      n <- nextIteration label fanout counter
      let correlationId = Text.pack label <> "-" <> Text.pack (show n)
          input = FanoutInput correlationId fanout
      runResourceStoreChecked runner do
        runDomainProcessManagerWorker
          defaultRunCommandOptions
          fanoutDomainProcessManager
          (fanoutAdapter input)
          (\message -> Just (iterationSourceEvent n, message))
        when (n <= 3) (verifyFanout "process-manager" correlationId fanout)

verifyFanout :: (IOE :> es, Store :> es) => Text -> Text -> Int -> Eff es ()
verifyFanout coordinator correlationId fanout =
  for_ [0 .. fanout - 1] \targetIndex -> do
    rows <- Store.readStreamForward (StreamName (fanoutTargetName coordinator correlationId targetIndex)) (StreamVersion 0) 2
    unless (Vector.length rows == 1) (liftIO (fail "fan-out benchmark did not append exactly one event per fresh target"))

iterationSourceEvent :: Int -> RecordedEvent
iterationSourceEvent n = fanoutSourceEvent & #eventId .~ EventId (UUID.fromWords 0 0 1 (fromIntegral n))

fanoutDomainRouter ::
  DomainRouter
    FanoutInput
    (HsPred '[] FanoutCommand)
    '[]
    FanoutState
    FanoutCommand
    FanoutEvent
    Text
    Text
    es
fanoutDomainRouter =
  DomainRouter
    { name = "bench-domain-router",
      key = \(FanoutInput correlationId _) -> correlationId,
      resolve = \(FanoutInput correlationId fanout) -> pure (fanoutCommands "router" correlationId fanout),
      targetHandler = fanoutDomainHandler,
      targetProjections = const []
    }

fanoutDomainProcessManager ::
  DomainProcessManager
    FanoutInput
    (HsPred '[] BenchCommand)
    '[]
    BenchState
    BenchCommand
    BenchEvent
    (HsPred '[] FanoutCommand)
    '[]
    FanoutState
    FanoutCommand
    FanoutEvent
    Text
    Text
fanoutDomainProcessManager =
  DomainProcessManager
    { name = "bench-domain-process-manager",
      correlate = \(FanoutInput correlationId _) -> correlationId,
      eventStream = legacyNoOpStream,
      streamFor = \correlationId -> stream ("bench-command-domain-process-manager:" <> correlationId),
      targetHandler = fanoutDomainHandler,
      targetProjections = const [],
      handle = \(FanoutInput correlationId fanout) ->
        ProcessManagerAction
          { command = SelectNoOp,
            commands = fanoutCommands "process-manager" correlationId fanout,
            timers = []
          }
    }

fanoutCommands :: Text -> Text -> Int -> [PMCommand FanoutCommand]
fanoutCommands coordinator correlationId fanout =
  [ PMCommand
      { target = stream (fanoutTargetName coordinator correlationId targetIndex),
        command = EmitFanout targetIndex
      }
  | targetIndex <- [0 .. fanout - 1]
  ]

fanoutTargetName :: Text -> Text -> Int -> Text
fanoutTargetName coordinator correlationId targetIndex =
  "bench-command-domain-"
    <> coordinator
    <> ":"
    <> correlationId
    <> ":"
    <> Text.pack (show targetIndex)

fanoutSourceEvent :: RecordedEvent
fanoutSourceEvent =
  RecordedEvent
    { eventId = EventId (UUID.fromWords64 0x018f0f1800007000 0x8000000000000abc),
      eventType = EventType "BenchFanoutSource",
      streamVersion = StreamVersion 1,
      globalPosition = GlobalPosition 1,
      originalStreamId = StreamId 1,
      originalVersion = StreamVersion 1,
      payload = toJSON ("fanout" :: Text),
      metadata = Nothing,
      causationId = Nothing,
      correlationId = Nothing,
      createdAt = fixedOccurredAt
    }

fanoutAdapter :: FanoutInput -> Adapter es FanoutInput
fanoutAdapter input =
  Adapter
    { adapterName = "bench-domain-coordinator",
      source =
        Streamly.fromList
          [ Ingested
              { envelope =
                  Envelope
                    { messageId = "bench-domain-fanout",
                      cursor = Nothing,
                      partition = Nothing,
                      enqueuedAt = Nothing,
                      traceContext = Nothing,
                      headers = Nothing,
                      attempt = Nothing,
                      attributes = mempty,
                      payload = input
                    },
                ack = AckHandle (\_ -> pure ()),
                lease = Nothing,
                deliveryStatus = pure DeliveryCurrent
              }
          ],
      shutdown = pure ()
    }

runResourceStoreChecked :: StoreRunner -> Eff '[Store, Error Store.StoreError, KirokuStoreResource, IOE] a -> IO a
runResourceStoreChecked (StoreRunner runner) action = do
  result <- runner action
  case result of
    Left err -> fail (show err)
    Right value -> pure value

runStoreChecked :: Store.KirokuStore -> Eff [Store, Error Store.StoreError, IOE] a -> IO a
runStoreChecked store action = do
  result <- Store.runStoreIO store action
  case result of
    Left err -> fail (show err)
    Right value -> pure value

-- A plain-IO diagnostic: it never contributes a tasty-bench timing or baseline.
runCommandGrowthProbe :: Store.KirokuStore -> IO ()
runCommandGrowthProbe store = do
  sample "empty" 0
  for_ [1 .. 5000 :: Int] $ \n -> append n
  sample "grown-5000-before-analyze" 5000
  runStoreChecked store (Store.runTransaction (Tx.sql "ANALYZE streams; ANALYZE stream_events; ANALYZE events"))
  sample "grown-5000-after-analyze" 5000
  for_ [5001 .. 10000 :: Int] $ \n -> append n
  sample "grown-10000" 10000
  for_ [10001 .. 100000 :: Int] $ \n -> append n
  sample "grown-100000" 100000
  for_ [100001 .. 3005000 :: Int] $ \n -> append n
  sample "grown-3005000" 3005000
  where
    append n = appendTarget (stream ("bench-probe-" <> Text.pack (show n)))
    appendTarget target = runStoreChecked store $ do
      result <- runDomainCommand defaultRunCommandOptions domainAcceptedOneHandler target EmitOne
      case result of
        Right value | value ^. #result . #eventsAppended == 1 -> pure ()
        other -> liftIO (fail (show other))
    sample :: Text -> Int -> IO ()
    sample label n = do
      commandPlanProbe store label
      for_ [1 .. 5 :: Int] $ \block -> do
        started <- getMonotonicTimeNSec
        replicateM_ 100 $ runStoreChecked store $ do
          result <- runDomainCommand defaultRunCommandOptions domainNoOpHandler (stream "bench-probe-absent") SelectNoOp
          case result of
            Right value | value ^. #result . #eventsAppended == 0 -> pure ()
            other -> liftIO (fail (show other))
        stopped <- getMonotonicTimeNSec
        freshStarted <- getMonotonicTimeNSec
        for_ [1 .. 100 :: Int] $ \sampleIndex ->
          appendTarget (stream (Text.intercalate "-" ["bench-probe-diagnostic", label, Text.pack (show block), Text.pack (show sampleIndex)]))
        freshStopped <- getMonotonicTimeNSec
        putStrLn ("FRESH_PROBE label=" <> Text.unpack label <> " block=" <> show block <> " mean_ns=" <> show ((freshStopped - freshStarted) `div` 100))
        putStrLn ("NOOP_PROBE label=" <> Text.unpack label <> " block=" <> show block <> " events=" <> show n <> " mean_ns=" <> show ((stopped - started) `div` 100))
      hFlush stdout

runResetProbes :: Store.KirokuStore -> Store.KirokuStore -> Store.KirokuStore -> IO ()
runResetProbes outboxStore inboxStore producerStore = do
  checkpointBenchmarkStore outboxStore
  let outboxSeed store count = do
        runStoreChecked store (Store.runTransaction (Tx.sql "TRUNCATE keiro.keiro_outbox"))
        seedOutbox store (take count (scenarioMessages (const Nothing)))
      inboxSeed = runInboxScenario inboxStore (InboxScenario "probe" Nothing PersistFullEnvelope Nothing inboxScenarioMessages)
  tableResetProbe outboxStore "keiro.keiro_outbox" 2000 (outboxSeed outboxStore 2000)
  tableResetProbe inboxStore "keiro.keiro_inbox" 2000 inboxSeed
  tableResetProbe producerStore "keiro.keiro_outbox" 1000 (outboxSeed producerStore 1000)