优化和收尾
This commit is contained in:
1 parent
7f5bcc289e
commit
9e8c5f1ef0
33 files changed
+3332
-598
No files matched your search
@@ -1,93 +0,0 @@
|
||||
# ChuSQL 基准测试
|
||||
|
||||
这个目录放**性能基准测试**,和 `chusql-engine`、`chusql-storage` 的功能代码分开,
|
||||
因为它回答的问题不一样:功能代码回答"对不对",基准测试回答"快不快、快多少"。
|
||||
|
||||
```
|
||||
benchmark/
|
||||
├── README.md 本文件:怎么跑、测什么、结果怎么解读
|
||||
├── chusql-engine/ Haskell 查询层:优化器开/关的对比
|
||||
│ ├── stack.yaml 把 chusql-engine 作为本地包引入
|
||||
│ ├── chusql-bench.cabal
|
||||
│ └── src/Main.hs
|
||||
└── chusql-storage/ Rust 存储层:预留(见该目录下的 README)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 怎么跑
|
||||
|
||||
```bash
|
||||
cd benchmark/chusql-engine
|
||||
stack run # 默认 users=500 行、orders=500 行
|
||||
stack run -- 1000 1000 0.5 # 也可以自己指定:users 行数、orders 行数、每批目标秒数
|
||||
```
|
||||
|
||||
第一次跑要编译,之后很快。整个默认跑完约 5 秒。
|
||||
|
||||
---
|
||||
|
||||
## 测什么
|
||||
|
||||
四个用例,**每个都跑两遍**:一遍走"翻译出来的原始工序单",一遍走"优化后的工序单"。
|
||||
两次的结果行数会被打印出来互相对照,**不一致会当场标出来**。
|
||||
|
||||
| 用例 | 想看什么 |
|
||||
| ---- | -------- |
|
||||
| `JOIN` + `WHERE u.age > 90` | 谓词下推的主场:条件能筛掉约 90% 的行 |
|
||||
| `JOIN`(无 WHERE) | 只剩投影下推在起作用,收益应该温和 |
|
||||
| `JOIN` + `WHERE` + `ORDER BY` + `LIMIT` | 规则叠加时的综合效果 |
|
||||
| 单表 `WHERE age > 90` | 对照组:没有下推空间,优化器只剩固定开销 |
|
||||
|
||||
---
|
||||
|
||||
## 计时怎么做(两个坑都踩过了)
|
||||
|
||||
**坑一:Haskell 是惰性的,不强制求值就等于没测。**
|
||||
如果只写 `let r = evalRelOp db plan`,`r` 根本没被算出来,测到的是"定义一个变量"的时间。
|
||||
所以每次必须把结果**算到底**(本项目里是逐行把列数累加起来当校验和)。
|
||||
|
||||
**坑二:Windows 上 CPU 时钟的精度只有约 15.6 毫秒。**
|
||||
优化后的查询只要几毫秒,直接测会得到 `0.0 ms` 这种没意义的数字。
|
||||
|
||||
所以计时用的是**自适应批量**:把同一个用例连续跑 `k` 次测一整批,`k` 不断翻倍,
|
||||
直到一批累计耗时超过目标值(默认 0.3 秒),再折算成单次耗时。
|
||||
这样时钟精度的误差会被 `k` 摊薄,几毫秒的用例也能测准。
|
||||
程序会打印实际用了多大的批次(例如"优化后 64 次/批")。
|
||||
|
||||
跑正式计时之前还会**预热两遍**,避免把首次构造表数据的开销算进去。
|
||||
|
||||
---
|
||||
|
||||
## 实测结果
|
||||
|
||||
**环境**:Windows,GHC 9.10.3,`-O2`
|
||||
**数据**:`users` 500 行(`id / name / age`,`age` 只有 0-99 共 100 种取值),
|
||||
`orders` 500 行(`id / user_id / product`),两表配对共 25 万对。
|
||||
|
||||
| 查询 | 结果行数 | 未优化 | 优化后 | 加速 |
|
||||
| ---- | -------- | ------ | ------ | ---- |
|
||||
| `JOIN` + `WHERE u.age > 90` | 45 | 125.0 ms | **7.1 ms** | **17.7x** |
|
||||
| `JOIN`(无 WHERE) | 500 | 132.8 ms | 105.5 ms | 1.3x |
|
||||
| `JOIN` + `WHERE` + `ORDER BY` + `LIMIT 10` | 10 | 171.9 ms | **10.3 ms** | **16.8x** |
|
||||
| 单表 `WHERE age > 90` | 45 | 0.111 ms | 0.153 ms | 0.7x |
|
||||
|
||||
### 怎么解读
|
||||
|
||||
- **前两行的差距来自谓词下推。** 条件筛掉了约 90% 的用户,配对次数从 25 万降到约 2.25 万,
|
||||
时间直接掉一个数量级。筛得越狠,省得越多。
|
||||
- **第二行只有投影下推。** 配对次数一点没变(没有条件可下推),只是每一行变窄了,
|
||||
所以只有 1.3 倍 —— 收益温和但确实是正的。
|
||||
- **第四行反而略慢。** 单表查询本来就没有下推空间(条件早已贴着扫描),
|
||||
而优化器自己要走一遍规则,有几十微秒的固定开销(0.153 − 0.111 ≈ 0.04 ms)。
|
||||
**优化不是零成本的**:查询越大、配对越多,这笔账才越划算。
|
||||
|
||||
---
|
||||
|
||||
## 加新用例
|
||||
|
||||
在 `src/Main.hs` 的 `queries` 列表里加一条 SQL 字符串就行,其余全自动
|
||||
(会自动翻译、优化、跑两遍、比结果、算加速比)。
|
||||
|
||||
想测别的规模,直接命令行传参;想在文档里引用数字,记得把**数据规模一起写上** ——
|
||||
脱离规模谈"快了几倍"是没有意义的。
|
||||
Generated
+334
@@ -0,0 +1,334 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "chusql-storage"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"interprocess",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chusql-storage-bench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chusql-storage",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "doctest-file"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2db04e74f0a9a93103b50e90b96024c9b2bdca8bce6a632ec71b88736d3d359"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interprocess"
|
||||
version = "2.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb02ffbf25e8ef0f4a95bff054eef75f6d1bf16919474cf2caa63b21997478a"
|
||||
dependencies = [
|
||||
"doctest-file",
|
||||
"libc",
|
||||
"recvmsg",
|
||||
"widestring",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "recvmsg"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "chusql-storage-bench"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# 存储层基准单独成包,不混进功能代码(见同目录 README)。
|
||||
[dependencies]
|
||||
chusql-storage = { path = "../../chusql-storage" }
|
||||
serde_json = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[[bench]]
|
||||
name = "storage"
|
||||
harness = false
|
||||
@@ -1,36 +0,0 @@
|
||||
# chusql-storage 基准测试(预留)
|
||||
|
||||
这个目录预留给 **Rust 存储层** 的基准测试代码,目前 `chusql-storage/` 还没有开工,
|
||||
所以这里先占位。
|
||||
|
||||
## 计划放什么
|
||||
|
||||
`chusql-storage` 做的是磁盘那一层的事情,性能指标和查询层完全不同 ——
|
||||
查询层关心"少算几行",存储层关心"少读几次盘"。所以基准要分开测:
|
||||
|
||||
| 待测模块 | 关心的指标 |
|
||||
| -------- | ---------- |
|
||||
| 页管理 | 单页读写延迟、顺序读 vs 随机读吞吐 |
|
||||
| B+ 树 | 插入/查找/范围扫描的耗时随数据量增长的趋势 |
|
||||
| 缓冲池 | 命中率、缺页次数、替换策略(LRU 等)的对比 |
|
||||
| WAL | 追加写吞吐、日志落盘延迟、恢复耗时 |
|
||||
| 崩溃恢复 | 从日志重放的时间、恢复后数据一致性 |
|
||||
|
||||
## 用什么
|
||||
|
||||
Rust 侧的基准测试一般用 [criterion](https://github.com/bheisler/criterion.rs):
|
||||
|
||||
```
|
||||
benchmark/chusql-storage/
|
||||
├── Cargo.toml
|
||||
└── benches/
|
||||
├── page.rs
|
||||
├── btree.rs
|
||||
├── buffer_pool.rs
|
||||
└── wal.rs
|
||||
```
|
||||
|
||||
跑法:`cargo bench`
|
||||
|
||||
另外还要和 Haskell 层对齐口径:查询层的基准是"整条查询的耗时",
|
||||
存储层的基准是"单次 IO / 单次查找的耗时",两边不要混在一起比。
|
||||
@@ -0,0 +1,353 @@
|
||||
//! 存储层基准:页 IO、B+ 树、堆表、WAL 与服务端写入路径。
|
||||
//!
|
||||
//! 用 `std::time::Instant` 计时,不引第三方基准框架,离线也能跑。
|
||||
//! 每项跑 `rounds` 轮取最快一轮:存储层的抖动主要来自 fsync 与调度,
|
||||
//! 取最快一轮最接近"这套代码本身要花多久"。
|
||||
//!
|
||||
//! 跑法:`cargo bench`(可选环境变量 `CHUSQL_BENCH_N` 改规模,默认 2000)
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chusql_storage::btree::DiskBTree;
|
||||
use chusql_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
|
||||
use chusql_storage::heap::HeapTable;
|
||||
use chusql_storage::page::{Page, PageFile};
|
||||
use chusql_storage::protocol::Row;
|
||||
use chusql_storage::wal::{Wal, WalOp};
|
||||
use serde_json::json;
|
||||
|
||||
// 工具
|
||||
/// 跑一个操作 `ops` 次,量总耗时并折算每次耗时
|
||||
fn bench<F: FnMut()>(label: &str, ops: usize, rounds: usize, mut f: F) {
|
||||
let mut best = Duration::MAX;
|
||||
for _ in 0..rounds {
|
||||
let t0 = Instant::now();
|
||||
f();
|
||||
let d = t0.elapsed();
|
||||
if d < best {
|
||||
best = d;
|
||||
}
|
||||
}
|
||||
let per = best.as_secs_f64() * 1e6 / ops as f64;
|
||||
println!(
|
||||
"{:<38} {:>9.2} ms {:>10.3} us/次",
|
||||
label,
|
||||
best.as_secs_f64() * 1e3,
|
||||
per
|
||||
);
|
||||
}
|
||||
|
||||
/// 只量一次(fsync 类操作太慢,不适合反复跑)
|
||||
fn bench_once<F: FnOnce()>(label: &str, ops: usize, f: F) {
|
||||
let t0 = Instant::now();
|
||||
f();
|
||||
let d = t0.elapsed();
|
||||
let per = d.as_secs_f64() * 1e6 / ops as f64;
|
||||
println!(
|
||||
"{:<38} {:>9.2} ms {:>10.3} us/次",
|
||||
label,
|
||||
d.as_secs_f64() * 1e3,
|
||||
per
|
||||
);
|
||||
}
|
||||
|
||||
/// 造一行
|
||||
fn row(id: i64) -> Row {
|
||||
let mut m = Row::new();
|
||||
m.insert("id".to_string(), json!(id));
|
||||
m.insert("name".to_string(), json!(format!("user{}", id)));
|
||||
m
|
||||
}
|
||||
|
||||
/// 小随机数发生器,避免引 rand
|
||||
fn next_rand(state: &mut u64) -> u64 {
|
||||
*state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
*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, || {
|
||||
pf.truncate().unwrap();
|
||||
for i in 0..n {
|
||||
pf.write_page(&Page::new(i as u64, DEFAULT_PAGE_SIZE)).unwrap();
|
||||
}
|
||||
pf.flush().unwrap();
|
||||
});
|
||||
drop(pf);
|
||||
|
||||
// 池子故意开小,让顺序读真的去读盘
|
||||
let mut pf = PageFile::with_options(&path, DEFAULT_PAGE_SIZE, 16).unwrap();
|
||||
bench("page 顺序读(池 16,基本都缺页)", n, 3, || {
|
||||
for i in 0..n {
|
||||
pf.read_page(i as u64).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let mut pf = PageFile::with_options(&path, DEFAULT_PAGE_SIZE, 16).unwrap();
|
||||
let mut state = 12345u64;
|
||||
bench("page 随机读(池 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, || {
|
||||
for _ in 0..n {
|
||||
let id = next_rand(&mut state) % n as u64;
|
||||
pf.read_page(id).unwrap();
|
||||
}
|
||||
});
|
||||
println!(
|
||||
" (池 1024:命中 {}/{})",
|
||||
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, || {
|
||||
bt.clear().unwrap();
|
||||
for i in 0..n {
|
||||
bt.insert(i as i64, i as u64).unwrap();
|
||||
}
|
||||
bt.flush().unwrap();
|
||||
});
|
||||
|
||||
bench("btree 点查(全部命中)", n, 3, || {
|
||||
for i in 0..n {
|
||||
bt.get(i as i64).unwrap().expect("key must exist");
|
||||
}
|
||||
});
|
||||
|
||||
bench("btree 全量遍历", 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");
|
||||
let idx = dir.join("heap.idx");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(&idx);
|
||||
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, 1024).unwrap();
|
||||
|
||||
bench("heap 插入(只在缓冲池,不 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, || {
|
||||
t.replace_all(&[]).unwrap();
|
||||
for i in 0..n {
|
||||
t.insert_row(&row(i as i64)).unwrap();
|
||||
}
|
||||
t.flush().unwrap();
|
||||
});
|
||||
|
||||
bench("heap 批量插入 + flush(一次落盘)", 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, || {
|
||||
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");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let wal = Wal::new(&path);
|
||||
let op = WalOp::Insert {
|
||||
table: "t".to_string(),
|
||||
row: row(1),
|
||||
};
|
||||
|
||||
bench_once("wal 追加(每条都 fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.append(&op).unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
|
||||
bench_once("wal 清空(每条都 fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
|
||||
bench_once("wal 截断(不落盘)", 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, || {
|
||||
for _ in 0..n {
|
||||
let mut f = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
f.write_all(&bytes).unwrap();
|
||||
f.sync_data().unwrap();
|
||||
}
|
||||
});
|
||||
let mut open_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
bench_once(" └ 复用句柄:write + fsync", n, || {
|
||||
for _ in 0..n {
|
||||
open_file.write_all(&bytes).unwrap();
|
||||
open_file.sync_data().unwrap();
|
||||
}
|
||||
});
|
||||
bench_once(" └ only fsync(不开不写)", n, || {
|
||||
for _ in 0..n {
|
||||
open_file.sync_data().unwrap();
|
||||
}
|
||||
});
|
||||
bench_once(" └ only open/close", n, || {
|
||||
for _ in 0..n {
|
||||
let _ = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
|
||||
// 服务端写入路径
|
||||
/// 复刻 server.rs 里 write_via_wal 的顺序:清 WAL -> 写 WAL -> 改数据 -> flush -> 清 WAL
|
||||
fn bench_write_path(dir: &std::path::Path, n: usize) {
|
||||
let path = dir.join("wp.db");
|
||||
let idx = dir.join("wp.idx");
|
||||
let wal_path = dir.join("wp.wal");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(&idx);
|
||||
let _ = std::fs::remove_file(&wal_path);
|
||||
|
||||
let mut t = HeapTable::open_indexed(
|
||||
&path,
|
||||
&idx,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_BTREE_ORDER,
|
||||
1024,
|
||||
)
|
||||
.unwrap();
|
||||
let wal = Wal::new(&wal_path);
|
||||
|
||||
bench_once("写入路径 单条 INSERT(端到端)", n, || {
|
||||
for i in 0..n as i64 {
|
||||
wal.truncate().unwrap();
|
||||
let op = WalOp::Insert {
|
||||
table: "t".to_string(),
|
||||
row: row(i),
|
||||
};
|
||||
wal.append(&op).unwrap();
|
||||
t.insert_row(&row(i)).unwrap();
|
||||
t.flush().unwrap();
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
// 拆开量,好定位时间花在哪一步
|
||||
bench_once(" └ wal.truncate()(不落盘)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.truncate().unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let op = WalOp::Insert {
|
||||
table: "t".to_string(),
|
||||
row: row(1),
|
||||
};
|
||||
bench_once(" └ wal.append()(含 fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.append(&op).unwrap();
|
||||
}
|
||||
});
|
||||
wal.clear().unwrap();
|
||||
|
||||
bench_once(" └ wal.clear()(含 fsync)", n, || {
|
||||
for _ in 0..n {
|
||||
wal.clear().unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
bench_once(" └ heap.insert_row()(含写索引)", n, || {
|
||||
// 主循环已经插过 0..n 了,这里换个号段,免得撞上"索引列唯一"
|
||||
for i in n as i64..2 * n as i64 {
|
||||
t.insert_row(&row(i)).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
bench_once(" └ heap.flush()", n, || {
|
||||
for _ in 0..n {
|
||||
t.flush().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let n: usize = std::env::var("CHUSQL_BENCH_N")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
|
||||
println!("chusql-storage 基准:n = {},页大小 = {} 字节,btree order = {}",
|
||||
n, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER);
|
||||
println!("每项取最快一轮;带 fsync 的项只跑一次(跑多了太慢)\n");
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
println!("[页 IO]");
|
||||
bench_page(dir.path(), n);
|
||||
|
||||
println!("\n[B+ 树]");
|
||||
bench_btree(dir.path(), n);
|
||||
|
||||
println!("\n[堆表]");
|
||||
bench_heap(dir.path(), n);
|
||||
|
||||
println!("\n[WAL]");
|
||||
bench_wal(dir.path(), n.min(200));
|
||||
|
||||
println!("\n[服务端写入路径]");
|
||||
bench_write_path(dir.path(), n.min(200));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
cabal-version: 2.4
|
||||
|
||||
name: chusql-fullchain
|
||||
version: 1.0.0
|
||||
synopsis: ChuSQL 全链路基准:Haskell 引擎 + Rust 存储进程一起跑
|
||||
build-type: Simple
|
||||
|
||||
executable chusql-fullchain
|
||||
main-is: Main.hs
|
||||
hs-source-dirs: src
|
||||
build-depends: base >= 4.14 && < 5
|
||||
, chusql-engine
|
||||
, directory
|
||||
, filepath
|
||||
, process
|
||||
default-language: Haskell2010
|
||||
ghc-options: -O2 -Wall
|
||||
@@ -0,0 +1,435 @@
|
||||
module Main where
|
||||
|
||||
import ChuSQL.Engine (runStatementM)
|
||||
import ChuSQL.Model (Row, Value (..))
|
||||
import ChuSQL.Storage (IndexResult (..))
|
||||
import ChuSQL.Storage.IPC (
|
||||
IPCStorage (..),
|
||||
Request (..),
|
||||
Response (..),
|
||||
TableInfo (..),
|
||||
closeConnection,
|
||||
doInsert,
|
||||
doListCatalog,
|
||||
doLookupByColumn,
|
||||
sendRequest,
|
||||
)
|
||||
import ChuSQL.Syntax.Parser (parseStatement)
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Exception (SomeException, bracket, evaluate, try)
|
||||
import Control.Monad (filterM, unless, when)
|
||||
import Data.List (intercalate, isInfixOf, isPrefixOf)
|
||||
import GHC.Clock (getMonotonicTime)
|
||||
import System.Directory (
|
||||
createDirectoryIfMissing,
|
||||
doesFileExist,
|
||||
getTemporaryDirectory,
|
||||
removeDirectoryRecursive,
|
||||
)
|
||||
import System.Environment (getArgs, getEnvironment, setEnv)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath ((</>))
|
||||
import System.Process (
|
||||
CreateProcess (..),
|
||||
ProcessHandle,
|
||||
StdStream (..),
|
||||
createProcess,
|
||||
proc,
|
||||
terminateProcess,
|
||||
waitForProcess,
|
||||
)
|
||||
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 # 指定存储进程
|
||||
|
||||
-- * 命令行与存储进程
|
||||
|
||||
-- | 找 Rust 存储进程:优先 release,其次 debug
|
||||
findServer :: FilePath -> IO FilePath
|
||||
findServer "" = do
|
||||
let dirs =
|
||||
[ "chusql-storage/target"
|
||||
, "../../chusql-storage/target"
|
||||
, "../chusql-storage/target"
|
||||
, "target"
|
||||
]
|
||||
modes = ["release", "debug"]
|
||||
exes = ["server.exe", "server"]
|
||||
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"
|
||||
pure p
|
||||
[] -> do
|
||||
putStrLn "找不到 Rust 存储进程。先编译:"
|
||||
putStrLn " cd chusql-storage && cargo build --release"
|
||||
exitFailure
|
||||
findServer p = do
|
||||
ok <- doesFileExist p
|
||||
if ok
|
||||
then pure p
|
||||
else do
|
||||
putStrLn ("指定的存储进程不存在:" ++ p)
|
||||
exitFailure
|
||||
|
||||
-- | 子进程环境:继承当前环境,但把 CHUSQL_* 换成本次专用的
|
||||
childEnv :: [(String, String)] -> IO [(String, String)]
|
||||
childEnv extra = do
|
||||
base <- getEnvironment
|
||||
let stripped = filter (\(k, _) -> not ("CHUSQL_" `isPrefixOf` k)) base
|
||||
pure (stripped ++ extra)
|
||||
|
||||
-- | 起存储进程并等管道可连
|
||||
startServer :: FilePath -> String -> FilePath -> IO ProcessHandle
|
||||
startServer serverPath pipe dataDir = do
|
||||
envs <-
|
||||
childEnv
|
||||
[ ("CHUSQL_PIPE", pipe)
|
||||
, ("CHUSQL_DATA_DIR", dataDir)
|
||||
, ("CHUSQL_PAGE_SIZE", "4096")
|
||||
, ("CHUSQL_BTREE_ORDER", "4")
|
||||
, ("CHUSQL_LOG", "error")
|
||||
]
|
||||
(_, _, _, ph) <-
|
||||
createProcess
|
||||
(proc serverPath []) {env = Just envs, std_out = NoStream, std_err = Inherit}
|
||||
ready <- waitReady (200 :: Int)
|
||||
unless ready $ do
|
||||
putStrLn "存储进程 5 秒内没就绪"
|
||||
exitFailure
|
||||
pure ph
|
||||
|
||||
-- | 轮询 ping,直到对面应答
|
||||
waitReady :: Int -> IO Bool
|
||||
waitReady 0 = pure False
|
||||
waitReady n = do
|
||||
r <- try (sendRequest ReqPing) :: IO (Either SomeException Response)
|
||||
case r of
|
||||
Right RespPong -> pure True
|
||||
_ -> threadDelay 25000 >> waitReady (n - 1)
|
||||
|
||||
-- | 停存储进程并清掉缓存连接
|
||||
stopServer :: ProcessHandle -> FilePath -> IO ()
|
||||
stopServer ph dataDir = do
|
||||
closeConnection
|
||||
terminateProcess ph
|
||||
_ <- waitForProcess ph
|
||||
_ <- try (removeDirectoryRecursive dataDir) :: IO (Either SomeException ())
|
||||
pure ()
|
||||
|
||||
-- * 计时
|
||||
|
||||
-- | 量一次墙上耗时,并把结果行数强制出来(Haskell 是惰性的,不强制就等于没测)
|
||||
timedRows :: IO (Either String [Row]) -> IO (Double, Either String Int)
|
||||
timedRows act = do
|
||||
t0 <- getMonotonicTime
|
||||
r <- act
|
||||
n <- case r of
|
||||
Left _ -> pure (-1)
|
||||
Right rows -> evaluate (length rows)
|
||||
t1 <- getMonotonicTime
|
||||
pure (t1 - t0, either Left (const (Right n)) r)
|
||||
|
||||
-- | 跑一条 SQL 文本(整条链路)
|
||||
runSql :: String -> IO (Either String [Row])
|
||||
runSql sql = case parseStatement sql of
|
||||
Left e -> pure (Left e)
|
||||
Right stmt -> runIPCStorage (runStatementM stmt)
|
||||
|
||||
-- | 打印一行结果
|
||||
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
|
||||
|
||||
-- | 单独量一条 SQL(先预热一遍):只适合可以重复跑的语句(SELECT)
|
||||
benchSql :: String -> String -> IO (Either String Int)
|
||||
benchSql label sql = do
|
||||
_ <- 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 =
|
||||
printf "INSERT INTO users (id, name, age) VALUES (%d, 'user%d', %d)" i i (i `mod` 100)
|
||||
|
||||
-- | 造一条订单的 INSERT
|
||||
orderSql :: Int -> Int -> String
|
||||
orderSql u j =
|
||||
printf
|
||||
"INSERT INTO orders (id, user_id, product) VALUES (%d, %d, 'item%d')"
|
||||
j
|
||||
(1 + (j - 1) `mod` u)
|
||||
(j `mod` 50)
|
||||
|
||||
-- | 批量插入并报平均耗时
|
||||
batchInsert :: String -> Int -> (Int -> String) -> IO (Either String ())
|
||||
batchInsert label n mkSql = do
|
||||
t0 <- getMonotonicTime
|
||||
results <- mapM (runSql . mkSql) [1 .. n]
|
||||
t1 <- getMonotonicTime
|
||||
let total = t1 - t0
|
||||
errs = [e | Left e <- results]
|
||||
case errs of
|
||||
[] -> do
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/条\n"
|
||||
label
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral n)
|
||||
pure (Right ())
|
||||
(e : _) -> do
|
||||
printf "%-38s %10s !! %s\n" label "失败" e
|
||||
pure (Left e)
|
||||
|
||||
-- | 一次点查(原始 IPC,不经过引擎)
|
||||
benchLookup :: String -> String -> Int -> Int -> IO ()
|
||||
benchLookup table column key times = do
|
||||
_ <- doLookupByColumn table column key
|
||||
t0 <- getMonotonicTime
|
||||
results <- mapM (const (doLookupByColumn table column key)) [1 .. times]
|
||||
t1 <- getMonotonicTime
|
||||
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)"
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral times)
|
||||
hit
|
||||
times
|
||||
|
||||
-- | 量 list_catalog:现在每条语句都要先来一发(只取结构,不取行)
|
||||
benchListCatalog :: Int -> IO ()
|
||||
benchListCatalog n = do
|
||||
t0 <- getMonotonicTime
|
||||
_ <- mapM (const doListCatalog) [1 .. n]
|
||||
t1 <- getMonotonicTime
|
||||
let total = t1 - t0
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/次\n"
|
||||
"list_catalog(取结构)"
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral n)
|
||||
|
||||
-- | 量原始 IPC 插入:不经过引擎,把"协议 + 存储"和"引擎"分开
|
||||
benchRawInsert :: String -> [Row] -> IO ()
|
||||
benchRawInsert table rows = do
|
||||
t0 <- getMonotonicTime
|
||||
results <- mapM (\r -> doInsert table r) rows
|
||||
t1 <- getMonotonicTime
|
||||
let total = t1 - t0
|
||||
n = length rows
|
||||
errs = [e | Left e <- results]
|
||||
case errs of
|
||||
[] ->
|
||||
printf
|
||||
"%-38s %8.1f ms %7.3f ms/条\n"
|
||||
("原始 IPC insert × " ++ show n)
|
||||
(total * 1000)
|
||||
(total * 1000 / fromIntegral n)
|
||||
(e : _) -> printf "%-38s %10s !! %s\n" "原始 IPC insert" "失败" e
|
||||
|
||||
-- | 造一行 users 数据(给原始 IPC 用)
|
||||
userRow :: Int -> Row
|
||||
userRow i = [("id", VInt i), ("name", VStr ("user" ++ show i)), ("age", VInt (i `mod` 100))]
|
||||
|
||||
-- | 依次跑一批语句,返回 (耗时秒, 错误清单)
|
||||
runBatch :: [String] -> IO (Double, [String])
|
||||
runBatch sqls = do
|
||||
t0 <- getMonotonicTime
|
||||
results <- mapM runSql sqls
|
||||
t1 <- getMonotonicTime
|
||||
pure (t1 - t0, [e | Left e <- results])
|
||||
|
||||
-- | 把 1..n 按 size 切成一段段
|
||||
chunked :: Int -> Int -> [[Int]]
|
||||
chunked size n = go [1 .. n]
|
||||
where
|
||||
-- \| 每次切下 size 个
|
||||
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
|
||||
" %-12s %6d 行 索引=%s\n 统计=%s\n"
|
||||
(tiTable i)
|
||||
(tiRows i)
|
||||
(if null (tiIndexes i) then "无" else unwords (tiIndexes i))
|
||||
( unwords
|
||||
[ n ++ ":" ++ show d ++ (if c then "+(下界)" else "")
|
||||
| (n, d, c) <- tiStats i
|
||||
]
|
||||
)
|
||||
|
||||
-- | 主场景
|
||||
runScenarios :: Int -> Int -> IO ()
|
||||
runScenarios u o = do
|
||||
putStrLn "\n[建表]"
|
||||
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 "建表失败,后面不用跑了"
|
||||
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[分解:同一条 INSERT 拆成两半,都用原始 IPC,不经过引擎]"
|
||||
_ <- 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[读取与查询:整条语句,含引擎侧的解析与语义检查]"
|
||||
_ <- 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[读取与查询:连接]"
|
||||
joinR <-
|
||||
benchSql
|
||||
"JOIN + WHERE(哈希连接)"
|
||||
"SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 90"
|
||||
|
||||
putStrLn "\n[点查]"
|
||||
benchLookup "users" "id" (u `div` 2) 100
|
||||
_ <- benchSql "SELECT ... WHERE id = k(走 Lookup)" (printf "SELECT name FROM users WHERE id = %d" (u `div` 2))
|
||||
|
||||
putStrLn "\n[批量插入:一条语句带多行,N 行只落一次盘]"
|
||||
_ <- 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"
|
||||
(bt * 1000)
|
||||
(bt * 1000 / fromIntegral u)
|
||||
(100 :: Int)
|
||||
(length groups)
|
||||
(e : _) -> printf "%-38s %10s !! %s\n" "多行 INSERT" "失败" 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[二级索引:没有索引回退扫描,建完索引走索引]"
|
||||
_ <- benchSqlOnce "CREATE TABLE items" "CREATE TABLE items (id int, code int)"
|
||||
let itemGroups = chunked 100 o
|
||||
itemSql ids =
|
||||
"INSERT INTO items (id, code) VALUES "
|
||||
++ intercalate ", " [printf "(%d, %d)" i (i * 7 + 1) | i <- ids]
|
||||
(it, itemErrs) <- runBatch (map itemSql itemGroups)
|
||||
case itemErrs of
|
||||
[] -> pure ()
|
||||
(e : _) -> printf " !! %s\n" e
|
||||
printf " (先把 %d 行灌进 items:%.1f ms)\n" o (it * 1000)
|
||||
let targetCode = (o `div` 2) * 7 + 1
|
||||
_ <-
|
||||
benchSql
|
||||
"SELECT ... WHERE code = k(无索引)"
|
||||
(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(走索引)"
|
||||
(printf "SELECT id FROM items WHERE code = %d" targetCode)
|
||||
|
||||
putStrLn "\n[数据字典统计]"
|
||||
cat <- doListCatalog
|
||||
case cat of
|
||||
Left e -> printf " 取统计失败:%s\n" e
|
||||
Right infos -> mapM_ printStats infos
|
||||
|
||||
putStrLn "\n[校验]"
|
||||
let expectAge = 9 * (u `div` 100) + max 0 (u `mod` 100 - 90)
|
||||
printf
|
||||
"age > 90 命中 %s(按数据分布应为 %d 行)\n"
|
||||
(either show show ageR)
|
||||
expectAge
|
||||
when (u == o) $
|
||||
printf
|
||||
"JOIN 命中 %s(每个用户恰好 1 张订单,应与上一行相同 = %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)
|
||||
|
||||
-- | 主入口
|
||||
main :: IO ()
|
||||
main = do
|
||||
args <- getArgs
|
||||
let pick i d = if length args > i then args !! i else d
|
||||
u = read (pick 0 "200") :: Int
|
||||
o = read (pick 1 "200") :: Int
|
||||
serverArg = pick 2 ""
|
||||
serverPath <- findServer serverArg
|
||||
|
||||
-- 每次跑用独立的管道名与数据目录,避免和别的进程抢
|
||||
tmp <- getTemporaryDirectory
|
||||
stamp <- getMonotonicTime
|
||||
let pipe = "chusql-fullchain-" ++ show (round (stamp * 1e6) :: Int)
|
||||
dataDir = tmp </> pipe
|
||||
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"
|
||||
|
||||
bracket
|
||||
(do
|
||||
t1 <- getMonotonicTime
|
||||
ph <- startServer serverPath pipe dataDir
|
||||
t2 <- getMonotonicTime
|
||||
printf "\n[启动] 存储进程就绪,用时 %.2f s\n" (t2 - t1)
|
||||
pure ph
|
||||
)
|
||||
(\ph -> do
|
||||
putStrLn "\n[收尾] 停存储进程、清理数据目录"
|
||||
stopServer ph dataDir
|
||||
)
|
||||
(const (runScenarios u o))
|
||||
@@ -0,0 +1,5 @@
|
||||
resolver: lts-24.59
|
||||
|
||||
packages:
|
||||
- .
|
||||
- ../../chusql-engine
|
||||
@@ -0,0 +1,12 @@
|
||||
# This file was autogenerated by Stack.
|
||||
# You should not edit this file by hand.
|
||||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/topics/lock_files
|
||||
|
||||
packages: []
|
||||
snapshots:
|
||||
- completed:
|
||||
sha256: 0f728e30e843d7f00460ad56858b995a3b227fca683659c7df9f2fc3988a3ce1
|
||||
size: 732465
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/59.yaml
|
||||
original: lts-24.59
|
||||
Reference in new issue
Block a user