packages feed

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

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Validation.Benchmark (benchmarks, runBenchmark) where

import Control.Concurrent
import Control.Exception hiding (assert)
import Control.Monad
import Data.Binary.Get
import Data.Binary.Put
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.List (intercalate, isPrefixOf)
import qualified Data.Text as T
import qualified Data.Vector as V
import Database.MySQL.Base
import GHC.Clock
import GHC.Stats
import qualified System.IO.Streams as S
import System.Mem
import Validation.Common

benchmarks :: [(String,Int)]
benchmarks =
    [("latency.text",500),("latency.prepared",500)
    ,("read.text.narrow",5),("read.prepared.narrow",5),("read.vector.narrow",5)
    ,("read.text.wide",3),("read.prepared.wide",3)
    ,("read.tls.narrow",5),("read.socket.narrow",5)
    ,("read.parallel-4",3),("read.parallel-16",3)
    ,("write.text",1000),("write.prepared",1000),("write.execute-many",1000)
    ,("blob.text",5),("blob.prepared",5)
    ,("micro.text-row",100000),("micro.binary-row",100000)
    ,("native.bulk",1000),("native.metadata-cache",500)]

narrow,wide :: String
narrow="SELECT id,n,payload FROM mb_bench_data ORDER BY id"
wide="SELECT id,n," ++ intercalate "," (replicate 30 "payload") ++ " FROM mb_bench_data ORDER BY id"

weight :: MySQLValue -> Integer
weight (MySQLText x)=toInteger (T.length x)
weight (MySQLBytes x)=toInteger (B.length x)
weight MySQLNull=0
weight x=number x

consume :: S.InputStream [MySQLValue] -> IO Integer
consume=S.fold (\ !acc row -> acc + sum (map weight row)) 0

consumeVector :: S.InputStream (V.Vector MySQLValue) -> IO Integer
consumeVector=S.fold (\ !acc row -> acc + V.foldl' (\ !s x -> s+weight x) 0 row) 0

textRead :: MySQLConn -> String -> IO Integer
textRead c q=query_ c (sql q) >>= consume . snd

preparedRead :: MySQLConn -> StmtID -> IO Integer
preparedRead c st=queryStmt c st [] >>= consume . snd

parallel :: [IO Integer] -> IO Integer
parallel actions=do
    results <- forM actions $ \action -> do
        done <- newEmptyMVar
        _ <- forkIO (try action >>= putMVar done)
        pure done
    values <- mapM takeMVar results
    sum <$> mapM (either throwIO pure) (values :: [Either SomeException Integer])

-- Each sample is one complete operation (query, transaction, or decoder batch).
-- SQL setup and warm-up happen before counters/timers start.
measure :: String -> Int -> Int -> Integer -> IO Integer -> IO ()
measure name iterations units expected action=do
    replicateM_ 2 (action >>= equal "warm-up checksum" expected)
    performGC
    before <- getRTSStats
    start <- getMonotonicTimeNSec
    samples <- replicateM iterations $ do
        t0 <- getMonotonicTimeNSec
        !checksum <- action
        t1 <- getMonotonicTimeNSec
        equal "benchmark checksum" expected checksum
        pure (t1-t0,checksum)
    end <- getMonotonicTimeNSec
    timed <- getRTSStats
    performGC
    after <- getRTSStats
    putStrLn $ intercalate "\t"
        ["BENCH",name,show (iterations*units),show (end-start)
        ,show (allocated_bytes after-allocated_bytes before)
        ,show (gc_cpu_ns timed-gc_cpu_ns before)
        ,show (max_live_bytes after),show (cpu_ns timed-cpu_ns before)
        ,show (sum (map snd samples)),intercalate "," (map (show . fst) samples)]

runBenchmark :: String -> Int -> IO ()
runBenchmark name n
    | name=="native.bulk" = throwIO (Unsupported "binary bulk API absent")
    | name=="native.metadata-cache" = throwIO (Unsupported "metadata caching absent")
    | name=="micro.text-row" = micro False n
    | name=="micro.binary-row" = micro True n
    | "read.parallel-" `isPrefixOf` name = do
        let workers=read (drop (length ("read.parallel-" :: String)) name)
        ci <- config
        bracket (replicateM workers (connect ci)) (mapM_ quietClose) $ \cs ->
            measure name n (workers*10000) (toInteger workers*201300000) (parallel [textRead c narrow | c <- cs])
    | otherwise=withConnect open $ \c -> case name of
        "latency.text" -> measure name n 1 1 (textRead c "SELECT 1")
        "latency.prepared" -> withStmt c "SELECT 1" $ \st -> measure name n 1 1 (preparedRead c st)
        "read.text.narrow" -> measure name n 10000 201300000 (textRead c narrow)
        "read.prepared.narrow" -> withStmt c narrow $ \st -> measure name n 10000 201300000 (preparedRead c st)
        "read.vector.narrow" -> measure name n 10000 201300000 (queryVector_ c (sql narrow) >>= consumeVector . snd)
        "read.text.wide" -> measure name n 10000 238420000 (textRead c wide)
        "read.prepared.wide" -> withStmt c wide $ \st -> measure name n 10000 238420000 (preparedRead c st)
        "read.tls.narrow" -> measure name n 10000 201300000 (textRead c narrow)
        "read.socket.narrow" -> measure name n 10000 201300000 (textRead c narrow)
        "blob.text" -> measure name n 1048576 1048576 (textRead c "SELECT CAST(REPEAT('x',1048576) AS BINARY)")
        "blob.prepared" -> withStmt c "SELECT CAST(REPEAT('x',1048576) AS BINARY)" $ \st -> measure name n 1048576 1048576 (preparedRead c st)
        "write.text" -> writeBench c Nothing
        "write.prepared" -> do
            exec c "CREATE TEMPORARY TABLE mb_bench_write(n BIGINT) ENGINE=InnoDB"
            withStmt c "INSERT INTO mb_bench_write(n) VALUES(?)" (writeBench c . Just)
        "write.execute-many" -> writeBench c Nothing
        _ -> throwIO (userError ("unknown benchmark " ++ name))
  where
    open=do
        ci <- config
        case name of
            "read.tls.narrow" -> tlsConnect ci "localhost"
            "read.socket.narrow" -> socketPath >>= \p -> connectUnixSocket p ci
            _ -> connect ci
    withStmt c q=bracket (prepareStmt c (sql q)) (closeStmt c)
    writeBench c stmt=do
        exec c "CREATE TEMPORARY TABLE IF NOT EXISTS mb_bench_write(n BIGINT) ENGINE=InnoDB"
        let action=do
                exec c "TRUNCATE TABLE mb_bench_write"
                exec c "BEGIN"
                case (name,stmt) of
                    ("write.execute-many",_) -> void (executeMany c "INSERT INTO mb_bench_write(n) VALUES(?)" [[MySQLInt64 (fromIntegral i)] | i <- [1..n]])
                    (_,Just st) -> forM_ [1..n] $ \i -> void (executeStmt c st [MySQLInt64 (fromIntegral i)])
                    _ -> forM_ [1..n] $ \i -> exec c ("INSERT INTO mb_bench_write(n) VALUES(" ++ show i ++ ")")
                exec c "COMMIT"
                count <- scalarInt c "SELECT COUNT(*) FROM mb_bench_write"
                equal "inserted benchmark row count" (toInteger n) count
                scalarInt c "SELECT COALESCE(SUM(n),0) FROM mb_bench_write"
        measure name 1 n (toInteger n*(toInteger n+1) `div` 2) action

{-# NOINLINE decodeWeight #-}
decodeWeight :: Bool -> [ColumnDef] -> L.ByteString -> IO Integer
decodeWeight binary cols input = case runGetOrFail parser input of
    Left (_,_,err) -> throwIO (userError err)
    Right (_,_,xs) -> evaluate (sum (map weight xs))
  where parser=if binary then getBinaryRow cols (length cols) else getTextRow cols

micro :: Bool -> Int -> IO ()
micro binary n=do
    let name=if binary then "micro.binary-row" else "micro.text-row"
        cols=[ColumnDef "" "" "" "id" "" 63 20 mySQLTypeLongLong 0 0
             ,ColumnDef "" "" "" "n" "" 63 20 mySQLTypeLongLong 0 0
             ,ColumnDef "" "" "" "payload" "" 224 128 mySQLTypeVarString 0 0]
        inputs=[runPut $ if binary
                then putWord8 0 >> putWord8 0 >> putInt64le i >> putInt64le (i*3) >> putLenEncBytes (B.replicate 128 120)
                else putLenEncBytes (fromStringBytes (show i)) >> putLenEncBytes (fromStringBytes (show (i*3))) >> putLenEncBytes (B.replicate 128 120)
               | i <- [1..16]]
        action=go 0 0
          where
            go i !acc
                | i>=n = pure acc
                | otherwise = decodeWeight binary cols (inputs !! (i `mod` 16)) >>= \v -> go (i+1) (acc+v)
        expected=sum [toInteger (4*(i `mod` 16+1)+128) | i <- [0..n-1]]
    measure name 1 n expected action
  where fromStringBytes=B.pack . map (fromIntegral . fromEnum)