Repository navigation
Benchmarking
简体中文 | English
本页说明基准测试的方法、已有结果和后续计划。每项结果都会标明测试环境;尚未测量的项目会注明仍在计划中。
服务端每次更新称为一个 tick;通常每秒更新 20 次,每次的目标耗时不超过 50 毫秒。
当前服务端基准通过 skript-reflect 调用 System.nanoTime():操作返回后立即记录总耗时(wallNs),校验查询和结果计数不计入其中。另由每 tick 运行的观察器记录相邻 tick 起点的最大间隔(gapNs)。报告以六位小数显示毫秒,JSON 报告和 CI 历史保留原始的整数纳秒值。纳秒只是计时单位,不代表单次结果具有纳秒级准确度;调度、JVM 垃圾回收(GC)、其他插件、数据库执行和脚本恢复运行的时机都会影响读数。
超过 5000 行的读取会失败,不会保存部分结果。写入时,一条语句绑定的值超过 30000 个,插件会将其拆成多条语句。这些限制可以避免请求无限增大,但允许执行的大批量写入仍可能拉长 tick。
从列表变量执行 insert many 时,插件跨 tick 分段读取源变量。每段最多处理 4096 个步骤,每 64 步检查一次时间,运行约 2 毫秒后暂停。单个值的转换仍可能超过 2 毫秒,因此这只是控制主线程耗时的目标,并非硬上限。整次写入可能需要更多 tick 才完成;写入语句返回前,脚本应保持源变量不变。
测试分别观察了三个方面:
-
JVM 基准中的插入耗时。 一次 5000 行的
insert many,在 Intel Xeon Platinum 8573C 上用时 8.022 毫秒,在 AMD EPYC 7763 上用时 8.241 毫秒。对应的 CI 记录分别位于dev/bench/intel-xeon-platinum-8573c/data.js和dev/bench/amd-epyc-7763-64-core-processor/data.js。 - 历史服务端 tick。 在 AMD EPYC 9V45 的 CI 主机上,写入 5000 行总耗时 220 毫秒,期间最长的 tick 比 50 毫秒目标耗时多了 90 毫秒;写入 10000 行的对应数字为 320 毫秒和 100 毫秒。该历史测试中,读取 5000 行没有超出 50 毫秒;读取 10000 行则被拒绝。其他结果见下表。
- 旧版数据库驱动的行为。 另一项测试连续写入二十批、每批 5000 行,MySQL 用时 17.04 秒,PostgreSQL 和 MariaDB 各用时 1.99 秒。对其中一批,MySQL 服务端统计到 5038 条语句,其中 5000 条是插入;MariaDB 统计到 2 条,其中 1 条是多行插入。这些数据来自旧版 MySQL 插入路径,只对应当时的驱动设置。
当前服务端基准在 SQLite、MySQL、MariaDB 和 PostgreSQL 上对比 5000 行的 insert many 与手写 SQL。MySQL 默认使用参数化多行 INSERT,每条语句最多绑定 30000 个值;其他 JDBC 路径使用预处理批次。手写 SQL 则是一条直接写入值的多行 INSERT,其字符串构造不计入耗时。两者的耗时差异还包含插件读取源变量、参数绑定和写入形式的成本,不能全部归因于 ORM 映射。MongoDB 只执行插件读写用例,并明确跳过 SQL 对照。
手写 Skript 循环如果只写入普通变量,测到的是内存操作,不能用来比较数据库写入性能。
旧版脚本用 Skript 的 now 计时(约每 10 毫秒变化一次),测得 5000 行写入在开发机上的 tick 超出约 10 毫秒,在 CI 主机上超出 90 毫秒。基准工作流会在每条结果旁记录主机信息。
tick 超出量是观察到的最大 tick 起点间隔减去 50 毫秒,负数按零计算。这个指标能显示 tick 延迟,却不能直接给出写入在主线程上用了多久;在一个正常 tick 内完成的工作也不会显示为超出。表中的“热读”和“热写”只是指在其他用例之后再执行一次 5000 行读取或写入,不表示缓存一定已预热。
tick 作业分别对下表的五种数据库实现(后端)运行 100、500、1000、2500、5000 和 10000 行的插件写入与读取,并在最后再次执行 5000 行写入和读取。支持 SQL 的数据库另有 5000 行的插件与原始 SQL 对照。输入行只填充 id 列,这样比较集中在行的传输和存储,而不是某种六列数据组合。
| 后端 | 插件曲线与热用例 | 原始 SQL 对照 | 新版 CI 结果 |
|---|---|---|---|
| SQLite | 已配置 | 已配置 | Run #4,AMD EPYC 7763,SQLite |
| MySQL | 已配置 | 已配置 |
Run #4,Intel Xeon 6973P-C,mysql:8.4
|
| MariaDB | 已配置 | 已配置 |
Run #4,AMD EPYC 7763,mariadb:11.4
|
| PostgreSQL | 已配置 | 已配置 |
Run #4,AMD EPYC 7763,postgres:17
|
| MongoDB | 已配置 | 跳过:不支持 SQL |
Run #4,Intel Xeon 6973P-C,mongo:8
|
SQL 对照分别记录 pluginwrite、pluginread、rawwrite 和 rawread 日志;MongoDB 则在插件读写后明确记录 SKRIPTORM_BENCH=SKIP。
Run #4 于 2026-10-03 在提交 6476c9b 上完整通过,环境为 Paper 26.2 build 124、Skript 2.16.2、
Java 25.0.4.1、Linux 6.17.0-1022-azure 和 4 个可见 CPU。下表是该次运行的 5000 行结果。
为便于阅读,wallNs 在这里换算成毫秒;上传的 JSON 和 CI 历史仍保留纳秒整数。每个数
只代表共享 GitHub runner 上的一次运行,不能据此给数据库做普遍排名。
| 后端 | 写入总耗时 | 写入最大 tick 间隔 | 读取总耗时 | 读取最大 tick 间隔 |
|---|---|---|---|---|
| SQLite | 735.828866 ms | 65.767643 ms | 103.186998 ms | 103.066121 ms |
| MySQL | 462.517747 ms | 50.395059 ms | 78.302884 ms | 78.218777 ms |
| MariaDB | 685.122311 ms | 66.066614 ms | 95.789059 ms | 95.813175 ms |
| PostgreSQL | 585.103286 ms | 66.926809 ms | 94.172914 ms | 94.082624 ms |
| MongoDB | 717.433104 ms | 50.968606 ms | 78.761947 ms | 78.579400 ms |
同一轮曲线中的 10000 行写入和重复执行的 5000 行热写入:
| 后端 | 10000 行写入耗时 | 最大 tick 间隔 | 5000 行热写入耗时 | 热写入最大 tick 间隔 |
|---|---|---|---|---|
| SQLite | 1501.908676 ms | 94.670715 ms | 779.342861 ms | 50.628368 ms |
| MySQL | 1398.534310 ms | 51.944607 ms | 684.881529 ms | 50.515912 ms |
| MariaDB | 1149.905662 ms | 101.135769 ms | 675.078706 ms | 50.746686 ms |
| PostgreSQL | 1255.801227 ms | 95.786030 ms | 623.383051 ms | 50.885682 ms |
| MongoDB | 1196.824500 ms | 53.798503 ms | 683.646486 ms | 50.583169 ms |
以下对照与上面的曲线分开运行,四列都是处理 5000 行的总耗时。插件写入包含跨 tick 读取输入变量的时间,原始 SQL 的字符串则在计时前就已构造完成。两者走的是不同的 写入路径,因此耗时比值不能直接当成 ORM 自身的开销。
| 后端 | 插件写入 | 原始 SQL 写入 | 插件读取 | 原始 SQL 读取 |
|---|---|---|---|---|
| SQLite | 727.519482 ms | 17.148948 ms | 112.968835 ms | 104.414372 ms |
| MySQL | 586.972838 ms | 46.793973 ms | 76.245264 ms | 91.635107 ms |
| MariaDB | 628.867037 ms | 74.916475 ms | 91.608649 ms | 89.937610 ms |
| PostgreSQL | 778.881589 ms | 61.349564 ms | 99.224999 ms | 102.880893 ms |
| MongoDB | 685.494020 ms | 不适用 | 78.850687 ms | 不适用 |
100、500、1000、2500 和 10000 行用例、5000 行热用例、影响行数或保存行数校验,以及
预期中的 10000 行读取拒绝也都已完成。gap 不是插件 CPU 耗时,而是观察到的最大 tick
起点间隔,其中包含调度和共享主机活动。本次运行早于提交 21898af 的 MySQL
max_allowed_packet 修复;要测量当前 MySQL 路径,需要在该修复推送后重新运行。
下表使用 Skript 的 now 计时(约每 10 毫秒变化一次),部分写入耗时还包含后续计数查询。这组旧数据仅供参考,不能与新版 wallNs、gapNs 直接比较。
以下历史结果来自提交 51aa760 的 Benchmarks #2 工作流(Tick benchmark 作业),保存在 dev/bench/amd-epyc-9v45-96-core-processor/tick/data.js:
| 操作 | 行数 | 总耗时 | tick 超出 |
|---|---|---|---|
| 读 | 100 | 60 ms | 10 ms |
| 读 | 500 | 60 ms | — |
| 读 | 1000 | 60 ms | — |
| 读 | 2500 | 100 ms | — |
| 读 | 5000 | 90 ms | 0 ms |
| 读 | 10000 | 60 ms | 0 ms |
| 热读 | 5000 | 70 ms | 0 ms |
| 写 | 100 | 90 ms | 10 ms |
| 写 | 500 | 30 ms | 20 ms |
| 写 | 1000 | 80 ms | 30 ms |
| 写 | 2500 | 70 ms | 30 ms |
| 写 | 5000 | 220 ms | 90 ms |
| 写 | 10000 | 320 ms | 100 ms |
| 热写 | 5000 | 190 ms | 0 ms |
“写入 500 行”的总耗时为 30 毫秒,tick 却超出 20 毫秒,说明共享主机上的其他活动可能影响了单次采样。这类耗时数据用于观察趋势,工作流不会据此判定失败。
下文区分现有测量结果与后续计划,并说明各项指标适合在哪一层采集。
为了分析耗时来源,基准测试分为三个层次:
- A:不启动 Paper 的 JVM 基准。 JMH 是用于反复运行并测量 Java 代码的基准测试工具,用例放在独立模块中。纯逻辑可以不连接数据库单独测量;现有插入用例使用内存 H2,因此结果同时包含数据库执行和插件代码的成本。
- B:真实数据库。 测量数据库往返、语句解析与执行、连接池及事务提交等成本。通过 Testcontainers 启动各后端实例,沿用集成测试的配置。
- C:Paper 服务端。 观察脚本执行期间的耗时和 tick 间隔,包括主线程读取源变量、保存查询结果等工作。基准脚本运行在一次性服务端上;内存占用与 MSPT(每个 tick 的耗时,单位毫秒)的分布仍需另外测量。
分层测量有助于定位问题:值转换属于 A,驱动批处理属于 B,主线程上的 Skript 变量读写属于 C。
触发器的总耗时包含数据库操作、主线程上的变量处理,以及等待脚本恢复运行的时间,不能单独用于判断服务端是否卡顿。最长 tick 间隔可以显示延迟,但不能准确拆分这些成本。
Paper 基准脚本目前记录 wallNs、gapNs、tick 数,以及影响行数或已保存行数。报告从 gapNs 推算 tick 超出量,并校验行数。这套测量尚不能单独得出主线程 CPU 耗时、数据库耗时、内存分配量或 MSPT 百分位。
下表是各测量层的目标,不代表每个现有服务端用例都已输出这些指标。
| 指标 | 拟记录内容 | 测量层 |
|---|---|---|
| 主线程耗时 | 操作在服务端主线程上占用的时间 | C |
| 后台耗时 | 数据库工作在后台线程执行的时间 | B、C |
| 触发器延迟 | 从语句开始到脚本恢复运行的总耗时 | B、C |
| 吞吐量 | 每秒处理的行数和值数 | A、B、C |
| 提交给驱动的语句数与行数 | 在 JDBC 边界统计调用与行数 | B |
| 数据库收到的语句数与写入行数 | 通过数据库计数器统计 | B |
| 每次操作的内存分配量 | 每次操作分配的字节数 | A、B |
| 峰值堆占用 | 操作期间持续采样得到的最大堆占用 | B、C |
| 回收后堆占用 | 用例结束并执行垃圾回收后的堆占用 | B、C |
| MSPT 分布(p50、p99、最大值) | 每个 tick 的耗时:中位数(p50)、99% 的 tick 不超过的耗时(p99)和最大值 | C |
要把触发器总耗时拆成数据库耗时与主线程耗时,还需要额外的测量手段;目前的 wallNs 包含这两部分,以及脚本等待恢复运行的时间。
在共享 CI 主机上,相同测试的耗时曾相差 10% 至 20%;语句数则比较稳定。因此,CI 可以直接检查计数,耗时只作报告,不据此判定构建失败。
前述旧版二十批写入测试对每个数据库使用相同脚本和表定义。MySQL 与 MariaDB 共用 SQL 方言,但驱动发送批次的方式不同:当时在 JDBC 层,插件只提交一个批次;服务端计数却显示,Connector/J 将其中 5000 行逐行发送。单看耗时无法找到这个原因。当前 MySQL 路径改为参数化多行插入,下述旧计数不能作为新版的预期值。
基准模块还会统计 JDBC 层的调用。使用内存 H2 和通用 JDBC 路径时,一次 5000 行、六列的插入提交了一个批次。插件准确报告了 5000 个影响行,调用结束后表中也有 5000 行。
ServerSideCountIntegrationTest 测量真实数据库收到的语句。对 MySQL 系数据库,它在插件调用前后读取 Questions,扣除探针自身的开销,同时读取 Innodb_rows_inserted、Com_insert 和 Com_stmt_execute。这些计数覆盖整个服务器,因此测试会先检查是否有其他连接;如有,就只报告数字,不据此断言结果。驱动版本和设置会改变计数,所以结果会连同环境信息一起记录,不作为 CI 通过条件。
在一次 5000 行测试中,MySQL 8.0.46 和 8.4.11 各记录到 5038 条语句,其中 5000 条是插入语句(comInsert=5000、comStmtExecute=0)。MariaDB 11.4.13 记录到 2 条语句,其中一条是多行插入(comInsert=1、comStmtExecute=1)。测试账号无法从 MariaDB 取得插入行数。PostgreSQL 16.15 为目标表统计到 5000 行、为整个数据库统计到 5057 行,但现有计数器无法提供语句数;等待异步统计视图更新用了 814 毫秒。各后端都由插件报告了准确的 5000 个影响行,表中也确实增加了 5000 行。
这些都是每个后端在一台主机上的单次结果,不代表区间。日志记录了环境信息:os=Linux 6.17.0-1022-azure arch=amd64 cores=4 java=25.0.4.1 commit=81526798498f5ad30068fcb3299f2085062f868d。即使在同一次工作流中,MySQL 和 MariaDB 的作业运行于 Intel Xeon Platinum 8370C,PostgreSQL 的作业则运行于 AMD EPYC 7763。比较耗时时应看各作业实际使用的主机。
测试读取 PostgreSQL 的 pg_stat_user_tables.n_tup_ins 和 pg_stat_database.tup_inserted,并等待这些异步统计数据更新。它们统计行数,无法推算发送了多少条语句。要获得语句数,需要配置 pg_stat_statements 并重启服务器,或在协议层另行计数。
JMH、Testcontainers 和基准历史记录已用于现有测试。其余工具列为补充指标的可选方案。
| 用途 | 工具 | 作用 |
|---|---|---|
| 微基准(A) | JMH | 正式计时前先运行代码,让 JVM 完成常见优化;用独立进程测量,并避免 JVM 把结果未被使用的被测代码删掉 |
| 提交给驱动的语句数与行数(B) |
datasource-proxy 或计数驱动 |
统计 JDBC 调用;单次批量调用实际包含多少行,还需另行记录 |
| 数据库收到的语句数与往返数(B) | 数据库会话计数器或网络抓包 | 观察驱动批处理后的实际行为;结果受驱动版本和连接参数影响 |
| tick 采样的百分位(C) | HdrHistogram | 对多次 tick 采样计算分位数 |
| 真实数据库(B) | Testcontainers | 启动测试数据库;复用实例可减少重复启动时间 |
| 历史与对比 | benchmark-action/github-action-benchmark |
按 CPU 和后端保存结果;耗时变化不作为构建失败条件 |
| 内存分配与停顿 | JMH -prof gc、JFR、async-profiler |
分析内存分配,以及 JVM 为垃圾回收等操作暂停线程的时间 |
| MSPT(C) | Paper 的 tick 采样 | 获取服务端自身记录的 tick 耗时 |
基准模块直接声明 JMH 及其注解处理器,因为现成的 Gradle 插件不支持本项目使用的 Gradle 版本。运行 ./gradlew :benchmarks:jmh,结果会写入 build/benchmarks/results.json。可用 -Pbenchmarks.filter=<正则> 选择用例,或用 -Pbenchmarks.forks=1 缩短本地测试时间。JMH 的注解处理器生成 Java 代码,因此基准类使用 Java。
编译、单元测试、ktlint 和集成测试编译已纳入 CI。
tick 基准作业会因结果行数不符或脚本报告错误而失败,不会因耗时变化而失败。后续如增加其他稳定且可复现的检查指标,可以与受共享 CI 主机影响较大的耗时数据分开处理。
基准结果只与硬件环境相近的运行比较。
-
CI 历史由基准测试 action 保存在
gh-pages分支的dev/bench/<slug>/下,按 CPU 型号分组。ubuntu-latest可能分配不同处理器:同一提交的两次运行分别落在 AMD EPYC 9V74 和 Intel Xeon Platinum 8573C 上,插入用例耗时相差 1.32 倍。每个作业都会单独记录 CPU 型号;新版 tick 结果还按后端分开,存于dev/bench/<slug>/tick-ns/<backend>/。 -
gh-pages分支用于存储基准数据,本项目没有从中发布图表站点。JMH 原始结果位于dev/bench/<slug>/data.js;tick 结果位于dev/bench/<slug>/tick-ns/<backend>/。 - **
benchmarks/baseline.json**仅供本地参考,不作为 CI 阈值。benchmarks/baseline.environment.txt记录生成基线时使用的机器。
结果还会记录 JDK、操作系统、CPU、容器镜像、数据库驱动、Paper 和 Skript 的版本,以及提交和工作区状态,以便区分代码变动与环境变动。
-
阶段 0:微基准。 为值转换、结果存储、批次输入及
insert many发送的语句数建立 JMH 用例,保存 JSON 结果和参考基线。 - 阶段 1:数据库矩阵。 为各后端、各类语句和不同表结构加入语句计数。
-
阶段 2:服务端 tick(已配置五种后端)。
./gradlew serverBenchmark启动装有 Skript、skript-reflect 和本插件的 Paper 服务端,运行server-benchmark/skript中的脚本。脚本通过SKRIPTORM_BENCH输出以纳秒为单位的耗时及最长 tick 间隔;如果写入行数不符或脚本输出FAIL,任务就会失败。CI 另行检查一次读取最多 5000 行、每条写入语句最多绑定 30000 个值的限制。夜间和按需运行的tick矩阵将结果存入dev/bench/<slug>/tick-ns/<backend>/。详见读取行和写入行。脚本计时器无法提供 MSPT 百分位。 - 阶段 3:压力与故障测试(计划中)。 覆盖并发、取消、执行途中断开数据库、查询期间重载插件、超限结果与批次,以及连接池耗尽。
- 阶段 4:完善 CI 报告(计划中)。 增加上述尚缺的指标,将稳定的正确性检查与耗时趋势分开处理。
- 在线玩家。 空闲服务端上的结果无法预测大量玩家和实体活动时的表现。
- 长时间运行。 现有用例仅持续数秒,无法发现几天后才出现的泄漏。
- 未纳入测试的驱动。 通用 JDBC 连接可使用本仓库未测试的驱动;MySQL 的结果不能代表这些驱动。
- PostgreSQL 语句数。 当前统计信息能提供行数和事务数,不能统计单条操作发送的语句数。
- 跨机器的绝对耗时。 耗时必须结合测试环境解读。
skript-orm
参考
实用指南
skript-orm (English)
Reference
- Connections
- Tables
- Raw statements
- Writing rows
- Reading rows
- Updating and deleting
- Affected rows
- Errors and waiting
- Transactions
- Types
Practical guides
Wiki 由仓库中的 README 和 docs/ 自动生成。修改文档请到仓库提交,直接编辑 Wiki 的内容会在下次同步时被覆盖。
This wiki is generated from the README files and docs/ in the repository. Please submit changes there; direct wiki edits are overwritten on the next sync.