产品优化不再盲目!一次MMO手游性能诊断全纪录

2017-05-03
文/侑虎科技

今天我们将以一款MMO手游为例,分享UWA技术诊断的全过程,希望能对大家的开发有所借鉴,同时也期待大家能从根本上逐渐掌握性能优化的思路,从此优化不再盲目。


我们将从优化的几大性能参数如CPU、内存、GPU出发,通过这些模块在UWA报告中展现的性能数据,结合报告独创的优化功能,为大家梳理出一条逻辑清晰、全面详实的优化思路。

◆◆◆◆CPU模块


1. 概览(定位CPU耗时的瓶颈)

我们先来看下CPU的平均耗时占用情况,在测试的十多分钟内产生了14539帧数据,其中帧数大于33ms的CPU耗时占比是44.2%,而一般我们推荐开发者将这指标尽量控制在10%以下来保证游戏整体的流畅度。


从上图中CPU走势上来看,战斗过程中有比较大的波动,下图是各大模块的耗时占比,可以看到占比较大的是渲染模块(42%),其次是脚本(39%),动画(4%),加载等。


值得大家注意的是,脚本模块的统计中不仅包含了逻辑代码的开销,还包括了UI模块的开销,因为NGUI、UGUI都属于脚本的开销。由于CPU模块中的渲染、脚本、动画模块开销比较大,所以后续我们将对这些模块进行比较详细的解读。

2. 渲染模块 & 粒子系统

从渲染模块的数据可以看到,半透明渲染均值为5.7ms,不透明渲染均值4.8ms,耗时加起来超过10ms,该值稍高,现在渲染模块的峰值是12W(属于偏高的级别),在中低端机器上会造成一定的压力。


在这里我们来看下半透明渲染和不透明渲染的耗时走势。不透明渲染的数值可以对应三角形面片,我们可以看到峰值处两者是比较一致的。一般来说,不透明物体的渲染优化需要简化场景中渲染面片量。


同时,我们还能观察到,多数情况下半透明渲染的耗时和Draw Call的走势接近。


对于半透明渲染优化,一般主要从两部分入手:粒子系统和UI模块,为此我们来看下粒子系统的数据。


上文我们截取了半透明和不透明渲染的耗时走势,下面我们就将其和粒子系统的走势做个对比。大致看到,紫色半透明和粒子系统的渲染曲线在很大程度上是吻合的;同时,我们也可以看到半透明渲染的耗时曲线在后续都高于10ms,由于粒子系统主要集中在3-4ms,所以我们可以大致判断出,渲染的问题很大程度上不是粒子系统的问题,具体分析可以见以下视频。


由上推断,我们不妨来看下UI模块耗时的走势。下图中,黄色线是NGUI中UIPanel.LateUpdate的开销,大致可以理解为NGUI在做网格重建和更新时的开销。正常情况下应该是稳定的,但是出现重建的时候会有峰值,在这个项目中有时候接近10ms甚至20ms。


一般来说,出现比较大的重建时,会对渲染模块的性能产生明显影响。所以通过UI模块和粒子模块的数据对比来看,我们基本上可以判定,渲染模块的开销主要集中在UI模块和粒子系统,相对来说,UI模块更多一点。针对这两个模块,从现在的数据来看主要优化UI模块,因为单从UI模块的数据上来看,6.1ms的CPU均值非常高,我们推荐控制在3ms。

另外需要研发团队考虑的一点是堆内存分配总值(143.8MB),主要是由NGUI网格重建所致,重建操作越频繁,该总值越高。


从截图画面来看,UI偏向于静态的面板,HUD类似于飘动的字体、血条的使用频率应该没那么高,所以10~20ms的开销不是很合理,需要研发团队对复杂的面板进行检测,尽量保证这些大的面板中没有频繁变动的UI元素。


注意:在UWA对某些项目进行深度优化的时候,会经常看到技能的面板,因为出现冷却的遮罩或者数字,引起了整个UI Panel的重建,导致较高的UIPanel.LateUpdate的开销。我们建议大家将消失和出现的UI元素从复杂的面板中独立出来,从而将网格重建的范围减小,也会对半透明渲染提升性能。

渲染模块的细节部分还可以通过UWA报告中代码效率的Camera.Render来看。


可以看到半透明渲染(Render.TransparentGeometry)占了40%,MeshRenderer.Render占了21%,拼合(ParticleSystem.RenderSingle)和没有拼合(ParticleSystem.RenderBatch)的加起来10%左右。所以半透明渲染这块,可以认为粒子系统占了10%,UI 和场景中半透明物件占了20%。就该项目的当前场景而言,其开销主要是UI界面造成的,所以,UI模块是当前半透明渲染的瓶颈,且具有比较大的优化空间。


现场提问


Q:空中的视野会影响渲染模块的性能吗?

A:这里的渲染面片数不是指所有场景游戏中的模型之和,而是看到的模型面数。一般来说,空中视角会比战斗的视角范围大很多,一般战斗中不会看到非常大的模型,所以三角面片不会很高。空中战斗的话压力就比较高了。

Q:UI的网格重建怎么理解?

A:大家可以通过我们官方博客上的一些技术推文和直播回顾对UI优化有些大致的把握,了解UI 网格重建会影响到UIPanel.lateupdate等机制等概念。比如说一个很小的元素,我仅仅改个颜色,或者隐藏了,就会导致整个UI Panel的重建,这样就造成更高的耗时。

Q:我们的策划要求每个UI都会动画,这样优化是不是就很难?

A:这是有可能的,因为从数据上来看,现在还是有很多不合理的地方, 我们可以看到图中大量动态的UI元素还是比较少,我们猜测应该是某些UI 元素在频繁消失和出现,导致整个UI Panel在重建,这里就需要大家自行去定位哪些UI Panel有问题了。

3. 动画模块

从报告中看到大家同时用到Animator和Animation两个组件,前者耗时均值0.7 ms,后者耗时均值为1 ms,偶尔有些峰值,但大部分都在合理范围内。


在动画模块的数据中,MeshSkinning的耗时有点偏高。比如上图这段区间内基本接近30 ms,看上去是和Boss战斗,推测是Boss的顶点数比较高导致,需要研发团队进一步确认。MeshSkinning的优化主要是通过降低顶点数的数量来优化。

动画模块的性能细节还能从UWA报告的代码效率查看,下图是Animation.Update的具体走势。


我们可以看到,在多数情况下都是1ms左右,偶尔有峰值,点开该场景下的堆栈信息按钮,我们看到详细的堆栈分配情况:


很明显,Animation.Update的耗时峰值主要由于Animation.RebuidInternalState 这个函数所致,该函数一般出现激活或实例化带有Animation组件的GameObject时,每次出现时开销都会比较高,避免的方法还是比较简单:我们现在看到大部分角色的缓存,我们都会禁用掉,但是如果开启禁用比较频繁,会导致这个函数经常出现。

一种比较好的优化方式是将怪物在隐藏或者放到缓冲池时,不是把GameOject的根节点禁用掉,而是把上面的Animation组件禁用掉(Enabled属性),激活时只需要开启Animation组件就可以,这样就可以避免这个函数的开销,从而降低这里的峰值。然后把一些逻辑等停掉,但是Animation根节点依然还在激活状态,不受影响。

Q:Animator也是有这个函数吗?

A:Animator.initialize和刚刚这个函数对应。这里的峰值也是因为带有Animator的组件做了SetActive(true)的操作,大家也可以后续做下检测。

4. GC

在关注CPU的时候,我们也会关注GC调用的情况。


GC的调用频率接近1000,目前看下来数据是比较合理的;另外GC的耗时在371ms,这个数值相对来说偏高。要优化CG耗时主要是通过两种方法:1)降低调用频率,即尽可能跑更多的帧再调用,2)降低GC每次的耗时。两种优化的方式不太一样,我们分别说明:

1)降低调用频率可以通过减少GC累积的分配数据来实现。我们可以跳转至代码效率-堆内存使用的页面。


这些是堆内存累积的分配量,堆内存分配越少,则GC触发也越少。可以看到, 排名前两个函数的累积分配加起来有200MB左右,这里有大量的提升空间。第一个函数UIPanel.LateUpdate()是NGUI的函数,优化的方法并不是通过优化代码,而是尽可能优化NGUI网格重建的频率;第二个函数UERoot.Update()是大家的主逻辑,需要通过代码函数去定位堆内存的分配,现在我们已经有Mono堆内存测试功能,大家可以通过Mono报告去看具体函数的开销,定位起来相对起来会容易得多(后文将详细说明)。总而言之,GC调用频率的优化主要通过这个面板去找累积堆内存分配最高的函数,一点点去优化。

2)降低GC的耗时可以通过优化堆内存的峰值来实现。GC调用一次的开销和堆内存里的对象有关,即对象越多、峰值越高,则GC越高。该项目接近100MB的堆内存峰值是较高的,我们推荐降低到40MB范围以内。

堆内存的峰值一方面影响内存的大小、另一方面影响GC的CPU开销,需要大家特别注意。

其他模块的性能较为正常,在此不多做说明。

◆◆◆◆内存模块

在UWA报告的内存模块来看,在测试的十多分钟内,内存峰值达到378MB,堆内存峰值达到92.6MB,都是属于比较偏高的,下面我们将通过内存模块的几大构成来逐一分析。


1. 堆内存

在堆内存的走势图中我们发现两个情况:首先是刚刚测试的时候就分配了68MB左右的堆内存,另一方面是堆内存的不断增长,最终达到了92MB。


关于前者,多数是配置文件占用的空间比较大,或者缓存机制所致;针对后者,在这里建议大家可以参考UWA的Mono报告中高堆内存留存函数列表。如下图演示,通过点击右边的“蓝色箭头”,可以查看某些函数中生成的驻留在内存中的详细变量情况,从而能更快地判断和定位堆内存的泄露点。


2.  资源内存—纹理

除了堆内存,我们再来看下资源内存。纹理的内存占用峰值为91MB,这个值在我们测试过的大量项目中看是属于偏高的。到底是哪些资源占了那么多的空间?我们可以跳转到UWA报告的具体资源信息中来各个击破。在这些资源的属性中,我们先来关注下数量峰值,如下图,我们可以看到不少资源的数量峰值出现了2、3等数值, 即相同的资源在测试包中出现了两份、三份。


同时,我们发现五页左右的纹理都存在冗余两份的情况,其中不少是100KB的纹理资源的情况,所以造成了不少纹理资源的浪费,建议大家通过UWA资源检测工具来查看AssetBundle中是否有冗余,接下来检测代码,是否有反复加载和反复Unload导致纹理依然残留的情况。此外,从纹理资源的格式上来看是比较正常的,基本上用了各个平台支持的格式。


3. 资源内存—网格

网格的内存峰值在24MB,相对合理,但从走势上来看也会有持续向上的趋势,数量稍微偏高。同样,网格资源也存在一定的冗余问题,如下图所示。


除了这个数量峰值,我们刚刚提到Color和Tangent属性,这两个属性对于大多数的Shader来说都用不上,所以需要研发团队进一步确认,是否有开启不必要的顶点属性。

Q:Tangent是否在NGUI中会被用到?

A:默认不会开启,最多生成normal。一般情况下是有color,但是没有normal和tangent。


4. 资源内存—动画资源

动画资源的数量也是比较多的,因此内存是明显偏高的。虽然存在一种可能,即如果大家采用了动画模块的缓存机制,的确会不断上涨,但建议研发团队也确认下这部分能否优化。毕竟报告中的数值相当高,足以引起大家重视了。


Q:为什么资源会有冗余的情况?

A: AssetBundle之间本身有冗余,它们分别被加载进来后就会产生冗余;卸载后再加载,也会产生冗余。

Q:这个无法达到没有冗余的吧?

A:可以做到零冗余,主要控制好管理的机制。1、避免AB本身没有冗余;2、通过管理的方式提前知道这个纹理是否已经被加载等。在UWA博客中有几篇相关的技术文章,大家可以参考下。

5. 资源内存—Shader

Shader的内存一向都是比较小的,这里主要还是看数量峰值,因为Shader资源数量会造成Shader.Parse函数的开销。在UWA报告的“重要参数解析”一栏中,可以看到频繁出现了56-60ms的峰值,这些都是Shader的解析造成。因此Shader的冗余可以理解成Shader.Parse的使用频率更多了。检查冗余的办法和上述的一样。


◆◆◆◆资源管理

在内存篇中,我们看到总体内存峰值为378MB,其中资源内存峰值将近227MB、堆内存的峰值92.6MB,那么剩余的将近60MB内存占用去哪儿了呢?这时,我们千万不能忽略这两大杀手:WebStream和SerializedFile。

1)WebStream内存占用

WebStream为Unity 5.3 以前版本的项目,通过特定API(new WWW、CreateFromMemory等)加载AssetBundle文件所开辟的较大块内存。主要用于存放AssetBundle的原始数据和解压后数据。

2)序列化信息内存占用

Unity引擎的序列化信息种类繁多,其中最为常见且内存占用较大的为 SerializedFile。在Unity 5.3之前的版本中,该序列化信息的内存分配主要为项目通过特定API(WWW.LoadFromCacheOrDownload、CreateFromFile等)加载AssetBundle文件所致。

对于这部分的优化,这就要结合UWA的另一项黑科技—资源管理。我们可以在该模块里看到加载资源时的总次数、加载方式、加载耗时等具体信息。


从表中看,AssetBundle加载的频率是比较一致的,基本上都是十几次,并且有一定间隔,但是大家可以注意到有些AssetBundle重复加载的频率比较高,如下图中连续三次加载,这可能就是存在一些问题,比如缓存时间过短等等。


另外可以通过UWA报告中的具体AssetBundle使用情况模块查看AssetBundle在内存中的驻留情况,如下图所示。大家在Unity 4.x 版本上用的是WWW,所以内存中具有一定的WebStream的占用,加载的AssetBundle比较多,WebStream也会较大。目前来看,AssetBundle最高值13个,在合理范围之内。


可以看到,资源加载主要是通过AB.load和AB.loadasync两个API,这个加载次数是比较合理的。


同时,我们也能通过该面板查看到具体的资源加载和卸载的情况:


资源实例化

做实例化操作的时候会有明显的开销,对于一些元素的SetActive的时候也会有开销。比如实例化的时候被操作了上百次,研发团队需要考虑是否在战斗中频繁出现,有这些元素存在的话,我们建议在关卡前做一次预加载,之后用到的时候通过缓冲池进行激活、禁用等等,来减少实例化的开销。

该项目SetActive操作比较高,总共有16万次的操作,我们看下频率较高的几个。


我们看到Skillicon元素 (蓝色线)在战斗过程中有持续的SetActive的操作,由于我们是每十帧汇总一次数据,所以每帧就会有5 个的调用,所以大家要特别注意这些元素,如果只是个空的Object,那么SetActive的开销是非常小的,但是如果这个元素身上带了些组件,大家需要确认下这些组件身上是否存在一些每帧都先禁用然后通过某些条件再打开的的一些操作,导致一些问题。

所以从资源管理的面板中,我们可以看到AssetBundle加载、驻留、资源实例化和激活等情况,帮助我们把加载部分做得更流畅。一旦资源加载和实例化发生在战斗中,那么峰值基本上是难避免,现在我们看到相对比较合理的方法是:实例化操作在战斗刚刚开始时候发生,然后随着战斗的时间慢慢加长,后面的实例化时间应该尽可能避免掉,利用缓冲池的方法等去优化。

◆◆◆◆GPU性能

最后,我们看来下该游戏在三星S6上的GPU的耗时情况。

1. Overdraw

我们主要关注填充倍数均值,4.0x 我们可以认为在测试的过程中,平均每一帧的像素会被填充4次,该值较高,一般我们建议把这个数值控制在3.0x左右。


虽然Overdraw总体问题不算大,但是我们也会通过曲线去找一下是否有比较高的地方。比方说某些区域的填充倍数会到18的情况,从对应的画面来看,可能是和BOSS战斗,或者进入一些区域的时候,看上去屏幕上的特效比较多,在峰值区域特效比较多,所以画面会比较亮,其实Overdraw较高在多数情况下就是因为半透明特效比较多、区域比较大所致。


另外, UI界面展开的情况下也会比较大 。这种情况下如果游戏时间比较长,GPU的负载比较高,导致发热比较快。所以Overdraw比较高的地方需要大家关注,特别是持续时间比较长的界面,类似UI界面(因为UI都是半透的,所以Overdraw会叠起来)。对此,我们一般建议减少UI和其背后场景的重叠,比如下图中UI后面的场景还在正常进行的(后面的人物和背景都看得到),大家可以考虑下能否在全屏UI出现后把相机关掉,这样的话可以减少不必要的Overdraw开销。


最后还有一些面积很大的特效,从截图上来看还好出现的时间比较短,所以优化的优先级略低。

以上就是该游戏的诊断内容,我们主要从性能的几大核心指标:CPU、内存、GPU三大模块反应的性能问题出发,通过数据报告的查看对比,整理出了一条较为完整的优化思路,希望能对大家的自身项目有所启发。也感谢该团队的分享,这也是鉴于我们相信这些数据的公开,能帮到更多游戏开发者省下优化的时间,将更多精力集中在游戏的开发和制作中去。

最新评论
暂无评论
参与评论