并发修改什么的不要鸭

并发修改什么的不要鸭

用于调试并发修改异常和索引越界异常的模组。

简介

玩家和整合包开发者有时会遇到游戏因 ConcurrentModificationExceptions(CME)和 IndexOutOfBoundsExceptions(IOOBE)而崩溃的情况,这些异常只提供当前线程的堆栈跟踪,很难追踪是哪个模组导致的。以下是 1.20.1 Forge 环境中由 SimpleReloadInstance 引起的一次崩溃:

来自 SimpleReloadInstance 的 CME

这正是本模组开发的初衷。安装了此模组并正确添加 javaagent 到 JVM 参数的玩家,将获得特定集合修改历史的完整日志:

本模组的日志

使用方法

首先,将此 jar 文件添加到 mods 文件夹中。

其次,在你的启动器中编辑 Java 虚拟机参数。添加 -javaagent:mods/CMESuckMyDuck-<version>.jar=<类全名>;<字段名>;<类型>;<阶段>。

最后,运行游戏,游玩并等待崩溃发生。

用于其他 Java 项目

此模组不仅可以用于 Minecraft 调试,还可用于调试其他 Java 项目。使用方式与 Minecraft 类似。唯一区别在于,你需要在 javaagent 之前将 gson 和 asm jar 添加到类路径(-cp)中,并在 javaagent 之后将我们的 CMESuckMyDuck-<version>.jar 添加到类路径中。

参数

<类全名>

包含你想要监控的容器的类的全名。使用 \ 代替 .(即类的内部名称)。

<字段名>

目标类中你想要监控的容器的字段名。对于 Forge,请使用 SRG 名称。对于 Fabric,请使用 intermediary 名称。对于 NeoForge,请使用官方名称。

<类型>

目前,我们仅支持三种容器:List、Set、Map。此参数表示所监控容器的类型。

<阶段>

static 或 nonstatic。此参数表示该容器是静态字段还是非静态字段。

如何获取 <类全名> 和 <字段名>

让我们从一个例子开始。

首先,查看 CME/IOOBE 的堆栈跟踪:

替换此处的描述

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

替换此处的描述

现在我们知道了应该监控映射 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-<version>.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-<version>.jar=<类全名>;<方法名>;<类型>。顺便说一下,该索引可能被不同的局部变量复用。因此,你可能需要使用 -Dcme_suck_my_duck.match_local_index=<序号> 来指定 ASTORE <索引> 操作的序号,以指明要监控哪一个局部变量。

结语

我喜欢吃北京烤鸭。真的太美味了!

图片由 Polina Tankilevitch 拍摄:https://www.pexels.com/photo/close-up-photo-of-savory-peking-duck-dish-on-a-ceramic-bowl-5848604/