使用React js完成web前端
This commit is contained in:
1 parent
9e8c5f1ef0
commit
7600f0f9ed
130 files changed
+12708
-788
No files matched your search
@@ -43,7 +43,7 @@ checksum :: Either String [Row] -> Int
|
||||
checksum (Left _) = -1
|
||||
checksum (Right rows) = go 0 rows
|
||||
where
|
||||
-- \| 累加行长
|
||||
-- | 累加行长
|
||||
go acc [] = acc
|
||||
go acc (r : rs) = go (acc + length r) rs
|
||||
|
||||
@@ -80,11 +80,9 @@ timed act = do
|
||||
t1 <- getCPUTime
|
||||
return (fromIntegral (t1 - t0) / 1e12)
|
||||
|
||||
-- | 每批至少跑几次(减少 CPU 时钟量化误差)
|
||||
minBatch :: Int
|
||||
minBatch = 8
|
||||
|
||||
-- | 取几批中位数
|
||||
rounds :: Int
|
||||
rounds = 3
|
||||
|
||||
@@ -103,7 +101,7 @@ benchOp target act = do
|
||||
ts <- mapM (const (timeBatch k act)) [1 .. rounds]
|
||||
return (median ts / fromIntegral k, k)
|
||||
where
|
||||
-- \| 批次翻倍直到累计超过目标
|
||||
-- | 批次翻倍直到累计超过目标
|
||||
pickBatch k = do
|
||||
t <- timeBatch k act
|
||||
if t < target || k < minBatch then pickBatch (k * 2) else return k
|
||||
@@ -116,17 +114,17 @@ report saltRef db target sql = do
|
||||
(tUn, kUn) <- benchOp target (runOnce saltRef db sql False)
|
||||
(tOp, kOp) <- benchOp target (runOnce saltRef db sql True)
|
||||
let speedup = if tOp <= 0 then 0 else tUn / tOp
|
||||
mismatch = if nUn == nOp then "" else " <<< 结果不一致!"
|
||||
mismatch = if nUn == nOp then "" else " <<< result mismatch!"
|
||||
printf "%s\n" sql
|
||||
printf
|
||||
" 结果行数 %-6d | 未优化 %9.3f ms | 优化后 %9.3f ms | 加速 %6.1fx%s\n"
|
||||
" rows %-6d | baseline %9.3f ms | optimized %9.3f ms | speedup %6.1fx%s\n"
|
||||
nOp
|
||||
(tUn * 1000)
|
||||
(tOp * 1000)
|
||||
speedup
|
||||
mismatch
|
||||
printf
|
||||
" (%d 次/批 × %d 批取中位数:未优化 %d 次/批,优化后 %d 次/批)\n\n"
|
||||
" (%d iters/batch x %d batches, median: baseline %d iters/batch, optimized %d iters/batch)\n\n"
|
||||
minBatch
|
||||
rounds
|
||||
kUn
|
||||
@@ -152,9 +150,9 @@ main = do
|
||||
m = pick 1 500
|
||||
target = pick 2 (0.3 :: Double)
|
||||
db = [("users", mkUsers n), ("orders", mkOrders n m)]
|
||||
printf "数据规模:users = %d 行,orders = %d 行\n" n m
|
||||
printf "计时方式:CPU 时间;每批至少 %d 次迭代、累计超过 %.2f 秒后固定批次,取 %d 批中位数\n" minBatch target rounds
|
||||
printf "(先跑一遍预热,避免把首次构造数据的开销算进去)\n\n"
|
||||
printf "dataset: users = %d rows, orders = %d rows\n" n m
|
||||
printf "timing: CPU time; at least %d iterations per batch, batch fixed after %.2f s, median of %d batches\n" minBatch target rounds
|
||||
printf "(one warmup run first, so building the data is not counted)\n\n"
|
||||
saltRef <- newIORef ""
|
||||
_ <- runOnce saltRef db warmupSql False
|
||||
_ <- runOnce saltRef db warmupSql True
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
//! 存储层基准:页 IO、B+ 树、堆表、WAL 与服务端写入路径。
|
||||
//!
|
||||
//! 用 `std::time::Instant` 计时,不引第三方基准框架,离线也能跑。
|
||||
//! 每项跑 `rounds` 轮取最快一轮:存储层的抖动主要来自 fsync 与调度,
|
||||
//! 取最快一轮最接近"这套代码本身要花多久"。
|
||||
//!
|
||||
//! 跑法:`cargo bench`(可选环境变量 `CHUSQL_BENCH_N` 改规模,默认 2000)
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chusql_storage::btree::DiskBTree;
|
||||
@@ -16,8 +8,11 @@ use chusql_storage::protocol::Row;
|
||||
use chusql_storage::wal::{Wal, WalOp};
|
||||
use serde_json::json;
|
||||
|
||||
// 工具
|
||||
/// 跑一个操作 `ops` 次,量总耗时并折算每次耗时
|
||||
// 存储层基准:页 IO、B+ 树、堆表、WAL 与服务端写入路径;
|
||||
// 用 std::time::Instant 计时,不引第三方基准框架,
|
||||
// 每项跑若干轮取最快一轮,离线也能跑。
|
||||
|
||||
/// 跑一个操作 ops 次,量总耗时并折算每次耗时
|
||||
fn bench<F: FnMut()>(label: &str, ops: usize, rounds: usize, mut f: F) {
|
||||
let mut best = Duration::MAX;
|
||||
for _ in 0..rounds {
|
||||
@@ -30,7 +25,7 @@ fn bench<F: FnMut()>(label: &str, ops: usize, rounds: usize, mut f: F) {
|
||||
}
|
||||
let per = best.as_secs_f64() * 1e6 / ops as f64;
|
||||
println!(
|
||||
"{:<38} {:>9.2} ms {:>10.3} us/次",
|
||||
"{:<38} {:>9.2} ms {:>10.3} us/op",
|
||||
label,
|
||||
best.as_secs_f64() * 1e3,
|
||||
per
|
||||
@@ -44,7 +39,7 @@ fn bench_once<F: FnOnce()>(label: &str, ops: usize, f: F) {
|
||||
let d = t0.elapsed();
|
||||
let per = d.as_secs_f64() * 1e6 / ops as f64;
|
||||
println!(
|
||||
"{:<38} {:>9.2} ms {:>10.3} us/次",
|
||||
"{:<38} {:>9.2} ms {:>10.3} us/op",
|
||||
label,
|
||||
d.as_secs_f64() * 1e3,
|
||||
per
|
||||
@@ -65,13 +60,12 @@ fn next_rand(state: &mut u64) -> u64 {
|
||||
*state >> 33
|
||||
}
|
||||
|
||||
// 页 IO
|
||||
/// 顺序写 / 顺序读 / 随机读
|
||||
fn bench_page(dir: &std::path::Path, n: usize) {
|
||||
let path = dir.join("page.db");
|
||||
let mut pf = PageFile::with_options(&path, DEFAULT_PAGE_SIZE, 1024).unwrap();
|
||||
|
||||
bench("page 顺序写(含 flush)", n, 3, || {
|
||||
bench("page sequential write (with flush)", n, 3, || {
|
||||
pf.truncate().unwrap();
|
||||
for i in 0..n {
|
||||
pf.write_page(&Page::new(i as u64, DEFAULT_PAGE_SIZE)).unwrap();
|
||||
@@ -80,9 +74,8 @@ fn bench_page(dir: &std::path::Path, n: usize) {
|
||||
});
|
||||
drop(pf);
|
||||
|
||||
// 池子故意开小,让顺序读真的去读盘
|
||||
let mut pf = PageFile::with_options(&path, DEFAULT_PAGE_SIZE, 16).unwrap();
|
||||
bench("page 顺序读(池 16,基本都缺页)", n, 3, || {
|
||||
bench("page sequential read (pool 16, mostly misses)", n, 3, || {
|
||||
for i in 0..n {
|
||||
pf.read_page(i as u64).unwrap();
|
||||
}
|
||||
@@ -90,37 +83,35 @@ fn bench_page(dir: &std::path::Path, n: usize) {
|
||||
|
||||
let mut pf = PageFile::with_options(&path, DEFAULT_PAGE_SIZE, 16).unwrap();
|
||||
let mut state = 12345u64;
|
||||
bench("page 随机读(池 16)", n, 3, || {
|
||||
bench("page random read (pool 16)", n, 3, || {
|
||||
for _ in 0..n {
|
||||
let id = next_rand(&mut state) % n as u64;
|
||||
pf.read_page(id).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// 池子开大、工作集比池子还大:每次缺页都要挑一个牺牲页
|
||||
let mut pf = PageFile::with_options(&path, DEFAULT_PAGE_SIZE, 1024).unwrap();
|
||||
let mut state = 999u64;
|
||||
bench("page 随机读(池 1024,工作集 > 池)", n, 3, || {
|
||||
bench("page random read (pool 1024, working set > pool)", n, 3, || {
|
||||
for _ in 0..n {
|
||||
let id = next_rand(&mut state) % n as u64;
|
||||
pf.read_page(id).unwrap();
|
||||
}
|
||||
});
|
||||
println!(
|
||||
" (池 1024:命中 {}/{})",
|
||||
" (pool 1024: hits {}/{})",
|
||||
pf.hits(),
|
||||
pf.hits() + pf.misses()
|
||||
);
|
||||
}
|
||||
|
||||
// B+ 树
|
||||
/// 插入、点查、全量遍历
|
||||
fn bench_btree(dir: &std::path::Path, n: usize) {
|
||||
let path = dir.join("bt.idx");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let mut bt = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, 1024).unwrap();
|
||||
|
||||
bench("btree 插入(含 flush)", n, 2, || {
|
||||
bench("btree insert (with flush)", n, 2, || {
|
||||
bt.clear().unwrap();
|
||||
for i in 0..n {
|
||||
bt.insert(i as i64, i as u64).unwrap();
|
||||
@@ -128,19 +119,18 @@ fn bench_btree(dir: &std::path::Path, n: usize) {
|
||||
bt.flush().unwrap();
|
||||
});
|
||||
|
||||
bench("btree 点查(全部命中)", n, 3, || {
|
||||
bench("btree point lookup (all hits)", n, 3, || {
|
||||
for i in 0..n {
|
||||
bt.get(i as i64).unwrap().expect("key must exist");
|
||||
}
|
||||
});
|
||||
|
||||
bench("btree 全量遍历", n, 3, || {
|
||||
bench("btree full scan", n, 3, || {
|
||||
let v = bt.iter_all().unwrap();
|
||||
assert_eq!(v.len(), n);
|
||||
});
|
||||
}
|
||||
|
||||
// 堆表
|
||||
/// 插行、全表扫描、按索引取行
|
||||
fn bench_heap(dir: &std::path::Path, n: usize) {
|
||||
let path = dir.join("heap.db");
|
||||
@@ -149,14 +139,14 @@ fn bench_heap(dir: &std::path::Path, n: usize) {
|
||||
let _ = std::fs::remove_file(&idx);
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, 1024).unwrap();
|
||||
|
||||
bench("heap 插入(只在缓冲池,不 flush)", n, 2, || {
|
||||
bench("heap insert (buffer pool only, no flush)", n, 2, || {
|
||||
t.replace_all(&[]).unwrap();
|
||||
for i in 0..n {
|
||||
t.insert_row(&row(i as i64)).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
bench("heap 插入 + flush", n, 2, || {
|
||||
bench("heap insert + flush", n, 2, || {
|
||||
t.replace_all(&[]).unwrap();
|
||||
for i in 0..n {
|
||||
t.insert_row(&row(i as i64)).unwrap();
|
||||
@@ -164,20 +154,19 @@ fn bench_heap(dir: &std::path::Path, n: usize) {
|
||||
t.flush().unwrap();
|
||||
});
|
||||
|
||||
bench("heap 批量插入 + flush(一次落盘)", n, 2, || {
|
||||
bench("heap batch insert + flush (single commit)", n, 2, || {
|
||||
t.replace_all(&[]).unwrap();
|
||||
let rows: Vec<Row> = (0..n as i64).map(row).collect();
|
||||
t.insert_rows(&rows).unwrap();
|
||||
t.flush().unwrap();
|
||||
});
|
||||
|
||||
bench("heap 全表扫描", n, 3, || {
|
||||
bench("heap full scan", n, 3, || {
|
||||
let rows = t.scan().unwrap();
|
||||
assert_eq!(rows.len(), n);
|
||||
});
|
||||
}
|
||||
|
||||
// WAL
|
||||
/// 追加写与清空
|
||||
fn bench_wal(dir: &std::path::Path, n: usize) {
|
||||
let path = dir.join("wal.log");
|
||||
@@ -188,32 +177,30 @@ fn bench_wal(dir: &std::path::Path, n: usize) {
|
||||
row: row(1),
|
||||
};
|
||||
|
||||
bench_once("wal 追加(每条都 fsync)", n, || {
|
||||
bench_once("wal append (fsync per record)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.append(&op).unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
|
||||
bench_once("wal 清空(每条都 fsync)", n, || {
|
||||
bench_once("wal clear (fsync per record)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
|
||||
bench_once("wal 截断(不落盘)", n, || {
|
||||
bench_once("wal truncate (no fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.truncate().unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// 拆分:一次 fsync 本身多贵,一次 open 多贵
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
// 大小和一条真实的 WAL 记录同量级(~200 字节)
|
||||
let bytes = vec![0u8; 200];
|
||||
bench_once(" └ open + write + fsync", n, || {
|
||||
bench_once(" - open + write + fsync", n, || {
|
||||
for _ in 0..n {
|
||||
let mut f = OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -229,18 +216,18 @@ fn bench_wal(dir: &std::path::Path, n: usize) {
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
bench_once(" └ 复用句柄:write + fsync", n, || {
|
||||
bench_once(" - reuse handle: write + fsync", n, || {
|
||||
for _ in 0..n {
|
||||
open_file.write_all(&bytes).unwrap();
|
||||
open_file.sync_data().unwrap();
|
||||
}
|
||||
});
|
||||
bench_once(" └ only fsync(不开不写)", n, || {
|
||||
bench_once(" - only fsync (no open, no write)", n, || {
|
||||
for _ in 0..n {
|
||||
open_file.sync_data().unwrap();
|
||||
}
|
||||
});
|
||||
bench_once(" └ only open/close", n, || {
|
||||
bench_once(" - only open/close", n, || {
|
||||
for _ in 0..n {
|
||||
let _ = OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -252,8 +239,7 @@ fn bench_wal(dir: &std::path::Path, n: usize) {
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
|
||||
// 服务端写入路径
|
||||
/// 复刻 server.rs 里 write_via_wal 的顺序:清 WAL -> 写 WAL -> 改数据 -> flush -> 清 WAL
|
||||
/// 复刻 write_via_wal 的端到端写入顺序
|
||||
fn bench_write_path(dir: &std::path::Path, n: usize) {
|
||||
let path = dir.join("wp.db");
|
||||
let idx = dir.join("wp.idx");
|
||||
@@ -272,7 +258,7 @@ fn bench_write_path(dir: &std::path::Path, n: usize) {
|
||||
.unwrap();
|
||||
let wal = Wal::new(&wal_path);
|
||||
|
||||
bench_once("写入路径 单条 INSERT(端到端)", n, || {
|
||||
bench_once("write path single INSERT (end to end)", n, || {
|
||||
for i in 0..n as i64 {
|
||||
wal.truncate().unwrap();
|
||||
let op = WalOp::Insert {
|
||||
@@ -286,8 +272,7 @@ fn bench_write_path(dir: &std::path::Path, n: usize) {
|
||||
}
|
||||
});
|
||||
|
||||
// 拆开量,好定位时间花在哪一步
|
||||
bench_once(" └ wal.truncate()(不落盘)", n, || {
|
||||
bench_once(" - wal.truncate() (no fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.truncate().unwrap();
|
||||
}
|
||||
@@ -297,27 +282,26 @@ fn bench_write_path(dir: &std::path::Path, n: usize) {
|
||||
table: "t".to_string(),
|
||||
row: row(1),
|
||||
};
|
||||
bench_once(" └ wal.append()(含 fsync)", n, || {
|
||||
bench_once(" - wal.append() (with fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.append(&op).unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
|
||||
bench_once(" └ wal.clear()(含 fsync)", n, || {
|
||||
bench_once(" - wal.clear() (with fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
bench_once(" └ heap.insert_row()(含写索引)", n, || {
|
||||
// 主循环已经插过 0..n 了,这里换个号段,免得撞上"索引列唯一"
|
||||
bench_once(" - heap.insert_row() (incl. index write)", n, || {
|
||||
for i in n as i64..2 * n as i64 {
|
||||
t.insert_row(&row(i)).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
bench_once(" └ heap.flush()", n, || {
|
||||
bench_once(" - heap.flush()", n, || {
|
||||
for _ in 0..n {
|
||||
t.flush().unwrap();
|
||||
}
|
||||
@@ -330,24 +314,24 @@ fn main() {
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
|
||||
println!("chusql-storage 基准:n = {},页大小 = {} 字节,btree order = {}",
|
||||
println!("chusql-storage benchmark: n = {}, page size = {} bytes, btree order = {}",
|
||||
n, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER);
|
||||
println!("每项取最快一轮;带 fsync 的项只跑一次(跑多了太慢)\n");
|
||||
println!("best of each round set; fsync entries run once only (repeats are too slow)\n");
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
println!("[页 IO]");
|
||||
println!("[page IO]");
|
||||
bench_page(dir.path(), n);
|
||||
|
||||
println!("\n[B+ 树]");
|
||||
println!("\n[B+ tree]");
|
||||
bench_btree(dir.path(), n);
|
||||
|
||||
println!("\n[堆表]");
|
||||
println!("\n[heap]");
|
||||
bench_heap(dir.path(), n);
|
||||
|
||||
println!("\n[WAL]");
|
||||
bench_wal(dir.path(), n.min(200));
|
||||
|
||||
println!("\n[服务端写入路径]");
|
||||
println!("\n[server write path]");
|
||||
bench_write_path(dir.path(), n.min(200));
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
全链路基准:Haskell 引擎 ←命名管道→ Rust 存储进程
|
||||
存储进程:../../chusql-storage/target\release\server.exe
|
||||
管道:chusql-fullchain-145262202226
|
||||
数据目录:C:\Users\32402\AppData\Local\Temp\chusql-fullchain-145262202226
|
||||
数据规模:users = 1000 行,orders = 1000 行
|
||||
计时:墙上时钟(单调);每条语句都走完整链路
|
||||
|
||||
[启动] 存储进程就绪,用时 0.05 s
|
||||
|
||||
[建表]
|
||||
CREATE TABLE users 2.3 ms 0 行
|
||||
CREATE TABLE orders 1.6 ms 0 行
|
||||
|
||||
[写入:每条 INSERT 都是一次完整落盘]
|
||||
1000 条 INSERT INTO users 2949.8 ms 2.950 ms/条
|
||||
1000 条 INSERT INTO orders 3244.4 ms 3.244 ms/条
|
||||
|
||||
[分解:同一条 INSERT 拆成两半,都用原始 IPC,不经过引擎]
|
||||
CREATE TABLE raw_users 1.9 ms 0 行
|
||||
list_catalog(取结构) 23.3 ms 0.116 ms/次
|
||||
原始 IPC insert × 1000 2992.4 ms 2.992 ms/条
|
||||
|
||||
[读取与查询:整条语句,含引擎侧的解析与语义检查]
|
||||
SELECT * FROM users 1.9 ms 1000 行
|
||||
SELECT name FROM users WHERE age > 90 2.5 ms 90 行
|
||||
|
||||
[读取与查询:连接]
|
||||
JOIN + WHERE(哈希连接) 5.4 ms 90 行
|
||||
|
||||
[点查]
|
||||
lookup_by_index(原始 IPC,按 id) 7.7 ms 0.077 ms/次(命中 100/100)
|
||||
SELECT ... WHERE id = k(走 Lookup) 0.4 ms 1 行
|
||||
|
||||
[批量插入:一条语句带多行,N 行只落一次盘]
|
||||
CREATE TABLE batch_t 3.0 ms 0 行
|
||||
多行 INSERT 58.8 ms 0.059 ms/行(100 行/条 × 10 条语句)
|
||||
|
||||
[行级删除:代价和删几行无关,只和扫描 + 一次落盘有关]
|
||||
DELETE FROM users WHERE id = k(1 行) 4.8 ms 0 行
|
||||
DELETE FROM users(全部) 20.2 ms 0 行
|
||||
|
||||
[二级索引:没有索引回退扫描,建完索引走索引]
|
||||
CREATE TABLE items 2.1 ms 0 行
|
||||
(先把 1000 行灌进 items:53.2 ms)
|
||||
SELECT ... WHERE code = k(无索引) 1.8 ms 1 行
|
||||
CREATE INDEX ON items (code) 9.8 ms 0 行
|
||||
SELECT ... WHERE code = k(走索引) 0.2 ms 1 行
|
||||
|
||||
[数据字典统计]
|
||||
batch_t 1000 行 索引=id
|
||||
统计=age:100 id:1000 name:1000
|
||||
items 1000 行 索引=id code
|
||||
统计=code:1000 id:1000
|
||||
orders 1000 行 索引=id
|
||||
统计=id:1000 product:50 user_id:1000
|
||||
raw_users 1000 行 索引=id
|
||||
统计=age:100 id:1000 name:1000
|
||||
users 0 行 索引=id
|
||||
统计=age:0 id:0 name:0
|
||||
|
||||
[校验]
|
||||
age > 90 命中 90(按数据分布应为 90 行)
|
||||
JOIN 命中 90(每个用户恰好 1 张订单,应与上一行相同 = 90)
|
||||
多行 INSERT 灌进去 1000 行(应为 1000)
|
||||
二级索引那一行:code = 3501 应该查到 id = 500
|
||||
|
||||
[收尾] 停存储进程、清理数据目录
|
||||
@@ -40,25 +40,11 @@ import System.Process (
|
||||
)
|
||||
import Text.Printf (printf)
|
||||
|
||||
-- 全链路基准:Haskell 引擎和 Rust 存储进程**一起启动**,跑真实情景。
|
||||
--
|
||||
-- 一条语句真正走过的路:
|
||||
-- SQL 文本 → 词法/语法分析 → 语义检查 → 关系代数 → 命名管道(JSON 行)
|
||||
-- → Rust:WAL(fsync)→ 堆表/B+ 树 → 页缓冲池 → 落盘
|
||||
-- → 结果原路返回
|
||||
--
|
||||
-- 计时用**墙上时钟(单调时钟)**,不是 CPU 时间:这条链路大部分工夫花在等 IO
|
||||
-- 和跨进程往返上,用 CPU 时间会把这些等待整个漏掉。
|
||||
--
|
||||
-- 跑法(先把 Rust 那边编译成 release):
|
||||
-- cd chusql-storage && cargo build --release
|
||||
-- cd benchmark/fullchain && stack run # 默认 users=orders=200
|
||||
-- stack run -- 500 500 # 换规模
|
||||
-- stack run -- 200 200 ../../chusql-storage/target/release/server.exe # 指定存储进程
|
||||
-- 全链路基准:Haskell 引擎与 Rust 存储进程一起跑,
|
||||
-- 语句走完整链路(解析、优化、命名管道、WAL、落盘),
|
||||
-- 用墙上时钟计时,结果打印到标准输出。
|
||||
|
||||
-- * 命令行与存储进程
|
||||
|
||||
-- | 找 Rust 存储进程:优先 release,其次 debug
|
||||
-- | 找 Rust 存储进程:优先 release
|
||||
findServer :: FilePath -> IO FilePath
|
||||
findServer "" = do
|
||||
let dirs =
|
||||
@@ -68,16 +54,16 @@ findServer "" = do
|
||||
, "target"
|
||||
]
|
||||
modes = ["release", "debug"]
|
||||
exes = ["server.exe", "server"]
|
||||
exes = ["chusql-storage.exe", "chusql-storage"]
|
||||
candidates = [d </> m </> e | d <- dirs, m <- modes, e <- exes]
|
||||
found <- filterM doesFileExist candidates
|
||||
case found of
|
||||
(p : _) -> do
|
||||
unless ("release" `isInfixOf` p) $
|
||||
putStrLn "!! 找到的是 debug 版存储进程,数字会偏慢;建议 cargo build --release"
|
||||
putStrLn "!! using the debug storage binary; numbers will be slower, run: cargo build --release"
|
||||
pure p
|
||||
[] -> do
|
||||
putStrLn "找不到 Rust 存储进程。先编译:"
|
||||
putStrLn "Rust storage binary not found. Build it first:"
|
||||
putStrLn " cd chusql-storage && cargo build --release"
|
||||
exitFailure
|
||||
findServer p = do
|
||||
@@ -85,10 +71,10 @@ findServer p = do
|
||||
if ok
|
||||
then pure p
|
||||
else do
|
||||
putStrLn ("指定的存储进程不存在:" ++ p)
|
||||
putStrLn ("storage binary not found: " ++ p)
|
||||
exitFailure
|
||||
|
||||
-- | 子进程环境:继承当前环境,但把 CHUSQL_* 换成本次专用的
|
||||
-- | 子进程环境:继承并覆盖 CHUSQL_*
|
||||
childEnv :: [(String, String)] -> IO [(String, String)]
|
||||
childEnv extra = do
|
||||
base <- getEnvironment
|
||||
@@ -111,7 +97,7 @@ startServer serverPath pipe dataDir = do
|
||||
(proc serverPath []) {env = Just envs, std_out = NoStream, std_err = Inherit}
|
||||
ready <- waitReady (200 :: Int)
|
||||
unless ready $ do
|
||||
putStrLn "存储进程 5 秒内没就绪"
|
||||
putStrLn "storage process was not ready within 5 s"
|
||||
exitFailure
|
||||
pure ph
|
||||
|
||||
@@ -133,9 +119,7 @@ stopServer ph dataDir = do
|
||||
_ <- try (removeDirectoryRecursive dataDir) :: IO (Either SomeException ())
|
||||
pure ()
|
||||
|
||||
-- * 计时
|
||||
|
||||
-- | 量一次墙上耗时,并把结果行数强制出来(Haskell 是惰性的,不强制就等于没测)
|
||||
-- | 量一次墙上耗时并强制求出行数
|
||||
timedRows :: IO (Either String [Row]) -> IO (Double, Either String Int)
|
||||
timedRows act = do
|
||||
t0 <- getMonotonicTime
|
||||
@@ -155,26 +139,24 @@ runSql sql = case parseStatement sql of
|
||||
-- | 打印一行结果
|
||||
report :: String -> Either String Int -> Double -> IO ()
|
||||
report label result dt = case result of
|
||||
Left e -> printf "%-38s %10s !! %s\n" label "失败" e
|
||||
Right n -> printf "%-38s %8.1f ms %8d 行\n" label (dt * 1000) n
|
||||
Left e -> printf "%-38s %10s !! %s\n" label "FAILED" e
|
||||
Right n -> printf "%-38s %8.1f ms %8d rows\n" label (dt * 1000) n
|
||||
|
||||
-- | 单独量一条 SQL(先预热一遍):只适合可以重复跑的语句(SELECT)
|
||||
-- | 单独量一条可重复的 SQL(先预热)
|
||||
benchSql :: String -> String -> IO (Either String Int)
|
||||
benchSql label sql = do
|
||||
_ <- runSql sql -- 预热:连接、页缓存、优化器都先热一次
|
||||
_ <- runSql sql
|
||||
(dt, r) <- timedRows (runSql sql)
|
||||
report label r dt
|
||||
pure r
|
||||
|
||||
-- | 只跑一次的语句(CREATE TABLE 这类重跑会报错的)
|
||||
-- | 只跑一次的语句(重跑会报错)
|
||||
benchSqlOnce :: String -> String -> IO (Either String Int)
|
||||
benchSqlOnce label sql = do
|
||||
(dt, r) <- timedRows (runSql sql)
|
||||
report label r dt
|
||||
pure r
|
||||
|
||||
-- * 场景
|
||||
|
||||
-- | 造一条 INSERT 的 SQL 文本
|
||||
userSql :: Int -> String
|
||||
userSql i =
|
||||
@@ -200,13 +182,13 @@ batchInsert label n mkSql = do
|
||||
case errs of
|
||||
[] -> do
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/条\n"
|
||||
"%-38s %8.1f ms %7.3f ms/row\n"
|
||||
label
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral n)
|
||||
pure (Right ())
|
||||
(e : _) -> do
|
||||
printf "%-38s %10s !! %s\n" label "失败" e
|
||||
printf "%-38s %10s !! %s\n" label "FAILED" e
|
||||
pure (Left e)
|
||||
|
||||
-- | 一次点查(原始 IPC,不经过引擎)
|
||||
@@ -219,14 +201,14 @@ benchLookup table column key times = do
|
||||
let total = t1 - t0
|
||||
hit = length [() | Right (IndexRow (Just _)) <- results]
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/次(命中 %d/%d)\n"
|
||||
"lookup_by_index(原始 IPC,按 id)"
|
||||
"%-38s %8.1f ms %7.3f ms/op (hits %d/%d)\n"
|
||||
"lookup_by_index (raw IPC, by id)"
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral times)
|
||||
hit
|
||||
times
|
||||
|
||||
-- | 量 list_catalog:现在每条语句都要先来一发(只取结构,不取行)
|
||||
-- | 量 list_catalog(只取结构,不取行)
|
||||
benchListCatalog :: Int -> IO ()
|
||||
benchListCatalog n = do
|
||||
t0 <- getMonotonicTime
|
||||
@@ -234,12 +216,12 @@ benchListCatalog n = do
|
||||
t1 <- getMonotonicTime
|
||||
let total = t1 - t0
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/次\n"
|
||||
"list_catalog(取结构)"
|
||||
"%-38s %8.1f ms %7.3f ms/op\n"
|
||||
"list_catalog (schema only)"
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral n)
|
||||
|
||||
-- | 量原始 IPC 插入:不经过引擎,把"协议 + 存储"和"引擎"分开
|
||||
-- | 量原始 IPC 插入(不经过引擎)
|
||||
benchRawInsert :: String -> [Row] -> IO ()
|
||||
benchRawInsert table rows = do
|
||||
t0 <- getMonotonicTime
|
||||
@@ -251,11 +233,11 @@ benchRawInsert table rows = do
|
||||
case errs of
|
||||
[] ->
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/条\n"
|
||||
("原始 IPC insert × " ++ show n)
|
||||
"%-38s %8.1f ms %7.3f ms/row\n"
|
||||
("raw IPC insert x " ++ show n)
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral n)
|
||||
(e : _) -> printf "%-38s %10s !! %s\n" "原始 IPC insert" "失败" e
|
||||
(e : _) -> printf "%-38s %10s !! %s\n" "raw IPC insert" "FAILED" e
|
||||
|
||||
-- | 造一行 users 数据(给原始 IPC 用)
|
||||
userRow :: Int -> Row
|
||||
@@ -273,7 +255,7 @@ runBatch sqls = do
|
||||
chunked :: Int -> Int -> [[Int]]
|
||||
chunked size n = go [1 .. n]
|
||||
where
|
||||
-- \| 每次切下 size 个
|
||||
-- | 每次切下 size 个
|
||||
go [] = []
|
||||
go xs = let (a, b) = splitAt size xs in a : go b
|
||||
|
||||
@@ -287,12 +269,12 @@ multiInsertSql ids =
|
||||
printStats :: TableInfo -> IO ()
|
||||
printStats i =
|
||||
printf
|
||||
" %-12s %6d 行 索引=%s\n 统计=%s\n"
|
||||
" %-12s %6d rows indexes=%s\n stats=%s\n"
|
||||
(tiTable i)
|
||||
(tiRows i)
|
||||
(if null (tiIndexes i) then "无" else unwords (tiIndexes i))
|
||||
(if null (tiIndexes i) then "none" else unwords (tiIndexes i))
|
||||
( unwords
|
||||
[ n ++ ":" ++ show d ++ (if c then "+(下界)" else "")
|
||||
[ n ++ ":" ++ show d ++ (if c then "+ (lower bound)" else "")
|
||||
| (n, d, c) <- tiStats i
|
||||
]
|
||||
)
|
||||
@@ -300,58 +282,58 @@ printStats i =
|
||||
-- | 主场景
|
||||
runScenarios :: Int -> Int -> IO ()
|
||||
runScenarios u o = do
|
||||
putStrLn "\n[建表]"
|
||||
putStrLn "\n[create tables]"
|
||||
r1 <- benchSqlOnce "CREATE TABLE users" "CREATE TABLE users (id int, name str, age int)"
|
||||
r2 <- benchSqlOnce "CREATE TABLE orders" "CREATE TABLE orders (id int, user_id int, product str)"
|
||||
case (r1, r2) of
|
||||
(Right _, Right _) -> pure ()
|
||||
_ -> do
|
||||
putStrLn "建表失败,后面不用跑了"
|
||||
putStrLn "table creation failed; stopping here"
|
||||
exitFailure
|
||||
|
||||
putStrLn "\n[写入:每条 INSERT 都是一次完整落盘]"
|
||||
_ <- batchInsert (printf "%d 条 INSERT INTO users" u) u userSql
|
||||
_ <- batchInsert (printf "%d 条 INSERT INTO orders" o) o (orderSql u)
|
||||
putStrLn "\n[write: every INSERT is a full durable commit]"
|
||||
_ <- batchInsert (printf "%d INSERT INTO users" u) u userSql
|
||||
_ <- batchInsert (printf "%d INSERT INTO orders" o) o (orderSql u)
|
||||
|
||||
putStrLn "\n[分解:同一条 INSERT 拆成两半,都用原始 IPC,不经过引擎]"
|
||||
putStrLn "\n[breakdown: same INSERT split in two, both via raw IPC, no engine]"
|
||||
_ <- benchSqlOnce "CREATE TABLE raw_users" "CREATE TABLE raw_users (id int, name str, age int)"
|
||||
benchListCatalog 200
|
||||
benchRawInsert "raw_users" [userRow i | i <- [1 .. u]]
|
||||
|
||||
putStrLn "\n[读取与查询:整条语句,含引擎侧的解析与语义检查]"
|
||||
putStrLn "\n[read/query: full statement, including parsing and semantic checks]"
|
||||
_ <- benchSql "SELECT * FROM users" "SELECT * FROM users"
|
||||
ageR <- benchSql "SELECT name FROM users WHERE age > 90" "SELECT name FROM users WHERE age > 90"
|
||||
|
||||
putStrLn "\n[读取与查询:连接]"
|
||||
putStrLn "\n[read/query: join]"
|
||||
joinR <-
|
||||
benchSql
|
||||
"JOIN + WHERE(哈希连接)"
|
||||
"JOIN + WHERE (hash join)"
|
||||
"SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 90"
|
||||
|
||||
putStrLn "\n[点查]"
|
||||
putStrLn "\n[point lookup]"
|
||||
benchLookup "users" "id" (u `div` 2) 100
|
||||
_ <- benchSql "SELECT ... WHERE id = k(走 Lookup)" (printf "SELECT name FROM users WHERE id = %d" (u `div` 2))
|
||||
_ <- benchSql "SELECT ... WHERE id = k (via Lookup)" (printf "SELECT name FROM users WHERE id = %d" (u `div` 2))
|
||||
|
||||
putStrLn "\n[批量插入:一条语句带多行,N 行只落一次盘]"
|
||||
putStrLn "\n[batch insert: one statement with N rows, one durable commit]"
|
||||
_ <- benchSqlOnce "CREATE TABLE batch_t" "CREATE TABLE batch_t (id int, name str, age int)"
|
||||
let groups = chunked 100 u
|
||||
(bt, batchErrs) <- runBatch (map multiInsertSql groups)
|
||||
case batchErrs of
|
||||
[] ->
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/行(%d 行/条 × %d 条语句)\n"
|
||||
"多行 INSERT"
|
||||
"%-38s %8.1f ms %7.3f ms/row (%d rows/stmt x %d statements)\n"
|
||||
"multi-row INSERT"
|
||||
(bt * 1000)
|
||||
(bt * 1000 / fromIntegral u)
|
||||
(100 :: Int)
|
||||
(length groups)
|
||||
(e : _) -> printf "%-38s %10s !! %s\n" "多行 INSERT" "失败" e
|
||||
(e : _) -> printf "%-38s %10s !! %s\n" "multi-row INSERT" "FAILED" e
|
||||
|
||||
putStrLn "\n[行级删除:代价和删几行无关,只和扫描 + 一次落盘有关]"
|
||||
_ <- benchSqlOnce "DELETE FROM users WHERE id = k(1 行)" (printf "DELETE FROM users WHERE id = %d" (u `div` 2))
|
||||
_ <- benchSqlOnce "DELETE FROM users(全部)" "DELETE FROM users"
|
||||
putStrLn "\n[row delete: cost tracks the scan plus one commit, not the row count]"
|
||||
_ <- benchSqlOnce "DELETE FROM users WHERE id = k (1 row)" (printf "DELETE FROM users WHERE id = %d" (u `div` 2))
|
||||
_ <- benchSqlOnce "DELETE FROM users (all)" "DELETE FROM users"
|
||||
|
||||
putStrLn "\n[二级索引:没有索引回退扫描,建完索引走索引]"
|
||||
putStrLn "\n[secondary index: falls back to scan, then uses the index once built]"
|
||||
_ <- benchSqlOnce "CREATE TABLE items" "CREATE TABLE items (id int, code int)"
|
||||
let itemGroups = chunked 100 o
|
||||
itemSql ids =
|
||||
@@ -361,39 +343,39 @@ runScenarios u o = do
|
||||
case itemErrs of
|
||||
[] -> pure ()
|
||||
(e : _) -> printf " !! %s\n" e
|
||||
printf " (先把 %d 行灌进 items:%.1f ms)\n" o (it * 1000)
|
||||
printf " (loaded %d rows into items: %.1f ms)\n" o (it * 1000)
|
||||
let targetCode = (o `div` 2) * 7 + 1
|
||||
_ <-
|
||||
benchSql
|
||||
"SELECT ... WHERE code = k(无索引)"
|
||||
"SELECT ... WHERE code = k (no index)"
|
||||
(printf "SELECT id FROM items WHERE code = %d" targetCode)
|
||||
_ <- benchSqlOnce "CREATE INDEX ON items (code)" "CREATE INDEX ON items (code)"
|
||||
_ <-
|
||||
benchSql
|
||||
"SELECT ... WHERE code = k(走索引)"
|
||||
"SELECT ... WHERE code = k (via index)"
|
||||
(printf "SELECT id FROM items WHERE code = %d" targetCode)
|
||||
|
||||
putStrLn "\n[数据字典统计]"
|
||||
putStrLn "\n[catalog stats]"
|
||||
cat <- doListCatalog
|
||||
case cat of
|
||||
Left e -> printf " 取统计失败:%s\n" e
|
||||
Left e -> printf " failed to read stats: %s\n" e
|
||||
Right infos -> mapM_ printStats infos
|
||||
|
||||
putStrLn "\n[校验]"
|
||||
putStrLn "\n[checks]"
|
||||
let expectAge = 9 * (u `div` 100) + max 0 (u `mod` 100 - 90)
|
||||
printf
|
||||
"age > 90 命中 %s(按数据分布应为 %d 行)\n"
|
||||
"age > 90 matched %s (expected %d rows from the data distribution)\n"
|
||||
(either show show ageR)
|
||||
expectAge
|
||||
when (u == o) $
|
||||
printf
|
||||
"JOIN 命中 %s(每个用户恰好 1 张订单,应与上一行相同 = %d)\n"
|
||||
"JOIN matched %s (one order per user, should equal the line above = %d)\n"
|
||||
(either show show joinR)
|
||||
expectAge
|
||||
unless (u == o) $
|
||||
putStrLn "(users 与 orders 行数不同,JOIN 行数没有简单公式,只打印不校验)"
|
||||
printf "多行 INSERT 灌进去 %d 行(应为 %d)\n" u u
|
||||
printf "二级索引那一行:code = %d 应该查到 id = %d\n" targetCode (o `div` 2)
|
||||
putStrLn "(users and orders differ in size; no simple formula, printing only)"
|
||||
printf "multi-row INSERT loaded %d rows (expected %d)\n" u u
|
||||
printf "secondary index: code = %d should find id = %d\n" targetCode (o `div` 2)
|
||||
|
||||
-- | 主入口
|
||||
main :: IO ()
|
||||
@@ -405,7 +387,6 @@ main = do
|
||||
serverArg = pick 2 ""
|
||||
serverPath <- findServer serverArg
|
||||
|
||||
-- 每次跑用独立的管道名与数据目录,避免和别的进程抢
|
||||
tmp <- getTemporaryDirectory
|
||||
stamp <- getMonotonicTime
|
||||
let pipe = "chusql-fullchain-" ++ show (round (stamp * 1e6) :: Int)
|
||||
@@ -413,23 +394,23 @@ main = do
|
||||
createDirectoryIfMissing True dataDir
|
||||
setEnv "CHUSQL_PIPE" pipe
|
||||
|
||||
printf "全链路基准:Haskell 引擎 ←命名管道→ Rust 存储进程\n"
|
||||
printf "存储进程:%s\n" serverPath
|
||||
printf "管道:%s\n" pipe
|
||||
printf "数据目录:%s\n" dataDir
|
||||
printf "数据规模:users = %d 行,orders = %d 行\n" u o
|
||||
printf "计时:墙上时钟(单调);每条语句都走完整链路\n"
|
||||
printf "full-chain benchmark: Haskell engine <--named pipe--> Rust storage\n"
|
||||
printf "storage : %s\n" serverPath
|
||||
printf "pipe : %s\n" pipe
|
||||
printf "data dir : %s\n" dataDir
|
||||
printf "dataset : users = %d rows, orders = %d rows\n" u o
|
||||
printf "timing : wall clock (monotonic); every statement runs the full chain\n"
|
||||
|
||||
bracket
|
||||
(do
|
||||
t1 <- getMonotonicTime
|
||||
ph <- startServer serverPath pipe dataDir
|
||||
t2 <- getMonotonicTime
|
||||
printf "\n[启动] 存储进程就绪,用时 %.2f s\n" (t2 - t1)
|
||||
printf "\n[startup] storage ready in %.2f s\n" (t2 - t1)
|
||||
pure ph
|
||||
)
|
||||
(\ph -> do
|
||||
putStrLn "\n[收尾] 停存储进程、清理数据目录"
|
||||
putStrLn "\n[teardown] stopping storage and removing the data dir"
|
||||
stopServer ph dataDir
|
||||
)
|
||||
(const (runScenarios u o))
|
||||
Reference in new issue
Block a user