Skip to content

Benchmarking

github-actions[bot] edited this page Oct 4, 2026 · 19 revisions

基准与压力测试

简体中文 | English

本页展示当前实现的基准方法和实测结果。所有成绩来自 Run #7,测试提交为 76dcc7e,运行日期为 2026-10-04。服务端表格统一以毫秒显示,保留六位小数;JSON 保存整数纳秒值。JMH 微基准另标明每次操作的计时单位。

服务端通常每秒更新 20 次,每次更新称为一个 tick,目标间隔约为 50 ms。脚本通过 skript-reflect 调用 System.nanoTime();纳秒是计时单位,不意味着单次测量有纳秒级准确度。调度、JVM 垃圾回收、数据库执行和脚本恢复时机都会影响读数。

这些数字对服务端意味着什么

当前脚本独占的局部输入可以在后台读取和检查;全局输入仍在主线程跨 tick 分段读取,每段最多 4096 个处理步骤,目标耗时约 2 ms。共享默认变量回到主线程处理。局部结果中的普通值也可以在脚本暂停期间于后台保存,继续执行前恢复局部上下文。

物品、位置等需要服务器 API 的转换共用每 tick 目标预算为 2 ms 的队列。单个耗时很长的转换无法中断,因此预算不是硬上限。全局结果最后仍在主线程发布,这一步不受转换队列预算约束。处理大量数据时,优先使用局部结果和较小分页。

一次读取最多保存 5000 行,更多结果会被拒绝且不保留部分结果。写入语句最多绑定 30000 个值,较大批次会拆成多条语句。拆分本身不保证全部成功或全部回滚。见读取行、写入行和事务。

CI 覆盖范围与环境

五种数据库的插件读写、JMH 和报告发布全部通过。环境为 Paper 26.2 build 124、Skript 2.16.2、skript-reflect 2.6.3 和 Java 25。下表用于核对测试覆盖范围,不比较速度。后端是被测数据库;插件读写包含行数曲线、重复用例和新增作用域用例;原始 SQL 对照说明是否运行手写 SQL;CPU/数据库给出作业环境。MongoDB 使用文档命令,不支持 SQL,因此只跳过 SQL 对照。

后端 插件读写 原始 SQL 对照 CPU/数据库
SQLite 已完成 已完成 AMD EPYC 9V74 / SQLite
MySQL 已完成 已完成 AMD EPYC 7763 / mysql:8.4
MariaDB 已完成 已完成 AMD EPYC 9V45 / mariadb:11.4
PostgreSQL 已完成 已完成 AMD EPYC 9V74 / postgres:17
MongoDB 已完成 不适用:不支持 SQL Intel Xeon Platinum 8573C / mongo:8

各作业的 CPU 不同,不能按跨数据库的绝对耗时排名。测试用的 bench:: 变量不写入 Skript 的变量文件,以免临时数据的保存队列积压;测试仍包含内存中的变量处理和 ORM 数据库操作。

总耗时从语句开始到脚本恢复运行,包含变量处理、数据库操作及调度等待;生成数据、后续计数、校验和清理不计入。最大 tick 间隔是操作开始到返回后一个 tick 的窗口内,观察器相邻两次执行的最大间隔,不是插件独占主线程的时间。tick 超出量为该间隔减去 50 ms,负数按零处理。每行的操作和请求行数标明数据规模。

5000 行读写:不同数据库的表现

这组用例每行只填 id,其余五列为 NULL,输入和结果均为全局变量。每种数据库处理相同的数据结构,横向看读写等待时间与 tick 间隔,可以区分“整次操作等待较久”和“期间某次更新迟到”。每项仅采样一次,不能据此确定稳定的性能差距。

后端 操作 请求行数 总耗时 最大 tick 间隔 tick 超出量
SQLite 写入 5000 738.019777 ms 51.039040 ms 1.039040 ms
SQLite 读取 5000 101.214641 ms 101.797048 ms 51.797048 ms
MySQL 写入 5000 653.115830 ms 50.743193 ms 0.743193 ms
MySQL 读取 5000 99.480507 ms 100.044641 ms 50.044641 ms
MariaDB 写入 5000 627.244008 ms 50.787066 ms 0.787066 ms
MariaDB 读取 5000 75.822950 ms 76.335655 ms 26.335655 ms
PostgreSQL 写入 5000 680.279059 ms 50.846258 ms 0.846258 ms
PostgreSQL 读取 5000 106.822833 ms 107.407271 ms 57.407271 ms
MongoDB 写入 5000 605.432622 ms 50.700463 ms 0.700463 ms
MongoDB 读取 5000 95.601344 ms 95.916557 ms 45.916557 ms

大批量写入与重复写入

这张表使用相同的只填 id、全局变量用例。10000 行写入耗时/最大 tick 间隔来自较大批次;5000 行重复写入耗时/最大 tick 间隔来自六种行数测试之后再次执行的批次。重复写入应与上一表的首次 5000 行写入比较,不能与 10000 行直接相减作为预热收益;重复运行也不保证缓存已经预热。

后端 10000 行写入耗时 最大 tick 间隔 5000 行重复写入耗时 重复写入最大 tick 间隔
SQLite 1416.792130 ms 50.810946 ms 786.039922 ms 50.662504 ms
MySQL 1208.194044 ms 50.795271 ms 676.070701 ms 50.894886 ms
MariaDB 1207.324490 ms 50.689455 ms 700.069976 ms 50.463965 ms
PostgreSQL 1294.077597 ms 50.748823 ms 654.585784 ms 50.558487 ms
MongoDB 1349.871031 ms 50.522086 ms 645.084583 ms 50.512251 ms

插件语句与原始 SQL

这组对照独立运行,每条路径处理 5000 行,只填 id,使用全局输入和结果。插件写入/读取分别计时 insert many 和 select many;原始 SQL 写入/读取分别计时 execute update 和 execute query,均为完整语句总耗时。SQL 字符串在计时前构造,插件写入则包含输入准备,因此比值不能直接当成 ORM 映射开销。把同一数据库的两条路径放在一起,是为了观察完整调用的成本差异。

**不适用(不支持 SQL)**表示 MongoDB 不接受这组 SQL 语句;它的插件读写仍已完成,缺少 SQL 成绩也不代表零耗时。MySQL 插件路径使用参数化多行插入,其他 JDBC 路径使用驱动批处理。

后端 插件写入 原始 SQL 写入 插件读取 原始 SQL 读取
SQLite 749.999370 ms 50.010275 ms 94.916647 ms 75.878837 ms
MySQL 750.014611 ms 81.068476 ms 109.862134 ms 92.715736 ms
MariaDB 699.836833 ms 49.816977 ms 81.606404 ms 69.435926 ms
PostgreSQL 750.100345 ms 94.180613 ms 85.262522 ms 112.546390 ms
MongoDB 699.951095 ms 不适用(不支持 SQL) 84.042793 ms 不适用(不支持 SQL)

MariaDB:不同数据量与重复运行

下面展开同一次运行的 MariaDB 结果,使用 AMD EPYC 9V45 和 mariadb:11.4。仍是每行只填 id、全局输入和结果。固定数据库与数据结构,再改变行数,可以观察任务规模与等待时间、tick 延迟的关系;重复 5000 行用于观察运行顺序的影响。

结果说明操作是否成功。10000 行读取超过上限,其耗时衡量拒绝请求和清空结果,不是成功读取 10000 行的成绩。“重复”指其他行数用例结束后再次运行,不保证缓存已预热。

操作 请求行数 总耗时 最大 tick 间隔 tick 超出量 结果
写入 100 100.166930 ms 52.934261 ms 2.934261 ms 成功
读取 100 51.661766 ms 52.577681 ms 2.577681 ms 成功
写入 500 49.721242 ms 50.633752 ms 0.633752 ms 成功
读取 500 55.148990 ms 56.118816 ms 6.118816 ms 成功
写入 1000 199.506323 ms 54.222904 ms 4.222904 ms 成功
读取 1000 56.909843 ms 57.477091 ms 7.477091 ms 成功
写入 2500 299.843110 ms 50.377625 ms 0.377625 ms 成功
读取 2500 70.805768 ms 71.374238 ms 21.374238 ms 成功
写入 5000 627.244008 ms 50.787066 ms 0.787066 ms 成功
读取 5000 75.822950 ms 76.335655 ms 26.335655 ms 成功
写入 10000 1207.324490 ms 50.689455 ms 0.689455 ms 成功
读取 10000 57.263183 ms 57.794867 ms 7.794867 ms 被拒绝:超过 5000 行读取上限
写入(重复) 5000 700.069976 ms 50.463965 ms 0.463965 ms 成功
读取(重复) 5000 83.557357 ms 84.144273 ms 34.144273 ms 成功

新版服务端基准

新增用例填满每行的六列,并比较局部输入/局部结果与全局输入/全局结果。还有混合作用域、替换已有结果,以及物品和位置用例。所有用例在计时前生成数据,每次使用新的主键范围,查询只读取本次范围。生成、校验、计数和清理都在计时窗口外。

每个新增用例预热三次、正式测量十次。表中展示十次正式测量的中位数;预热不参与汇总。每个后端的附件包含 442 条原始记录、34 组汇总,另有 p95 和最大值。p95 用最近秩法计算,十个正式样本时等于最大值,对罕见延迟的判断能力有限。只填 id 的曲线和 SQL 对照各采样一次,不混入这些中位数。

用例 行数 比较目的
六个数值列全部填值:局部与全局 100、500、1000、2500、5000 固定数据库工作量,观察变量准备和结果保存成本
局部输入/全局结果、全局输入/局部结果 5000 分别观察输入与结果作用域的影响
替换已有局部或全局结果 5000 计入清除旧结果的工作,校验旧行和旧列被移除
四个数字、一个物品和一个位置 1000 观察普通值与主线程对象转换混合时的成本
带名称和描述的物品、位置,替换局部结果 1000 覆盖对象属性保留与结果替换

新版实测:Run #7

下表固定读取 5000 行,每行六个数值列全部有值。操作区分局部或全局结果,主线程处理耗时是本次操作中已计时的主线程处理区间累计耗时中位数;它不是整个 tick 的耗时,也没有覆盖语句的所有零散开销。总耗时和最大 tick 间隔各取十次样本的中位数;tick 超出量由表中的间隔中位数减去 50 ms 后取非负值,不代表十次采样中最严重的延迟。将总耗时与主线程处理放在一起,可以看到工作移到了哪里。局部与全局来自同一次服务端运行,适合在同一数据库内比较。

后端 操作 请求行数 总耗时 最大 tick 间隔 tick 超出量 主线程处理耗时
SQLite 读取到局部变量 5000 49.925225 ms 50.208298 ms 0.208298 ms 0.018713 ms
SQLite 读取到全局变量 5000 65.414039 ms 65.743510 ms 15.743510 ms 15.483043 ms
MySQL 读取到局部变量 5000 49.923762 ms 50.204954 ms 0.204954 ms 0.015720 ms
MySQL 读取到全局变量 5000 86.510248 ms 87.054996 ms 37.054996 ms 36.381696 ms
MariaDB 读取到局部变量 5000 50.005949 ms 50.250288 ms 0.250288 ms 0.017637 ms
MariaDB 读取到全局变量 5000 66.299079 ms 66.890665 ms 16.890665 ms 15.888968 ms
PostgreSQL 读取到局部变量 5000 49.942947 ms 50.217965 ms 0.217965 ms 0.018167 ms
PostgreSQL 读取到全局变量 5000 69.211086 ms 69.465648 ms 19.465648 ms 19.242586 ms
MongoDB 读取到局部变量 5000 49.950872 ms 50.178014 ms 0.178014 ms 0.014090 ms
MongoDB 读取到全局变量 5000 71.939851 ms 72.119047 ms 22.119047 ms 21.958078 ms

局部结果保存的主要工作移到后台,但完整读取仍约需一个 tick,因为 Skript 在主线程恢复运行。这里比较当前实现的两种作用域,没有比较不同版本或其他插件。

物品和位置:每个 tick 的工作量

每行包含四个数字、一个物品和一个位置,共 1000 行。总耗时和最大 tick 间隔为十次样本的中位数,tick 超出量按间隔中位数减去 50 ms 后取非负值。最忙 tick 的处理耗时先取每次操作在同一个 tick 内已计时处理段的累计最大值,再取十次结果的中位数。它属于本次操作,不是整个服务端的 MSPT。把完整等待时间与最忙 tick 放在一起,可以观察分批转换如何减少单个 tick 的工作量,以及因此增加的等待。

后端 操作 请求行数 总耗时 最大 tick 间隔 tick 超出量 最忙 tick 的处理耗时
SQLite 读取到局部变量 1000 499.969523 ms 52.032646 ms 2.032646 ms 1.947452 ms
SQLite 读取到全局变量 1000 521.950934 ms 112.811151 ms 62.811151 ms 64.061272 ms
MySQL 读取到局部变量 1000 699.948048 ms 52.012222 ms 2.012222 ms 1.960147 ms
MySQL 读取到全局变量 1000 775.946264 ms 175.476904 ms 125.476904 ms 126.033681 ms
MariaDB 读取到局部变量 1000 449.839210 ms 52.125599 ms 2.125599 ms 1.939399 ms
MariaDB 读取到全局变量 1000 448.441535 ms 95.115534 ms 45.115534 ms 45.820466 ms
PostgreSQL 读取到局部变量 1000 599.943042 ms 52.004495 ms 2.004495 ms 1.948226 ms
PostgreSQL 读取到全局变量 1000 634.938087 ms 134.294649 ms 84.294649 ms 84.902243 ms
MongoDB 读取到局部变量 1000 499.969330 ms 52.020147 ms 2.020147 ms 1.956798 ms
MongoDB 读取到全局变量 1000 515.116861 ms 114.311325 ms 64.311325 ms 65.141387 ms

局部结果的最忙 tick 约承担 2 ms 已计时工作,但整次读取会等待更多 tick。全局结果最终发布仍可能占用几十毫秒,共享转换预算没有覆盖这一步。

MariaDB:六列填满的行数曲线

同一 MariaDB 作业中,每行六个数值列都有值。操作说明读写与变量作用域;请求行数是处理规模;总耗时和最大 tick 间隔为十次正式测量的中位数,tick 超出量按间隔中位数减去 50 ms 后取非负值。固定环境后同时改变行数与作用域,可以观察变量处理对等待和 tick 间隔的影响。全局写入包含跨 tick 分片读取,较长总耗时不表示这些 tick 都被阻塞。此表的数据内容与只填 id 的表不同,两者不能直接相减作为优化收益。

操作 请求行数 总耗时 最大 tick 间隔 tick 超出量
局部写入 100 50.002091 ms 50.365996 ms 0.365996 ms
局部读取 100 49.948891 ms 50.296366 ms 0.296366 ms
全局写入 100 99.999130 ms 50.318539 ms 0.318539 ms
全局读取 100 50.561922 ms 50.852497 ms 0.852497 ms
局部写入 500 50.003658 ms 50.298183 ms 0.298183 ms
局部读取 500 49.969411 ms 50.278538 ms 0.278538 ms
全局写入 500 200.041385 ms 50.843437 ms 0.843437 ms
全局读取 500 53.469929 ms 53.751575 ms 3.751575 ms
局部写入 1000 50.029800 ms 50.227414 ms 0.227414 ms
局部读取 1000 49.985885 ms 50.250870 ms 0.250870 ms
全局写入 1000 400.035662 ms 50.880191 ms 0.880191 ms
全局读取 1000 55.762635 ms 55.953148 ms 5.953148 ms
局部写入 2500 50.027120 ms 50.249912 ms 0.249912 ms
局部读取 2500 49.943913 ms 50.207857 ms 0.207857 ms
全局写入 2500 1000.028616 ms 50.811210 ms 0.811210 ms
全局读取 2500 58.051739 ms 58.323916 ms 8.323916 ms
局部写入 5000 50.068590 ms 50.309630 ms 0.309630 ms
局部读取 5000 50.005949 ms 50.250288 ms 0.250288 ms
全局写入 5000 1845.764052 ms 50.874572 ms 0.874572 ms
全局读取 5000 66.299079 ms 66.890665 ms 16.890665 ms

JVM 微基准

同次 Run #7 在 AMD EPYC 7763 上使用 JMH 测量,两次独立进程、每次两轮预热和三轮正式测量。用例说明被测工作;每次操作耗时是 JMH 汇总的时间;测量范围说明包含哪些成本。它们用于观察不启动 Paper 时的代码和数据库调用成本,不能解释为游戏服务端上的完整语句耗时。

用例 每次操作耗时 测量范围
5000 行批量插入 9.871089 ms/op 内存 H2、值准备、参数绑定与数据库执行
计算每条语句的行数上限 15.115187 ns/op 行数上限计算,不访问数据库

JMH 记录

各阶段计时

完整语句和观察器间隔由脚本记录。插件的阶段计时只在基准服务端开启,语句返回后立即读取,避免后续查询覆盖记录。字段是 JSON 中的键名,含义说明计时范围。阶段范围有重叠,不能直接全部相加。

字段 含义
wallNs 语句开始到脚本恢复,不含后续校验
gapNs 语句开始到返回后一个 tick,观察器的最大执行间隔
mainNs 本次操作中已计时的主线程处理区间累计耗时
mainTickMaxNs 本次操作在同一个 tick 内已计时处理段的累计耗时最大值
prepareMainNs、prepareAsyncNs 分别在主线程和后台记录的输入准备耗时
conversionMainNs 主线程值转换耗时
resultMainNs、resultAsyncNs 分别在主线程和后台记录的结果保存耗时
executionNs 异步查询任务的耗时,可能包含输入准备、转换等待、数据库操作和游标读取,不是数据库纯执行时间
queueWaitNs 等待主线程转换队列的时间
syncConversions 已记录的主线程转换次数
largestConversionNs 最慢一次主线程转换的耗时

报告与复现

运行页面的 tick-report-* 附件包含 pipeline-results.json、tick-results.json 和环境记录;pipeline-results.json 保存原始样本(含预热)及中位数/p95/最大值,tick-results.json 保存 CI 使用的记录点。下表的报告给出数据库,数据来源链接到本次提交的已发布记录,供核对数值;它不是性能排名。

报告 数据来源
SQLite 纳秒记录
MySQL 纳秒记录
MariaDB 纳秒记录
PostgreSQL 纳秒记录
MongoDB 纳秒记录

运行 ./gradlew serverBenchmark 可在本地测试 SQLite;其他后端通过 -Pskriptorm.benchmark.server.type=<类型> 和连接参数指定。结果写入 build/benchmarks/。./gradlew :benchmarks:jmh 运行 JVM 基准,结果写入 benchmarks/build/benchmarks/results.json。各作业记录提交、CPU、JDK、操作系统、数据库镜像、驱动、Paper 和 Skript 版本。

CI 因脚本失败、影响行数或保存行数不符、样本缺失等问题失败;耗时变化只作报告,不作为构建失败条件。基准历史保存在 gh-pages 的 dev/bench/<CPU>/,服务端结果另按后端分组。

如何解读与尚未覆盖的情况

  • 普通主线程处理段记录实际经过时间,不是 CPU 时间;代码还没有为语句的所有零散开销设置计时。
  • 不能将 executionNs 与其他阶段直接相加,其中可能已经包含它们。
  • 最大 tick 间隔能显示更新延迟,不能给出完整 MSPT(每个 tick 的耗时)分布。内存分配、峰值与长期保留量也需另行测量。
  • 空闲服务端不能代表大量在线玩家与实体活动时的表现;短测试也不能排除长期泄漏。
  • 通用 JDBC 可以使用本仓库未测过的驱动,现有成绩不代表这些驱动。
  • 跨版本对照需要同脚本、同数据、同机器及相同数据库设置;不同 CPU 的 CI 成绩不能直接算成优化百分比。

skript-orm

参考

实用指南


skript-orm (English)

Reference

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.

Clone this wiki locally