Repository navigation
Benchmarking
简体中文 | English
本页说明基准测试的方法、已有结果和后续计划。前面的数值表保留提交 6476c9b 的 Run #4 历史结果;这次运行早于局部变量后台处理和主线程共享转换队列,不能用于说明这些改动的性能。后面的新版服务端基准章节说明新流程,并给出 Run #7 的五种数据库实测结果。
服务端每次更新称为一个 tick;通常每秒更新 20 次,每次的目标耗时不超过 50 毫秒。
当前服务端基准通过 skript-reflect 调用 System.nanoTime():操作返回后立即记录总耗时(wallNs),校验查询和结果计数不计入其中。每 tick 运行的观察器另行记录相邻两次执行的最大间隔(gapNs)。观察窗口从操作开始持续到返回后再等待一个 tick,因此与总耗时的结束时刻不同;这个间隔也不是直接从服务端内部测得的单个 tick 耗时。
本页和自动生成的报告以六位小数显示毫秒,完整保留纳秒计时值;JSON 报告和 CI 历史保存原始的整数纳秒值。纳秒只是计时单位,不代表单次结果具有纳秒级准确度;调度、JVM 垃圾回收(GC)、其他插件、数据库执行和脚本恢复运行的时机都会影响读数。
超过 5000 行的读取会失败,不会保存部分结果。写入时,一条语句绑定的值超过 30000 个,插件会将其拆成多条语句。这些限制可以避免请求无限增大,但允许执行的大批量写入仍可能拉长 tick。
从列表变量执行 insert many 时,插件可以在后台读取和检查当前脚本独占的局部源变量。全局源变量仍在主线程分段读取,每段最多 4096 个处理步骤,目标耗时约 2 毫秒。来自脚本共享默认变量的值改用主线程处理。需要服务器 API 的转换共用一个每 tick 目标预算为 2 毫秒的队列;队列无法中断单个转换,因此这不是硬上限。全局源变量在写入完成前应保持不变,详见写入行。
局部查询结果中的普通值也可以在当前脚本暂停期间保存到后台独占的变量容器中。需要服务器 API 的对象由主线程构建,完成后恢复局部上下文并继续脚本。全局结果变量仍在主线程写入。转换器分别声明读、写所需的线程,未知转换器默认使用主线程。新版测试同时观察移到后台的工作量和转换调度增加的等待时间。
测试分别观察了三个方面:
-
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。 -
历史服务端读写。 使用
System.nanoTime()分别记录五种数据库的操作总耗时,以及 tick 观察器的最大执行间隔。该次运行的结果见下表。 - 旧版数据库驱动的行为。 另一项测试连续写入二十批、每批 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 循环如果只写入普通变量,测到的是内存操作,不能用来比较数据库写入性能。
tick 超出量是观察到的最大 tick 间隔减去 50 毫秒,负数按零计算。这个指标能显示 tick 延迟,却不能直接给出写入在主线程上用了多久;在一个正常 tick 内完成的工作也不会显示为超出。表中的“热读”和“热写”只是指在其他用例之后再执行一次 5000 行读取或写入,不表示缓存一定已预热。
Run #4 对下表的五种数据库实现(后端)运行了 100、500、1000、2500、5000 和 10000 行的插件写入与读取,并在最后再次执行 5000 行写入和读取。支持 SQL 的数据库另有 5000 行的插件与原始 SQL 对照。当时的输入行只填充 id 列,比较集中在行的传输和存储,而不是某种六列数据组合。
???? bench:: ??????? Skript ??????????????????????????????????????????? ORM ????????? Skript ???????????
先确认每种数据库跑了哪些用例,以及结果来自什么环境。这张表用于检查测试覆盖范围,不比较耗时。
-
后端:被测数据库;SQLite 使用插件的
"JDBC"连接类型。 - 插件多行数与重复测试:是否运行前述六种行数,以及最后重复执行的 5000 行读写。
-
原始 SQL 对照:是否用
execute query和execute update执行手写 SQL,与插件语句对照。MongoDB 不支持 SQL,因此跳过这组对照;它的插件读写仍会测试。 - 历史运行与环境:该次工作流链接、运行主机的 CPU 型号,以及数据库容器镜像或数据库名称。
| 后端 | 插件多行数与重复测试 | 原始 SQL 对照 | 历史运行与环境 |
|---|---|---|---|
| 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 行结果。
以下实测表以毫秒显示,保留六位小数:78.302884 ms 对应 78302884 ns,没有取整到毫秒。
正常情况下,tick 观察器每隔约 50 ms 执行一次,因此采用纳秒计时后,测得的间隔仍可能是几十毫秒;
计时精度与操作本身花了多久是两回事。每个数只代表共享 GitHub runner 上的一次运行,
不能据此给数据库做普遍排名。
各数据库都处理 5000 行,将任务规模固定下来,便于观察本次运行中读写等待时间和 tick 延迟的差异。总耗时与 tick 间隔放在一起,可以区分“整次操作等待较久”和“期间某次服务端更新迟到”;数据库操作即使跨多个 tick 完成,也不意味着这些 tick 全都被阻塞。
- 后端:执行这一行测试的数据库,环境见上一表。
- 写入总耗时、读取总耗时:从开始执行插件语句到返回的时间,包含源变量读取或结果存储、数据库执行,以及等待脚本恢复运行的时间;后续校验不计入。
- 写入最大 tick 间隔、读取最大 tick 间隔:在对应操作的观察窗口内,tick 观察器相邻两次执行之间的最大间隔。窗口持续到操作返回后一个 tick;正常间隔约为 50 毫秒,超过它表示观察到更新延迟,这不是插件独占主线程的时间。
| 后端 | 写入总耗时 | 写入最大 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 行写入放在一起,检查更大的任务是否伴随更长的 tick 间隔。重复的 5000 行应与上一表中首次执行的 5000 行对照,才能观察重复运行时的变化;不能将不同规模的两列直接相减,作为缓存预热的收益。
- 后端:执行写入的数据库,与上一表使用同一次运行。
- 10000 行写入耗时:首次执行 10000 行写入的总耗时,计时范围与上一表相同。
- 最大 tick 间隔:10000 行写入开始到返回后一个 tick 的观察窗口中,tick 观察器相邻两次执行之间的最大间隔。
- 5000 行热写入耗时、热写入最大 tick 间隔:六种行数用例结束后,再执行一次 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 的完整调用时间,能看出这两条读写路径在本次环境中的耗时差异。写入与读取分列,是因为输入变量处理、数据库执行和结果存储的成本不同,不能合成一个“ORM 性能”数字。
- 后端:两条读写路径使用的数据库。
-
插件写入、插件读取:分别执行
insert many与select many的总耗时,包含插件在主线程上的变量处理与等待数据库返回的时间。 -
原始 SQL 写入、原始 SQL 读取:分别执行
execute update和execute query的总耗时。写入 SQL 在计时前构造完毕;读取仍包含结果存储。 - 不适用(不支持 SQL):MongoDB 不接受 SQL,不能运行这组原始 SQL 用例。这不表示它没有读写能力,也不表示耗时为零;其插件读写照常列出。
| 后端 | 插件写入 | 原始 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 | 不适用(不支持 SQL) | 78.850687 ms | 不适用(不支持 SQL) |
下面展开同一次 Run #4 的 MariaDB 结果,环境为 AMD EPYC 7763、mariadb:11.4,其余服务端版本与前文一致。数据取自原始纳秒记录,没有另外启动一轮测试。
固定数据库和运行环境,再改变行数,可以观察任务变大时总耗时和 tick 延迟如何变化。将首次与重复的 5000 行放在一起,也能观察相同任务在不同运行顺序下的表现;每项只有一次采样,不能据此判断耗时必然随行数线性增长。
- 操作:插件写入或读取;“重复”指其他行数用例结束后再次执行。
- 请求行数:脚本要求处理的行数,不一定是实际返回的行数。
- 总耗时:从开始执行语句到返回的时间,包含变量处理、数据库执行和等待恢复运行,后续校验不计入。
- 最大 tick 间隔:操作开始到返回后一个 tick 的观察窗口中,tick 观察器相邻两次执行之间的最大间隔,正常约为 50 毫秒。
- tick 超出量:最大 tick 间隔减去 50 毫秒,负数按零计算,便于直接观察相对正常更新周期的延迟。
- 结果:操作是否成功。10000 行读取超过 5000 行上限,记录的是查询被拒绝时的耗时,不是成功读取 10000 行的耗时。
| 操作 | 请求行数 | 总耗时 | 最大 tick 间隔 | tick 超出量 | 结果 |
|---|---|---|---|---|---|
| 写入 | 100 | 183.019540 ms | 77.813136 ms | 27.813136 ms | 成功 |
| 读取 | 100 | 58.978086 ms | 54.904413 ms | 4.904413 ms | 成功 |
| 写入 | 500 | 125.585622 ms | 50.931863 ms | 0.931863 ms | 成功 |
| 读取 | 500 | 59.719091 ms | 59.571377 ms | 9.571377 ms | 成功 |
| 写入 | 1000 | 122.445042 ms | 50.273827 ms | 0.273827 ms | 成功 |
| 读取 | 1000 | 59.743000 ms | 59.761403 ms | 9.761403 ms | 成功 |
| 写入 | 2500 | 139.899172 ms | 64.122445 ms | 14.122445 ms | 成功 |
| 读取 | 2500 | 79.123322 ms | 79.081254 ms | 29.081254 ms | 成功 |
| 写入 | 5000 | 685.122311 ms | 66.066614 ms | 16.066614 ms | 成功 |
| 读取 | 5000 | 95.789059 ms | 95.813175 ms | 45.813175 ms | 成功 |
| 写入 | 10000 | 1149.905662 ms | 101.135769 ms | 51.135769 ms | 成功 |
| 读取 | 10000 | 59.551810 ms | 59.517446 ms | 9.517446 ms | 被拒绝:超过 5000 行读取上限 |
| 写入(重复) | 5000 | 675.078706 ms | 50.746686 ms | 0.746686 ms | 成功 |
| 读取(重复) | 5000 | 82.868479 ms | 82.686620 ms | 32.686620 ms | 成功 |
本次 10000 行写入的总耗时和最大 tick 间隔都高于 5000 行写入;重复的 5000 行写入总耗时相近,但最大 tick 间隔更短。100 行写入却比 500 行慢,可见这次结果并未随行数单调增加。运行顺序、启动阶段和共享主机活动都可能影响单次读数,但仅凭这张表无法确认原因,也不能只按行数推算耗时。
各用例的影响行数或保存行数已校验,10000 行读取也按预期被拒绝。gap 不是插件 CPU 耗时,而是 tick
观察器的最大执行间隔,其中包含调度和共享主机活动。本次运行早于提交 21898af 的 MySQL
max_allowed_packet 修复;要测量当前 MySQL 路径,需要在该修复推送后重新运行。
下文区分现有测量结果与后续计划,并说明各项指标适合在哪一层采集。
为了分析耗时来源,基准测试分为三个层次:
- A:不启动 Paper 的 JVM 基准。 JMH 是用于反复运行并测量 Java 代码的基准测试工具,用例放在独立模块中。纯逻辑可以不连接数据库单独测量;现有插入用例使用内存 H2,因此结果同时包含数据库执行和插件代码的成本。
- B:真实数据库。 测量数据库往返、语句解析与执行、连接池及事务提交等成本。通过 Testcontainers 启动各后端实例,沿用集成测试的配置。
- C:Paper 服务端。 观察脚本执行期间的耗时和 tick 间隔,包括主线程读取源变量、保存查询结果等工作。基准脚本运行在一次性服务端上;内存占用与 MSPT(每个 tick 的耗时,单位毫秒)的分布仍需另外测量。
分层测量有助于定位问题:值转换属于 A,驱动批处理属于 B,主线程上的 Skript 变量读写属于 C。
触发器的总耗时包含数据库操作、主线程上的变量处理,以及等待脚本恢复运行的时间,不能单独用于判断服务端是否卡顿。最长 tick 间隔可以显示延迟,但不能准确拆分这些成本。
新版脚本保留只填 id 的行数曲线和 SQL 对照,便于延续已有测试;新增的 SAMPLE 用例用于观察局部变量和转换流程的变化。生成输入、校验结果、计数查询和清理都在语句计时窗口之外执行。每次写入使用新的主键范围,查询也只读取本次范围。
测试用的 bench:: 全局变量不写入 Skript 的变量文件,避免反复创建和删除临时数据导致保存队列积压。这些结果衡量内存中的变量处理与 ORM 数据库操作,不包含 Skript 自身的变量持久化成本。
下表说明测试内容,不是性能成绩。用例指出数据和变量作用域;行数是请求处理的数据量;比较目的说明为什么把这些结果放在一起。五种数据库都已配置插件读写用例;MongoDB 不执行 SQL,因此跳过原始 SQL 对照,不能把“不适用”写成零耗时。
| 用例 | 行数 | 比较目的 |
|---|---|---|
| 六个数值列全部填值:局部输入、局部结果,与全局输入、全局结果对照 | 100、500、1000、2500、5000 | 固定数据库工作量,观察变量作用域对准备输入和保存结果的影响 |
| 局部输入、全局结果;全局输入、局部结果 | 5000 | 分别观察输入处理和结果保存的成本 |
| 替换已有结果:局部与全局对照 | 5000 | 把清除旧结果的工作计入耗时,并校验旧行和旧列确实被移除 |
| 数值列加物品(ItemStack)和位置(Location):局部与全局对照 | 1000 | 观察普通值与主线程对象转换混合时的成本 |
| 带名称、描述的物品加位置,替换已有局部结果 | 1000 | 同时覆盖对象转换与结果替换,校验物品属性和位置 |
原有只填 id 的曲线及重复用例 |
100~10000,随后重复 5000 | 保留原有工作量;10000 行读取应被拒绝,并且不留下部分结果 |
每个新增用例先预热三次,再测量十次。预热记录使用负数样本编号,不参与汇总。报告按操作、数据内容、行数、输入和结果作用域、是否替换旧结果分组,展示中位数、p95 和最大值。中位数表示样本中间的表现;p95 表示估计有 95% 的样本不超过的耗时;最大值是本次最慢的样本。只有十个样本时,p95 等于最大值,对罕见延迟的判断能力有限。这是操作耗时的分位数,不是 MSPT 分位数。原始样本(含预热)和汇总保存在 build/benchmarks/pipeline-results.json;tick-results.json 保存供 CI 历史使用的中位数点。
局部与全局用例在同一次服务端运行中交替执行,适合在同一数据库、同一种数据内容内比较。不同 runner CPU 或不同物品内容仍需分别解读。下方结果来自已完整运行并通过校验的新版矩阵;前面的表仍保留为历史记录。
Run #7 于 2026 年 10 月 4 日测试提交 76dcc7e。五种数据库、JMH 和历史发布全部通过;每个后端都产生了 442 条原始样本和 34 组汇总。环境使用 Paper 26.2 build 124、Skript 2.16.2、skript-reflect 2.6.3 和 Java 25。不同作业的 CPU 不同,应该比较同一行内的局部与全局变量,不应跨行给数据库排名。运行页面的 tick-report-* 附件包含 pipeline-results.json、tick-results.json 和完整环境记录。
第一张表固定读取 5000 行,每行的六个数值列都有值。后端/CPU 标明作业环境;局部/全局总耗时是从读取语句开始到脚本恢复的耗时中位数;局部/全局主线程是已计时的主线程区间累计耗时中位数。所有数值均为毫秒,保留六位小数。把两类时间放在一起,可以看到工作移到了哪里:局部结果在主线程上的已计时区间变得很短,但总耗时仍包含后台处理和等待下一个 tick。这张表没有比较新旧插件版本,也没有测量语句的全部零散开销。
| 后端/CPU | 局部总耗时 | 全局总耗时 | 局部主线程 | 全局主线程 |
|---|---|---|---|---|
| SQLite / EPYC 9V74 | 49.925225 ms | 65.414039 ms | 0.018713 ms | 15.483043 ms |
| MySQL / EPYC 7763 | 49.923762 ms | 86.510248 ms | 0.015720 ms | 36.381696 ms |
| MariaDB / EPYC 9V45 | 50.005949 ms | 66.299079 ms | 0.017637 ms | 15.888968 ms |
| PostgreSQL / EPYC 9V74 | 49.942947 ms | 69.211086 ms | 0.018167 ms | 19.242586 ms |
| MongoDB / XEON 8573C | 49.950872 ms | 71.939851 ms | 0.014090 ms | 21.958078 ms |
第二张表比较 1000 行混合数据的读取,每行包含四个数字、一个物品和一个位置。局部/全局最忙 tick先取每次操作的 mainTickMaxNs,再取十次结果的中位数,以毫秒显示;它只是这次操作在某个 tick 中记录到的工作量,不是整个服务端的 MSPT。局部总耗时是读取语句到脚本恢复的耗时中位数。将两者一起看,可以看到分批转换的取舍:每个 tick 承担的工作更少,整个操作却需要等待更多 tick。全局结果最后赋值时仍可能占用几十毫秒,共用的转换预算不包含这一步。
| 后端 | 局部最忙 tick | 全局最忙 tick | 局部总耗时 |
|---|---|---|---|
| SQLite | 1.947452 ms | 64.061272 ms | 499.969523 ms |
| MySQL | 1.960147 ms | 126.033681 ms | 699.948048 ms |
| MariaDB | 1.939399 ms | 45.820466 ms | 449.839210 ms |
| PostgreSQL | 1.948226 ms | 84.902243 ms | 599.943042 ms |
| MongoDB | 1.956798 ms | 65.141387 ms | 499.969330 ms |
第三张表固定使用 MariaDB(AMD EPYC 9V45,mariadb:11.4)和六列全部填值的数据。行数是请求处理的数据量;局部/全局写入和局部/全局读取均为语句开始到脚本恢复的耗时中位数,单位毫秒。它用于观察任务变大后,变量作用域如何影响等待时间。生成数据和校验不计入其中,全局写入包含跨 tick 分片读取输入。这些用例与历史表中只填 id 的数据不同,不能直接相减作为优化收益。
| 行数 | 局部写入 | 全局写入 | 局部读取 | 全局读取 |
|---|---|---|---|---|
| 100 | 50.002091 ms | 99.999130 ms | 49.948891 ms | 50.561922 ms |
| 500 | 50.003658 ms | 200.041385 ms | 49.969411 ms | 53.469929 ms |
| 1000 | 50.029800 ms | 400.035662 ms | 49.985885 ms | 55.762635 ms |
| 2500 | 50.027120 ms | 1000.028616 ms | 49.943913 ms | 58.051739 ms |
| 5000 | 50.068590 ms | 1845.764052 ms | 50.005949 ms | 66.299079 ms |
第一张表的后端名称链接到已发布的中位数记录。原始附件还保留三次预热、十次正式测量和中位数/p95/最大值汇总;十个正式样本使用最近秩法计算 p95 时,p95 等于最大值。原有 10000 行读取用例用于检查拒绝行为,没有成功读取的吞吐量;MongoDB 使用文档命令,因此没有原始 SQL 对照。
脚本继续用 System.nanoTime() 记录完整语句和 tick 观察器间隔。插件另外提供只在基准服务端开启的阶段计时。语句返回后,脚本立即读取对应操作的记录,避免后续数据库调用将其覆盖。字段是 JSON 和日志中的原始键名;含义说明实际测量范围。
| 字段 | 含义 |
|---|---|
wallNs |
从语句开始到脚本恢复运行,后续校验不计入 |
gapNs |
从语句开始到恢复后一个 tick,观察器相邻两次执行的最大间隔 |
mainNs |
本次操作中设有计时记录的主线程处理段的实际经过时间;不是 CPU 时间,也不包含服务端的全部工作 |
mainTickMaxNs |
本次操作在同一个 tick 内这些处理段的累计耗时最大值;不是所有请求合计的主线程耗时 |
prepareMainNs、prepareAsyncNs
|
分别在主线程和后台记录的输入准备耗时 |
conversionMainNs |
主线程值转换耗时 |
resultMainNs、resultAsyncNs
|
分别在主线程和后台记录的结果保存耗时 |
executionNs |
后台执行阶段,包含局部输入准备、写入值转换、数据库调用和游标结果读取;不是纯数据库耗时 |
queueWaitNs |
等待主线程转换队列的时间 |
syncConversions |
已记录的主线程值转换次数 |
largestConversionNs |
最慢一次主线程值转换的耗时,用于发现单个值超过队列预算的情况 |
operationId |
当前记录对应的已完成操作编号 |
这些指标的范围有重叠。尤其是 executionNs 可能包含输入准备和等待转换,不能将它与其他阶段直接相加,否则会重复计算。主线程处理段的耗时直接测量,不再从 gapNs 推算。现有记录仍不能单独得出数据库服务端的纯执行时间、内存分配量或完整的 MSPT 分布。
下表列的是后续测量计划,不是实测成绩。把指标与适合采集它的层次放在一起,是为了决定应在插件代码、真实数据库还是完整服务端中补充测量,从而定位耗时或内存占用来自哪里。
指标是要观察的量;拟记录内容说明具体测什么;测量层引用前文的 A(不启动 Paper 的 JVM 基准)、B(真实数据库)和 C(Paper 服务端)。列在表中不代表当前用例已经输出该指标。
| 指标 | 拟记录内容 | 测量层 |
|---|---|---|
| 吞吐量 | 每秒处理的行数和值数 | A、B、C |
| 提交给驱动的语句数与行数 | 在 JDBC 边界统计调用与行数 | B |
| 数据库收到的语句数与写入行数 | 通过数据库计数器统计 | B |
| 每次操作的内存分配量 | 每次操作分配的字节数 | A、B |
| 峰值堆占用 | 操作期间持续采样得到的最大堆占用 | B、C |
| 回收后堆占用 | 用例结束并执行垃圾回收后的堆占用 | B、C |
| MSPT 分布(p50、p99、最大值) | 每个 tick 的耗时:中位数(p50)、99% 的 tick 不超过的耗时(p99)和最大值 | C |
前面的阶段记录已经覆盖插件的部分处理段;要分离纯数据库时间,或取得本计划中的内存和 MSPT 指标,仍需额外工具。
在共享 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、B、C 对应测量层;工具列出可使用的库或诊断工具;作用解释它能提供什么证据,以及仍需另外记录什么。
| 用途 | 工具 | 作用 |
|---|---|---|
| 微基准(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.