2026-10-02 14:53:02 +08:00
{-# LANGUAGE OverloadedStrings #-}
2026-09-25 14:21:55 +08:00
module Main where
2026-10-02 14:53:02 +08:00
import ChuSQL.Core.Protocol ( QueryResult ( .. ), TableInfo ( .. ))
import ChuSQL.Interface.Link ( Client , clientPing , closeClient , connectClient )
import ChuSQL.Interface.Session (
Session ,
authenticateSession ,
catalog ,
newSession ,
runStatement ,
switchDatabase ,
2026-09-25 14:21:55 +08:00
)
2026-10-05 00:15:01 +08:00
import Control.Concurrent ( forkIO , forkFinally , threadDelay )
import Control.Concurrent.MVar ( newEmptyMVar , putMVar , takeMVar )
import Control.Exception ( IOException , bracket , evaluate , throwIO , try )
import Control.Monad ( forM , forM_ , replicateM , unless , when )
import Data.List ( intercalate , sort )
import ChuSQL.Core.Model ( Value ( .. ))
2026-10-02 14:53:02 +08:00
import Data.Text ( Text )
import qualified Data.Text as T
2026-09-25 14:21:55 +08:00
import GHC.Clock ( getMonotonicTime )
import System.Directory (
createDirectoryIfMissing ,
2026-10-02 14:53:02 +08:00
findExecutable ,
2026-09-25 14:21:55 +08:00
getTemporaryDirectory ,
removeDirectoryRecursive ,
2026-10-05 00:15:01 +08:00
removeFile ,
2026-09-25 14:21:55 +08:00
)
2026-10-01 20:48:38 +08:00
import System.Environment ( getArgs )
2026-10-02 14:53:02 +08:00
import System.Exit ( ExitCode ( .. ), exitFailure )
2026-09-25 14:21:55 +08:00
import System.FilePath (( </> ))
2026-10-01 20:48:38 +08:00
import System.Info ( os )
2026-10-02 14:53:02 +08:00
import System.IO ( Handle , hGetLine )
2026-09-25 14:21:55 +08:00
import System.Process (
2026-10-02 14:53:02 +08:00
CreateProcess ( std_err , std_out ),
2026-09-25 14:21:55 +08:00
ProcessHandle ,
2026-10-02 14:53:02 +08:00
StdStream ( CreatePipe , NoStream ),
2026-09-25 14:21:55 +08:00
createProcess ,
proc ,
2026-10-03 02:17:46 +08:00
readProcessWithExitCode ,
2026-09-25 14:21:55 +08:00
terminateProcess ,
waitForProcess ,
)
import Text.Printf ( printf )
2026-10-05 00:15:01 +08:00
import Text.Read ( readMaybe )
2026-09-25 14:21:55 +08:00
2026-10-02 14:53:02 +08:00
-- 全链路基准:客户端经 TCP 连 chusql-server,落盘到进程内 Rust 存储。
-- 每条语句都走完整链路:解析、语义检查、提交。
-- 对照项:ping 与 catalog。
2026-09-25 14:21:55 +08:00
2026-10-02 14:53:02 +08:00
-- | 一个基准世界:真 server 进程 + 一条 TCP 会话
data Bench = Bench
{ bProcess :: ProcessHandle
, bDataDir :: FilePath
, bClient :: Client
, bSession :: Session
2026-10-05 00:15:01 +08:00
, bPort :: Int
, bConfigPath :: FilePath
2026-10-02 14:53:02 +08:00
}
2026-09-25 14:21:55 +08:00
2026-10-02 14:53:02 +08:00
-- | 跑一条 SQL 文本(整条链路)
runSql :: Bench -> String -> IO ( Either Text QueryResult )
2026-10-05 00:15:01 +08:00
runSql bench sql = do
result <- runStatement ( bSession bench ) ( T . pack sql )
case result of
Left err -> ioError ( userError ( T . unpack err ))
Right _ -> pure result
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
-- | 量一次墙上耗时并强制求出行数
2026-10-02 14:53:02 +08:00
timedQuery :: Bench -> String -> IO ( Double , Either Text Int )
timedQuery bench sql = do
2026-09-25 14:21:55 +08:00
t0 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
r <- runSql bench sql
2026-09-25 14:21:55 +08:00
n <- case r of
Left _ -> pure ( - 1 )
2026-10-02 14:53:02 +08:00
Right result -> evaluate ( qrRowCount result )
2026-09-25 14:21:55 +08:00
t1 <- getMonotonicTime
pure ( t1 - t0 , either Left ( const ( Right n )) r )
-- | 打印一行结果
2026-10-02 14:53:02 +08:00
report :: String -> Either Text Int -> Double -> IO ()
2026-09-25 14:21:55 +08:00
report label result dt = case result of
2026-10-02 14:53:02 +08:00
Left e -> printf "%-38s %10s !! %s \n " label ( "FAILED" :: String ) ( T . unpack e )
2026-09-25 17:41:51 +08:00
Right n -> printf "%-38s %8.1f ms %8d rows \n " label ( dt * 1000 ) n
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
-- | 单独量一条可重复的 SQL(先预热)
2026-10-02 14:53:02 +08:00
benchSql :: Bench -> String -> String -> IO ( Either Text Int )
benchSql bench label sql = do
_ <- runSql bench sql
( dt , r ) <- timedQuery bench sql
2026-09-25 14:21:55 +08:00
report label r dt
pure r
2026-09-25 17:41:51 +08:00
-- | 只跑一次的语句(重跑会报错)
2026-10-02 14:53:02 +08:00
benchSqlOnce :: Bench -> String -> String -> IO ( Either Text Int )
benchSqlOnce bench label sql = do
( dt , r ) <- timedQuery bench sql
2026-09-25 14:21:55 +08:00
report label r dt
pure r
-- | 批量插入并报平均耗时
2026-10-02 14:53:02 +08:00
batchInsert :: Bench -> String -> Int -> ( Int -> String ) -> IO ( Either Text () )
batchInsert bench label n mkSql = do
2026-09-25 14:21:55 +08:00
t0 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
results <- mapM ( runSql bench . mkSql ) [ 1 .. n ]
2026-09-25 14:21:55 +08:00
t1 <- getMonotonicTime
let total = t1 - t0
errs = [ e | Left e <- results ]
case errs of
[] -> do
printf
2026-09-25 17:41:51 +08:00
"%-38s %8.1f ms %7.3f ms/row \n "
2026-09-25 14:21:55 +08:00
label
( total * 1000 )
( total * 1000 / fromIntegral n )
pure ( Right () )
( e : _ ) -> do
2026-10-02 14:53:02 +08:00
printf "%-38s %10s !! %s \n " label ( "FAILED" :: String ) ( T . unpack e )
2026-09-25 14:21:55 +08:00
pure ( Left e )
2026-10-02 14:53:02 +08:00
-- | 纯 TCP 往返:只过协议层,不过引擎与存储
benchPing :: Bench -> Int -> IO ()
benchPing bench times = do
_ <- clientPing ( bClient bench )
2026-09-25 14:21:55 +08:00
t0 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
results <- mapM ( const ( clientPing ( bClient bench ))) [ 1 .. times ]
2026-09-25 14:21:55 +08:00
t1 <- getMonotonicTime
let total = t1 - t0
2026-10-02 14:53:02 +08:00
hits = length [ () | Right _ <- results ]
2026-09-25 14:21:55 +08:00
printf
2026-10-02 14:53:02 +08:00
"%-38s %8.1f ms %7.3f ms/op (pongs %d/%d) \n "
( "tcp round trip (ping, no engine)" :: String )
2026-09-25 14:21:55 +08:00
( total * 1000 )
( total * 1000 / fromIntegral times )
2026-10-02 14:53:02 +08:00
hits
times
2026-10-05 00:15:01 +08:00
unless ( hits == times ) ( ioError ( userError "ping failed during benchmark" ))
2026-10-02 14:53:02 +08:00
-- | 一次点查(走语句,等于把 id 索引那一支也量进去)
benchLookup :: Bench -> Int -> Int -> IO ()
benchLookup bench key times = do
let sql = printf "SELECT name FROM users WHERE id = %d" key
_ <- runSql bench sql
t0 <- getMonotonicTime
results <- mapM ( const ( runSql bench sql )) [ 1 .. times ]
t1 <- getMonotonicTime
let total = t1 - t0
hits = length [ () | Right result <- results , qrRowCount result > 0 ]
printf
"%-38s %8.1f ms %7.3f ms/op (hits %d/%d) \n "
( "point lookup by id (statement, over TCP)" :: String )
( total * 1000 )
( total * 1000 / fromIntegral times )
hits
2026-09-25 14:21:55 +08:00
times
2026-10-05 00:15:01 +08:00
unless ( hits == times ) ( ioError ( userError "point lookup failed during benchmark" ))
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
-- | 量 list_catalog(只取结构,不取行)
2026-10-02 14:53:02 +08:00
benchCatalog :: Bench -> Int -> IO ()
benchCatalog bench n = do
_ <- catalog ( bSession bench )
2026-09-25 14:21:55 +08:00
t0 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
results <- mapM ( const ( catalog ( bSession bench ))) [ 1 .. n ]
2026-09-25 14:21:55 +08:00
t1 <- getMonotonicTime
let total = t1 - t0
2026-10-02 14:53:02 +08:00
hits = length [ () | Right _ <- results ]
2026-09-25 14:21:55 +08:00
printf
2026-10-02 14:53:02 +08:00
"%-38s %8.1f ms %7.3f ms/op (%d/%d ok) \n "
( "list_catalog (schema only, over TCP)" :: String )
2026-09-25 14:21:55 +08:00
( total * 1000 )
( total * 1000 / fromIntegral n )
2026-10-02 14:53:02 +08:00
hits
n
2026-10-05 00:15:01 +08:00
unless ( hits == n ) ( ioError ( userError "catalog failed during benchmark" ))
2026-09-25 14:21:55 +08:00
-- | 依次跑一批语句,返回 (耗时秒, 错误清单)
2026-10-02 14:53:02 +08:00
runBatch :: Bench -> [ String ] -> IO ( Double , [ Text ])
runBatch bench sqls = do
2026-09-25 14:21:55 +08:00
t0 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
results <- mapM ( runSql bench ) sqls
2026-09-25 14:21:55 +08:00
t1 <- getMonotonicTime
pure ( t1 - t0 , [ e | Left e <- results ])
-- | 把 1..n 按 size 切成一段段
chunked :: Int -> Int -> [[ Int ]]
chunked size n = go [ 1 .. n ]
where
2026-10-02 14:53:02 +08:00
-- | 递归切分
2026-09-25 14:21:55 +08:00
go [] = []
go xs = let ( a , b ) = splitAt size xs in a : go b
-- | 一条带多行的 INSERT(一条语句 N 行,只落一次盘)
multiInsertSql :: [ Int ] -> String
multiInsertSql ids =
"INSERT INTO batch_t (id, name, age) VALUES "
++ intercalate ", " [ printf "(%d, 'b%d', %d)" i i ( i ` mod ` 100 ) | i <- ids ]
-- | 打印一张表的统计
printStats :: TableInfo -> IO ()
printStats i =
printf
2026-09-25 17:41:51 +08:00
" %-12s %6d rows indexes=%s \n stats=%s \n "
2026-09-25 14:21:55 +08:00
( tiTable i )
( tiRows i )
2026-09-25 17:41:51 +08:00
( if null ( tiIndexes i ) then "none" else unwords ( tiIndexes i ))
2026-09-25 14:21:55 +08:00
( unwords
2026-09-25 17:41:51 +08:00
[ n ++ ":" ++ show d ++ ( if c then "+ (lower bound)" else "" )
2026-09-25 14:21:55 +08:00
| ( n , d , c ) <- tiStats i
]
)
-- | 主场景
2026-10-02 14:53:02 +08:00
runScenarios :: Bench -> Int -> Int -> IO ()
runScenarios bench u o = do
2026-09-25 17:41:51 +08:00
putStrLn " \n [create tables]"
2026-10-02 14:53:02 +08:00
r1 <- benchSqlOnce bench "CREATE TABLE users" "CREATE TABLE users (id int, name str, age int)"
r2 <- benchSqlOnce bench "CREATE TABLE orders" "CREATE TABLE orders (id int, user_id int, product str)"
2026-09-25 14:21:55 +08:00
case ( r1 , r2 ) of
( Right _ , Right _ ) -> pure ()
_ -> do
2026-09-25 17:41:51 +08:00
putStrLn "table creation failed; stopping here"
2026-09-25 14:21:55 +08:00
exitFailure
2026-10-05 00:15:01 +08:00
putStrLn " \n [write: 500 rows per durable INSERT commit]"
loadBatch bench "users" "id,name,age" u ( \ i -> printf "(%d,'user%d',%d)" i i ( i ` mod ` 100 ))
loadBatch bench "orders" "id,user_id,product" o ( \ i -> printf "(%d,%d,'item%d')" i ( 1 + ( i - 1 ) ` mod ` u ) ( i ` mod ` 50 ))
checkCount bench "users" u
checkCount bench "orders" o
2026-09-25 14:21:55 +08:00
2026-10-02 14:53:02 +08:00
putStrLn " \n [breakdown: protocol round trip vs one catalog call, both over TCP]"
benchPing bench 200
benchCatalog bench 200
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
putStrLn " \n [read/query: full statement, including parsing and semantic checks]"
2026-10-02 14:53:02 +08:00
_ <- benchSql bench "SELECT * FROM users" "SELECT * FROM users"
ageR <- benchSql bench "SELECT name FROM users WHERE age > 90" "SELECT name FROM users WHERE age > 90"
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
putStrLn " \n [read/query: join]"
2026-09-25 14:21:55 +08:00
joinR <-
benchSql
2026-10-02 14:53:02 +08:00
bench
2026-09-25 17:41:51 +08:00
"JOIN + WHERE (hash join)"
2026-09-25 14:21:55 +08:00
"SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 90"
2026-09-25 17:41:51 +08:00
putStrLn " \n [point lookup]"
2026-10-02 14:53:02 +08:00
benchLookup bench ( u ` div ` 2 ) 100
2026-10-05 00:15:01 +08:00
pressureScenarios bench u o
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
putStrLn " \n [batch insert: one statement with N rows, one durable commit]"
2026-10-02 14:53:02 +08:00
_ <- benchSqlOnce bench "CREATE TABLE batch_t" "CREATE TABLE batch_t (id int, name str, age int)"
2026-09-25 14:21:55 +08:00
let groups = chunked 100 u
2026-10-02 14:53:02 +08:00
perStmt = maximum ( 0 : map length groups )
( bt , batchErrs ) <- runBatch bench ( map multiInsertSql groups )
2026-09-25 14:21:55 +08:00
case batchErrs of
[] ->
printf
2026-09-25 17:41:51 +08:00
"%-38s %8.1f ms %7.3f ms/row (%d rows/stmt x %d statements) \n "
2026-10-02 14:53:02 +08:00
( "multi-row INSERT" :: String )
2026-09-25 14:21:55 +08:00
( bt * 1000 )
( bt * 1000 / fromIntegral u )
2026-10-02 14:53:02 +08:00
( perStmt :: Int )
2026-09-25 14:21:55 +08:00
( length groups )
2026-10-02 14:53:02 +08:00
( e : _ ) -> printf "%-38s %10s !! %s \n " ( "multi-row INSERT" :: String ) ( "FAILED" :: String ) ( T . unpack e )
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
putStrLn " \n [row delete: cost tracks the scan plus one commit, not the row count]"
2026-10-02 14:53:02 +08:00
_ <- benchSqlOnce bench "DELETE FROM users WHERE id = k (1 row)" ( printf "DELETE FROM users WHERE id = %d" ( u ` div ` 2 ))
_ <- benchSqlOnce bench "DELETE FROM users (all)" "DELETE FROM users"
2026-09-25 14:21:55 +08:00
2026-09-25 17:41:51 +08:00
putStrLn " \n [secondary index: falls back to scan, then uses the index once built]"
2026-10-02 14:53:02 +08:00
_ <- benchSqlOnce bench "CREATE TABLE items" "CREATE TABLE items (id int, code int)"
2026-09-25 14:21:55 +08:00
let itemGroups = chunked 100 o
itemSql ids =
"INSERT INTO items (id, code) VALUES "
++ intercalate ", " [ printf "(%d, %d)" i ( i * 7 + 1 ) | i <- ids ]
2026-10-02 14:53:02 +08:00
( it , itemErrs ) <- runBatch bench ( map itemSql itemGroups )
2026-09-25 14:21:55 +08:00
case itemErrs of
[] -> pure ()
2026-10-02 14:53:02 +08:00
( e : _ ) -> printf " !! %s \n " ( T . unpack e )
2026-09-25 17:41:51 +08:00
printf " (loaded %d rows into items: %.1f ms) \n " o ( it * 1000 )
2026-09-25 14:21:55 +08:00
let targetCode = ( o ` div ` 2 ) * 7 + 1
_ <-
benchSql
2026-10-02 14:53:02 +08:00
bench
2026-09-25 17:41:51 +08:00
"SELECT ... WHERE code = k (no index)"
2026-09-25 14:21:55 +08:00
( printf "SELECT id FROM items WHERE code = %d" targetCode )
2026-10-02 14:53:02 +08:00
_ <- benchSqlOnce bench "CREATE INDEX ON items (code)" "CREATE INDEX ON items (code)"
2026-09-25 14:21:55 +08:00
_ <-
benchSql
2026-10-02 14:53:02 +08:00
bench
2026-09-25 17:41:51 +08:00
"SELECT ... WHERE code = k (via index)"
2026-09-25 14:21:55 +08:00
( printf "SELECT id FROM items WHERE code = %d" targetCode )
2026-09-25 17:41:51 +08:00
putStrLn " \n [catalog stats]"
2026-10-02 14:53:02 +08:00
cat <- catalog ( bSession bench )
2026-09-25 14:21:55 +08:00
case cat of
2026-10-02 14:53:02 +08:00
Left e -> printf " failed to read stats: %s \n " ( T . unpack e )
2026-09-25 14:21:55 +08:00
Right infos -> mapM_ printStats infos
2026-09-25 17:41:51 +08:00
putStrLn " \n [checks]"
2026-09-25 14:21:55 +08:00
let expectAge = 9 * ( u ` div ` 100 ) + max 0 ( u ` mod ` 100 - 90 )
printf
2026-09-25 17:41:51 +08:00
"age > 90 matched %s (expected %d rows from the data distribution) \n "
2026-10-02 14:53:02 +08:00
( either T . unpack show ageR )
2026-09-25 14:21:55 +08:00
expectAge
when ( u == o ) $
printf
2026-09-25 17:41:51 +08:00
"JOIN matched %s (one order per user, should equal the line above = %d) \n "
2026-10-02 14:53:02 +08:00
( either T . unpack show joinR )
2026-09-25 14:21:55 +08:00
expectAge
unless ( u == o ) $
2026-09-25 17:41:51 +08:00
putStrLn "(users and orders differ in size; no simple formula, printing only)"
2026-10-02 14:53:02 +08:00
loadedCheck <- runSql bench "SELECT id FROM batch_t"
printf
"multi-row INSERT loaded %s rows (expected %d) \n "
( either T . unpack ( show . qrRowCount ) loadedCheck )
u
2026-09-25 17:41:51 +08:00
printf "secondary index: code = %d should find id = %d \n " targetCode ( o ` div ` 2 )
2026-10-05 00:15:01 +08:00
checkCount bench "batch_t" u
putStrLn "BENCHMARK_OK all scenarios completed"
2026-09-25 14:21:55 +08:00
2026-10-02 14:53:02 +08:00
-- | 基准用的库名
benchDatabase :: String
benchDatabase = "bench"
-- | 服务端没有默认库:先开一个库,再把之后的语句都落进它
prepareDatabase :: Session -> IO ()
prepareDatabase session = do
created <- runStatement session ( "CREATE DATABASE " <> T . pack benchDatabase )
case created of
Left e -> die ( "cannot create the benchmark database: " ++ T . unpack e )
Right _ -> do
used <- switchDatabase session ( T . pack benchDatabase )
case used of
Left e -> die ( "cannot select the benchmark database: " ++ T . unpack e )
Right () -> pure ()
where
-- | 报错退出
die message = do
putStrLn message
exitFailure
-- | 按平台补上 .exe 后缀
binaryName :: String -> String
binaryName stem = if os == "mingw32" then stem ++ ".exe" else stem
-- | 在 PATH 里找 server 可执行文件
locateServerExe :: IO FilePath
locateServerExe = do
found <- findExecutable ( binaryName "chusql-server" )
case found of
Just path -> pure path
Nothing -> do
putStrLn "chusql-server was not found on PATH: run `stack build` first"
exitFailure
2026-10-03 02:17:46 +08:00
-- | 在 PATH 里找引导程序(server 需要先引导)
locateBootstrapExe :: IO FilePath
locateBootstrapExe = do
found <- findExecutable ( binaryName "csql-bootstrap" )
case found of
Just path -> pure path
Nothing -> do
putStrLn "csql-bootstrap was not found on PATH: run `stack build` first"
exitFailure
2026-10-02 14:53:02 +08:00
-- | 从 server 输出里读出监听端口
readListeningPort :: Handle -> IO Int
readListeningPort handle = do
line <- hGetLine handle
case portFromBanner line of
Just port -> pure port
Nothing -> readListeningPort handle
-- | 取一行里最后一串数字
portFromBanner :: String -> Maybe Int
portFromBanner line
| not ( "listening:" ` isInfixOf ` line ) = Nothing
| otherwise = case span isDigit ( dropWhile ( not . isDigit ) ( reverse line )) of
( [] , _ ) -> Nothing
( digits , _ ) -> Just ( read ( reverse digits ))
-- | 判断子串是否出现
isInfixOf :: String -> String -> Bool
isInfixOf needle haystack = any ( prefix needle ) ( tails haystack )
where
-- | 前缀匹配
prefix [] _ = True
prefix _ [] = False
prefix ( x : xs ) ( y : ys ) = x == y && prefix xs ys
-- | 所有后缀
tails [] = [ [] ]
tails s @ ( _ : rest ) = s : tails rest
-- | 判断是不是数字字符
isDigit :: Char -> Bool
isDigit ch = ch >= '0' && ch <= '9'
-- | 把 server 的 stdout 抽干,免得管道塞满
drainHandle :: Handle -> IO ()
drainHandle handle = do
result <- try ( hGetLine handle ) :: IO ( Either IOException String )
case result of
Left _ -> pure ()
Right _ -> drainHandle handle
-- | 起 server、连上去、登录、开库
startBench :: IO Bench
startBench = do
bin <- locateServerExe
tmp <- getTemporaryDirectory
stamp <- getMonotonicTime
2026-10-05 00:15:01 +08:00
let name = "benchmark4csql-" ++ show ( round ( stamp * 1e6 ) :: Int )
2026-10-02 14:53:02 +08:00
dataDir = tmp </> name
configPath = tmp </> ( name ++ ".toml" )
-- TOML 的普通字符串会吃反斜杠,路径统一用正斜杠
slashed = map ( \ c -> if c == '\\ ' then ' / ' else c) dataDir
createDirectoryIfMissing True dataDir
writeFile configPath ( benchConfigText slashed )
printf "config : %s \n " configPath
printf "data dir : %s \n " dataDir
2026-10-03 02:17:46 +08:00
bootstrap <- locateBootstrapExe
2026-10-05 00:15:01 +08:00
( bootCode , bootOut , bootErr ) <- readProcessWithExitCode bootstrap [ "--config" , configPath , "--passwordless" , "--user" , "admin" ] ""
2026-10-03 02:17:46 +08:00
case bootCode of
ExitSuccess -> printf "bootstrap: system catalog ready \n "
_ -> do
putStrLn ( "the bootstrap program failed (exit " ++ show bootCode ++ "): " ++ bootOut ++ bootErr )
exitFailure
2026-10-02 14:53:02 +08:00
( _ , Just out , _ , process ) <-
2026-10-05 00:15:01 +08:00
createProcess ( proc bin [ "--config" , configPath , "--user" , "admin" ]){ std_out = CreatePipe , std_err = NoStream }
2026-10-02 14:53:02 +08:00
port <- readListeningPort out
_ <- forkIO ( drainHandle out )
printf "server : pid launched, tcp port %d \n " port
linked <- connectClient "127.0.0.1" port
case linked of
Left err -> do
putStrLn ( "cannot connect to the server: " ++ T . unpack err )
stopProcess process
exitFailure
Right client -> do
session <- newSession client
2026-10-03 02:17:46 +08:00
auth <- authenticateSession session ( T . pack "admin" ) ( T . pack "" )
2026-10-02 14:53:02 +08:00
case auth of
Left err -> do
putStrLn ( "cannot sign in: " ++ T . unpack err )
closeClient client
stopProcess process
exitFailure
Right () -> do
prepareDatabase session
2026-10-05 00:15:01 +08:00
pure ( Bench process dataDir client session port configPath )
2026-10-02 14:53:02 +08:00
2026-10-05 00:15:01 +08:00
-- | 基准用的配置:随机端口、临时数据目录
2026-10-02 14:53:02 +08:00
benchConfigText :: String -> String
benchConfigText dataDir =
unlines
2026-10-05 00:15:01 +08:00
[ "[server]"
2026-10-02 14:53:02 +08:00
, "port = 0"
, ""
, "[storage]"
, "data_dir = \" " ++ dataDir ++ " \" "
, ""
, "[log]"
, "level = \" error \" "
]
-- | 收摊:关会话、停 server、清数据目录
stopBench :: Bench -> IO ()
stopBench bench = do
putStrLn " \n [teardown] closing the TCP session, stopping the server, removing the data dir"
closeClient ( bClient bench )
stopProcess ( bProcess bench )
2026-10-05 00:15:01 +08:00
removeDirectoryRecursive ( bDataDir bench )
removeFile ( bConfigPath bench )
2026-10-02 14:53:02 +08:00
-- | 杀掉 server 进程
stopProcess :: ProcessHandle -> IO ()
stopProcess process = do
terminateProcess process
2026-10-05 00:15:01 +08:00
_ <- waitForProcess process
2026-10-02 14:53:02 +08:00
pure ()
2026-09-25 14:21:55 +08:00
-- | 主入口
main :: IO ()
main = do
args <- getArgs
2026-10-05 00:15:01 +08:00
unless ( length args <= 2 ) ( ioError ( userError "usage: benchmark4csql [users=10000] [orders=50000]" ))
let values = take 2 ( args ++ drop ( length args ) [ "10000" , "50000" ])
sizes <- mapM positiveSize values
( u , o ) <- case sizes of
[ usersSize , ordersSize ] -> pure ( usersSize , ordersSize )
_ -> ioError ( userError "expected two benchmark sizes" )
2026-10-02 14:53:02 +08:00
printf "full-chain benchmark: TCP client --> chusql-server --> in-process Rust storage \n "
2026-09-25 17:41:51 +08:00
printf "dataset : users = %d rows, orders = %d rows \n " u o
2026-10-02 14:53:02 +08:00
printf "timing : wall clock (monotonic); every statement runs the full chain over TCP \n "
printf "note : the client no longer links the engine or the storage library \n "
2026-09-25 14:21:55 +08:00
bracket
( do
t1 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
bench <- startBench
2026-09-25 14:21:55 +08:00
t2 <- getMonotonicTime
2026-10-02 14:53:02 +08:00
printf " \n [startup] server up, storage ready, signed in: %.2f s \n " ( t2 - t1 )
threadDelay 25000
pure bench
2026-09-25 14:21:55 +08:00
)
2026-10-02 14:53:02 +08:00
stopBench
( \ bench -> runScenarios bench u o )
2026-10-05 00:15:01 +08:00
-- | 校验压力规模是至少一百的整数
positiveSize :: String -> IO Int
positiveSize raw = case readMaybe raw of
Just size | size >= 100 -> pure size
_ -> ioError ( userError "benchmark row counts must be integers >= 100" )
-- | 批量加载并报告每秒写入行数
loadBatch :: Bench -> String -> String -> Int -> ( Int -> String ) -> IO ()
loadBatch bench table columns count row = do
( milliseconds , () ) <- measure $ forM_ ( chunked 500 count ) $ \ ids -> do
_ <- runSql bench ( "INSERT INTO " ++ table ++ " (" ++ columns ++ ") VALUES " ++ intercalate "," ( map row ids ))
pure ()
printf "LOAD %-16s rows=%d total_ms=%.3f rows_s=%.2f \n " table count milliseconds ( fromIntegral count * 1000 / milliseconds )
-- | 校验完整数据加载数量
checkCount :: Bench -> String -> Int -> IO ()
checkCount bench table expected = do
result <- runSql bench ( "SELECT COUNT(*) FROM " ++ table )
case result of
Right rows | qrRows rows == [[ VInt expected ]] -> pure ()
_ -> ioError ( userError ( "count mismatch in " ++ table ++ ": " ++ show result ))
-- | 用单调时钟测量动作耗时
measure :: IO a -> IO ( Double , a )
measure action = do
start <- getMonotonicTime
result <- action >>= evaluate
end <- getMonotonicTime
pure (( end - start ) * 1000 , result )
-- | 打印均值分位数和请求吞吐量
latencies :: String -> Int -> [ Double ] -> IO ()
latencies label rows times = do
let ordered = sort times
count = length times
total = sum times
-- | 按最近秩取分位数
percentile fraction = ordered !! ( ceiling ( fraction * fromIntegral count ) - 1 )
printf "CASE %-24s n=%d rows=%d mean_ms=%.3f p50_ms=%.3f p95_ms=%.3f max_ms=%.3f ops_s=%.2f \n " label count rows ( total / fromIntegral count ) ( percentile ( 0.5 :: Double )) ( percentile ( 0.95 :: Double )) ( last ordered ) ( fromIntegral count * 1000 / total )
-- | 预热后反复测量并校验结果行数
sampleQuery :: Bench -> String -> String -> Int -> IO ()
sampleQuery bench label statement expected = do
-- | 执行一次并校验结果数量
let action = do
result <- runSql bench statement
case result of
Right rows | qrRowCount rows == expected -> pure ()
_ -> ioError ( userError ( label ++ ": result mismatch " ++ show result ))
_ <- action
times <- replicateM 7 ( fst <$> measure action )
latencies label expected times
-- | 运行索引聚合连接事务与并发压力
pressureScenarios :: Bench -> Int -> Int -> IO ()
pressureScenarios bench u o = do
putStrLn " \n [pressure: one warmup, seven measured samples per read case]"
let selected = length [ () | i <- [ 1 .. o ], ( 1 + ( i - 1 ) ` mod ` u ) ` mod ` 100 > 90 ]
point = printf "SELECT id FROM users WHERE name = 'user%d'" ( u ` div ` 2 )
sampleQuery bench "scan_users" "SELECT * FROM users" u
sampleQuery bench "scan_orders" "SELECT * FROM orders" o
sampleQuery bench "id_point" ( printf "SELECT id FROM users WHERE id = %d" ( u ` div ` 2 )) 1
sampleQuery bench "name_scan" point 1
_ <- benchSqlOnce bench "CREATE INDEX users(name)" "CREATE INDEX ON users (name)"
sampleQuery bench "name_index" point 1
sampleQuery bench "id_range100" "SELECT id FROM users WHERE id >= 1 AND id <= 100" 100
sampleQuery bench "sort_top100" "SELECT name FROM users ORDER BY age DESC LIMIT 100" 100
sampleQuery bench "group100" "SELECT age, COUNT(*) FROM users GROUP BY age" 100
sampleQuery bench "filtered_join" "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 90" selected
sampleQuery bench "derived_join" "SELECT d.name FROM (SELECT id,name FROM users WHERE age > 90) d JOIN orders o ON d.id = o.user_id" selected
sampleQuery bench "cte_group" "WITH ages AS (SELECT age FROM users) SELECT age, COUNT(*) FROM ages GROUP BY age" 100
sampleQuery bench "scalar_subquery" "SELECT name, (SELECT COUNT(*) FROM orders) FROM users LIMIT 100" 100
sampleQuery bench "correlated_exists20" "SELECT u.name FROM (SELECT id,name FROM users WHERE id <= 20) u WHERE EXISTS (SELECT id FROM orders o WHERE o.user_id = u.id)" 20
_ <- runSql bench "CREATE TABLE wide (id int, payload str)"
loadBatch bench "wide" "id,payload" ( min 5000 u ) ( \ i -> printf "(%d,'%s')" i ( replicate 512 'x' ))
checkCount bench "wide" ( min 5000 u )
sampleQuery bench "scan_wide512B" "SELECT * FROM wide" ( min 5000 u )
mapM_ ( concurrentLookup bench ) [ 1 , 4 , 8 ]
_ <- runSql bench "CREATE TABLE writes (id int, name str, age int)"
_ <- batchInsert bench "200 individual durable INSERTs" 200 ( \ i -> printf "INSERT INTO writes (id,name,age) VALUES (%d,'w%d',%d)" i i i )
( milliseconds , () ) <- measure $ do
_ <- runSql bench "BEGIN"
forM_ [ 201 .. 400 :: Int ] $ \ i -> runSql bench ( printf "INSERT INTO writes (id,name,age) VALUES (%d,'w%d',%d)" i i i ) >> pure ()
_ <- runSql bench "COMMIT"
pure ()
printf "WRITE transaction200 total_ms=%.3f rows_s=%.2f \n " milliseconds ( 200000 / milliseconds )
checkCount bench "writes" 400
_ <- benchSqlOnce bench "UPDATE writes (400 rows)" "UPDATE writes SET age = age + 1"
_ <- runSql bench "BEGIN"
_ <- benchSqlOnce bench "transaction UPDATE (400 rows)" "UPDATE writes SET age = 0"
_ <- benchSqlOnce bench "ROLLBACK 400 changed rows" "ROLLBACK"
restored <- runSql bench "SELECT age FROM writes WHERE id = 1"
unless ( fmap qrRows restored == Right [[ VInt 2 ]]) ( ioError ( userError "rollback value mismatch" ))
_ <- benchSqlOnce bench "DELETE writes (200 rows)" "DELETE FROM writes WHERE id > 200"
checkCount bench "writes" 200
putStrLn "PRESSURE_OK all result checks passed"
-- | 用多条独立会话同步开始点查
concurrentLookup :: Bench -> Int -> IO ()
concurrentLookup bench workers = do
completions <- forM [ 1 .. workers ] $ \ _ -> do
start <- newEmptyMVar
done <- newEmptyMVar
client <- connectClient "127.0.0.1" ( bPort bench ) >>= either ( ioError . userError . T . unpack ) pure
session <- newSession client
authenticateSession session "admin" "" >>= either ( ioError . userError . T . unpack ) pure
switchDatabase session "bench" >>= either ( ioError . userError . T . unpack ) pure
_ <- forkFinally ( takeMVar start >> bracket ( pure client ) closeClient ( \ _ -> replicateM 100 ( fst <$> measure ( do
result <- runStatement session "SELECT id FROM users WHERE id = 1"
unless ( fmap qrRows result == Right [[ VInt 1 ]]) ( ioError ( userError "concurrent lookup mismatch" )))))) ( putMVar done )
pure ( start , done )
( milliseconds , results ) <- measure $ do
mapM_ ( \ ( start , _ ) -> putMVar start () ) completions
mapM ( takeMVar . snd ) completions
times <- concat <$> mapM ( either throwIO pure ) results
latencies ( "concurrent" ++ show workers ++ "_point" ) 1 times
printf "CONCURRENT workers=%d ops=%d wall_ms=%.3f aggregate_ops_s=%.2f \n " workers ( workers * 100 ) milliseconds ( fromIntegral ( workers * 100 ) * 1000 / milliseconds )