在当前项目之前,我有过两个手游项目的上线经验,每个项目都在后期优化过程中遇到了技术规范逐步明确、各类美术资源跟随技术规范大范围调整甚至返工的情况。因此,在项目建立初期,我就试图推动程序和美术制定项目的制作规范,提前确认好各类资源的规格和制作标准,希望减少后续的返工情况。结果并不令人意外,这个努力并没有成功,项目还是走了一条“先产出后优化”的路子,和过往项目的优化过程并无二致。
02 为重大的性能决策增加数据打点
高市占率低配机型ASTC格式兼容情况(2020.5测试),引自知乎用户Ssiya
性能优化内容加开关在不同的阶段有不同的意义。
在性能优化初期,首要的任务是确定哪些逻辑是消耗性能的主要内容。一种可行的方式是增加profile打点,统计每段逻辑的性能开销。这种方式下,所有功能都处于开启状态,采集高端机的数据没什么问题,但如果需要调试低端设备上的效果就会比较困难。此时,可以引入不同逻辑的开关,通过直接在游戏中打开/关闭开关的方式,快速切换功能的状态,并借助自带的FPS展示功能,直观地验证不同模块对性能的影响程度。在发现低端机型粉丝团整体消耗过大的情况下,我们果断砍掉了低端机上的该功能。
性能调优初期用到的功能切换开关,能快速在游戏中切换功能的启用状态
在项目上线前的性能优化中,开关能帮助程序和测试验证一项性能优化的有效性。以比赛内的优化为例,验证时基于相同的性能包,在开关开启和关闭的两种状态下,分别采集性能数据,然后观察指定函数的性能变化。
一次性能优化的结果记录,使用开关切换是否启用优化,对比优化前后的性能
04 根据测试目标选择合适的测试包
测试包体接近线上配置,这一点不用过多解释,相信所有人都能明白测试包体和线上配置越接近,测试数据越准确。然而,在执行的过程中,可能会遇到各种各样的需求,使得测试包体的配置需求复杂化。
一个最直观的影响因素是游戏的日志等级。程序在客户端运行过程中记录了很多日志,有些日志是报错性质的,有些日志则是调试性质的。调试日志在开发期对于排查问题出现的原因十分重要,而到了正式上线时,调试性质的日志会被剔除,以减少总日志量的大小。根据我们项目优化初期的一份测试数据,普通带调试日志的包在中端机型上只能跑23帧(平均耗时约43ms),而去除了调试日志的包在同样机型上能跑到接近30帧(平均耗时约33ms),单帧耗时相差约10ms,仅剔除调试一项就减少了近23%的帧均耗时。
另一个和客户端性能直接相关的因素是是否开启Development Build。Unity生成指定平台的游戏包体使用BuildPlayer函数,其参数中有一项BuildOptions支持不同的生成选项。启用BuildOptions.Development选项时,生成的游戏包体可以被Unity Profiler读取和采集性能相关的数据指标。Unity提供的性能分析工具UPR(Unity Performance Reporting)和我们内部的深度性能分析工具 ,都依赖游戏客户端启用Development Build才能正确采集数据。因此,程序在性能优化过程中需要大量使用启用Development Build的游戏包体来测试。关于是否在日常所有包体上启用Development Build选项,在程序和测试间还有过一段拉扯的过程,最终因为启用Development Build的包体在一些兼容性问题上和关闭状态下不一致、不利于在内部尽早发现问题,程序同意了只在性能优化相关调试包上启用该选项。
我们项目目前使用的3种比较常见的打包参数组合如下表所示。
在需要测试最准确的各档位性能指标时,我们会用g_perf_test命令打出的包体。根据其他项目的经验,是否启用Development Build,性能差异大约在10%左右,使用不带Development Build的包体能获得更准确的性能数据。当发现初步性能数据有异常或需要针对特定内容进行性能优化时,我们会使用g_perf命令打出的包体,通过Unity Profiler等调试工具采集深度的性能数据。采集到的数据除了整体比发布版本耗时偏高一些以外,各函数和渲染耗时的分布都是准确的。如果在优化过程中,还是需要结合更多的日志来分析,那么程序会使用g_perf_with_log命令打出的包体,此时采集到的数据和分布可能会有一些偏差,只适用于特定需要采集性能数据同时配合日志分析的场景。
日常工作中,如果要测试某种设备在某个条件下的性能数据,我们一般会预设好测试环境,直接测完一轮得到数据结果。如果需要多轮对比测试,为了减少对比误差,我们一般还会在每次测试后重启整个环境,以确保多轮测试的基准是一致的。一个更极端一些的例子是,在游戏前中期(此时一般性能较差发热严重)测同一个设备的多组数据时,我们甚至有可能中间需要让设备冷却降温一段时间,再开始下一轮的测试,以防手机降频给测试结果带来干扰。这些测试方法都能减少多次测试间的误差,对我们获取对比数据是有意义的。
然而,用户的使用场景和我们测试的场景是有一定区别的。一个最直观的例子是,用户不会只打一局比赛就停下来,几十分钟甚至更长的游戏时间是常见的游戏时长。具体到我们项目,单局比赛的时长一般在3分钟到5分钟,半小时间大约能打6-10局。如果要保证玩家的游戏体验,我们不能仅以单局的数据作为验证的标准,而是要模拟和玩家相同的游戏强度,采集到连续多局比赛的性能数据。项目组每周会组织一次集体测试,体验最新制作的游戏内容和真实的比赛对局。我们会利用集体测试的时机,单独出带development的性能包,由负责性能验证的同事连续完成10局比赛,并在第1局、第5局和第10局各采集一份性能数据。第1局的数据最接近我们的峰值性能数据,可以比较直观的评价当前版本的性能;第5局和第10局的性能数据则可以用于分析多轮比赛后,是否有帧率下降、内存持续增加、非预期的函数调用等情况。
相较于第1局,第5局的比赛帧率开始出现明显的波动,Mono内存占用增加了约50MB。单局的高耗时函数也发生了变化,新增了一个非正常的函数,该函数为动态骨骼更新函数,原本在比赛内部应该有调用,说明动态骨骼计算的开关被意外打开了,这是单局测试很难发现的问题。
06 对有代价的优化进行谨慎的评估
客户端性能优化中,有一部分优化是对原方案的纯改良,比如原来的写法有大量字符串拼接,新方法去掉了拼接,降低了内存的GC量,提升了客户端运行的稳定性。另一部分优化可能是对原问题做一些权衡(trade-off),支付一定的代价,选择对我们有利的一面。
对于我们项目来说,游戏中比赛内的卡顿是一个非常影响体验的因素,所以降低比赛内的突发瞬间CPU高耗时行为就成为了我们主力优化手段之一。通过各种预先计算和预先加载,在loading过程中提前提前加载好比赛中可能需要的各种动作、图标、特效、音效、球运动轨迹数据等资源,在实际需要用到的时候快速实例化甚至是提前实例化好放在缓存池中随时取用,对于降低比赛过程中的突发高耗时行为有很明显的帮助,越是低端的机器体验改善越明显。预加载优化的代价是更高的内存占用和更长的加载时长,必须小心地控制预加载的数量和耗时。我们的新手场景在海外菲律宾低端机型上的加载耗时较长,单次加载耗时在7-45秒不等,游戏安装后第一次冷启动耗时在某次测试中出现了最高99秒的数据。对于这个结果,鉴于大部分的机型加载时间可以接受,我们当时并没有详细分析各部分的耗时。
在后续一次其他loading问题的定位过程中,程序为了分析各部分逻辑的耗时,也将预加载各部分的耗时进行了拆分统计。测试结果表明,球运动轨迹的耗时占比非常高。为了让游戏中球的运动轨迹更真实,策划预设了非常多的球轨迹,每次投篮、传球后的球轨迹都会从符合条件的球轨迹中随机一条。程序在进行比赛内数据预加载的优化工作时,球轨迹的数量还不多,因此采用了全量加载的方式。后续随着策划逐步完善和加入更多的球轨迹,这部分耗时变得越来越大,但因为缺乏对应数据的记录,并未及时发现。特别是新手引导的比赛,其整个流程都是按照设计的方案走,完全不需要加载那么多比赛中各种条件下才会出现的球轨迹。针对性地减少新手引导比赛的球轨迹后,新手引导比赛的平均加载耗时降低了约15秒,整个新手引导的通过率也提高了不少。
球轨迹优化的事例提醒我们,在面对有一定代价的性能优化方案时,需要对方案进行更为谨慎的评估。评估方案可以是做更多更详细的测试验证,也可以是适当增加对性能优化结果的日志记录,辅以定期的性能回顾,应该能有效避免这类非预期优化问题的发生。
客户端性能优化过程很多时候是一个做减法的过程,哪个部分的消耗大、削减后影响小,就把哪个部分优化掉。以我们游戏的大厅场景优化为例,渲染的耗时包括3D场景和2D UI两部分的耗时。当我们打开一个全屏或者几乎全屏的2D UI界面时,3D场景的渲染其实还是一直在进行中,这一消耗对于游戏表现并无作用,属于可被优化的内容。根据这个想法,每个界面都被打上了标签:全屏界面出现时,背后的场景相机关闭停止场景渲染;几乎全屏的界面出现时,使用一张预先准备的大厅固定背景作为2D渲染的底图;界面关闭后,重新恢复场景相机的渲染。
这个优化想法很简单,效果也是立竿见影,唯一的问题是,在各种界面相互跳转的过程中,容易出现计算是否回到大厅不正确的问题,其结果是大厅场景完全变黑,无法正确显示场景和球员。
客户端性能优化的跟进一般来说都是由特定的几个人在处理,一些优化措施和影响只有少数人清楚。如果没有同步这些信息给全组,其他人不清楚问题出现的原理,就会出现很多“xx界面打开后关闭,大厅场景变黑”的BUG,分别开给对应功能的程序去修改。如果负责的程序也不清楚该优化,可能就会一头雾水,查半天也未必能发现问题。这种信息上的不对称在越大的项目组中越容易出现,此时信息的同步就显得尤为重要。
通知到全组我们做了什么样的优化,可能出现什么样的异常,如果出现了异常要找谁来处理,能够极大地降低性能优化可能带来的负面影响,提高问题的排查和解决效率。
本文的篇幅写了不少,内容比较零散,干货有限,主要还是分享一些个人心得,感谢阅读,希望能给你带来一点收获。
推荐阅读