我是千里马,是一位软件工程师,最近几天完成了用户中心全套内容设计和游戏中大大小小的各种bug处理解决,准备开始游戏的正式填充,突然想起来还有两件抛之脑后的事情没有做。因为之前一直都是忙碌大方向内容设计研发,有一些小的bug就直接暂时性质的忽略。
刚好,最近几天比较大的基础工程都已经完工了,所以专门抽出时间来解决这些小一些的问题,那下面就正式开始这次debug过程吧。
这次的任务是解决白鹭引擎RES资源加载模块相关问题,算是引擎的坑。
每次进入游戏的时候都会跑3个进度条,但是其中的问题就是每次读条都会有10%-20%的几率加载卡死,加载卡死就会导致什么问题呢?导致玩家一直在等这个加载进度条,而难以发现其实已经卡死了,每次遇到都得重新刷新一下游戏而避免开这10%的中奖率,所以带给了玩家很糟糕的体验。
之前的解决方案都是给加载界面添加一个提示语,让玩家不要一直等,多多刷新
这是一个很棘手的bug,几率性的触发bug,所以我并不能用传统断点查来源的debug方式去调试这个bug,因为可能这次是正常的,下次不正常,我也不知道我这次到底是正常还是不正常,只有跑出来结果才可以知道正常不正常。
所以传统简单方便的调试方式在这里就无效了。
最开始应该是做什么事情呢?当然是应该在白鹭论坛,看一看网友们和官方提示是怎么解决的。
根据官方提示把资源加载线程设置为1,不用多线程加载就可以避开这个bug。但是网友开发者们都表示这是无效的,没什么作用,当然我很早之前也测试了这个解决方案,没用,该卡照样卡。
这位就是官方,也同时给出了一个并不能解决的解决方案修改加载线程为1
RES.setMaxLoadingThread(1)
所以后面就正式的开始了这次debug之路。
首先我进行测试出现这个bug的频率,基本上每5次出现1次,频率算是比较高的。然后我就好奇想看一看加载卡住了,我直接关闭掉这个加载界面直接进游戏是什么样子的。
场景和邮箱系统变成了这样
背包系统呢?变成了这样。
正常情况应该是这样的
从这里我们就可以发现一部分事情,这里记为A0事件
- 一部分资源加载了,一部分资源没有加载,而且大部分资源都没有加载。
- 点击那几个背包的文字还是可以切换页面的代表游戏业务逻辑不受当前bug影响。
- 一部分放在res管理器中配置好的预先加载的资源都已经正常加载并且引擎已经使用。
- 已经正常加载进缓存的资源可以通过读缓存的方式使用该资源
基本上可以判定为,资源动态加载失效,进一步测试,发现eui资源即时加载也就是source属性失效,RES资源管理异步加载RES.getResAsync也失效,不管怎么加载返回都是undefined。
所以我们理一下逻辑。
- 加载完资源的时候>>>RES资源管理器卡死>>>不触发资源加载事件完成>>>卡死在加载页面
- RES资源管理器卡死+不触发资源加载事件完成>>>任何资源即时加载都失效>>>不触发资源加载事件完成
好了,这里开始就有一个疑点了,加载完成之前都经过了什么关键的事件,首先在出现bug情况的开始页面,前几次是可以正常即时加载资源的。也就是说开始页面的加载完成回调一点问题都没有,并且也可以进行资源组加载资源。
那为什么后面几次加载就出这种bug呢?所以我也和这个bug干上了,就要查出来。我先进行对自己代码检查。顶层代码是龙骨页面展示内容,没有一点问题。
再往核心看,看封装起来的我们自己的lib库框架。
正常的加载也没什么问题,不过资源加载是调用另一个核心库的。这时候我就开始怀疑了,是不是说,我们封装的这个库这里资源加载是用了一个资源组,而每次使用加载函数都是放一个资源进去,有一些研发不对应导致的问题,不管怎么样我都试一试,把这次的资源组组改成单个资源组加载,避免掉这种组处理bug,可能就会解决这次问题了。
然后我就把代码改成了这样的单独组的加载模式。
然后又测试了几遍,没一点用啊,bug出现的几率也没变,问题依然存在,那么这就表示和资源组处理导致的bug没关系,也就排除了我们后期写业务逻辑代码导致问题的可能性了。
所以我就继续深入架在egret引擎上的核心库。看看他是什么样的业务逻辑。
core核心库也表示和自己没一点关系,并且把锅直指egret引擎上的RES资源管理系统本统。到目前为止没有一点成果。所以我就应该换方向。多次触发试试。
弄成多次触发,看看是不是和资源加载名字,加载顺序,和重复加载,回调嵌套,以及作用域有关联,所以我写了一个这样的一个无限回调地狱屎山代码。
tubao.Appli.loading.play("preload", () => {
tubao.Appli.loading.play("scene5000", () => {
tubao.Appli.loading.play("scene5001", () => {
tubao.Appli.loading.play("scene5002", () => {
tubao.Appli.loading.play("scene5003", () => {
tubao.Appli.loading.play("scene1000", () => {
tubao.Appli.loading.play("scene1001", () => {
tubao.Appli.loading.play("scene1002", () => {
tubao.Appli.loading.play("scene1003", () => {
tubao.Appli.loading.play("scene1004", () => {
tubao.Appli.loading.play("preload", () => {
tubao.Appli.loading.play("scene5000", () => {
tubao.Appli.loading.play("scene5001", () => {
tubao.Appli.loading.play("scene5002", () => {
tubao.Appli.loading.play("scene5003", () => {
tubao.Appli.loading.play("scene1000", () => {
tubao.Appli.loading.play("scene1001", () => {
tubao.Appli.loading.play("scene1002", () => {
tubao.Appli.loading.play("scene1003", () => {
tubao.Appli.loading.play("scene1004", () => {
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
}, this);
然后又测试了几遍,没一点用啊,bug出现的几率也没变,问题依然存在,那么这就表示和资源加载名字,加载顺序,和重复加载,回调嵌套,以及作用域,没什么关系。
后面我做的第一步是什么,我确定下来有两个点,这两个是外面可以监听到的,所以我就这样先把加载过程都输出下来,包括每个资源加载的回调结果,每个资源组加载开始点和结束点确定下来。
结果是这样的
这张图告诉了我们什么呢?加载开始没问题,加载过程没问题,资源加载流程回调没问题,加载完成也没问题,手动关闭没问题,加载完成触发事件也没问题,问题是出在了那里呢?就出在了开始加载的时候。
综合起来前面的一大堆测试结果,又是一波推理分析。顺便整合一下。
- 只要结果任何一次资源组加载完成且开始第二个资源组加载的时候
- 那么必然就有
- 20%几率导致动态资源加载功能失效
- 因此导致
- 就导致没办法让第二个资源组加载资源
- 因此导致
- 加载卡死不动
第一个资源组已经加载完成且触发回调,卡死第二个资源组资源加载之前,没有加载第二个资源组中的任何资源的时候触发了bug,这代表了什么事情,已经第三次了,都说我写的代码没问题,是白鹭引擎底层那边有问题。
好,那我就进真正的底层去看一看。
为此我做了一些微小的工作,当然你不用做。
- 进白鹭引擎5.2.30版本核心部分源码中assetsmanager模块的复制下来
- 把编译管理文件egretProperties.json中的编译默认配置res管理器编译库的工作撤销下来。
- 进我的代码里面的lib库中把官方的res管理器代码删除
- 把刚刚复制的引擎源码核心粘贴到src编译目录中。
- 清理引擎,清理项目,处理掉wing 也就是vs code中编译缓存这些避免后面编译报错最终没办法开始愉快地玩耍。
直接去编译白鹭源码,直接看ts代码,直接下断点调,这样便于我后面的调试。顺便我直接就给白鹭源码写一些注释流程。
白鹭引擎资源管理模块有好几个接口类配置类这些,不太重要,首先我是做了一部分基础的测试和阅读代码,也有看官方文档参考,这部分比较无聊就不说了。比较核心的部分就在ResourceLoader里面。
后面阅读代码debug重点是正常和错误情况的对比、加载单个资源过程。
当然我也不可能预知到这次是成功还是失败,所以我准备了测试代码,流程就是
- 我先手动刷新
- 刷出错误情况和正确情况
- 依靠自带eval调试去这种方式调试
- 不过在此之前我必须关闭掉我框架的安全类,这个是关闭的,
没问题了。测试代码就是
RES.loadGroup("preload");
//资源组加载测试代码
RES.getResAsync("daodao2_png");
//单个资源异步加载测试代码
一部分无聊的调用过程就略过了,直接说比较关键的部分,这是加载队列
资源管理系统拿到加载信息列表,这里基本上已经分为了单个资源加载,最终所有类型方式的加载都变成单个资源的加载,这是没问题的
list是列表的资源组配置,groupname就是组名字,其他也不重要。
业务逻辑做的事情首先判断是否已经异步加载过这个资源,加载过就不进行加载直接返回资源给你就搞定了。否则就进行一波资源加载。
total表示资源加载的数量。并且下面进行整理资源组名字归类。等等操作,然后下面建立一个异步函数开始刚刚处理好的加载,其中监听加载完成和监听错误,加载完成回调resolve事件内容返回。然后关键的代码就是
后面我也测试了资源异步加载, 过程不描述了,无聊的各个层级回调,以及不同资源测试,最终绕来绕去都归到了这个函数里面,这个摸底基本上花了我3个小时的时间,所以现在的重点就是看loadNextResource这个函数的问题。
按照正常情况异步资源加载都应该返回资源本身就像这样返回一个纹理,或者mp3音乐,mp4视频。
但是他不管几次加载都返回undefined。看这里的代码问题。所以后面头脑就清净了很多,直接看这个关键的函数内容就可以了,也可以看见这个关键的函数被各个加载方式最终调用,前面不管遇到什么困难就都不用管了。
经过2天调试努力
下面就是正常情况和不正常情况区别开始了
我们可以看见一个循环,两个变量,那个函数当然也是关键函数了,不过在这里已经不重要,里面是具体加载过程,我们不用太专注的了解。但是我们需要好好了解这两个变量,先看结果
错误的情况是这两个变量为错误false状态一直没办法加载,两个值都是1,1<1,这显然是错误的,一直处于跳过状态
正确的情况是这两个变量一个数值是不断变化的,这样就导致可以产生为true,然后就可以运行这个关键的加载函数了
要说原理还是得看这个关键函数,关键函数里面干了一件对这次debug很重要的事情,那就是对loadingCount 进行递加和递减,递加完必定会有一次递减追平。最终趋向0,而thread必定又是一个固定的数字且必定极限要比loadingCount大,所以没问题。
那么下面揭秘这两个到底是一个什么变量吧,也是后面我才研究明白的,thread是我们用RES.setMaxLoadingThread设置的加载线程数,而loadingCount则是当前正在加载的资源数量。
这说明了什么呢?
想要修复无法即时加载资源的bug,和后面无法加载资源的bug,只需要将thread设置的大一点,也就是RES.setMaxLoadingThread设置的加载线程数多一点,这样就不会很容易卡死了,加载速度也更快一些。容错率更高一些
而官方告诉我们的加载卡死的解决方案是设置线程数为1,这必然就会导致资源加载隐患失误,一次资源加载这里没有减回去追平,就会卡死。这个几率可能是20%,如果我们按常规4次走呢?0.8%,不到1%的触发几率。
毕竟官方这么大引擎,一点点小的疏忽也是很正常的。
那么后面就是调试第二个问题,加载到90% 100%卡死的问题,不过这个bug我目前还没有开始测试,现在就开始测试了。
加载到90%卡死
首先开始测试触发几率,大概15次触发一次卡死。流程输出结果
从图中可以看到第一个加载没问题,回调没问题,问题出在第二个加载上,开始加载没问题,加载事件回调也没问题,但是只有加载到第6个的时候触发该bug,第七个无法加载完成,且没有回调,然后卡死,第七个加载的资源是什么呢
没有一点问题,资源配置没有问题,问题基本上指向了引擎本身代码。又是一次翻引擎的debug了。
那么第七个资源真的是没有加载完成吗?还是加载完成没有回调呢?来一波测试代码
RES.getResAsync("scene1002_10_png");
通过这个代码测试下异步返回包含结果就知道了,如果是texture纹理就表示资源加载完成但没有触发回调进度从而导致卡死,如果是undefined就说明资源加载中卡死。从而卡死在加载界面。
是资源加载完成但没有触发回调进度从而导致卡死。这表示了什么呢?资源加载过程没有一点问题就在触发加载流程回调时候出问题了。
所以我想还是先看一看loadNextResource是否在无尽加载循环中卡死。
直接下断点,没有进入debug的模式暂停,这就表示不是流程加载过程问题导致的卡死。而是在于一些其他方面,那么我对于loadingCount的值应该检查一波。如果loadingCount为大于0的数字,那么他就表示了这次加载卡死和线程加载过程错误有关,反之亦然。
我该如何拿到loadingCount呢?私有变量我们eval调试是拿不到的。
唯有再加载一个资源的时候才能看到,因为既然加载卡死了那么这个数据就一定会残留下来,不会自动重置,那么我们就再加载一个资源但是加载又有一个验证。加载一个没有加载过在缓存中的资源就可以进行验证这个问题了。而且也不用前面重置底层缓存逻辑内容,这样就麻烦了一点,那么我们开始,首先保持上面的断点,然后上测试代码。
RES.getResAsync("map5003_png");
我很清楚的知道map5003这个资源是绝对没有加载过的可以放心大胆的测试
很好,已经被debug暂停了
loadingCount竟然会是一个1而不是0,因前面动态资源加载失败的测试结论,这样固然就会导致第二个bug加载到99%卡死,意料之中。
但这又代表了什么呢?,我们必须剖析加载时业务逻辑,理清楚加载时处理。loadSingleResource核心加载函数干的事情。
继续挖~
所以后面我直接写注释在res资源管理模块。
dic中内容是
this.loadResource(r)传入加载正式加载。
阅读代码阅读到这里,有了一个很兴奋的成果了,感谢egret白鹭引擎有记录日志的好习惯,只要触发加载成功就会追平,同时删除加载时,默认情况dic是没有内容的,但是出错这个日志记录就有内容,所以我只需要看一下dic中的内容是什么就可以了,就知道是什么原因导致的加载卡死了。
就是这个mp3文件,写入名字是加载配置根目录加名字作为key的,我的资源加载目录中也没有这个文件,当然没办法加载成功了
但我需要关键验证的内容是什么呢?
这个真的是url加载目录名字吗?不一定,为什么不一定呢?根目录+文件名字肯定不是真正的文件所在的目录了,这里还是要感谢一下egret白鹭引擎,白鹭引擎的工程师把两个重要信息告诉了我们,第一个资源配置文件名字和资源名字告诉我们了。 所以下面我们整理一下逻辑内容:
1.加载的资源位于resource资源配置组
2.加载没有追平也就是加载过程中出现错误的文件是BGM_1002.mp3
我们知道了这个资源加载失败了并没有触发回调追平,而这个资源加载过程中发生了什么,我们不得而知。
这里的可能性有很多,网络波动,交换机失灵,更底层的网络bug,阿帕奇,nginx服务器的静态资源处理,底层问题。
这时候我们需要做的事情就是跑测试代码看一看这个加载失败的资源是否已经缓存入内存,如果缓存入内存表示是之前加载是成功的只不过没有触发回调罢了,加载过程没毛病这是回调部分出了毛病,如果返回是undefined则表示在开始加载的时候就有了问题。
上测试代码
RES.getRes("BGM_1002_mp3");
我们不用之前常用的RES.getResAsync,这样避免eval前端ide污染案发现场。
好的,很棒棒结果是undefined,于是乎有了,网络波动,交换机失灵,更底层的网络bug,阿帕奇,nginx服务器的静态资源处理,等等底层问题。
也证明了一件事情白鹭引擎研发团队有可能也是受害者。
目前距离真相越来越近了,但是很可惜我并不是太懂nginx这些...
目前进度貌似卡住,还是先看一看这个文件到底怎么样,是不是文件本身坏了
刚刚测试音乐没问题
让我们确定一下白鹭的服务器环境是什么,这个一般只需要一个http报文就可以确定下来,还是比较简单的,我在浏览器开黄虫子debug,然后监听网络请求。然后随便找一个资源去加载。
并没有结果,我们看一看错误请求,看上去是白鹭自己搭建的服务器环境。
所以我们扒开返回html内容看一看,我发现了错误请求中竟然有一个隐藏的div标记
让我们看一看这个svg图片是什么 ,直接把svg粘贴到一个文本文档中然后改名用html查看,ai ,false查看也可以但是没什么必要了。
svg内容就是这个。
这可能是表示白鹭引擎重写了这个服务器工具所以隐藏了这个错误提示。也有可能是我浏览器安装插件注入的内容。 这个分支基本上到尽头了,返回头看源码
所以我有了一个猜想,如果再加载一遍这个mp3文件会怎么样?通过异步处理加载sync,如果加载成功表示这次加载失败是一件底层的几率处理错误事件,如果加载失败就表示是我们源码处理问题,和底层没关系,和白鹭引擎有没有关系就看这一次测试了,上测试代码。
RES.getResAsync("BGM_1002_mp3");
这个一般需要跑两次,第一次加载,第二次回调
测试了3次 ,结果是undefined,表示是我们源码处理问题,和底层没关系,是我们和白鹭引擎这边的问题,为什么你就加载不了呢?在核心加载下断点调试
成功钓鱼上来
这个资源加载因为已经被记录入缓存。所以这里就直接从缓存取出来异步返回给调用者,而异步呢?里面是空的没内容是undefined自然返回undefined表示加载失败。
但是他为什么会把加载失败的存入缓存呢?,还有上面的scene1002很怪异,里面也是undefined,为什么名字不是按常理出牌根+文件资源名字而是去后缀名的文件名字。
这时候可能就需要更多常规的测试了,测试正常情况下缓存存入什么,和promisehash到底是什么内容。
强行翻译一波
看一看调用
初始为空, 写入点有两个。
正常加载操作写入
组加载中写入一波
基本上明白了,这个研究分支是错误的,有一个叫scene1002的资源组。这个只不过是加载资源组写入一下 。同时也是卡住的这个资源加载组。
我们回到原处继续分析。
有这个哈希记录了自然就不会再次的进行加载逻辑了,既然有了一次触发加载那么必然直接返回这个异步加载就可以了。但是这个异步加载是错误的,自然直接给你返回undefined。同时有一个删除点,这是关于组的删除点。
那么进一步的debug过程是怎么样的呢?
第一个是当前异步状态,第二个是当前异步值,这里表示是等待处理的状态,异步卡死
我把代码这部分核心的代码已经阅读翻译完成了 ,看红框的部分,只要异步加载成功就会处理追平,并且发出事件,删除dic,删除hash,发出完成事件。
打了一场太极拳,问题又从我们和白鹭引擎的原因回到了网络波动,交换机失灵,更底层的网络bug,阿帕奇,nginx服务器静态资源处理,等等底层问题。
刚好我的电脑剪贴板也出了问题,不能复制粘贴内容,这个很难受,也没办法备份,调试也差不多结束了,最终归结原因就是资源加载问题,我重启电脑后继续调试触发,用浏览器环境配合上黄虫子debug去纠出来加载错误原因,等等内容纠出来最终问题所在。
后面的debug计划是什么呢?按20%的触发几率在浏览器环境黄虫子debug下复现。然后看资源到底是成功加载还是没有成功加载。先写一波测试代码
console.log(this.loadingCount, this.itemLoadDic)
这行代码可以向我们展示当前加载状态。然后就是一直刷新游戏,刷出来bug。
但是我遇到的问题是我刷新游戏了40多次没有触发一次,是不是这个bug在真实用户客户端这里已经解决了。触发几率已经是小数点后两位以下了,也就是0.025%触发几率
这里说明了一个结果,那就是客户端问题导致的资源加载失败。bug原因打太极又推回到了我们和白鹭引擎这边,这也是没办法了,我也没办法预料是不是在其他类型客户端会复现出来,所以我写一个补丁代码,资源加载到98%就关闭加载界面。把几率拉到更低0.00000就可以了。
可能这就是最终的处理结果了。
皆大欢喜,两个问题基本上都解决了,写了差不多9000字,这可能是我写过最长的debug调试文章了。
我是千里马,也欢迎大家访问我们的兔宝世界文化创意产品兔宝世界文化创意IP
敬请关注兔宝核芯框架
我是千里马