一.什么是客户端性能测试
关于客户端性能测试,没有一个严格统一的定义。这里仅根据个人的理解来进行定义。
客户端性能测试是通过模拟用户在客户端实际使用过程中的各种操作场景;对应用程序在运行过程中的响应速度、资源消耗、运行稳定性等方面进行测试和评估;以发现客户端在不同负载、设备及网络环境下可能存在的性能问题,从而衡量客户端的性能表现,确保其能够在实际使用中保持良好的响应能力、稳定性和用户体验。
根据上述定义我们可以看到客户端性能测试所要做的事情了,首先便是模拟用户在客户端实际使用过程中的各种操作场景,一方面是各个机型,另一方面是客户端性能测试用例编写。
其次是客户端性能测试所关注的指标,常见的指标有fps、jank、big jank、CPU usage、GPU usage、memory、DrawCall、SetPassCalls、Batches
最后是发现性能问题后的修复和验证
二、如何进行客户端性能测试
仁者见仁,智者见智,每个人在不同的项目可能都有一套完全不同的测试流程,但本质上都差不多。编写客户端性能测试用例,规范测试的机型,跑测测试用例收集客户端性能数据,最后将有问题的地方反馈给程序修复并验证。
1. 工具使用:
一般真机上使用perfdog,IOS还可以使用Xcode Instruments,微信小游戏可以使用其提供的微信性能面板,PC端一般采用unity自带的unity profiler,upr(付费),当然也可以程序代码逻辑中加入埋点(比如该app在某类手机性能表现不好,但是在该地区该低端机使用率不足0.1%,那就没有必要为了这部分用户专门去做优化提升,可以利用埋点去获取设备和芯片的情况,启动时间等数据)。手机端也可以通过修改打包参数来获取更详细的数据,不过为了更接近用户使用情况,一般都不会勾选development build选项,但如果是已经发现问题,方便调试,也可以根据自身需要选取不同的打包策略。比如抖音小游戏,打包就可以勾选上development build和deep profiler,可以直接看真机上的堆栈信息
perdog具体使用方法可以见官方文档PerfDog | 全平台性能测试分析专家
2.客户端性能测试用例编写
相信大家都有过这样一种时刻,第一次接触客户端测试用例的时候不知道如何下手,甚至分不清客户端性能测试和服务器压测的区别。说个大白话,客户端性能测试就是你模拟用户行为,体验看看手机烫不烫,卡不卡,运行内存够不够用。
下面以登录场景为例举例几个测试用例
模块 | 场景 | 性能检测时长 | 备注 |
登录 |
| 1min | 基准 |
| 热启动的影响 |
3.客户端性能测试常见指标及详解
要想真正吃透客户端性能测试,一定要了解每个指标代表什么,计算机底层是如何运作的
FPS:
Frame Per Second,每秒帧数,帧率
通常情况下,帧率越高,代表游戏性能越好,画面越流畅。(这应该是大多数人所认为的fps了吧,但fps远不是表面看起来这么简单)
fps并非越大越好,如果你的设备屏幕刷新率最高只有200并且没有启用帧同步策略,但游戏的实际fps已经来到了240,试想一下屏幕每秒只能刷新200帧画面,但是游戏一秒内提供了240帧画面,会导致约40帧的画面都展示不出来,当然真正的结果取决于渲染完成时间、帧缓冲区的组织方式、显示扫描时序、交换链策略以及操作系统的呈现机制
电影的帧率只有24fps,为什么看起来毫无卡顿?因为电影是连续拍摄的,游戏画面是计算机一帧一帧渲染的
上图左边帧率高,反而看起来有一卡一顿的现象,这就是Micro Stuttering(微型口吃)导致的,左边FrameTime如下图:
通过FrameTime可以看出,左边高帧率FPS=40帧率中出现一次FrameTim>=117ms,理论平均FrameTime=25ms。所以非均匀渲染,虽然帧率高达40,但依然觉得非常卡。
右边低帧率30帧运行FrameTime如下图:
从上面FrameTime可以看出,帧率FPS=30,FrameTime均匀为33ms,画面会非常平滑。
游戏的一帧是如何渲染的: 输入 → 游戏逻辑更新 → 准备渲染数据 → CPU 提交渲染命令 → GPU 渲染 → Swap Chain / Present Queue / Display System交换链/当前队列/显示系统 → 显示器扫描显示。
注意并不是GPU渲染完了,显示器就立刻展示,中间还有一些VRR,V-Sync等策略,而且一般情况是当前帧展示前面渲染完的帧序列,上图也能较为明显的反映这一点
CPU和GPU具体参与了哪些工作:我们先来了解一些概念,我们主机都会有听说8核16线程,虽然硬件上只有一个CPU,但是CPU被分为了8个cpu core,每个cpu core又都包含2个Logical Processor逻辑处理器。
在我们打开游戏的.exe文件的时候,这个.exe相当于一个进程,一个游戏进程可能包含三十多甚至更多的线程。CPU执行和分配线程,这里进程和线程的关系就不深入讲解了
例如一个游戏可能创建:
- 1 个 Game Thread1 个游戏主题帖子
- 1 个 Render Thread1 个渲染线程
- 8 个 Worker Threads8 个工作线程
- 1 个 Audio Thread1 个音频线程
- 2 个 IO Threads2 个 IO 线程
- 1 个 Network Thread1 个网络线程
- 其他引擎/第三方库线程
Game Thread 主要负责游戏逻辑、世界状态更新的线程
Render Thread把游戏世界的数据转换成渲染系统需要执行的 Render Commands
例如 Game Thread 得到:“这里有一个角色,这个角色使用这个 Mesh、这个 Material,位于这个位置。”
Render Thread 就进一步处理:“这个 Mesh 怎么提交、使用什么 Pipeline、需要哪些资源、生成哪些 Draw/Render Commands……”
Work Thread 专门从任务池中领取任务并执行。例如:
Worker Thread 1 → 动画 Job(工作线程 1 → 动画任务)
Worker Thread 2 → Physics Job(工作线程 2 → 物理计算任务)
Worker Thread 3 → AI Job(工作线程 3 → AI 任务处理)
计算线程:负责执行 CPU 计算任务的 Worker Thread / Task Thread(计算线程是一类泛称,并非操作系统中具体的线程类型)
常见的帧同步策略:V-Sync,G-Sync,VRR
Jank:
连续某几帧的平均帧率明显小于前几帧或者后几帧的平均帧率,表现上就会卡顿,我们称之为Jank,再具体一些也可以定义为big jank,small jank
并不是平均帧率符合预期就证明没有卡顿了,Jank也是我们重点关注的指标
CPU usage:
CPU使用率,前面具体介绍了CPU各个线程之间的运作逻辑,在CPU使用率偏高的时候也方便定位可能是哪一步出现的问题。
Game Thread过忙 :大量 NPC、复杂 AI、大量游戏对象、复杂 Gameplay Logic复杂的游戏逻辑机制、物理计算、动画计算、大量碰撞检测、可见性计算、游戏脚本执行
Worker Thread很忙:物理、动画、AI人工智能、粒子、地形、Streaming流媒体传输、骨骼计算。有时候cpu使用率过高也不是一件坏事,恰恰证明游戏很好的利用了多核cpu的性能,提高了游戏的性能和表现。
这里有一个经常容易被忽视的问题,cpu总使用率只有45%,但其中有一个线程已经满载了,所以不要只看 CPU Total Usage,其他各个线程也都应该关注:
Game Thread Time游戏讨论时间到
Render Thread Time渲染线程耗时
Worker Thread Time工作线程运行时间
每个 Core / Logical Processor 的利用率
CPU Frame TimeCPU 帧时间
CPU过高也可能还有其他原因,不仅局限于渲染,Asset Streaming资产流式传输、Shader Compilation着色器编译、Resource Loading资源加载中、解压缩、网络、音频、反作弊、第三方 Middleware、操作系统后台任务等。eg:玩家高速移动 → 游戏疯狂读取和解压资源 → CPU 突然升高。这里要注意的是,如果是首次加载某个场景资源,客户端性能表现一定是差的,各项数据看上去都不好,所以会有loading图,特效等丝滑的过度,玩家感官上并无任何卡顿。
GPU usage:
GPU使用率
和CPU同理,GPU使用率过高也并非是bug,能证明很好的利用了GPU的性能。
为什么GPU使用率会过高呢?4k和1080p所占用的像素相差4倍,一张图720*720和1440*1440,像素也相差4倍。很多时候游戏内防止图片过大都会先进行压缩,再自适应缩放图片,当然像素变低也会以图片的清晰程度为代价,这就需要均衡表现和性能了。同时在日常生产中,也可以加入对图片场景等的资源自动化检查,对于三角形面数过高,分辨率过大的图片提前检测出来。
画面设置也会让GPU满载:eg:
- Ray Tracing光线追踪
- Path Tracing路径追踪
- Shadow Quality阴影质量
- Volumetric Fog体积雾效
- Global Illumination全局光照
- Reflection反思/思考
- Ambient Occlusion环境光遮挡效果
- Anti-Aliasing抗锯齿处理
- Post Processing后期处理
- Particle Effects粒子效果
GPU 高还可能是 Shader 很重,我们之前项目就是因为在小游戏端使用了NBShader,导致加载进游戏的时间非常长,采用轻量化的shader或者自己写的shader能有效解决。每一个 Pixel 都需要执行大量 Shader 指令。于是Pixel 数量 × Shader Complexity像素数量 × 着色器复杂度
GPU 高还有一个特别容易忽略的因素:Draw Call / Geometry
例如场景里:10 万个物体
CPU 需要准备大量 Draw Calls。
同时 GPU 也需要处理:Vertex顶点、Rasterization光栅化、Geometry几何学、Visibility能见度
因此有时候不是“像素太多”,而是场景几何复杂度太高。
例如:大量 Mesh、高 Polygon Count高多边形数量、植被、建筑、粒子、复杂场景。
我们通常在测试性能的时候,也会通过gm或者脚本在场景里加入AI角色,来模拟真实的场景,这时候场景里面的性能消耗也会增加,就是这个原理。
如果CPU和GPU使用率都高怎么判断呢?应该聚焦frame time,再具体看CPU和GPU到底哪一步使用率突高,在等哪一步执行卡住了。
DrawCalls:
CPU 向 GPU 提交一次绘制请求,即GPU被要求画一次的次数
SetPassCall:
在unity下,SetPass Call 可以理解成“GPU 渲染状态 / Shader Pass 被设置或切换”的次数。“我要换一套渲染状态/Shader Pass” 。因为场景物品的材质,纹理等可能不同,频繁的setpasscall也会带来额外的开销
Batches:
一次提交/批处理过程中被一起处理的一组绘制内容。游戏引擎会尝试把多个可以合并处理的物体组织到一起,从而减少实际提交成本。减少 CPU 为大量小物体进行重复渲染准备/提交的成本。
4.静态美术资源检查:
这里仅阐述思想:在资源被提交时进行一次检查。
①三角形数量Triangles
②顶点数
③LOD0 / LOD1 / LOD2 各级面数
④单个 Mesh 面数
⑤整个 Prefab 的总面数
⑥Texture Size纹理尺寸,是否压缩,压缩方式
⑥Shader:是否使用特殊 Rendering Feature,是否使用昂贵shader,尤其是透明材质
⑦角色骨骸数量:Bone 数量、Skin Weight 数量皮肤重量 数量、每顶点最大影响骨骼数、Skinned Mesh 数量Skinned Mesh 的数量、Animation Clip 数量动画片段数量、Animation Clip 长度动画片段长度、动画采样率
5.unity profiler使用
官方文档:应用程序性能分析 - Unity 手册
unity中可以使用unity profiler,快捷键ctrl+7
在真机上发现性能瓶颈后,想要进一步定位到问题,就需要使用到unity profiler和memory profiler了。
在发现某处场景下客户端性能较差,就可以在编辑器上打开unity profiler,针对那一场景捕获函数调用情况,下图可以看出是BoxAll.AddHero()的GC偏高,然后再定位到该函数看具体逻辑,哪里产生了额外的gc,解决后再次进行验证即可。
当然想要使用好unity profiler和memory profiler还需要更多的对底层的一些理解,有时候即便你不熟练业务代码,仅凭对unity底层对象的创建销毁这些机制也是可以定位到问题的,作者水平有限,也还在不断学习中,这里就不过多阐述了