玩过游戏的人经常会感叹:游戏为什么这么卡?手机为什么这么热?耗电为什么这么快?其实这些问题都是性能问题。一般一款游戏会同时在安卓和iOS两个平台上发布,有些游戏在iOS系统上运行非常流畅,体验和操作都很好,但在安卓各种设备上的表现却非常差。一款好的游戏能否在热门设备上流畅运行,能否给不同玩家带来同样畅快的体验,是决定游戏成败的关键。所以我们需要在低端机上尽可能的满足游戏体验,避免手机性能成为选择游戏的障碍。
那么什么是游戏客户端性能呢?简单来说,游戏客户端性能决定了你的游戏能否运行得更稳定、更长久、更快。那么什么样的手游更稳定、更长久、更快呢?我们一般可以看以下几个指标。这里我把这些指标分为两类,一类是越高越好,一类是越低越好:
越高越好:FPS
网络流量、CPU、内存(PSS、mono)、Drawcalls、三角形数量、功耗、包大小等越低越好。
这些指标看起来可能有些抽象,不太直观,那么在实际的游戏性能方面它们到底对应着什么呢?一般来说,当我们遇到游戏画面失真、闪退、卡顿、延迟等情况时,这些指标就出现了问题,也就是说,这些问题的本质就是内存过大、大量的 Drawcall、严重的 FPS 波动、CPU 占用过高、资源句柄泄漏等。因此,我们需要进行性能优化工作来解决这些问题,给玩家带来良好的游戏体验。客户端性能优化是一项技术要求特别高的工作,对游戏引擎、图形、计算机语言等都有一定的要求。网上也有很多相关的技术分享,介绍如何分析问题、定位问题、修改问题,但大部分文章都是程序员写的,站在程序员的角度看待客户端性能这个话题。那么,作为一名 QA,是否需要对性能负责?如何负责?负责什么?本文将尝试站在 QA 的角度,阐述我在客户端性能优化过程中的所见、所想、所为。也是对自己参与客户端性能优化工作的一次总结和反思,为以后的客户端性能优化工作提供一些指导。
大部分关于客户端优化的技术文章都会从内存、CPU、GPU的分类开始讲起,或者从逻辑层、渲染层讲解。作为QA,我们在性能测试中的主要职责应该是发现问题,在有能力的情况下进一步定位问题,然后向程序提供相关数据和站点,最后在程序修改完成后进行验收。因此站在QA的角度,客户端性能工作并不一定非要按照大部分技术文章那样划分,在这篇文章中,我将按照如下划分方式进行客户端性能测试:
静态客户端性能:顾名思义就是不运行游戏即可进行的性能测试,包括无用资源检查、重复资源检查、美术资源合规性检查等。
动态客户端性能:游戏过程中通过手动或自动化测试的方式收集数据,包括网络流量、CPU、内存、FPS、基本场景性能等。之所以将动静态数据结合起来,是因为有些性能开销是在游戏过程中才会产生的,比如同屏面板,有些模型(NPC)可能在接受某些任务后才会出现。
为了叙述方便,下文中很多地方我会以自己参与过的使用 Unity 引擎开发的手机游戏项目作为例子,大家可以借鉴并运用到其他商业或者自研引擎中。
1:静态客户端性能-无用资源检查
随着项目的进展,资源数量也会爆发式增长,资源管理面临极大挑战。如何找到无效、废弃的资源以减少包的大小是一件非常重要的事情。但这个检查有难度,没有一劳永逸的方法,需要工具和人力的结合。在我们的项目中,我们采用了以下三种方法来辅助:
方法一:读取prefab对应的meta文件,获取所有引用资源的GUID,最后计算出那些引用计数为0的资源,这些资源可能会被丢弃。这里要注意的是,游戏中动态创建的资源会被误扫描出来,所以这个扫描结果并不是很正确,需要进一步的人为确认。
方法二:有些特殊资源可以用专门的工具扫描,比如特效一般都是配置在规划表中(当然有些特效是直接在场景中的),我们可以通过扫描表中是否存在特效资源来发现无效的资源。同时我们也会分析资源的SVN提交时间记录,比如项目组处于快速开发期,最后修改日期在2年前的特效很有可能是无用资源。
方法三:在游戏过程中hook资源的加载。由于所有资源加载在代码底层都使用同一个接口,所以我们在接口中添加相关日志记录,并上传到指定端口进行采集。经过几周甚至几个月的数据采集后,我们将所有捕获的资源与目前SVN上的所有资源进行对比,就可以发现SVN上哪些资源还没有被使用过,这些资源很可能就是废弃的资源。
总之,检查无效资源并没有一个统一的标准,每个项目对于资源的使用方式都有自己的一套方式,所以需要结合自己的项目开发相应的扫描工具。
2:静态客户端性能-重复资源检查
主要目的和上面的检查相同,这里我们可以简单的通过比较每个资源的MD5值来检查,将MD5值相同的资源扫描出来进行去重。
3:静态客户端性能-美术资源合规性检查
病从口入,资源如同入口,如果资源出现问题,会引发一系列的性能问题,相反,如果资源优化得当,那么后续所有篇章的性能都会受益。美术资源的合规性检查,主要是根据之前和美术、策划、TA、编程约定好的各种资源标准,对美术提交的资源进行批量自动化检查。这里的检查分为两种,一种是before,一种是after。before是指在美术提交资源的时候进行检查,比如svn hook、unity import pipe等,不合规的资源禁止提交;after是指在指定的时间(比如每天凌晨)对项目中的美术资源进行合规性检查,对不合规的资源通过发送邮件或者即时通讯工具提醒对应人员进行修改。
美术资源合规性检查涉及的类别比较多,与具体项目密切相关,这里我主要列举一些比较常见的检查。
答:特殊效果
对于特效的合规性检查,我们使用了开源工具:,并在此基础上结合自己的项目做了一些修改和完善。我们会在一个空场景中批量播放所有特效,然后采集Batches、Triangles、粒子数量、OverdrawPixel(平均像素数量)、粒子系统数量、Factor(特效消耗因子)、特效图数量、特效图大小等数据。对于数据超标的特效,我们会提醒创作者进行修改。
B:纹理
纹理优化的目的是在保证视觉效果的情况下,尽量减少内存占用。同时,纹理的大小不仅影响内存占用,对加载时间也有很大影响。纹理资源的分辨率越高,加载越耗时,设备性能越差,耗时差异越明显。根据第三方统计,超大纹理是Top1局部资源问题。这种情况通常是因为一张大纹理实际只用到了一小部分;或者使用纹理时没有考虑实际显示效果。一张1024*1024的纹理的显示效果相对于一张512*512的纹理,可能并没有太大的感官提升,为了提升一点点显示效果,却要占用近4倍的内存空间。纹理加载到内存后,大小计算公式如下:
纹理内存大小 = 纹理宽度 * 纹理高度 * 像素字节数
像素字节 = 像素通道数 * 通道大小
基于以上原则,我们可以确定各种纹理检查规则,比如:纹理大小(不大于 1024)、是否开启 Alpha 通道、压缩格式是否为 ASTC、是否使用 2 的幂、尽量不使用纯色纹理(纯色可以通过 Shader 实现,可以直接省下这部分纹理的显存占用和计算时间)等等,然后通过定期扫描找出不合规的资源。虽然单个纹理大小的影响看似微不足道;但当这些显存和加载时间累积起来,最终就会影响到整体项目的性能。
与纹理相对应的另一个重要概念是纹理图集。图集就是一堆小尺寸的纹理元素组成的纹理图。图集可以减少 IO 负载,也可以减少 DrawCall。检查图集合理性的常用维度一般有三个:第一是图集的大小,图集的大小一般会限制图集的最大尺寸(一般不超过 2048x2048),如果超过则会拆分成多个图集,因为超过 2048 的图集可能会超出设备支持的最大尺寸,在某些低端机上造成兼容性崩溃;第二是图集中的空白空间,可能会有大面积的空白像素,如下图所示:
这种情况下,我们只能扫描出这张图集,然后让相关制作人员有针对性地调整纹理元素的布局或者大小,让合成后的图集尽可能占据有效的像素;第三是尽量保证每个界面只引用自己的图集和共享库图集,避免引用其他界面的图集,这样界面销毁的时候才能及时释放UI纹理,关于这方面的检查后面会在UI界面介绍。
C:模型
游戏中有各种各样的模型:主角、NPC、怪物、宠物、各种建筑等等,尤其在MMO游戏中,视野中会出现非常多的模型,如果堆积起来,单一模型的性能问题最终就会导致整个游戏客户端卡死。
不同模型根据重要性设定了不同的性能规格,我们会定期扫描模型文件检查是否符合要求,对于严重超标的,比如超过基准值10%,我们会督促制作者进行优化。这里的性能指标主要指面数和顶点数,面数很大程度上决定了整体显示效果的好坏,面数少的网格可能只能做到“心领神会”;而面数多的网格往往能做到“所见即所得”。下图是魔兽重制版与之前的模型对比。
我们可以发现,三角形数量其实是一把“双刃剑”:大量的三角形可以实现非常好的显示效果;但一旦三角形数量过多,势必会对项目的内存占用和加载时间造成相当大的压力,以及渲染性能开销。因此,在设计和制作模型时,需要考虑物体在游戏中的实际应用。比如作为远景背景的物体仍然按照近景设计的要求来制作,或者只是打酱油的NPC按照主角的美术要求来制作。这些都是不合理的,需要从一开始就避免。
这里需要注意的是,面数和顶点数对于模型来说是通用的检查,每个项目组都应该做这个检查。但是一个模型的性能不只是面数,还有很多其他的性能维度,而且要结合项目的技术实现,和程序一起定位性能的关键点,针对关键点增加一些自动化的检查。比如在我们的项目组,对于模型,我们还有以下检查:软布带组件数量、碰撞体数量、动画数量、UV数量、骨骼数量等。
D:UI界面
MMO游戏中有大量的UI界面,UI操作也是玩家的高频操作之一,所以UI性能也是客户端性能的重要维度。在我们的项目组,我们通过修改NGUI底层代码来获取UI所需的性能指标,然后结合自动化测试框架遍历所有UI并进行多次开启和关闭操作,从中获取数据并取平均值,最后与性能标准进行对比,并让相关制作人对超标的UI进行修改。这里我们获取的性能指标包括:UI面板加载时间、内存增长、DrawCalls、图集数量、纹理大小、渲染器数量等。
UI性能测试注意事项
1:一般的游戏中,我们都会将UI进行缓存,因此在多次打开UI获取平均值的时候,需要增加一个清除缓存的步骤。
2:有些UI,比如排行榜、组队等,在性能测试的时候需要我们增加一个预处理步骤,也就是填入数据。这样才能保证这些UI和玩家打开时的状态一致。如果没有数据或者数据太少,收集到的数据就不能反映真正的问题。
3:UI通常禁用MipMaps。MipMaps的原理是根据绘制对象在绘制空间的大小选择合适的纹理等级,这会增加30%的内存/显存开销。UI通常是等长等宽的,与相机距离无关,因此UI的MipMaps应该禁用。
还有一些其他的小checkpoint,比如一般不使用Unity默认shader(变体多,内存占用大),字体文件(ttf)的数量,遮挡剔除数据文件的大小(可能是因为烘焙的时候设置不正确,导致数据量很大),高低模一一对应(一般游戏为了适配高中低,会做两套或者多套不同规则的模型)等等。这些性能checkpoint大部分都是自己项目开发过程中逐渐发现的问题,然后总结不断增加的。
上述静态检查,归根结底是对资源的合规性检查。资源包括代码、策划数据、美术资源,而美术资源又占了性能的大头,所以绝大多数的检查都是针对美术资源的。在制定美术规范时,需要和美术、策划、编程紧密配合,根据项目具体情况给出合理的美术规范参数,并写入生产文档。在这个过程中,QA 需要非常积极主动,反思从 Bug 中增加检查的可行性,不断将测试左移,降低潜在的性能问题风险。除了美术资源,项目组一般还会对代码、策划数据做一些性能相关的优化和检查,比如:
1:利用开源库对代码进行静态扫描,发现一些明显的漏洞或者语法热点
2:常规的编码习惯,比如把每帧需要处理的数据缓存起来(更新),运行时少使用GetComponent接口遍历等。
3:删除无用的计划数据;优化稀疏计划数据
4:合理使用预处理,着色器预加载,模型预加载等。
上述代码级的优化工作通常来源于两个方面:一是来自于过往的经验,二是发现游戏中具体的性能问题后,有针对性地优化相关代码。过往的经验固然重要,但发现性能问题再伺机而动更靠谱。而在这个过程中,QA 最重要的工作就是发现问题并跟进,越早发现性能问题,越能帮助程序定位和解决问题。同时,QA 还需要将性能测试作为一种常规的测试行为,监控各项性能指标的变化,从趋势中发现异常,从而有效追踪版本性能。那么如何才能有效发现游戏运行过程中的性能问题呢?
动态客户端性能—基本性能指标
基本的性能指标一般是指FPS、内存、CPU使用率、功耗、网络流量等。游戏过程中,数据是通过第三方工具,第三方SDK,或者是游戏自带的性能采集接口来获取的,下面我会介绍一下如何获取和使用环境。这里有一个注意点,Unity Profiler给出的数据和Android系统的数据(adb dumpsys meminfo)差别比较大,Profiler里看到的内存是Unity自带引擎看到的内存分配,如果引擎使用了第三方库,我们就不能统计库分配的内存了。同时,Profiler不会统计缓存的物理内存,而Xcode的Instrument或者Android的PSS会记录这部分内存。所以两个数值会有一些差别。
我们使用了 4 种工具:
1:PrefDog。腾讯的一款性能测试工具,可以非侵入式地收集游戏的各项性能指标。不过我们公司已经开发了类似的工具,所以改用自己的工具。如果贵公司没有类似的产品,建议使用腾讯的这款,非常好用。
2:UWA。UWA 是需要在打包时植入的侵入式 SDK,是付费项目。它分为四个维度进行性能测试:整体性能、mono 堆内存分析、运行时资源检测、lua 性能分析。相比于 PrefDog,UWA 在深度和广度上要先进很多,建议线上项目都使用它来检查相关性能指标。
3:Unity自带的Profiler和Stats。这两个工具是Unity内置的工具,随时随地使用都非常方便。
4:游戏中定义的数据采集接口,比如当前帧中的粒子数量,单位数量,动画组件数量,lua内存数量等。
我们一般结合以下游戏场景使用这些工具:
1:进入游戏前半小时性能数据采样
新手关卡是游戏呈现给玩家的第一印象,除了游戏内容、画质之外,性能也是决定玩家留存的重要因素,因此对新手关卡进行基础性能数据采样十分必要,下图是工具在前半小时内采集到的一些性能指标。
2:结合自动化测试框架,在自动化测试过程中,会自动采集相关数据,最后形成曲线,和历史数据进行对比。这里采集数据的时候,可以每隔1秒甚至更长时间采集一次,而不是每一帧都采集。我们更关心的是趋势、平均值、峰值。
3:特定模块采样
对于多人玩法、核心玩法、公共场景、角色出生地等重要场景,会进行专门的客户端性能测试。测试时QA会使用其中一个客户端进行数据采集。测试方式一般有两种:
第一种是多个QA一起进行的手工测试,这种测试方式通常用于多人游戏(例如5人左右),通过多个QA的协作,可以构建多人游戏的极端场景,比如多个玩家聚集在一起,同时使用技能。
第二种方式是通过机器人模拟大量玩家,一般用在公共场景、角色出生地、团战等QA人员数量无法满足需求的场合,通过给机器人设置合适的脚本,模拟玩家的正常操作,同时使用其他真实客户端,收集此时的客户端性能指标。
从上面的描述中我们可以看出,监控基础性能指标的主要目的是在日常开发中尽早发现可能出现的性能问题,以便程序端通过提交记录找到性能问题的原因。虽然从基础性能指标中我们无法看到性能问题的原因,但是可以帮助 QA 发现问题,这是性能优化过程中最重要的一步。当发现可能出现的性能问题后,QA 和程序端可以进一步利用更有针对性的、更专业的工具来定位性能问题的具体原因并进行优化。
动态客户端性能—内存
上面提到的基础性能中,还包括内存指标,这里需要说明一下,因为相对于其他指标,内存对于手游来说尤为重要,闪退、卡顿一般都和内存占用过大有关,QA一般会关注内存的三个方面:内存过大、内存泄漏、内存未卸载。
1:内存过高
当内存占用过高时,游戏很容易因为 OOM 而崩溃。同时,有些手机操作系统会监控各个 APP 的内存使用情况,当发现某个 APP 占用内存过多时,会主动移除该 APP。内存占用的一般安全线如下图腾讯 2019 年统计报告所示,其中 PeakFootPrint 代表内存使用情况。在基础性能数据采集过程中,当发现内存占用超标时,我们会记录一些辅助信息,比如当前玩家状态、屏幕状态等,帮助我们后期定位内存占用过高的原因。
除了上面通过长时间运行游戏对高内存的数据曲线监控外,我们还对某些时刻的内存进行分析,比如PVP结束、公共场景待机、战斗等,在这些时刻使用Unity的profiler-memory
并且从顶部回头看一些内存占用高的资源是否合理。比如上图中我们发现某个纹理的内存占用过大,于是对其进行了详细分析和优化。以下是前后结果对比:
再比如,我们通过这种方式发现防止游戏闪退的软件,在一个非战斗场景中,主角也加载了所有的战斗动作,这也是非常不合理的。
2:内存泄漏
如果游戏越来越卡,甚至崩溃,那么很有可能你的游戏出现了内存泄漏,这是一个非常严重的问题。那么 QA 如何才能发现此类问题呢?一般来说,监控内存泄漏有两种方式:
第一种方法类似于服务器压力测试的方法,长时间运行游戏并收集数据,最后观察内存曲线是否一直呈上升趋势。这个长期运行既包括自动化测试,也包括手动测试。
第二种方法是对每个模块单独进行测试。将每个模块分开,对每个模块进行内存泄漏测试。在启动被测模块之前,记录初始内存数据,然后多次运行被测模块(通常超过 3 次),最后检查曲线是否持续上升。如果持续上升,则表示该模块可能有内存泄漏。
3:内存未卸载
在上面提到的第二种方法中,有时候我们会发现内存曲线并不是呈上升趋势,而是相对于初始内存大小有或多或少的增加,这种情况下就需要进一步分析内存增加是否是由于没有及时卸载导致的,这里推荐两个工具:
a:内存分析器。
通过Unity的PackgaeManager下载此工具,通过此工具拍下内存快照之后,可以离线分析各个资源在内存中的占比情况。
B:Unity自带的Profiler
选择 Unity 内置 Profiler 中的 Memory 列,并从 Simple 切换到 Detailed,以捕获某一帧的当前内存快照。
通过以上两个工具,我们可以对比两个时刻的内存快照,找出新增内存的具体内容,评估是否合理,是否可以及时卸载。
动态客户端性能 - 场景性能
我所负责的项目是一个包含众多游戏场景的游戏,单个游戏场景包含大量物体。场景的性能对最终游戏的客户端性能有重大影响。如何监控和测试场景的性能是 QA 的重要工作。针对场景性能,我们主要进行了三类测试:
1:当一个场景完成后,我们会首先对场景资源做一个初步的统计,给出该场景的基本资源数量,如下图所示。
2:游戏过程中,玩家会涉及大量的场景切换,场景加载时间的长短对玩家的体验至关重要。我们将同类型游戏的场景加载时间进行对比,并给出一个粗略的对比图表,如下:
3:基于自动化测试框架,我们运行游戏,控制主角移动到场景中各个坐标点,实时采集主角所在坐标点的性能数据,对每个坐标采集水平方向4个方向的数据并取最大值,最后形成可视化图标。性能数据包括同屏面数、同屏顶点数、DrawCalls。为什么选择4个方向呢?因为每个点的性能数据和LOD有关,而LOD和镜头的视角有关,所以这里简单选取4个方向。
第一张图是2D图,颜色越深代表数值越大,x、y对应地图的坐标。第二张图是在Unity编辑器的Scene视图中生成的,可以更直观的看到地图中各个点的性能指标。最后将各个坐标点的性能数据与之前确定的基准值进行对比,如果超过10%,就会督促美术编辑进行修改。
一般来说,地面编辑器可以通过模型缩减、LOD配置、模型合并,甚至物体摆放位置调整等方式达到优化目标。但也存在地面编辑器使用各种优化手段后无法达到既定性能指标的情况。这里主要问题就是DrawCall达不到目标,面数和顶点数没问题。这时候我们通常会组织程序来参与场景的优化。这里介绍一个程序常用来做场景渲染优化的工具:frame Debug。
frame Debug 是 Unity 自带的一个工具,通过 frame Debug 可以精准的看到某一帧的渲染顺序,从而发现一些对当前画面没有贡献的 DrawCall,程序就可以顺着线索定位到如何移除这些 DrawCall。
The above is a summary from the perspective of QA during the client performance optimization process. The essence of optimization is not to render or to render less or to render in a more economical way, which involves a lot of professional knowledge, such as batching, occlusion culling, LOD, GPU Instance, vertex animation, description, real-time shadows, etc. QA must actively learn various theoretical knowledge to better communicate with the program and TA to prevent being confused after hearing some terms. During the performance test, I also found several points worth your attention:
1:在绩效测试中起着非常重要的作用。问题是性能测试的关键。
2:在某些游戏中,加强质量检查的绩效测试非常重要,这与某些功能本身更为重要。随着时间的流逝,即使您想改善硬件配置,内存最终将耗尽,并最终导致系统崩溃。
3:绩效优化不是一次性的工作,但在项目的每个阶段都有绩效问题,只有基于用户的经验来抛光产品。不能同时实现运行空间和时间。
4:我们必须清楚地表明,游戏性能优化不仅是程序员的工作,而且是质量保证,计划和艺术的联合工作防止游戏闪退的软件,我们无法使用特别强大的优化技术来恢复性能,并且只有一起工作才能不断地优化性能并不断地改善玩家的游戏经验。