背景:
咱们的引擎是Egret,应用的是原生的EUI,转微信小游戏;
工程第一版进去后应用PerfDog测试一波数据。后果发现很多问题,本文次要分两局部

  • 第一局部次要介绍通过PerfDog发现问题,
  • 第二局部次要介绍通过PerfDog的数据定位并解决问题。

PerfDog具体操作能够看文档PerfDog应用阐明

第一局部————数据分析

本次的案例多见于游戏第一版时的状况,比拟常见,所以拿进去做个剖析。
这里强调一点。剖析问题须要整体数据联动剖析,独自看某繁多信息是没是意义的

第一次测试数据

FPS:

内存:

CPU:

论断:

1.咱们发现在战斗时FPS稳定较大
2.内存出现持续上升的趋势
3.CPU的APP Usage太小,仅占1%左右

首先针对问题3的阐明:
我之前抉择测试的是微信app,而小游戏是作为子过程而存在的,所以应该抉择PerfDog的子过程进行测试,这样失去的数据会更加的精准;下图的深色过程示意正在运行的顶层过程

针对这种多过程的利用测试:

iOS平台,APP多过程分为APP Extension和零碎XPC Server。
比方:某电竞直播软件用到APP Extension扩大过程(扩大过程名LABroadcastUpload)。当然也可能用到零碎XPC Server服务过程,如个别web浏览器会用到webkit。

Android平台,个别大型APP,比方游戏有时候是多过程合作运行(微信小游戏,微视等APP及王者光荣等游戏多子过程),可抉择指标子过程进行针对性测试。默认是主过程。如图王者光荣


具体的应用阐明能够看这里:PerfDog应用说明书

为了判断是什么导致的FPS稳定较大,也为了判断是否存在OOM,当初咱们来抉择子过程进行第二次测试;

第二次测试数据

测试数据组成:
为了验证我的一些猜测,也为了更粗疏的定位问题,咱们在测试过程中做了一些非凡操作:

1.战斗挂机 【为了判断是否是战斗过程中触发的内存泄露】
2.重复关上敞开UI 【为了判断UI创立与销毁是否存在内存泄露】
3.静止在某一UI页面 【为了与其余场景作辨别】
4.息屏挂机 【为了判断是否是由图像资源引起的内存泄露还是代码资源引起的泄露】
FPS数据:

CPU数据:

内存数据:

GPU压力山大

FPS与GPU剖析:

咱们通过FPS数据发现在游戏过程Jank非常重大,FPS稳定过于激烈,尤其是集中在UI开启或者敞开的时候,游戏来说,渲染画面,相对来说GPU可能呈现瓶颈,逐对GPU进行查看,这个时候咱们进行数据排查发现GPU的使用率也变得异样高,很显著渲染的压力很大,而咱们游戏UI关上时实际上战斗也会被渲染,这和咱们游戏的设计无关,所以渲染的压力很大。

内存剖析:

咱们通过PerfDog的数据发现内存是出现始终回升的状态,这样上来最终的后果就是被System Kill掉。其实当初曾经能够确定是产生了内存泄露,在72分钟的工夫里内存从726M到了956M,而且还在一直回升;

这里额定说下,看是否存在OOM不能只看PSS(PerfDog默认的memory是PSS),同样要留神VSS,有的游戏可能会存在PSS个别大小,VSS一直增大的状况,这也是不迷信的。
简略分享下常见内存指标关系

内存耗用
VSS - Virtual Set Size 虚构耗用内存(蕴含共享库占用的内存)
RSS - Resident Set Size 理论应用物理内存(蕴含共享库占用的内存)
PSS - Proportional Set Size 理论应用的物理内存(比例调配共享库占用的内存)
USS - Unique Set Size 过程单独占用的物理内存(不蕴含共享库占用的内存)
一般来说内存占用大小有如下法则:VSS >= RSS >= PSS >= USS

这里再略微介绍下安卓的LMK(Low memory killer),详细信息就不多赘述了。

1.Android零碎 会定时执行一次查看,内存达到某个值后,就会杀死相应的过程,开释掉内存。
2.每个程序都会有一个oom_adj值,这个值越小,程序越重要,被杀的可能性越低
3.Low memory killer 次要是通过过程的oom_adj 来断定过程的重要水平。oom_adj的大小和过程的类型以及过程被调度的秩序无关
4.阈值表能够通过/sys/module/lowmemorykiller/parameters/adj和/sys/module/lowmemorykiller/parameters/minfree进行配置

当初综合两次测试数据得出结论

论断:

1.FPS稳定过于激烈,很不稳固,尤其是在uI创立与敞开时候;
2.存在内存泄露,因为不论什么操作内存都始终涨,大概率是公共组件局部引起的
3.其实还有一些其余小问题,不过优先解决这两个

第二局部————问题定位

内存泄露问题剖析

有了PerfDog以上的数据,接下来咱们就要开始定位排查问题啦,

我的项目部分架构:

1.咱们的我的项目的基础架构是所有的根底性能都调用的同一份根底class(祖传代码),例如通信类等等;
2.咱们发现内存在始终回升,无论是角色在什么环境下,甚至是在息屏的时候内存也在回升,那么咱们其实能够大概率定位是我的项目外部的根底class外部出了问题;

接下来开始细细排查;

内存泄露排查

首先要先理解一些JS的内存管理机制

回收机制
JS中内存的调配和回收都是VM主动实现的,不须要像C/C++为每一个new/malloc操作去写配对的delete/free代码,JS引擎中对变量的存储次要是在栈内存,堆内存。内存透露的本质是一些对象出现意外而没有被回收,而是常驻内存。
GC原理
JavaScript虚拟机有一个特点,就是对象创立的开销远远大于对象计算的开销,并且对象创立会导致垃圾回收,而垃圾回收会导致游戏不定期卡顿。
在堆中查看无用的对象,把这些对象占用的内存空间进行回收。浏览器上的GC(Gabage Collection垃圾回收)实现,大多是采纳可达性算法,对于可达性的对象,便是能与GC Roots形成连通图的对象。当一个对象到GC Roots没有任何援用链时,则会成为垃圾回收器的指标,零碎会在适合的时候回收它所占的内存。

我这里应用的谷歌浏览器的Head Profiling,或者你也能够应用白鹭引擎的profiler:
应用很简略:

1.关上Google浏览器,关上要监控的网页,win下按F12弹出开发者工具
2.切换到Memory,抉择堆类型,选中Take Heap SnapShot开始进行快照
3.左边的视图列出了heap里的对象列表,点击对象能够看到对象的援用层级关系
4.进入游戏后拍下快照,关上某个界面,敞开界面,拍下快照
5.将新的快照转换到Comparsion比照视图,进行内存比照剖析
须要额定留神的是:
每次拍快照前,都会先主动执行一次GC,保障视图里的对象都是root可及的。GC的触发是依赖浏览器的,所以不能通过时时察看内存峰值而判断是否有内存透露。

咱们能够每隔一段时间来拍一次快照(因为公司我的项目起因,我就不展现实在我的项目了,此处仅作为教学):

咱们能够关上谷歌浏览器的内存剖析工具后有三个选项,咱们能够依据本人的调试形式交替应用;

1.Heap snapshot - 用以打印堆快照,堆快照文件显示页面的 javascript 对象和相干 DOM 节点之间的内存调配
2.Allocation instrumentation on timeline - 在时间轴上记录内存信息,随着工夫变动记录内存信息。
3.Allocation sampling - 内存信息采样,应用采样的办法记录内存调配。此配置文件类型具备最小的性能开销,可用于长时间运行的操作。它提供了由 javascript 执行堆栈细分的良好近似值调配。


这里举例应用堆快照剖析,

右侧查看详细信息

可见rect对象始终在增高,那么咱们能够查看一下导致rect对象未被开释的起因:

是因为Rect对象中存在一个属性rect始终被援用导致内存无奈开释,那么咱们到代码对应的地位去找,就能够较快的定位起因;最终咱们发现是因为在自定义的一个全局事件监听器中实例化了一个对象,然而这个对象的一些属性会继续被这个事件监听器所援用而不会被回收

当然为了更快的定位哪个函数,咱们也能够应用

个别后果是这个样子

Overview的HEAP(堆)曲线图示意JS堆。
Call Stack通常来说,垂直方向并没有太大的意义,仅仅示意函数嵌套比拟深而已,然而横向示意调用工夫,如果调用工夫太长,那么就须要优化优化了。录制后果的调用堆栈,横向示意时会呈现带有更多详情的浮窗间,垂直方向示意调用栈,从上往下示意函数调用。滑动鼠标滚轮能够查看某段时间的调用栈信息。把鼠标放到Call Stacks调用栈的某个函数下面能够查看函数详细信息。这个个别是性能优化时关注,对于内存透露,次要用于帮忙定位进行了什么操作。
Counter(计数器)窗格。在这里你能够看到内存应用状况(与Overview(概述)窗格中的HEAP(堆)曲线图雷同),别离显示以下内容:JS heap(JS堆),documents(文档),DOM nodes(DOM节点),listeners(侦听器)和GPU memory(GPU内存)。勾选或勾销勾选复选框能够将其从图表中显示或暗藏。

次要关注第三个的JS堆内存、节点数量、监听器数量。鼠标移到曲线上,能够在左下角显示具体数据。这些数据若有一个在继续上涨,没有降落趋势,都有可能是透露。
因为篇幅起因,这里不过多介绍这些工具的应用,网上有很多相干教程;

卡顿优化

咱们通过PerfDog的数据发现GPU压力很大,游戏来说,渲染画面久个别是drawcall过多,或者每次draw的工夫较长。

而咱们的游戏在查看在drawcall后确定是因为游戏运行时drawcall过多,导致每帧的渲染耗时比拟长,所以会出现一种卡顿的景象;
对于查看drawcall等能够通过白鹭本身的FPS面板查看 白鹭debug文档
在优化前首先要理解egret在渲染的一帧里做了什么工作内容

细分的话又能够分成

每一帧的工作内容:

1.执行一次EnterFrame,此时,引擎会执行游戏中的逻辑。并且抛出EnterFrame事件
2.引擎会执行一个clear。将上一帧的画面全副擦除
3.Egret内核会遍历游戏场景中的所有DisplayObject,并从新计算所有显示对象的transform
4.所有的图像全副draw到画布

当初来优化一下:
首先要升高drawcall:

1.把小图全都换成图集
2.实现文字合批,通过自定义字体,应用图片字体的形式代替原生的字体
3.动静拆散,将须要变动的和不变的别离放在不同的层级下,比方背景层、图标层和动态变化层
4.动画尽量应用dragon bones帧动画而不是spine 动画
5.应用cacheAsBitmap,把矢量图在运行时以位图模式进行计算

升高帧事件的开销:

1.不要的DisplayObject,间接removeChild 而不是设置他的visible属性为false,否则在第三步还会参加计算
2.不在主循环里创立任何对象,游戏中的人物、怪物、技能特效通通做成对象池
3.不在EnterFrame事件中做过多的操作,非要用能够自定义一些事件

咱们能够用以下的函数统计创立的gameobject的数量

它是显示了每一秒钟去拿一个hashCount跟上一个hashCount作比照,这个hashCount是由白鹭引擎外部 API,用于统计引擎对象的创立数量。如果游戏静止搁置不动,实践上hashCount diff的后果应该是0,实际上要尽可能管制在120以下,如果超标,只须要在引擎的 HashObject 的构造函数这里增加一个断点,在运行时去查看调用堆栈就排查就能够了。

查看PerfDog详情:https://perfdog.qq.com/?ADTAG...