尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpacetimeDB 索引扫描性能基准:`perf-test` 模块与 `index_scan_gate` 门控详解

SpacetimeDB 索引扫描性能基准:`perf-test` 模块与 `index_scan_gate` 门控详解 SpacetimeDB 索引扫描性能基准perf-test模块与index_scan_gate门控详解【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB导读本文以 SpacetimeDB 仓库中 modules/perf-test/README.md 为线索深入讲解一个专用于索引index scan工作负载压测的 Rust 模块perf-test它构造一张包含 120 万行的Location表通过四个 reducer 分别验证单列主键索引、单列 B-tree 索引、多列复合索引的点查与范围扫描性能。同时结合crates/index-scan-gate的基准门benchmark gate源码说明该模块如何被自动调用、如何以中位数耗时作为通过判据帮助读者掌握 SpacetimeDB 索引性能基准从模块编写、数据加载到门控判定的完整链路。perf-test模块的定位与作用perf-test是一个面向 SpacetimeDB 的 Rust 基准测试模块其 README 中明确了两个核心定位内置多种index scan工作负载覆盖单列索引、多列复合索引在不同扫描规模下的查询场景由index_scan_gate基准门调用它不是独立演示用的 demo而是被crates/index-scan-gate这个可执行程序驱动用于确保系统按预期工作README 原文Called by theindex_scan_gatebenchmark to ensure the system is working as expected。换句话说perf-test扮演的是被测负载的提供方模块内表结构、数据量与各 reducer 的设计决定了每次基准测试实际测量什么而index_scan_gate扮演的是调度与判定方负责编译加载模块、执行 reducer、采样耗时并依据阈值放行或报错。从 modules/perf-test/Cargo.toml 可以看到模块的工程属性[package] name perf-test-module version 0.1.0 edition.workspace true license-file LICENSE [lib] crate-type [cdylib] [dependencies] spacetimedb { path ../../crates/bindings }关键点有两个crate-type [cdylib]表明它被编译成动态库形式的模块SpacetimeDB 模块的标准产物形态且仅依赖crates/bindings提供的模块端 SDK。被测表结构Location与索引设计perf-test的核心被测数据表是Location定义在 modules/perf-test/src/lib.rs#[spacetimedb::table(accessor location, index(accessor coordinates, btree(columns [x, z, dimension])))] #[derive(Debug, PartialEq, Eq)] pub struct Location { #[primary_key] pub id: u64, #[index(btree)] pub chunk: u64, #[index(btree)] pub x: i32, pub z: i32, pub dimension: u32, }这张表共设计了三类索引恰好对应后文四种扫描工作负载索引类型字段用途主键索引隐式唯一索引id单行点查primary key lookup单列索引B-treechunk单列等值过滤 范围扫描单列索引B-treex复合索引的前缀列复合索引B-treex, z, dimension多列等值点查 / 前缀扫描其中复合索引通过#[spacetimedb::table(...)]宏的index(accessor coordinates, btree(columns [x, z, dimension]))声明accessor coordinates为它在 SDK 侧生成的访问器命名后文 reducer 中出现的location().coordinates()即源于此。而x上单独的#[index(btree)]与复合索引共存的写法也体现了 SpacetimeDB 表宏支持普通列索引 多列复合索引组合声明的能力。数据装载1.2M 行的确定性构造在测量索引扫描性能之前必须先让表拥有足够的数据量。load_location_tablereducerlib.rs负责一次性灌入1000 个 chunk × 每 chunk 1200 行 120 万行const NUM_CHUNKS: u64 1000; const ROWS_PER_CHUNK: u64 1200; #[spacetimedb::reducer] pub fn load_location_table(ctx: ReducerContext) { for chunk in 0u64..NUM_CHUNKS { for i in 0u64..ROWS_PER_CHUNK { let id chunk * 1200 i; let x 0i32; let z chunk as i32; let dimension id as u32; ctx.db.location().insert(Location { id, chunk, x, z, dimension }); } } }数据构造是完全确定性的几个设计细节值得注意id为全局连续编号chunk * 1200 i因此id与chunk之间存在固定映射关系x恒为 0z等于 chunk 编号dimension等于id同一chunk内的 1200 行共享相同的chunk与(x, z)值仅id、dimension不同。这种布局使得后续四个测试 reducer 能精确预测查询返回的行数例如按chunk过滤必然命中 1200 行按(0, z, dimension)三元组过滤必然命中恰好 1 行。可预测的返回规模是基准正确性的前提——断言失败即代表索引行为异常基准门会因此报错。此外模块在文件顶部通过const ID: u64 989_987;与const CHUNK: u64 ID / ROWS_PER_CHUNK;预计算了一个贯穿四个测试的靶点数据对应 chunk 824保证所有扫描都落在同一批数据上测试之间可比。四个索引扫描工作负载详解四个测试 reducer 按单列 / 多列与点查 / 批量扫描两个维度覆盖四种组合且每个 reducer 都在执行后用assert_eq!校验返回结果的行数与字段值。1. 主键单行点查test_index_scan_on_id#[spacetimedb::reducer] /// Probing a single column index for a single row should be fast! pub fn test_index_scan_on_id(ctx: ReducerContext) { let span LogStopwatch::new(Index scan on {id}); let location ctx.db.location().id().find(ID).unwrap(); span.end(); assert_eq!(ID, location.id); }通过主键索引的访问器location().id().find(ID)做单行等值点查。这是索引的最快路径——按唯一主键定位恰好一行。断言ID location.id验证取回的行确实是目标行而非任意行。2. 单列索引批量扫描test_index_scan_on_chunk#[spacetimedb::reducer] /// Scanning a single column index for ROWS_PER_CHUNK rows should also be fast! pub fn test_index_scan_on_chunk(ctx: ReducerContext) { let span LogStopwatch::new(Index scan on {chunk}); let n ctx.db.location().chunk().filter(CHUNK).count(); span.end(); assert_eq!(n as u64, ROWS_PER_CHUNK); }通过单列 B-tree 索引chunk()做等值过滤再用.count()统计命中行数。由于每个 chunk 恰好 1200 行断言命中数等于ROWS_PER_CHUNK——这验证了索引扫描返回的集合是完整的而非部分或重复。3. 复合索引精确点查test_index_scan_on_x_z_dimension#[spacetimedb::reducer] /// Probing a multi-column index for a single row should be fast! pub fn test_index_scan_on_x_z_dimension(ctx: ReducerContext) { let z CHUNK as i32; let dimension ID as u32; let span LogStopwatch::new(Index scan on {x, z, dimension}); let n ctx.db.location().coordinates().filter((0, z, dimension)).count(); span.end(); assert_eq!(n, 1); }使用复合索引访问器coordinates()以完整三元组(0, z, dimension)过滤。由于(x, z)相同但dimension不同的行恰好只有一行断言命中数为 1对应复合索引的完整键点查。4. 复合索引前缀扫描test_index_scan_on_x_z#[spacetimedb::reducer] /// Probing a multi-column index for ROWS_PER_CHUNK rows should also be fast! pub fn test_index_scan_on_x_z(ctx: ReducerContext) { let z CHUNK as i32; let span LogStopwatch::new(Index scan on {x, z}); let n ctx.db.location().coordinates().filter((0, z)).count(); span.end(); assert_eq!(n as u64, ROWS_PER_CHUNK); }同样是复合索引coordinates()但只提供前两个前缀列(0, z)。B-tree 复合索引支持最左前缀匹配命中该 chunk 下所有 1200 行。这验证了 SpacetimeDB 复合索引的前缀扫描能力也是索引必须能用前缀键查询这一数据库通用语义在模块层的落实。计时机制LogStopwatch的底层实现四个测试 reducer 都用LogStopwatch包裹被测查询其实现位于 crates/bindings/src/log_stopwatch.rspub struct LogStopwatch { stopwatch_id: u32, } impl LogStopwatch { pub fn new(name: str) - Self { let name name.as_bytes(); let id unsafe { spacetimedb_bindings_sys::raw::console_timer_start(name.as_ptr(), name.len()) }; Self { stopwatch_id: id } } pub fn end(self) { // just drop self } } impl std::ops::Drop for LogStopwatch { fn drop(mut self) { unsafe { spacetimedb_bindings_sys::raw::console_timer_end(self.stopwatch_id); } } }实现机制值得注意new调用console_timer_start向宿主机申请一个计时器并获得 idend本身是空操作真正的计时结束发生在Drop——当span变量离开作用域时自动调用console_timer_end上报耗时。这种RAII 式计时保证了即使函数提前return或panic计时也不会泄漏。计时器名称如Index scan on {id}会进入宿主机日志便于在控制台输出中定位每个工作负载的耗时。基准门index_scan_gate如何运行与判定运行方式README 给出的一行命令即整个基准的入口cargo bench -p spacetimedb-bench --bench index_scan_gate-p spacetimedb-bench指向基准 cratecrates/bench其 Cargo.toml 中name spacetimedb-bench并声明了多个[[bench]]目标--bench index_scan_gate指定只运行索引扫描基准门。需要说明的是仓库内crates/bench/benches目录下是callgrind.rs、delete_table.rs、generic.rs、index.rs、special.rs、subscription.rs等 bench 目标而index_scan_gate的门控程序主体位于独立 cratecrates/index-scan-gate其包名为spacetimedb-index-scan-gate。整个门控流程由 crates/index-scan-gate/src/main.rs 承载。门控流程与判定阈值index_scan_gate的执行逻辑main.rs分为五步编译模块CompiledModule::compile(perf-test, CompilationMode::Release)以 Release 模式编译perf-test模块基准测试必须用优化构建否则测不到真实性能加载模块通过start_runtime()启动内存运行时并以IN_MEMORY_CONFIG内存存储配置加载模块基准不涉及持久化装载数据调用load_location_tablereducer灌入 120 万行采样与统计对REDUCERS列表中的四个 reducer每个先做5 次预热WARMUP_RUNS再采集31 次有效样本MEASURED_RUNS取样本中位数作为该工作负载的代表耗时阈值判定若任一 reducer 的中位数耗时 ≥MEDIAN_THRESHOLDDuration::from_micros(100)即100 微秒则整体失败并输出失败明细全部低于阈值则输出通过信息。四个被测 reducer 在门控程序中以数组形式列出main.rsconst REDUCERS: [str] [ test_index_scan_on_id, test_index_scan_on_chunk, test_index_scan_on_x_z_dimension, test_index_scan_on_x_z, ];判定结果示例门控程序运行后会按 reducer 对齐打印中位数耗时并给出最终结论。失败时输出形如test_index_scan_on_id median... ... index scan benchmark failed; median threshold is 100µs; failures: ...全部通过时输出index scan benchmark passed; all medians are below 100µs选用中位数而非平均值作为统计量是为了抵抗调度抖动等偶发噪声对结果的干扰而WARMUP_RUNS MEASURED_RUNS的两段式采样5 31 次则确保索引与缓存状态稳定后再进入测量。另外在非 MSVC 目标上程序还通过tikv_jemallocator将全局分配器切换为 jemallocmain.rs以接近生产环境的内存分配行为。实战指引如何查看、验证与扩展查看表结构与负载直接阅读 modules/perf-test/src/lib.rs 即可了解全部表定义、数据规模与四个工作负载的断言逻辑运行完整基准门在仓库根目录执行cargo bench -p spacetimedb-bench --bench index_scan_gate程序会自动完成模块编译、数据装载、采样与阈值判定观察单次 reducer 耗时四个 reducer 内部通过LogStopwatch向宿主日志输出Index scan on {id}、Index scan on {chunk}等计时信息可在运行时日志中检索这些标记关注判定结果以 100 微秒中位数阈值为界高于阈值意味着索引扫描性能出现明显回退CI 或本地开发中应视为回归信号。需要说明的适用前提该基准门以 Release 构建 内存配置IN_MEMORY_CONFIG运行其 100µs 阈值是针对此环境的回归门控设计不直接等同于对外承诺的端到端延迟指标若在调试构建或持久化配置下运行数值会明显不同不宜直接套用同一阈值。文章所述全部细节均基于当前仓库代码若仓库后续调整表结构、采样次数或阈值常量请以对应源码为准。小结perf-test模块与index_scan_gate基准门共同构成了一套自洽的索引性能回归测试体系模块层用 120 万行确定性数据与四个 reducer 定义负载门控层用5 次预热 31 次采样 中位数 100µs 阈值定义判定标准。阅读本文后你可以从 modules/perf-test/README.md 出发对照 lib.rs 理解每个工作负载的语义再对照 crates/index-scan-gate/src/main.rs 掌握整条基准链路的调度与判读方式从而在需要时自行运行、分析甚至扩展 SpacetimeDB 的索引扫描基准。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表