关于 memory-benchmark
memory-benchmark 是用于测量 WAL 和 MVCC 模式下 SQL 工作负载内存用量的 Rust 基准测试与分析工具,涵盖堆分配、RSS、FTS 查询和栈用量报告。
主要功能
- 01使用 dhat 测量堆分配
- 02采集进程 RSS 快照
- 03基准测试 WAL 与 MVCC 模式下的 SQL 工作负载
- 04分析 FTS 查询分配
- 05按 SQL 语句报告栈用量
适用场景
- 01比较不同 SQL 工作负载配置的内存用量
- 02调查 FTS 查询中的内存分配
- 03测量 MVCC 事务和连接的内存用量
- 04定位栈用量较高的 SQL 语句
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tursodatabase/ turso --skill memory-benchmark安装后在 Claude Code 中输入/memory-benchmark确认已加载。
详细介绍
内存基准测试与分析
perf/memory crate 用于基准测试 WAL 和 MVCC 日志模式下 SQL 工作负载的内存用量。它使用 dhat 作为全局分配器来跟踪每次堆分配,并使用 memory-stats 获取进程级 RSS 快照。
其中还包含一个用于调查栈用量的 stack-report 辅助二进制文件。
该二进制文件在启用 stacker 功能的情况下运行 SQL 负载,并在进程内捕获
turso_stack 跟踪事件,通过聚合结构化跟踪字段,而不是解析 stderr 日志文本。
位置
- 基准测试 crate:
perf/memory/ - CodSpeed 基准测试 crate:
perf/memory/codspeed/(用于 CI 分配回归跟踪) - 分析脚本:
perf/memory/analyze-dhat.py - dhat 输出:
dhat-heap.json(每次运行后写入 CWD)
该 crate 分为一个库和多个二进制文件。工作负载引擎位于
memory_benchmark::workload(run_workload、WorkloadConfig、
WorkloadObserver、JournalMode/WorkloadProfile 枚举以及
create_profile);memory-benchmark 二进制文件是其精简 CLI 封装,负责添加 dhat/RSS 测量。随机化配置(read-heavy、mixed)使用固定 RNG 种子(profile::WORKLOAD_RNG_SEED),因此每次运行的工作负载都相同。
FTS 查询分配
fts-memory 二进制文件和 perf/memory/codspeed/benches/fts_queries.rs 共享
memory_benchmark::fts,且独立于现有的 SQL 配置运行器。
命令、语料库详情和测量限制请参见 perf/memory/README.md。
cargo run -p memory-benchmark --features fts --bin fts-memory -- \
--query common --state warm --documents 10000 --queries 100 \
--dhat-file /tmp/fts-warm.json > /tmp/fts-warm-report.json
python3 perf/memory/analyze-dhat.py /tmp/fts-warm.json --modules --top 20
cargo test -p memory-benchmark-codspeed --features fts --bench fts_queries -- --test
正确性检查请使用调试运行;进行优化后的 dhat 调查时,请使用 --profile bench-profile,它会保留调试信息且不启用 LTO。查询用例包括 rare、
common、and、or、phrase 和 ranked。状态 first 需要执行一个查询(默认值);warm 默认在一个持久连接上执行 100 个查询,并先进行一次不计入测量的预热。默认语料库包含 10000 篇文档;1000 和 100000 是适合本地对比的规模。WAL 和一个连接仍为默认设置。
如需完整的事务生命周期,请传入 --mode mvcc --connections 2 --transactions-per-connection 10 --queries-per-transaction 2。这会测量 20
个事务和 40 个查询,包括 BEGIN CONCURRENT 和 COMMIT。WAL 使用
BEGIN。显式事务数与 --queries 冲突;否则,查询数按每个连接计算。所有事务都会在查询工作线程启动前开始,所有工作线程完成后才提交。各轮之间连接保持不变。一个连接涵盖重复事务;两个或四个连接则涵盖重叠快照。选择显式事务时,预热会为每个连接运行一个完整事务,其中包含一个查询。报告包含已完成的事务/查询数以及 max_active_transactions;堆总量汇总所有连接和调度产生的数据。
FtsWorkload 通过 FtsObserver 发出 setup、open、warmup、run、cleanup 和 done 事件。CLI 的 DhatObserver 会在 run 时开始性能分析,并在 cleanup 时停止,早于连接关闭。每次状态转换都会向 stderr 发出 JSON 阶段事件,并保存到最终报告的 phases 数组中。stderr 包含 dhat 诊断信息;使用 jq -Rc 'fromjson? | select(.event == "phase")' 筛选阶段事件。事件中记录的耗时包含性能分析器的开销;请检查退出状态以确认是否成功。after_batch 会在每个连接执行一次查询后,或每轮事务中的所有提交完成后,采样当前存活堆内存。工作线程错误会在回滚和清理前汇总处理。
初始化、打开数据库、预热和清理均不计入测量。堆总量、峰值、保留字节数和每查询存活字节数样本跟踪的是查询阶段分配,而不是性能分析开始前分配的缓存。性能分析结束后的 RSS 包含 dhat 自身的栈和报告开销。JSON 输出到 stdout,分配栈输出到 --dhat-file。
Divan 目标使用工作区兼容 CodSpeed 的 Divan 依赖,不使用 dhat 分配器,并在 fts-queries 内存 CI 分片中运行。使用 cargo codspeed build -m memory -p memory-benchmark-codspeed --features codspeed,fts 进行构建,并使用 cargo codspeed run -m memory -p memory-benchmark-codspeed --bench fts_queries 运行。每个查询都有针对 1000 和 10000 个文档的首次/预热变体,以及针对 10000 个文档的额外 10/100 次查询预热变体。名称编码为 (state, documents, queries)。
额外的 24 个 transactions 用例使用 1000 个文档、每个连接十个事务,以及每个事务两个查询:所有六个查询用例都涵盖 WAL/一个连接,以及 MVCC/一个、两个或四个连接。名称编码为 (mode, connections, transactions_per_connection, queries_per_transaction)。Divan 使用相同的 FtsWorkload::prepare 和 run 方法,并搭配空操作观察器。
Divan/CodSpeed 目标对所有连接数都使用 Tokio 的单线程运行时。事务会重叠,但查询任务在一个线程上执行。dhat CLI 保留多线程运行时,用于并行查询测量。
不带 codspeed 功能的本地构建会在系统分配器上安装 Divan 的 AllocProfiler。运行 cargo bench --profile bench-profile -p memory-benchmark-codspeed --features fts --bench fts_queries -- transactions --sample-count 3 --sample-size 1
以输出本地分配数据。所有查询任务都在 Divan 的测量线程上运行。
对于进程范围的并行总量和峰值,请使用 dhat CLI。alloc 统计分配调用次数;realloc 的增长量单独计入 grow。max alloc 是整个测量序列中的峰值存活内存,而非每个查询的峰值。
现有 Criterion 内存配置及其包含设置阶段的指标保持不变。
对于超出缓存容量的测试,请结合上述事务标志使用 --documents 20000 --extra-tokens 1024 --cache-pages 200 --min-index-bytes 268435456。
额外词元使用固定种子,不会改变查询匹配结果。最小值是根据存储的 FTS 块字节数检查的,而非语料库或数据库大小。设置阶段会使用核心测试辅助工具的底层行读取器,然后关闭检查连接。JSON 中的 index 报告 segment_bytes、segments、largest_segment_bytes、page_size 和 configured_cache_pages;corpus 记录这三个选项。测量或拒绝之前,stderr 会输出一条 event: "index" 记录,报告相关大小。
缓存页数必须至少为 200;省略时使用引擎默认值。额外词元数和最小值为零时,会保留小型语料库。256 MiB 索引超过当前 192 MiB 的保留段预算,但缓存的搜索器可能会保留段数据,且活动游标会加载每个可见段。这并不能证明缓存会逐出数据,也不能证明存在内存上限。只读事务不会触发跨越不同快照的四搜索器限制。写入器 arena(64 MiB)和文档刷新(1000)的限制在设置阶段生效,处于查询性能分析之外。所有注意事项请参阅 README。
对应的 Divan 用例需选择启用:--features fts-stress --bench fts_queries -- oversized_index,使用一个/两个 MVCC 连接、三个事务、每个事务两个查询,以及一个样本。CI 使用 codspeed,fts 进行构建,在编译时排除大型用例,并在 fts-queries 中运行较小用例。在 CodSpeed 下,每种配置的大型用例耗时约 18 分钟;位置筛选器无法隔离 Divan 用例。超出缓存容量的运行请使用本地 Divan 或 dhat。
构建作业还会运行 cargo test -p memory-benchmark --features fts --locked。
运行堆栈报告
调查 SQL 转换/执行探针的堆栈使用情况时,请使用此功能。
与服务器日志或 CI 堆栈大小输出进行比较时,请在发布模式下使用 --features stacker 运行堆栈报告。调试构建可能会显著夸大堆栈差值,因此仅应用于快速本地迭代。
cargo run --release -q -p memory-benchmark --features stacker --bin stack-report -- \
--sql path/to/payload.sql \
--top 40
实用选项:
--sql FILE|- # SQL payload, or stdin with -
--format human|json|csv # output format
--top N # aggregate/span rows per statement in human output
--statement N[,N...] # only include reports for 1-based statement indexes
--sql-contains TEXT # only include reports for statements containing TEXT, ASCII case-insensitive
报告以语句为单位。对于每条 SQL 语句,它会记录执行前剩余的堆栈、语句运行期间采样到的最小剩余堆栈,以及 stack_used = baseline_remaining_stack - min_remaining_stack。
语句按 stack_used 降序排列,因此堆栈占用最严重的 SQL 语句会排在最前面。人类可读报告还会输出全局和按语句统计的跨度聚合数据,并按 total_inclusive_stack_used 降序排列。这些聚合行按 label 和 detail 分组,并包含调用次数、自身堆栈使用总量/最大值、包含子跨度的堆栈使用总量/最大值、跨度进入时的累计堆栈最大值,以及在语句剩余堆栈采样最小值时处于活动状态的跨度的 peak_path_hits。
在每条语句中,原始跨度行仍按 stack_used 降序排列,原始追踪发出顺序保留在 trace_sequence 字段中(人类可读输出中为 seq)。原始跨度行包含 inclusive_stack_used,其测量方式是从跨度的父级堆栈层级,计算到跨度处于活动状态期间采样到的最深剩余堆栈。这是包含子跨度的分析器指标,因此嵌套跨度有意重叠;请用它来排列可能的贡献项,而不要将其相加以计算语句的堆栈总量。
JSON 和 CSV 格式具有确定性,适用于比较不同运行结果。CSV 使用 row_type 列,包含 global_aggregate、statement_aggregate、span 和 statement 行。
语句筛选器只影响报告。运行器仍会按顺序执行完整的 SQL 负载,因此架构/数据设置以及较早的语句仍会对之后选中的语句可见。允许使用多个 --statement 和 --sql-contains 筛选器;如果两种筛选器都存在,语句必须同时匹配这两种筛选条件。
stack-report 使用 turso_parser::parser::Parser::next_cmd() 拆分负载。
随后,它会执行不含结果列的语句,并查询会生成结果行的语句且读取完其所有结果。请勿更改堆栈报告的绑定 execute_batch 语义。
运行器目前使用固定的内存数据库,并在内部启用生成列、自定义类型和物化视图。没有用于选择数据库路径或切换这些实验性功能的堆栈报告 CLI 标志。
运行基准测试
始终在发布模式下运行——调试构建的分配模式差异极大,结果无法反映真实使用情况。
# Basic: single connection, WAL mode, insert-heavy workload
cargo run --release -p memory-benchmark -- --mode wal --workload insert-heavy -i 100 -b 100
# MVCC with concurrent connections
cargo run --release -p memory-benchmark -- --mode mvcc --workload mixed -i 100 -b 100 --connections 4
# Run a final checkpoint after the workload
cargo run --release -p memory-benchmark -- --mode wal --workload read-heavy --checkpoint
# Exercise recursive queues at a 10k-row target cardinality
cargo run --release -p memory-benchmark -- --mode wal --workload recursive-cte -i 20 -b 10000
# Guarantee automatic MVCC checkpoints during the run by lowering the
# logical-log threshold (default is ~4 MB, more than small workloads write)
cargo run --release -p memory-benchmark -- --mode mvcc --workload insert-heavy --mvcc-checkpoint-threshold 16384
# All CLI options
cargo run --release -p memory-benchmark -- \
--mode wal|mvcc \
--workload insert-heavy|read-heavy|mixed|scan-heavy|recursive-cte|series-blob|update-churn \
-i <iterations> \
-b <batch-size> \
--connections <N> \
--checkpoint \
--timeout <ms> \
--cache-size <pages> \
--mvcc-checkpoint-threshold <bytes> # MVCC only; -1 disables auto-checkpoint
--mvcc-gc-threshold <versions> # MVCC only; -1 disables inline GC
--format human|json|csv
两个 --mvcc-*-threshold 标志会在运行前为共享的 mv_store 设置对应的 PRAGMA。这些参数用于隔离 MVCC GC 行为:
禁用检查点(--mvcc-checkpoint-threshold=-1),使内联 GC 成为唯一的回收机制,然后在 update-churn 工作负载上对比 GC 阈值(例如,-1 表示关闭、16384 表示默认值,或者使用更小、更激进的值)。
每次运行都会在当前目录中生成一个 dhat-heap.json。此文件包含整个运行过程中按分配位置统计的数据。
内置负载配置
| 配置 | 描述 | 设置 |
|---|---|---|
insert-heavy | 100% INSERT 语句 | 创建表 |
read-heavy | 90% 按 id 执行 SELECT / 10% INSERT | 写入 10k 行种子数据 |
mixed | 50% SELECT / 50% INSERT | 写入 10k 行种子数据 |
scan-heavy | 使用 LIKE 执行全表扫描 | 写入 10k 行种子数据 |
recursive-cte | 重复执行线性、优先队列、UNION-distinct 和外层 LIMIT 递归 CTE 查询 | 无需设置 schema;batch-size 是递归结果的目标基数 |
series-blob | INSERT INTO bench(data) SELECT zeroblob(2048) FROM generate_series(1, ?) | 创建 bench;batch-size 是序列长度 |
update-churn | 对固定的 10k 行集合重复执行 UPDATE(按连接划分键空间,以避免写-写冲突) | 写入 10k 行种子数据。会生成过时版本——这正是 MVCC GC 累积的情况。 |
各配置在 perf/memory/src/profile/ 中实现 Profile trait。要添加新的负载,请创建一个实现该 trait 的新文件,并将其接入 main.rs 中的 WorkloadProfile 枚举。
理解输出
基准测试会报告三类指标:
RSS(进程级)
通过 memory-stats crate 测量。包含所有内容:堆、mmap 文件(WAL、加载到 OS 页缓存中的 DB 页面)、tokio 运行时等。在阶段转换(setup -> run)时以及每个批次结束后采集快照。
- 基线:执行任何 DB 操作前的 RSS(运行时开销)
- 峰值:运行期间观测到的最高 RSS
- 净增长:最终 RSS 减去基线——归因于负载的内存
堆(dhat)
通过 dhat 全局分配器精确跟踪分配。只统计显式堆分配(malloc/alloc),不统计 mmap。
- 当前值:测量时仍在分配中的字节数
- 峰值:整个运行期间同时存活的分配字节数的最高值
- 总分配次数:单次分配调用的次数
- 总字节数:累计分配的字节数(包括已释放的内存)——用于衡量分配压力
磁盘
基准测试完成后的文件大小:
- DB 文件:
.db文件 - WAL 文件:
.db-wal文件(仅 WAL 模式) - Log 文件:
.db-log文件(仅 MVCC 逻辑日志)
分析 dhat 输出
运行基准测试后,使用分析脚本从 dhat-heap.json 生成易读报告:
# Overview: top allocation sites by bytes live at global peak
python3 perf/memory/analyze-dhat.py dhat-heap.json --top 15 --modules
# Focus on a specific subsystem
python3 perf/memory/analyze-dhat.py dhat-heap.json --filter mvcc --stacks
python3 perf/memory/analyze-dhat.py dhat-heap.json --filter btree --stacks
python3 perf/memory/analyze-dhat.py dhat-heap.json --filter page_cache --stacks
# Sort by different metrics
python3 perf/memory/analyze-dhat.py dhat-heap.json --sort-by eb # bytes at exit (leaks)
python3 perf/memory/analyze-dhat.py dhat-heap.json --sort-by tb # total bytes (pressure)
python3 perf/memory/analyze-dhat.py dhat-heap.json --sort-by mb # max live bytes per site
# JSON output for programmatic use
python3 perf/memory/analyze-dhat.py dhat-heap.json --json
排序指标
| 标志 | 指标 | 适用场景 |
|---|---|---|
gb | 全局峰值时存活的字节数(默认) | 查找高水位时占用内存最多的内容 |
eb | 退出时存活的字节数 | 查找内存泄漏或始终未释放的内容 |
tb | 分配的总字节数 | 查找分配压力热点(GC churn) |
mb | 每个位置存活字节数的最大值 | 查找各位置的高水位 |
tbk | 总分配次数 | 查找频繁分配的分配器(大量小额分配) |
分析标志
--top N— 显示排名前 N 的位置(默认 15)--filter PATTERN— 筛选包含子字符串的代码位置/调用栈(例如mvcc、btree、wal、pager)--stacks— 显示排名靠前的分配位置的完整调用栈--modules— 按 crate/module 汇总,提供高层级细分--json— 输出机器可读的汇总结果
典型工作流
调查内存使用情况或疑似回归时:
-
运行基准测试,参数与目标场景匹配:
cargo run -p memory-benchmark -- --mode mvcc --workload mixed -i 500 -b 100 --connections 4 -
了解整体情况——哪些模块占用的内存最多:
python3 perf/memory/analyze-dhat.py dhat-heap.json --modules --top 20 -
深入热点模块——例如,如果
turso_core占主导:python3 perf/memory/analyze-dhat.py dhat-heap.json --filter turso_core --stacks --top 10 -
检查泄漏——查找退出时仍然存活但本不应存活的内容:
python3 perf/memory/analyze-dhat.py dhat-heap.json --sort-by eb --top 10 -
比较模式——在 WAL 和 MVCC 下运行相同负载,并比较报告,以了解 MVCC 版本控制的内存开销。
并发详情
当 --connections > 1 时:
- 设置阶段(创建架构、填充数据)始终在单个连接上按顺序运行
- 运行阶段会为每个连接启动一个 tokio 任务,每个任务并发执行其批次
--checkpoint会在运行阶段之后增加一个最终的单连接PRAGMA wal_checkpoint(TRUNCATE)阶段- 每个连接都会设置
busy_timeout(默认 30s,可通过--timeout配置) - WAL 模式使用
BEGIN,MVCC 使用BEGIN CONCURRENT Profiletrait 的next_batch(connections)会为每个连接返回一个批次,且行 ID 互不重叠
CI 中的 CodSpeed 分配跟踪
.github/workflows/codspeed-memory.yml 会在两种日志模式下运行每个工作负载配置文件,并使用 CodSpeed 的内存检测工具(基于 eBPF 的 malloc 跟踪:峰值内存、总分配量、分配次数),以便在 PR 中发现分配回归。基准测试框架是独立的 crate perf/memory/codspeed/(Criterion 基准测试命名为 <mode>/<workload>/<total-ops>,例如 mvcc/insert-heavy/2000,迭代次数远少于 CLI 默认值)。每个(模式、工作负载)组合都会以 1x/2x/4x 的规模运行——批次大小相同,迭代次数更多——因此比较不同规模可以看出内存如何随工作负载量增长。此外还包含一个 8x 的 <ops>-checkpoint 变体,确保检查点操作属于测量的一部分:它会将 mvcc_checkpoint_threshold 降至 16 KiB,让 MVCC 自动检查点在运行过程中触发(WAL 的 1000 帧阈值在 core/storage/wal.rs 中硬编码),并以显式的 PRAGMA wal_checkpoint(TRUNCATE) 结束。工作流会先构建一次基准测试二进制文件,然后为每个工作负载配置文件分派一个 CI 作业,并按名称筛选基准测试——采用 CodSpeed 分片基准测试文档中的分片模式。
基准测试 crate 必须保持不含 [[bin]] 目标:cargo 会同时构建 package 的二进制文件(release 配置下使用 panic=abort)和基准测试(使用 panic=unwind),而重复的 turso_sdk_kit cdylib/staticlib 编译单元会因未哈希的输出文件名而冲突,导致构建失败。因此,基准测试不会直接放在 perf/memory 中。
本地运行:
# Quick correctness pass (runs each benchmark once)
cargo bench -p memory-benchmark-codspeed --bench memory_profiles -- --test
# What CI runs (requires cargo-codspeed; uninstrumented outside the CodSpeed runner)
cargo codspeed build -m memory -p memory-benchmark-codspeed --features codspeed
cargo codspeed run -m memory -p memory-benchmark-codspeed --bench memory_profiles "insert-heavy"
除非你想进行完整的 Criterion 采样(每个样本都会执行整个工作负载),否则不要在没有 -- --test 的情况下直接运行 cargo bench -p memory-benchmark-codspeed。
添加新的配置文件
- 创建
perf/memory/src/profile/your_profile.rs,实现Profiletrait - 在
perf/memory/src/profile/mod.rs中添加pub mod your_profile; - 在
src/workload.rs的WorkloadProfileenum 中添加一个变体 - 在
src/workload.rs的create_profile()中为其添加关联逻辑 - 将其添加到
perf/memory/codspeed/benches/memory_profiles.rs中的WORKLOADS(和base_workload_size),并添加到.github/workflows/codspeed-memory.yml中的workload矩阵,以便 CI 跟踪
Profile trait:
pub trait Profile {
fn name(&self) -> &str;
fn next_batch(&mut self, connections: usize) -> (Phase, Vec<Vec<WorkItem>>);
}
架构创建和数据填充返回 Phase::Setup(单个批次),测量工作返回 Phase::Run(每个连接一个批次),完成后返回 Phase::Done。
保持此技能文档为最新
此技能文档是 Agent 使用内存基准测试工具的权威依据。如果你修改了 perf/memory crate——添加配置文件、更改 CLI 标志、调整输出格式、更新分析脚本、更改 Profile trait 等——请相应更新此 SKILL.md。具体而言:
- 新增 CLI 标志:添加到“运行基准测试”一节
- 新增配置文件:添加到“内置工作负载配置文件”表格
- 更改输出指标:更新“理解输出”一节
- 新增 analyze-dhat.py 标志或排序指标:更新“分析 dhat 输出”一节
- 更改
Profiletrait 签名:更新“添加新的配置文件”一节
未来的 Agent 依赖此文档保持准确。过时的说明会造成无谓的工作。