LagMap

LagMap

一个服务端Minecraft模组,回答了性能分析器通常回避的问题:现在究竟是哪个维度、区块和基地在加载服务器?

LagMap

一个服务端 Minecraft 模组,回答了分析器通常回避的问题:
当前是哪个维度、区块和基地在给服务器造成负载?

像 spark 这样的工具会告诉你什么代码很慢。LagMap 则会告诉你
世界中的哪个位置在进行大量工作。它会定时对每个已加载区块进行采样,根据估算的
负载评分对它们进行排序,并生成一个快照,你可以在聊天栏、JSON 文件或独立的
HTML 报告中查看。

  • Minecraft 版本: 1.20.1
    - 加载器: Forge 47.x
    - 端类型: 仅服务端(专用服务器或单人游戏中的内置服务器)
    - 是否需要客户端模组: 否

功能说明

  1. 刻计时。 每刻测量 MSPT,并维护滚动的 10 秒 / 30 秒 / 60 秒窗口
       (平均值、最小值、最大值),以及由此得出的 TPS 数值。
    2. 按维度和区块采样。 每 N 秒(默认 5 秒)遍历每个维度的
       已加载区块并记录:
       - 实体数量,以及其中有多少实体位于实体刻 tick 的区块中
       - 方块实体数量,以及其中有多少带有 ticker 且位于
         tick 的区块中
       - 区块是否被强制加载(这些区块会绕过评分阈值,不过空的
         区块仍然会排在前 N 名列表之外;每个维度的
         forceLoadedChunkCount 始终报告总数)
       - 区块中主要的实体 / 方块实体类型
       - 可配置区块半径内的玩家
    3. 启发式负载评分,针对每个区块和每个维度(见下方警告)。
    4. 导出。 生成稳定的 JSON 快照和静态 HTML 报告,在
       服务端线程之外以原子方式写入。

尚未实现的功能

  • 不测量真实的每区块或每方块实体刻成本。请参阅
      ROADMAP.md。
    - 没有客户端 GUI、没有实时网页仪表板、没有游戏内地图覆盖层。
    - 没有特定模组集成(紧凑机器、FTB 区块、领地声明、spark 导入)。
    - 没有历史时间序列——磁盘上只保留最新的快照。
    - 多版本支持不在本版本范围内;请参阅下方移植部分。

该评分是估算值,而非测量值

estimatedScore 是区块内容的加权计数。一个满是 tick 方块实体的
区块评分会很高,因为这类区块通常消耗刻时间——但单个
病态实体可能消耗超过一千个漏斗,而 LagMap 无法察觉。请使用
该评分来决定接下来该查看哪里,然后用 spark 确认。每次导出都会重复
此免责声明,以确保报告被粘贴到 Bug 追踪器时不会丢失。


命令

默认情况下,所有命令都需要权限等级 2(可配置,请参阅 permissionLevel)。

命令 效果
/lagmap status 显示 MSPT 窗口、TPS、采样器状态,以及上次采样的各维度总计。
/lagmap top [count] 显示服务端范围内上次采样中最热的区块。count 默认为 10,最大 100。
/lagmap dump 立即采样并写入 latest.json 和 latest.html,不受配置影响。
/lagmap start 开始周期性采样。
/lagmap stop 停止周期性采样。刻计时继续运行。
/lagmap tp <dimension> <chunkX> <chunkZ> 将你传送到该区块的中心。仅限玩家使用。

典型的排查流程:

/lagmap status           # MSPT 是否真的很高?哪个维度负载最重?
/lagmap dump             # 立刻获取一份新样本
/lagmap top 20           # 哪些区块在该维度中占主导?
/lagmap tp minecraft:overworld 145 -302

/lagmap tp 从地表高度图推导 Y 坐标,这在类主世界维度中有意义,
在下界或自定义维度中则会产生误导——它会发出警告。传送会加载目标区块,
这是 LagMap 唯一会导致区块加载的地方。


输出文件

写入相对于服务器目录的 config/lagmap/ 中:

文件 内容
latest.json 机器可读的快照。键顺序稳定,schemaVersion: 1。
latest.html 内嵌快照的独立报告。双击即可打开。

两者都先写入临时文件再移动到目标位置,因此轮询 JSON 的查看者
永远不会读取到写入一半的文档。

本仓库中的 web/viewer.html 是同一个查看器,但没有内置数据:打开它,
将任何 latest.json 拖放到上面即可。这对于阅读别人发给你的快照很有用。

阅读 JSON

{
  "schemaVersion": 1,
  "timestamp": "2026-08-20T12:00:00Z",
  "chunkEnumeration": "chunkmap (complete: every fully-loaded chunk)",
  "scoreFormula": "estimated (idleEntities*0.10) + ...",
  "mspt": {
    "last": 48.20,
    "tps10s": 20.00,
    "last10s": { "samples": 200, "average": 45.10, "min": 38.00, "max": 91.40 },
    "last30s": { ... },
    "last60s": { ... }
  },
  "totals":     { "dimensions": 3, "loadedChunks": 1841, "entities": 2204, "blockEntities": 9033 },
  "dimensions": [ { "dimensionId": "...", "totalEstimatedScore": 0, "hottestChunks": [ ... ] } ],
  "topChunks":  [ { "dimensionId": "...", "chunkX": 0, "chunkZ": 0, "estimatedScore": 0, ... } ]
}

值得了解的字段:

  • chunkEnumeration — 已加载区块的发现方式。chunkmap 表示列表是
      完整的。fallback 表示由特殊模组 ticket 保持打开的区块可能会缺失;请将
      该报告视为下限值。
    - entityCount 对比 tickingEntityCount — 超出 tick 范围的区块仍然保有
      其内容,但不消耗刻时间。两者差距大意味着“内容很多,但目前并不昂贵”。
    - scannedChunkCount 对比 loadedChunkCount — 这两者通常不同,差距并非
      bug。loadedChunkCount 是 Minecraft 自己的区块持有者计数,包括仅部分生成
      或位于加载区域边缘的区块。
      scannedChunkCount 统计的是 LagMap 实际检查的完全加载的区块——
      只有这些区块才包含实体和方块实体。只有当 scannedChunkCount 等于
      maxChunksPerDimension 时,差距才表示有截断。
    - sampleDurationMicros — LagMap 自身花费的时间。注意此项;工具自身
      不能成为卡顿源。

配置

config/lagmap.toml,首次启动时生成。要点:

键 默认值 说明
sampling.enabledOnStart true 自动开始采样。
sampling.sampleIntervalSeconds 5 更低的值响应更快但开销更大。
sampling.maxChunksPerDimension 20000 每次采样每个维度的安全上限。
sampling.collectTypeBreakdown true 在大型服务器上可关闭以降低采样成本。
sampling.nearbyPlayerRadiusChunks 8 用于将玩家归属到热点区块的半径。
reporting.topChunksGlobal 25 每次快照保留的服务端热点区块数。
reporting.writeJsonEachSample true 设为 false 则仅在 /lagmap dump 时写入。
reporting.permissionLevel 2 使用 /lagmap 所需的原版权限等级。
scoring.*Weight 见文件 启发式评分的权重。

评分权重默认为 idleEntity 0.10、idleBlockEntity 0.05、tickingEntity 1.00、
tickingBlockEntity 1.50。空闲内容不将权重设为零,因为它们仍消耗内存、区块保存
和网络带宽——只是不消耗刻时间。


性能开销

LagMap 是一个卡顿排查工具,因此它的设计目标就是不给服务器增加卡顿:

  • 采样是周期性的,而非每刻运行。只有 MSPT 测量每刻运行,那只是向
      预分配环形缓冲区写入两次 System.nanoTime() 调用。
    - 采样只读取已加载的状态。它从不强制加载区块,也从不触碰
      磁盘。(/lagmap tp 是唯一例外,且只针对目标区块。)
    - “这个方块实体是否 tick”按方块状态只回答一次,并在本次会话中缓存。
    - 所有文件写入都在低优先级守护线程上进行。
    - 每个快照中的 sampleDurationMicros 准确告诉你一次采样的成本。

构建

需要 JDK 来运行 Gradle。Minecraft 1.20.1 本身需要 Java 17,如果你没有,Gradle 会
通过 foojay 工具链解析器自动下载。

./gradlew build

模组 jar 位于 build/libs/lagmap-1.20.1-0.1.0.jar。

Gradle 8.14 支持 Java 17 到 24。如果你的默认 java 版本更新,请将 Gradle 指向
受支持的 JDK:

JAVA_HOME=/path/to/jdk-21 ./gradlew build

使用 ./gradlew runServer 运行测试服务器(首次启动时在 run/eula.txt 中接受 EULA)。


移植到新版本 / NeoForge

版本特定代码被刻意隔离:

包 依赖 Minecraft? 说明
com.lagmap.model 否 纯记录。
com.lagmap.core 否 刻窗口、采样调度。
com.lagmap.score 否 仅评分。
com.lagmap.export 否 JSON + HTML。
com.lagmap.platform 否 SnapshotCollector 接缝。
com.lagmap.platform.forge1201 是 1.20.1 实现。
com.lagmap.command、LagMapMod、LagMapConfig 是 Brigadier、Forge 事件、Forge 配置。

移植意味着编写一个新的 SnapshotCollector、重新接入入口点、适配命令/配置类。
其他一切原样迁移。

模组中唯一使用反射的代码位于
platform/forge1201/LoadedChunkAccess.java:ChunkMap#getChunks() 是受保护的,且没有
公共等效方法。它在类初始化时解析一次(同时尝试 Mojang 映射名和 SRG 名),
如果失败则回退到公共 API 扫描。采样路径本身不进行任何名称查找。


项目文档

文件 用途
README.md 什么是 LagMap 以及如何使用(本文件)。
ROADMAP.md 计划中的工作,按价值与投入比排序。长期愿望清单。
MEMORY.md 当前开发状态:什么是已验证的、什么是未测试的、下一步做什么。每次会话更新。
DECISIONS.md 代码为何如此设计的原因。仅追加。
CLAUDE.md 面向 AI 编码代理的仓库特定指南。

许可证

MIT。