[MD]
# Endstone 版 spark
这是 [spark](https://spark.lucko.me/) 性能分析器在
[Endstone](https://github.com/EndstoneMC/endstone) 上的实现——将 spark 原生移植到
基岩版专用服务器(BDS)。你可以通过 spark 自带的网页查看器,准确找出服务器的
游戏刻(tick)时间究竟消耗在了哪里。
它是一款原生统计采样性能分析器:通过周期性捕获 BDS 服务端线程的真实调用栈,
不仅能够分析插件代码,还能覆盖 BDS 内部的全部工作,例如区块生成、实体刻更新、
红石运算、寻路等。即使服务端程序已经移除了符号信息,也仍然可以进行分析。
生成的结果为标准 spark 性能分析文件,可上传至 spark 的 bytebin,并通过
`https://spark.lucko.me/<id>` 以交互式火焰图的形式查看。
> 本项目是移植到 Endstone 的 spark。性能分析文件格式、通信协议和网页查看器均来自
> spark;相关成果与荣誉归属于 [lucko/spark](https://github.com/lucko/spark)。
## 命令
| 命令 | 说明 |
| --- | --- |
| `/spark profiler start [参数]` | 在后台开始分析选定的原生线程。 |
| `/spark profiler start --alloc` | 分析原生内存分配调用栈。 |
| `/spark profiler stop` | 停止分析并生成最终结果。 |
| `/spark profiler info` | 查看当前性能分析器的运行状态。 |
| `/spark profiler cancel` | 停止分析,但不生成性能分析结果。 |
| `/spark tps` | 显示每秒游戏刻数(TPS)和单刻耗时(MSPT)。 |
| `/spark health` | 显示 TPS、MSPT、进程内存、线程数量和运行时间。 |
| `/spark tickmonitor` | 报告耗时超过阈值或相较基线明显变慢的游戏刻。 |
默认情况下,停止性能分析器后,生成的结果会上传至 spark 的 bytebin,并在控制台中
输出查看链接。使用 `--save-to-file` 后,结果将改为保存到本地 `.sparkprofile` 文件。
如果上传失败,Spark 会自动将压缩后的性能分析文件保存在其数据目录中,并报告本地路径。
权限:`endstone.command.spark`(默认仅管理员可用)。
### `/spark tickmonitor`
执行 `/spark tickmonitor` 后,插件会先建立一个持续 120 个游戏刻的基线,并报告耗时
比基线高出 100% 以上的游戏刻。使用 `--threshold <percent>` 可以修改相对阈值;
使用 `--threshold-tick <ms>` 可以改为指定绝对游戏刻耗时。再次执行该命令即可关闭监控。
### `/spark profiler start` 参数
* `--interval <value>` — 执行时间分析时表示采样间隔,单位为毫秒,默认值为 `4`,
最大值为 `1000`;配合 `--alloc` 使用时表示内存分配采样间隔,单位为字节,
默认值为 `524287`。
* `--timeout <seconds>` — 在超过 10 秒后自动停止并生成最终结果。不指定该参数时,
性能分析会持续运行,直到执行 `stop` 或 `cancel`。
* `--only-ticks-over <ms>` — 仅保留持续时间超过指定毫秒数的游戏刻样本;参数必须为
正整数。
* `--comment <text>` — 为性能分析结果附加备注。包含空格的文本需要使用引号包裹。
* `--save-to-file` — 不上传结果,而是写入本地 `.sparkprofile` 文件。可将该文件拖入
spark 网页查看器中打开。
* `--thread <name>` — 仅适用于执行时间分析。按不区分大小写的完整线程名称选择线程;
可重复使用该参数以选择多个线程,名称中包含空格时需要使用引号包裹。
* `--thread *` — 仅适用于执行时间分析。选择 BDS 进程中的全部线程,并为每个被采样的
操作系统线程生成独立的查看器根节点。不能与其他 `--thread` 参数或 `--regex` 同时使用。
* `--regex` — 仅适用于执行时间分析。将每个 `--thread <pattern>` 解释为不区分大小写的
完整匹配正则表达式;至少需要提供一个匹配模式。
* `--include-sleeping` — 仅适用于执行时间分析。在线程空闲时也进行采样。未指定该参数时,
Linux 会根据任务状态、Windows 会根据各线程 CPU 周期增量,避免采集未实际运行的线程。
* `--alloc` — 不再分析执行时间,改为记录采样到的原生内存分配调用栈。不支持自定义
线程选择器。
* `--alloc-live-only` — 仅记录在停止分析时仍未释放的采样内存分配,用于泄漏分析;
该参数会自动启用 `--alloc`。
多线程执行时间分析会将采样间隔视为全局调用栈遍历预算,并在所有匹配线程之间公平轮换。
`/spark profiler stop` 同样支持 `--save-to-file` 和 `--comment <text>`;停止时提供的参数
会应用于最终输出。
## 工作原理
* Linux:独立采样线程按照设定间隔向服务端线程发送 `SIGPROF` 信号;信号处理器通过
[cpptrace](https://github.com/jeremy-rifkin/cpptrace) 的
`safe_generate_raw_trace`,以异步信号安全的方式捕获调用栈。栈帧优先通过 `dladdr`
解析动态符号;对于已经移除符号的 BDS 内部代码,则回退为 `module+0xRVA`,之后可以
使用 IDA 数据库或 Windows PDB 离线还原符号。
* Windows:采样器会暂停服务端线程,并使用 `StackWalk64` 遍历线程上下文;栈帧通过
随附的 PDB 解析为真实函数名称。
* 所有样本会聚合为调用树,序列化为 spark 的 protobuf 格式并使用 gzip 压缩,随后上传至
bytebin 或保存为本地 `.sparkprofile` 文件。符号解析和输出处理均在后台线程中完成,
因此不会阻塞服务器游戏刻。执行时间样本使用两次采样点之间测得的实际经过时间作为权重,
并排除目标线程因调用栈遍历而被暂停的时间。因此,即使多线程轮询后的实际采样周期长于
请求的间隔,也能保持正确的时间权重。
* 每份性能分析结果都包含当前运行的 BDS 可执行文件的 SHA-256。离线分析人员可以据此选择
完全匹配的服务端程序版本,而无需获得服主的可执行文件、文件路径、配置或世界数据。
### 原生内存分配分析器
`--alloc` 用于分析由 BDS 服务端线程成功发起的原生内存分配请求。样本会按照请求的字节数
进行加权,并采用随机化的固定字节间隔采样,默认间隔为 524287 字节。生成的结果与执行时间
分析共用同一套 spark 查看器、上传流程和本地保存流程。
`--alloc-live-only` 会持续跟踪采样到的内存分配,包括后续的 `realloc`、`free`,以及由其他
线程执行的释放操作,并且只报告在分析停止时仍然存活的分配。该功能用于定位长期保留内存和
潜在内存泄漏;通常需要对多次分析结果进行对比,才能区分持续增长的问题与正常的长期对象。
Windows 通过 funchook 拦截 UCRT 和进程堆的内存分配入口。Linux 则以原子方式重定向 BDS
可执行文件中的 glibc 导入槽,不会改写内存分配器指令。直接虚拟内存调用、自定义分配器内部
操作,以及完全由其他线程发起的内存分配,目前不在分析范围内。
调用栈符号解析和调用树聚合均在钩子路径之外执行。固定大小的预分配队列在样本过多时会丢弃
并报告超额样本,而不是阻塞服务端线程。两次分析会话之间,钩子会保持禁用并直接透传调用;
插件关闭时,会等待正在执行的调用结束后彻底移除钩子,从而支持安全、干净地重新加载插件。
## 构建
> Windows 内存分配分析器:CMake 会自动获取并静态构建上游 funchook `v1.1.3`,
> 因此它不是 Conan 依赖。Linux 使用原子 ELF 导入槽重定向,不链接 funchook。
各平台要求如下:
* Linux:Clang、libc++、Ninja 和 Conan 2。
* Windows:LLVM clang-cl、Visual Studio Build Tools、Windows SDK、Ninja 和 Conan 2。
clang-cl 必须以 MSVC ABI 为目标。
安装 Conan、解析依赖,然后使用生成的工具链文件直接配置 CMake:
```shell
pip install conan
conan install . --build=missing
cmake -S . -B build -G Ninja "-DCMAKE_TOOLCHAIN_FILE=build/RelWithDebInfo/generators/conan_toolchain.cmake" "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
cmake --build build
```
在 Linux 上,项目自带的 Conan Profile 会选择 libunwind,因为基于 `SIGPROF` 的采样器需要
cpptrace 提供异步信号安全的栈回溯路径。Windows 不使用 libunwind;cpptrace 会使用原生
Windows 后端,而 spark 则通过 `StackWalk64` 捕获调用栈。
生成的插件文件位于 `build/endstone_spark.so`(Linux)或
`build/endstone_spark.dll`(Windows)。将其放入服务器的 `plugins/` 目录即可。
> 工具链与 ABI 注意事项:C++ Endstone 插件必须使用其目标 Endstone 构建所要求的运行时
> ABI。编译器、编译器 ABI、C++ 标准和标准库/运行时必须保持一致。Linux 上应使用 ABI 兼容的
> libc++;Windows 上应使用 clang-cl,并匹配对应的 MSVC 运行时。不要混用不兼容的 STL 或
> 运行时 ABI:所有跨越 Endstone 插件边界的 C++ 类型,在插件与 Endstone 两侧都必须具有完全
> 相同的 ABI。ABI 不匹配可能导致跨插件边界传递的对象损坏。
## 许可证
本项目采用 GPLv3 许可证,与 spark 保持一致;本项目基于 spark 的性能分析文件格式和网页
查看器构建。详情请参阅 [LICENSE](LICENSE)。
[/MD]