移动游戏客户端性能测试探索
想必玩过游戏的人会经常感叹:游戏怎么那么卡?手机怎么那么烫?耗电怎么那么快?其实这些问题都归结于性能问题,。一般情况下,一款游戏都会在Android和iOS平台上同时发布,有些游戏在iOS系统上运行时流畅度非常高,用户体验和操作都很好,但换到种类繁多的Android设备时表现却是非常不好。一款好的游戏是否能在大众的设备上流畅运行,是否能给不同的玩家相同的畅快体验,都是决定游戏成败的关键。因此,我们需要尽可能多的满足在低端机器上的游戏体验,避免手机性能成为游戏选择时的壁垒。
那么游戏客户端性能是什么呢?简单来说,游戏客户端性能决定了你的游戏能否跑的更稳,跑的更久,跑的更快。那么什么样的手游才是更稳,更久,更快的呢?我们一般可以看这样以下指标,这里我将这些指标分成两类,一类是越高越好,一类是越低越好:
越高越好:FPS
越低越好:网络流量、CPU、内存(PSS、mono)、Drawcalls、三角形数、耗电量、包体大小等
可能光看这些指标有点抽象没有直观的感受,那么对应到游戏的实际表现是什么呢?一般情况下,当我们遇到游戏花屏、闪退、卡顿和延迟的时候,就是这些指标出现了问题,即这些问题的本质是内存超标、Drawcall数量多、FPS波动严重、CPU占用居高不下、资源句柄泄漏等。所以我们需要开展性能优化工作来解决这些问题,给玩家带来好的游戏体验。客户端性能优化是一项技术要求特别高的工作,对游戏引擎、图形学、计算机语言等都有一定的要求,网络上也有很多相关的技术分享,介绍如何分析问题、定位问题和修改问题,但大部分文章都是出自程序员之手,从程序员的角度去看待客户端性能这个课题,那么作为一名QA,需不需要对性能负责?如何负责?负责什么呢?本文将试图从QA的角度阐述在客户端性能优化过程中的所见、所想和所做,同时也作为自己参与客户端性能优化工作的一个总结和反思,为以后的客户端性能优化工作提供一些指导。
大多客户端优化的技术文章都会从内存、cpu和gpu的分类去展开,或者从逻辑层和渲染层说明。作为QA,我们参与性能测试工作的主要职责应该是发现问题,有能力的情况下,进一步定位问题,然后提供相关的数据和现场给程序,最后程序修改完,我们再进行验收,所以从QA的角度来看待客户端性能工作,不一定非得按照大多数的技术文章那样来划分,在这篇文章中,我将按照如下的划分方式来开展客户端性能测试工作:
静态客户端性能:顾名思义,就是不需要运行游戏就可以开展的性能测试工作。包括包体无用资源检查、包体重复资源检查、美术资源合规检查。
动态客户端性能:在运行游戏过程中收集数据,可以是手动运行游戏过程中,也可以是在自动化测试过程中收集,包括网络流量、CPU、内存、FPS、场景基础性能等。之所以需要动静结合,是因为有些性能开销只有在游戏运行过程中才会产生,比如同屏面片,有些模型(NPC)可能只有在接了某些任务才出现。
为了方便描述,下面很多地方我会以自己参与的采用unity引擎开发的移动游戏项目来举例,可举一反三用到其他商业或自研引擎中。
1:静态客户端性能—无用资源检查
随着项目的研发进度,资源数量也会爆发式地增长,资源管理面临着很大的挑战。如何查找无效、废弃的资源,从而减少包体大小是一件很重要的事情。但这块检查的难度比较大,没有一个套之皆可的办法,需要工具和人肉互相结合。在我们项目中,我们采用过如下3种方法辅助:
方法1:通过读取prefab对应的meta文件,获取所有被引用资源的guid,最后计算出那些被引用计数为0的资源就可能废弃资源。这里要注意的是,游戏内动态创建的资源是会被误扫描出来了,所以这个扫描结果不是非常正确,还需要进一步的人肉确认。
方法2:一些特殊资源可以用特定的工具进行扫描。比如特效来说,一般特效都是配置在策划配表中的(当然也有一些特效直接在场景中),我们通过扫描特效资源是否在存在配表中,可以得出无效的资源,同时,我们也会配合该资源的svn提交时间记录分析,比如项目组在快速开发期,那么最后修改日期是2年前的特效,很可能也是无用的资源了。
方法3:在游戏运行过程中,对资源的加载进行的hook。因为所有的资源加载,在代码底层是同一个接口,我们通过在该接口中加入相关的日志记录,并上传到指定端口进行收集。这样经过几周,甚至几个月的数据收集,我们将所有捕获的资源和目前svn上的所有资源进行比对,可以得出svn上哪些资源是一直没有被用到的,那么这些资源很可能就是废弃资源。
总之无效资源的检查没有唯一的标准,每个项目对资源的使用方法存在特殊性,所以需要结合自身所在项目,开发工具进行扫描。
2:静态客户端性能—重复资源检查
主要目的和上述检查一样,这里我们可以简单的通过比较每个资源的MD5值来检查,扫描出MD5值一样的资源进行去重。
3:静态客户端性能—美术资源合规检查
病从口入,资源好比是入口,它们若出现问题,会引发一连串性能问题。相反,资源若是优化得好,后面所有章节的性能都可受益。美术资源合规检查主要是根据先前和美术、策划、TA、程序商定好的各种资源标准,对美术提交的资源进行批量自动化检查。这里的检查有两种类型,一种是事前,一种是事后。事前是指在美术进行提交资源的时候进行检查,比如svn hook、unity导入管线,对于不合规的资源禁止提交;事后是指定时间(比如每天的凌晨)对项目工程内的美术资源进行合规检查,对于不合规的资源通过发送邮件或者及时通讯工具提醒对应人进行修改。
美术资源合规检查涉及的种类繁多,和具体项目息息相关,这里我主要列举一些比较通用的检查。
A:特效
对于特效的合规检查,我们使用了开源工具:https://github.com/sunbrando/ParticleEffectProfiler,并在这个基础上结合自身项目进行了一定的改造和完善。我们会在一个空场景中批量播放所有的特效,然后收集Batchs、Triangles、粒子数、OverdrawPixel(像素平均数)、粒子系统个数、Factor(特效消耗因子)、特效贴图数量、特效贴图大小数据。对于数据超标的特效,提醒制作者进行修改。
B:纹理
纹理优化的目的是在保证视觉效果的基础上,让它们占用的内存尽量的小。同时纹理尺寸大小,不单单影响到内存上的占用,对于加载的耗时也有较大影响,纹理资源的分辨率越高,其加载越为耗时,设备性能越差,其耗时差别越为明显。根据第三方统计,尺寸过大的纹理是本地资源问题中排名Top1的。造成这种情况一般是一张大型纹理中,实际使用的只是这张大型纹理中的一小部分;或者是纹理使用上,没有考虑到实际的显示效果,一张1024*1024纹理的展示效果,可能相较于512*512纹理的展示效果,并没有多大感官上的提升,为了些许的展示效果提升,却要耗费将近4倍的内存空间。纹理加载进内存后,大小计算公式如下:
纹理内存大小=纹理宽度*纹理高度*像素字节
像素字节=像素通道数*通道大小
根据上述原理,我们可以确定纹理的各项检查规则,比如:纹理尺寸(不要大于1024)、是否开启Alpha通道、压缩格式是否为ASTC、使用2的n次幂大小、尽量不要使用纯色纹理(纯色可以通过shader实现,这部分纹理的内存占用和计算耗时就可以直接省下来)等,然后通过定期扫描发现不合规的资源。虽然单个纹理尺寸大小带来的影响,可能显得微不足道;但当这些一点一滴的内存与加载耗时累积起来,最终都会作用到整体项目的性能表现。
纹理对应的还有一个比较重要的概念就是纹理图集。图集就是一堆小尺寸纹理元素合成的纹理贴图。图集可以降低IO加载次数,也可以减少DrawCalls。对于图集的合理性检查,一般有3个比较通用的维度:第一是图集的大小,一般限制图集的最大尺寸(通常不要超过2048x2048),如果超过,就分拆成多张图集,因为超过2048的图集,有可能超出设备支持的最大尺寸,造成一些低端机上出现兼容性的崩溃问题;第二是图集的空白,有可能出现大片的空白像素,如下图:
针对这个情况,我们只能扫描出来这种图集,然后让相关制作人针对性地调整纹理元素的布局或尺寸,使得合成的图集尽可能占满有效像素;第三是要尽量确保每个界面只引用到自己的图集和共享库图集,避免引用到其它界面的图集,这样可以在界面销毁时可以及时释放UI纹理,针对这个的检查,会在后面的UI界面中介绍。
C:模型
游戏中存在各种模型:主角、npc、怪物、宠物、各种建筑物等,特别是MMO类型的游戏,视眼内模型会存在很多,积少成多,单个模型的性能问题最终会导致游戏客户端整体卡顿。
根据重要程度不同,对不同的模型制定不同的性能规范,我们会定期扫描模型文件,检查是否合规,对于严重超标,比如超过基准值10%的,督促制作者进行优化。这里的性能指标主要指面数、顶点数。面片数的多寡,很大程度上决定了整体展示效果的优劣。面片数少的网格,可能仅能做到“只可意会”;而面片数多的网格,则往往能够做到“所见即所得”。下图是魔兽争霸重置版的模型和之前的对比。
我们可以发现,三角形面片的数量其实是把“双刃剑”:大量的三角形面片能够达成非常好的展示效果;可是一旦三角形面片的数量过大,则势必会对项目的内存占用和加载耗时,以及渲染方面的性能开销上造成不容忽视的压力。所以在设计和制作模型的时候,要考虑到物体在游戏内的实际应用情况,比如作为远方背景的物体,依然以近景设计的要求制作,或者打酱油的npc以主角的美术要求来制作,这些都是不合理的,需要从一开始就避免。
这里需要说明的是,面数、顶点数是针对模型的通用的检查,各个项目组都应该要做这个检查。但模型的性能,不单单是面数,还有很多其他的性能维度,应该结合自身所在项目的技术实现,和程序一起定位性能的关键点,针对关键点,加入一些自动化检查。比如在我们项目组组,对于模型,我们还有如下的检查:柔布飘带组件数量、碰撞体数量、Animation数量、uv数量、骨骼数等。
D:UI界面
MMO类型的游戏存在大量的UI界面,对UI的操作也是玩家的高频操作之一,所以UI性能也是客户端性能的一个重要维度。在我们项目组,通过对底层NGUI的代码的修改获取UI所需的性能指标,然后结合自动化测试框架,遍历所有UI并进行多次打开关闭操作,从中获取数据后取平均值,最后对照性能标准,对于超标的UI让相关制作者进行修改。这里我们获取的性能指标包括:UI面板加载耗时、内存增长、DrawCalls、图集数量、纹理大小、renderer数量。
UI性能测试需要注意的点
1:一般游戏中,我们都会对UI进行缓存操作,所以当多次打开UI取平均值的时候,需要增加一步清缓存的操作。
2:一些UI,比如排行榜、组队等,需要我们在性能测试的时候,增加一步预处理的过程,即填充数据。保证这些UI和玩家打开的时候是一样的情况,如果没有数据,或者数据过小,都会让采集的数据无法反映出真实问题。
3:一般UI都会禁用MipMaps。MipMaps的原理是根据绘制对象在绘制空间的大小选取合适的纹理层级,它会增加30%的内存/显存开销。而UI通常都是等长等宽的,跟摄像机距离无关,所以要禁用UI的MipMaps。
还有一些其他的细小的检查点,比如一般不采用unity默认的shader(变种多,内存占用大)、字体文件(ttf)数量、遮挡剔除数据文件大小(可能因为烘焙时设置不对,造成数据很大)、高低模一一对应(一般游戏为了适配高中低,会做两套以上规则不同的模型)等,大部分这些性能检查点,都是在自身项目开发过程中,逐步发现的问题,然后加以总结后,不断增加的。
上述这些静态检查,归根到底是资源的合规性检查。资源包括代码、策划数据和美术资源,而美术资源是性能占比的大头,所以绝大多数都是针对美术资源的检查。在制定美术规范时,我们需与美术、策划、程序,TA通力合作,结合项目具体情况,给出合理的美术规范参数,并撰写成制作文档。在这个过程中,QA需要很强的主动性,以及从bug中反思增加检查的可行性,不断把测试左移,减少产生性能问题的潜在风险。除了美术资源,针对代码和策划数据项目组一般也会做一些性能相关的优化和检查,比如:
1:利用开源库对代码做静态扫描检查,发现一些明显的漏洞或者语法热点
2:常规的代码编写习惯,如对每帧(update)需要处理的数据进行缓存、运行时少用GetComponent接口遍历等
3:无用策划数据删除;稀疏策划数据的优化
4:合理利用预处理,shader预加载、模型预加载等
上述这些代码层面的优化工作,通常来源有两方面:一方面是来自过往经验,另一方面是发现游戏中某个具体的性能问题后,针对性的去优化相关的代码。过往经验固然重要,但发现性能问题,再伺机而动更为可靠。在这个过程中,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一般关注内存的3个方面:内存过高、内存泄露、内存不卸载。
1:内存过高
当内存占用过高时,游戏容易因为OOM出现闪退现象,同时有些手机操作系统会监控各个APP的内存使用情况,当发现某个APP内存占用过高时,会主动移除该APP,一般内存占用的安全线如下图腾讯2019年的统计报告所示,其中PeakFootPrint就是代表内存占用。在基础性能数据采集的过程中,当发现内存占用超标时,我们会记录下一些辅助信息,比如当前的玩家状态、画面情况等,帮助我们后期定位内存过高的原因。
除了上述通过长时间运行游戏后获取的数据曲线监控内存过高之外,我们也会对某些时刻下的内存进行分析,比如pvp结束、公共场景待机、战斗中等,在这些时刻下使用unity的profiler-memory
并从高往后看,查看一些内存占用高的资源是否合理。比如上图中,我们发现某个贴图的内存占用偏大,然后具体分析并进行优化,以下是前后的结果对比:
还有一个例子,我们通过这种方法发现在一个非战斗场景,主角也加载了所有的战斗动作,这也是非常不合理的。
2:内存泄漏
当游戏越玩越卡,甚至最后直接闪退了,那么很有可能是你的游戏存在内存泄漏了,这是非常严重的问题。那么QA该如何发现这类问题呢?一般而言,我们有2种手段去监控内存泄漏:
第一种:类似服务器压测的方法,长时间运行游戏并采集数据,最后观察内存曲线是否一直处于上升趋势。这里长时间运行,包括自动化测试和手工测试。
第二种:分模块测试。将各个模块拆解开来,各自测试是否存在内存泄漏。在被测模块开始前,记录好初始内存数据,然后多次(一般3次以上)运行被测模块,最后查看曲线是否一直上升,如果一直处于上升趋势,就说明该模块可能存在内存泄漏。
3:内存不卸载
上述第二种方法中,有时我们会发现内存曲线不是处于一直上升的趋势,而是相对于开始的内存大小,有一个或多或少的增加,这种情况下,我们需要进一步分析增加的内存是否是由于没有及时卸载而导致的。这里推荐2个工具:
a:Memory Profiler。
通过Unity的PackgaeManager下载此工具,通过该工具进行内存快照后,可以离线分析内存中各资源占比。
B:Unity自带的Profiler
选择Unity自带的Profiler中的Memory一栏,并从Simple切换到Detailed,可以针对某一帧抓取当前的内存快照。
通过上述两种工具,我们可以对比两个时刻下的内存快照,找到新增内存的具体内容是什么,并评估是否合理,能否做到及时卸载。
动态客户端性能—场景性能
笔者所在的项目是一个游戏场景众多的游戏,并且单个游戏场景内包含的物件数量众多。场景的性能对最终游戏的客户端性能影响是举足轻重的。如何做好场景性能的监控和测试,是QA的一项重要任务。针对场景性能,我们主要开展了三种测试:
1:当一个场景制作完成后,我们将先对这个场景资源进行一个初步的统计,给出场景的基本资源量,如下图。
2:玩家在游戏过程中,会涉及到大量的场景切换,场景loading时间的长短对于玩家的体验来说至关重要。我们会对比同类型游戏的场景loading时间,给出一个大致的对比图,如下:
3:采取基于自动化测试框架,通过运行游戏并控制主角移动到场景内每个坐标点,实时采集主角所在坐标点的性能数据,每个坐标采集4个水平方向的数据然后取最大值,最后形成可视化图标。性能数据包括同屏面数、同屏顶点数和DrawCalls。为什么要选取4个方向呢?因为每个点的性能数据和LOD相关,LOD和镜头的视角有关,所以这里我们简单的选取了4个方向。
第1张图是2D的图,颜色越深代表数值越大,其中x和y分别对应地图的坐标,第2张图是在Unity编辑器的Scene视图下生成的,能够较为直观的看到地图中各个点的性能指标。最后将每个坐标点的性能数据和之前确定的基准值相比,如果超过10%,就督促美术地编进行修改。
一般情况下,地编通过模型减面、LOD配置、模型合并,甚至物件摆放调整后,都能达到优化的目标。但也遇到过地编运用各种方法优化后,还是不能很好的达到既定的性能指标,这里主要是 DrawCall达不到目标,面数和顶点数都没问题。这时候我们一般会组织程序一起参与到该场景的优化中,这里介绍一款程序常用的针对场景渲染优化的工具:Frame Debug。
Frame Debug,是unity自带的工具。通过使用Frame Debug,可以精确看到某一帧画面渲染的先后顺序,从而发现一些对当前画面没有贡献的DrawCall,程序可以顺藤摸瓜从而定位如何去除这些DrawCall的方法。
以上就是在客户端性能优化过程中,从QA角度出发的一些总结。优化的本质就是不渲染或少渲染或用更省的方法渲染,其中涉及了非常多的专业知识,比如合批、遮挡剔除、LOD、GPU Instance、顶点动画、描述、实时阴影等,QA必须主动学习各种理论知识,才能更好的和程序、TA沟通,防止出现听到一些名词后,一脸懵逼的情况。在性能测试的过程中,我也发现了几个值得大家注意的点:
1:工具在性能测试中起到了非常大的作用,工欲善其事必先利其器,善用分析工具可以快速定位出性能瓶颈,达到事半功倍的效果。但性能测试没有做好,把责任归结为工具不行,或者对工具不熟悉是是不合理的,测试工具只是辅助我们去发现问题和定位问题,测试方案、测试场景的分析、问题的定位这才是性能测试的关键。不要期望测试工具能够生成你想要的所有东西(报表、瓶颈分析),工具只能尽可能多的提供给我们分析的依据。
2:加强QA性能测试意识。性能测试是非常重要的事情,和功能测试一样重要,甚至在某些游戏类型中,其重要程度要超过功能本身。不能觉得提高一下硬件配置就可以提高性能了。随着软件规模的扩大,提高硬件配置只是解决性能问题的一个基本手段。因为如果软件自身存在性能问题,再多的资源可能也不够用,例如:内存泄漏问题,随着时间的增加,内存终究会被耗尽,最后导致系统崩溃。即使要提高硬件配置,也要首先用性能测试的方式得出哪些硬件可能存在瓶颈。
3:性能优化并不是一劳永逸的工作,而是一个漫长而具有挑战的任务;项目的各个阶段都会有性能上的问题,在用户体验的基础上持续进行打磨,持续保持产品的良好性能才能赢得好口碑。同样性能测试工作也是一件长期的、需要不断重复开展的工作,一方面是因为项目随时有新的资源、新的模块加入,另一方面任何优化手段都是一种平衡的追求,就和代码运行空间和时间不可兼得一样,比如lod的优点就是能减少同屏顶点数,但带来的是内存开销;MipMap的优点是能减少显存和带宽压力,也能使锯齿得到缓解,但是带来的还是内存压力;所以当采取一种优化手段后,必须再次回归测试,检查相关数据是否合乎预期。
4:我们必须明确,游戏性能优化不仅是程序的工作,而是QA、策划、美术共同参与的工作,不能寄希望于用一个特别牛逼的优化技术就能把性能起死回生,万事大吉了。只有大家齐心协力,才能把性能不断优化,不断提升玩家的游戏体验。