关于人工智能:实战PerfDog优化小游戏性能

39次阅读

共计 4784 个字符,预计需要花费 12 分钟才能阅读完成。

背景:
咱们的引擎是 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…

正文完
 0