mcpField
M
memory-benchmark

memory-benchmark

github.com/tursodatabase测试与质量25.1k59 次安装今天更新
代码仓库

关于 memory-benchmark

memory-benchmark 是用于测量 WAL 和 MVCC 模式下 SQL 工作负载内存用量的 Rust 基准测试与分析工具,涵盖堆分配、RSS、FTS 查询和栈用量报告。

主要功能

  1. 01使用 dhat 测量堆分配
  2. 02采集进程 RSS 快照
  3. 03基准测试 WAL 与 MVCC 模式下的 SQL 工作负载
  4. 04分析 FTS 查询分配
  5. 05按 SQL 语句报告栈用量

适用场景

  1. 01比较不同 SQL 工作负载配置的内存用量
  2. 02调查 FTS 查询中的内存分配
  3. 03测量 MVCC 事务和连接的内存用量
  4. 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-heavy100% INSERT 语句创建表
read-heavy90% 按 id 执行 SELECT / 10% INSERT写入 10k 行种子数据
mixed50% SELECT / 50% INSERT写入 10k 行种子数据
scan-heavy使用 LIKE 执行全表扫描写入 10k 行种子数据
recursive-cte重复执行线性、优先队列、UNION-distinct 和外层 LIMIT 递归 CTE 查询无需设置 schema;batch-size 是递归结果的目标基数
series-blobINSERT 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 — 输出机器可读的汇总结果

典型工作流

调查内存使用情况或疑似回归时:

  1. 运行基准测试,参数与目标场景匹配:

    cargo run -p memory-benchmark -- --mode mvcc --workload mixed -i 500 -b 100 --connections 4
    
  2. 了解整体情况——哪些模块占用的内存最多:

    python3 perf/memory/analyze-dhat.py dhat-heap.json --modules --top 20
    
  3. 深入热点模块——例如,如果 turso_core 占主导:

    python3 perf/memory/analyze-dhat.py dhat-heap.json --filter turso_core --stacks --top 10
    
  4. 检查泄漏——查找退出时仍然存活但本不应存活的内容:

    python3 perf/memory/analyze-dhat.py dhat-heap.json --sort-by eb --top 10
    
  5. 比较模式——在 WAL 和 MVCC 下运行相同负载,并比较报告,以了解 MVCC 版本控制的内存开销。

并发详情

当 --connections > 1 时:

  • 设置阶段(创建架构、填充数据)始终在单个连接上按顺序运行
  • 运行阶段会为每个连接启动一个 tokio 任务,每个任务并发执行其批次
  • --checkpoint 会在运行阶段之后增加一个最终的单连接 PRAGMA wal_checkpoint(TRUNCATE) 阶段
  • 每个连接都会设置 busy_timeout(默认 30s,可通过 --timeout 配置)
  • WAL 模式使用 BEGIN,MVCC 使用 BEGIN CONCURRENT
  • Profile trait 的 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。

添加新的配置文件

  1. 创建 perf/memory/src/profile/your_profile.rs,实现 Profile trait
  2. 在 perf/memory/src/profile/mod.rs 中添加 pub mod your_profile;
  3. 在 src/workload.rs 的 WorkloadProfile enum 中添加一个变体
  4. 在 src/workload.rs 的 create_profile() 中为其添加关联逻辑
  5. 将其添加到 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 输出”一节
  • 更改 Profile trait 签名:更新“添加新的配置文件”一节

未来的 Agent 依赖此文档保持准确。过时的说明会造成无谓的工作。

评论

支持 Markdown