packages feed

valiant-0.1.0.0: bench/BenchQuery.hs

{-# LANGUAGE BangPatterns #-}

module BenchQuery (benchmarks) where

import Criterion.Main
import Data.Int (Int32, Int64)
import Data.IORef
import Data.Text (Text)
import Data.Text qualified as T
import Valiant
import System.IO.Unsafe (unsafePerformIO)
import TestSupport

-- | Global connection and pool, initialized once via unsafePerformIO.
-- This avoids NFData requirements on Connection/Pool.
{-# NOINLINE globalConn #-}
globalConn :: IORef (Maybe Connection)
globalConn = unsafePerformIO (newIORef Nothing)

{-# NOINLINE globalPool #-}
globalPool :: IORef (Maybe Pool)
globalPool = unsafePerformIO (newIORef Nothing)

getConn :: IO Connection
getConn = do
  mc <- readIORef globalConn
  case mc of
    Just c -> pure c
    Nothing -> do
      url <- requireDatabaseUrl
      c <- connectString url
      writeIORef globalConn (Just c)
      -- Ensure schema + seed data
      _ <- simpleQuery c "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now())"
      _ <- simpleQuery c "TRUNCATE TABLE users RESTART IDENTITY"
      insertBulkUsers c 10000
      pure c

getPool :: IO Pool
getPool = do
  mp <- readIORef globalPool
  case mp of
    Just p -> pure p
    Nothing -> do
      _ <- getConn  -- ensure schema + seed data exist
      url <- requireDatabaseUrl
      p <- newPool defaultPoolConfig
        { poolConnString = url
        , poolSize = 4
        , poolAcquireTimeout = 5
        }
      writeIORef globalPool (Just p)
      pure p

benchmarks :: [Benchmark]
benchmarks =
  [ bgroup "query"
    [ bench "SELECT 1 (simple)" $
        whnfIO (getConn >>= \c -> simpleQuery c "SELECT 1")

    , bench "SELECT 1 (extended/prepared)" $
        whnfIO (getConn >>= \c -> fetchScalar c stmtSelectLiteral ())

    , bench "fetchOne by PK" $
        whnfIO (getConn >>= \c -> fetchOne c stmtSelectById (1 :: Int32))

    , bench "fetchAll 5 rows" $
        whnfIO (getConn >>= \c -> fetchAll c stmtListFive ())

    , bench "fetchAll 1000 rows" $
        whnfIO (getConn >>= \c -> fetchAll c stmtListAll1k ())

    , bench "fetchScalar COUNT" $
        whnfIO (getConn >>= \c -> fetchScalar c stmtCount ())

    , bench "execute INSERT (in txn, rolled back)" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx -> do
            _ <- execute (txConn tx) stmtInsertUser ("bench_user", Just ("b@t.com" :: Text))
            -- We let the transaction commit; the user is real but harmless
            pure ()

    , bench "pool acquire/release (SELECT 1)" $
        whnfIO $ do
          p <- getPool
          withResource p $ \c -> simpleQuery c "SELECT 1"

    , bench "fetchAllVec 1000 rows" $
        whnfIO (getConn >>= \c -> fetchAllVec c stmtListAll1k ())

    , bench "executeWithFold 1000 rows" $
        whnfIO (getConn >>= \c -> executeWithFold c stmtListAll1k () (RowFold (0 :: Int) (\n _ -> n + 1)))

    , bench "forEach 1000 rows" $
        whnfIO (getConn >>= \c -> forEach c stmtListAll1k () (\_ -> pure ()))

    , bench "executeBatch 100 inserts (pipelined)" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx -> do
            _ <- executeBatch (txConn tx) stmtInsertUser
              [ ("bench_" <> T.pack (show i), Just ("b@t.com" :: Text))
              | i <- [1 :: Int .. 100]
              ]
            pure ()

    , bench "executeBatch 1000 inserts (pipelined)" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx -> do
            _ <- executeBatch (txConn tx) stmtInsertUser
              [ ("bench_" <> T.pack (show i), Just ("b@t.com" :: Text))
              | i <- [1 :: Int .. 1000]
              ]
            pure ()

    , bench "executeBatch 5000 inserts (pipelined)" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx -> do
            _ <- executeBatch (txConn tx) stmtInsertUser
              [ ("bench_" <> T.pack (show i), Just ("b@t.com" :: Text))
              | i <- [1 :: Int .. 5000]
              ]
            pure ()

    , bench "transaction overhead (BEGIN+COMMIT)" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx ->
            fetchScalar (txConn tx) stmtCount ()

    , bench "cursor 1k rows, batch 100" $
        whnfIO (cursorDrain stmtListAll1k 100)
    , bench "cursor 1k rows, batch 500" $
        whnfIO (cursorDrain stmtListAll1k 500)
    , bench "cursor 1k rows, batch 1000" $
        whnfIO (cursorDrain stmtListAll1k 1000)
    , bench "cursor 10k rows, batch 100" $
        whnfIO (cursorDrain stmtListAll10k 100)
    , bench "cursor 10k rows, batch 1000" $
        whnfIO (cursorDrain stmtListAll10k 1000)
    , bench "cursor 10k rows, batch 10000" $
        whnfIO (cursorDrain stmtListAll10k 10000)

    -- fetchAllCursor variants (decode + materialise the full result list).
    -- Compared against cursorDrain (which only counts), the gap is
    -- decode + accumulation cost.
    , bench "fetchAllCursor 1k rows, batch 100" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx ->
            length <$> fetchAllCursor (txConn tx) stmtListAll1k () 100
    , bench "fetchAllCursor 10k rows, batch 100" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx ->
            length <$> fetchAllCursor (txConn tx) stmtListAll10k () 100
    , bench "fetchAllCursor 10k rows, batch 1000" $
        whnfIO $ do
          p <- getPool
          withTransaction p $ \tx ->
            length <$> fetchAllCursor (txConn tx) stmtListAll10k () 1000
    ]
  ]

-- | Drain a cursor with the given batch size. Returns the row count so
-- the benchmark can't optimize the fetch away.
cursorDrain :: Statement () (Int32, Text) -> Int -> IO Int
cursorDrain !stmt !batch = do
  p <- getPool
  withTransaction p $ \tx ->
    withCursor (txConn tx) stmt () batch $ \cs -> do
      let loop !n = do
            rows <- fetchBatch cs batch
            if null rows then pure n else loop (n + length rows)
      loop 0

-- Statements ----------------------------------------------------------------

stmtSelectLiteral :: Statement () Int32
stmtSelectLiteral = mkStatement "SELECT 1::int4" [] ["?column?"] "<bench>"

stmtSelectById :: Statement Int32 (Int32, Text, Maybe Text)
stmtSelectById = mkStatement
  "SELECT id, name, email FROM users WHERE id = $1"
  [23] ["id", "name", "email"] "<bench>"

stmtListFive :: Statement () (Int32, Text)
stmtListFive = mkStatement
  "SELECT id, name FROM users ORDER BY id LIMIT 5"
  [] ["id", "name"] "<bench>"

stmtListAll1k :: Statement () (Int32, Text)
stmtListAll1k = mkStatement
  "SELECT id, name FROM users ORDER BY id LIMIT 1000"
  [] ["id", "name"] "<bench>"

stmtListAll10k :: Statement () (Int32, Text)
stmtListAll10k = mkStatement
  "SELECT id, name FROM users ORDER BY id LIMIT 10000"
  [] ["id", "name"] "<bench>"

stmtInsertUser :: Statement (Text, Maybe Text) ()
stmtInsertUser = mkStatement
  "INSERT INTO users (name, email) VALUES ($1, $2)"
  [25, 25] [] "<bench>"

stmtCount :: Statement () Int64
stmtCount = mkStatement "SELECT count(*) FROM users" [] ["count"] "<bench>"