摘要
玩家和模组包开发者有时会发现游戏因 ConcurrentModificationExceptions (CME) 和 IndexOutOfBoundsExceptions (IOOBE) 而崩溃,这些异常仅提供当前线程的堆栈跟踪,难以追踪是哪个模组导致了它们。以下是由 1.20.1 Forge 中的 SimpleReloadInstance 引起的一次崩溃示例:

因此,本模组的开发目的就在于此。安装此模组并正确向 JVM 参数添加 javaagent 的玩家将获得特定集合修改历史的完整日志:

使用方法
首先,将此 jar 文件添加到 mods 文件夹中。
其次,在启动器中编辑您的 Java 虚拟机参数。添加 -javaagent:mods/CMESuckMyDuck-<版本>.jar=<类全名>;<字段名>;<类型>;<阶段>。
最后,运行游戏,游玩并等待崩溃发生。
其他 Java 项目的使用方法
此模组不仅可用于 Minecraft 调试,还可用于调试其他 Java 项目。与 Minecraft 类似。唯一不同的步骤是,您需要在 javaagent 之前将 gson 和 asm jar 添加到类路径 (-cp) 中,并在 javaagent 之后将我们的 CMESuckMyDuck-<版本>.jar 添加到类路径中。
参数说明
<类全名>
包含您想要监控的容器的类的完整名称。使用 \ 代替 .(即类的内部名称)。
<字段名>
目标类中您想要监控的容器的字段名。对于 Forge,使用 SRG 名称;对于 Fabric,使用中介名称;对于 NeoForge,使用官方名称。
<类型>
目前,我们仅支持三种容器:List、Set、Map。此参数指示被监控容器的类型。
<阶段>
static 或 nonstatic。此参数指示该容器是静态字段还是非静态字段。
如何获取 <类全名> 和 <字段名>
让我们从一个例子开始。
首先,查看 CME/IOOBE 的堆栈跟踪:

其次,阅读 SoundEngine 的源代码并确认哪个容器面临此问题:

现在我们知道应该监控 map field_217942_m(SoundEngine 中的 instanceToChannel)。继续下一步。
第三,安装此模组,并将 "-javaagent:mods/CMESuckMyDuck-1.0.0.jar=net/minecraft/client/audio/SoundEngine;field_217942_m;Map;nonstatic" 添加到 Java 虚拟机参数中。重新启动游戏并等待下一次崩溃。
最后,打开 CMESuckMyDuck.log 文件,您将看到是哪个线程以及哪个模组并发修改了该容器。
示例 (JVM 参数)
Forge 1.16.5 环境中来自 SoundEngine 的 ConcurrentModificationException
-javaagent:mods/CMESuckMyDuck-1.0.0.jar=net/minecraft/client/audio/SoundEngine;field_217942_m;Map;nonstatic
Forge 1.20.1 环境中来自 PotionBrewing 的 ConcurrentModificationException
-javaagent:mods/CMESuckMyDuck-1.0.0.jar=net/minecraft/world/item/alchemy/PotionBrewing;f_43494_;List;static
来自 Zeta 模组的 ArrayIndexOutOfBoundsException
-javaagent:CMESuckMyDuck-1.0.0.jar=org/violetmoon/zetaimplforge/event/ForgeZetaEventBus;convertedHandlers;Map;nonstatic
其他选项
日志级别
使用系统属性 -Dcme_suck_my_duck.log_level=<级别> 设置自定义日志级别。
默认为 1,意味着不会记录调试消息。
用户可以将其设置为 0 以输出调试消息,但不推荐这样做——当设置为 0 时,查询函数如 Map#get、Set#containsAll 也会被记录,导致文件非常非常长。
ASM API 版本 (v1.0.2+)
为了保持与最新版 Minecraft 的兼容性,此模组使用 asm 版本 9.7 编译。对于较旧版本的 Minecraft(例如 1.12.2),无法应用诸如 ASM_9 之类的 API 级别操作,因此玩家需要使用 -Dcme_suck_my_duck.asm_api_version=<版本> 来修改 ASM API 版本兼容性。例如,游戏版本 1.12.2:-Dcme_suck_my_duck.asm_api_version=5。
文件最大条目数 (v1.0.3+)
由于我们注意到当崩溃发生时,日志中的最后几次操作比之前的操作重要得多。因此,我们的模组在 v1.0.3 之后采用了分页策略,最终仅保留最后两页。每页输出的日志数量(即操作调用堆栈的数量)是固定的,默认值为 500。玩家可以使用 -Dcme_suck_my_duck.file_max_entries=<大小> 来修改页面上的最大元素数量。
构造函数白名单 (v1.0.3+)
有时,某个类中的容器会在许多地方被使用(例如 CompoundTag#tags)。直接使用此模组会导致日志过长,或者有效内容被后续操作覆盖。因此,玩家可以指定额外的构造函数白名单,以监控特定模块中对应类的相应容器。默认为空,即没有白名单。玩家可以使用 -Dcme_suck_my_duck.whitelist_constructor_stacktrace=<字符串> 来指定它。如果容器构建时的堆栈跟踪中的任何一行包含白名单字符串的内容,则该容器将被监控——否则,该容器将不会被监控,这极大地简化了日志输出信息。
转换为线程安全 (v1.0.4+)
使用系统属性 -Dcme_suck_my_duck.transform_to_thread_safe=true 将字段转换为线程安全的容器。
除非您喜欢缓慢且不打算修复问题,否则不推荐使用此选项。
注入方法 (v1.0.4+)
使用系统属性 -Dcme_suck_my_duck.inject_method=true 切换到注入模式。
如果设置此项,您应使用 -javaagent:CMESuckMyDuck-<版本>.jar=<类全名>;<方法名>,每当调用此方法时,您将在日志文件中收到堆栈跟踪。
忽略线程 (v1.0.5+)
使用系统属性 -Dcme_suck_my_duck.ignore_threads=<线程名 1>;<线程名 2>;<线程名 3>;... 来忽略某些预期会修改给定容器(或调用给定方法)的线程。例如:-Dcme_suck_my_duck.ignore_threads="Server thread"。
提前停止记录 (v1.0.7+)
使用系统属性 -Dcme_suck_my_duck.stop_logging_if_exception_created=false 在发生严重错误时停止记录。
追踪 ID 更新器 (v1.0.8+)
使用系统属性 -Dcme_suck_my_duck.trace_id_updater=<类全名>;<方法名> 在调用给定方法时更新追踪 ID。这在注入模式下可能很有用。
监控局部变量 (v1.1.0+)
使用系统属性 -Dcme_suck_my_duck.local_var_index=<索引> 来监控给定方法中指定索引处的局部变量。
如果设置此项,您应使用 -javaagent:CMESuckMyDuck-<版本>.jar=<类全名>;<方法名>;<类型>。顺便说一句,索引可以被不同的局部变量复用。因此,您可能需要使用 -Dcme_suck_my_duck.match_local_index=<序号> 来指定 ASTORE <索引> 操作的序号,以指示要监控哪个局部变量。
结论
我喜欢吃北京烤鸭。它太美味了!

选择礼物
使用社区积分,为喜欢的作品送上一份支持
打赏记录
感谢每一位送出礼物的社区成员


