packages feed

mariadb-haskell-0.1.0.0: validation/hs/Validation/Stage6Bench.hs

-- | Additive Stage 6 workloads. Every alternative consumes and verifies the
-- same values. Two complete warmups precede each measured process. Cursor
-- materialization and upload/execution are timed; connection/preparation are not.
module Validation.Stage6Bench (run, retentionNegativeControl) where

import Control.Exception (evaluate)
import Control.Monad
import qualified Data.ByteString as B
import qualified Data.ByteString.Internal as BI
import Data.List (intercalate, isPrefixOf, isSuffixOf)
import Data.IORef
import qualified Data.Text as T
import Database.MySQL.Base
import GHC.Clock
import GHC.Stats
import System.Mem
import Foreign.Marshal.Utils (fillBytes)
import qualified System.IO.Streams as S
import Validation.Common

run :: String -> Int -> IO ()
run ident iterations
    | iterations <= 0 = fail "positive Stage 6 work count required"
    | ident == "upload.retention" = withConn $ \conn -> do
        exec conn "CREATE TEMPORARY TABLE mb_stage6_upload_heap(v LONGBLOB) ENGINE=InnoDB"
        withPreparedStatement conn "INSERT INTO mb_stage6_upload_heap VALUES(?)" $ \statement -> do
            let operation = do
                    exec conn "TRUNCATE mb_stage6_upload_heap"
                    source <- retentionSource iterations
                    void (executePreparedStream statement [PreparedBytes source])
                    actual <- scalarInt conn "SELECT OCTET_LENGTH(v) FROM mb_stage6_upload_heap"
                    equal "retention upload size" (toInteger iterations) actual
                    equal "retention upload every byte" 1 =<< scalarInt conn
                        ("SELECT BINARY v=BINARY REPEAT('*'," ++ show iterations ++ ") FROM mb_stage6_upload_heap")
                    pure actual
            measure ident (toInteger iterations) (toInteger iterations) operation
    | "upload." `isPrefixOf` ident = withConn $ \conn -> do
        exec conn "CREATE TEMPORARY TABLE mb_stage6_upload_bench(n INT,v LONGBLOB) ENGINE=InnoDB"
        withPreparedStatement conn "INSERT INTO mb_stage6_upload_bench VALUES(?,?)" $ \statement -> do
            let operation = do
                    exec conn "TRUNCATE mb_stage6_upload_bench"
                    -- Generate equivalent data inside timing. The buffered
                    -- alternative must materialize a full MiB; the streamed
                    -- alternative retains only its reusable 16 KiB chunk.
                    withTransaction conn $ forM_ [1..iterations] $ \n -> do
                        -- IO allocation prevents full-laziness from hoisting
                        -- payload construction out of the warmup/measured runs.
                        chunk <- BI.create 16384 (\ptr -> fillBytes ptr (fromIntegral n) 16384)
                        if ident == "upload.streamed" then do
                            source <- S.fromList (replicate 64 chunk)
                            ok <- executePreparedStream statement [PreparedValue (MySQLInt32 (fromIntegral n)),PreparedBytes source]
                            equal "stream insert" 1 (okAffectedRows ok)
                        else if ident == "upload.buffered" then do
                            let payload = B.concat (replicate 64 chunk)
                            ok <- executePrepared statement [MySQLInt32 (fromIntegral n),MySQLBytes payload]
                            equal "buffered insert" 1 (okAffectedRows ok)
                        else fail "unknown upload profile"
                    actual <- scalarInt conn "SELECT SUM(OCTET_LENGTH(v)) FROM mb_stage6_upload_bench"
                    equal "all stored bytes" (toInteger iterations * 1048576) actual
                    equal "every stored byte matches its row" (toInteger iterations) =<<
                        scalarInt conn "SELECT SUM(BINARY v=BINARY REPEAT(CHAR(MOD(n,256)),1048576)) FROM mb_stage6_upload_bench"
                    pure actual
            measure ident (toInteger iterations * 1048576) (toInteger iterations * 1048576) operation
    | "cursor." `isPrefixOf` ident = withConn $ \conn ->
        withPreparedStatement conn "SELECT seq,REPEAT('x',128) FROM seq_1_to_10000" $ \statement -> do
            let operation = if ident == "cursor.stream" then do
                    (_,stream) <- queryPrepared statement []
                    consume stream
                  else withCursor statement [] $ \cursor -> do
                    let batchSize = case ident of
                            "cursor.batch32" -> 32
                            "cursor.batch512" -> 512
                            "cursor.batch4096" -> 4096
                            _ -> 0
                        loop total = do
                            done <- cursorExhausted cursor
                            if done then pure total else do
                                value <- fetchCursor cursor batchSize >>= consume
                                loop (total + value)
                    loop 0
            measure ident (toInteger iterations * 10000) (toInteger iterations * 51285000)
                (sum <$> replicateM iterations operation)
    | "transport." `isPrefixOf` ident = do
        ci <- config
        let compressed = ".zlib" `isSuffixOf` ident
            wide = "transport.wide." `isPrefixOf` ident
            width = if wide then 2048 else 32 :: Int
            statementText = sql ("SELECT seq,REPEAT('x'," ++ show width ++ ") FROM seq_1_to_1000")
            expected = 500500 + 1000 * toInteger width
        withConnect (connectWithOptions defaultConnectionOptions {enableCompression=compressed} ci) $ \conn ->
            withPreparedStatement conn statementText $ \statement -> do
                let operation = queryPrepared statement [] >>= consume . snd
                measure ident (toInteger iterations * 1000) (toInteger iterations * expected)
                    (sum <$> replicateM iterations operation)
    | "progress." `isPrefixOf` ident = do
        ci <- config
        let enabled = ident == "progress.enabled"
        withConnect (connectWithOptions defaultConnectionOptions {enableProgressReporting=enabled} ci) $ \conn -> do
            when enabled (setProgressCallback conn (Just (const (pure ()))))
            let operation = scalarInt conn "SELECT 937"
            measure ident (toInteger iterations) (toInteger iterations * 937)
                (sum <$> replicateM iterations operation)
    | otherwise = fail "unknown Stage 6 benchmark"

-- Each producer read owns fresh payload storage. Repeating slices of one
-- immutable chunk would let a whole-input buffer retain only that one chunk,
-- making the heap checkpoints incapable of detecting the regression.
retentionSource :: Int -> IO (S.InputStream B.ByteString)
retentionSource bytes = do
    produced <- newIORef 0
    S.makeInputStream $ do
        n <- readIORef produced
        if n >= bytes then pure Nothing else do
            let size = min 65536 (bytes-n)
                total = n+size
            chunk <- BI.create size (\ptr -> fillBytes ptr 42 size)
            writeIORef produced total
            when (total `elem` [65536,1048576,16777216,67108864] || total == bytes) $ do
                performGC
                stats <- getRTSStats
                putStrLn ("STAGE6_HEAP\t" ++ show total ++ "\t" ++ show (gcdetails_live_bytes (gc stats)))
            pure (Just chunk)

-- Deliberately wrong consumer, run as a separate correctness process. The same
-- source/checkpoints must expose retaining every chunk before upload starts.
-- Python applies the actual publication guard and requires it to reject this
-- output. No timings from this negative control enter benchmark comparisons.
retentionNegativeControl :: IO ()
retentionNegativeControl = replicateM_ 3 $ do
    source <- retentionSource 67108864
    chunks <- S.toList source
    count <- evaluate (sum (map B.length chunks))
    equal "buffering control consumed the complete source" 67108864 count

consume :: S.InputStream [MySQLValue] -> IO Integer
consume = S.fold (\total row -> total + sum (map value row)) 0
  where
    value (MySQLText text) = toInteger (T.length text)
    value (MySQLBytes bytes) = toInteger (B.length bytes)
    value other = number other

measure :: String -> Integer -> Integer -> IO Integer -> IO ()
measure ident units expected operation = do
    replicateM_ 2 (operation >>= equal "Stage 6 warmup checksum" expected)
    performGC
    before <- getRTSStats
    start <- getMonotonicTimeNSec
    checksum <- operation >>= evaluate
    end <- getMonotonicTimeNSec
    performGC
    after <- getRTSStats
    equal "Stage 6 measured checksum" expected checksum
    putStrLn (intercalate "\t" ["STAGE6",ident,show units,show (end-start),
        show (allocated_bytes after-allocated_bytes before),show (cpu_ns after-cpu_ns before),
        show (gc_cpu_ns after-gc_cpu_ns before),show checksum])