最近在维护一个Unity性能分析工具,类似UPR,客户端采集信息,WEB端显示数据。下面简单介绍下原理。
数据来源
- Profiler数据 熟悉Unity的同学对Profiler一定不会陌生,我们的性能数据主要来源于它,主要包含函数耗时,GC等等等等
- 自定义数据 除了Profiler数据外,我们还需要一些其他数据,比如手机温度,PSS大小,lua GC,缓存池的使用情况,等等
一系列你想要,而profiler没有的
数据传输
Unity构建的时候勾选Development Build,在设备上启动后,Profiler就已经启动,并在某个端口进行监听,我们只需要
连接上这个端口就能够和Profiler进行通信,从而获得性能数据,就是平时在Profiler Window看到的那些。
之所以说某个端口,是因为这个端口并不是固定的,在Android和iOS平台,端口以55000开始,55495结束。
下图就是Unity对端口进行扫描,通过建立连接并设置连接超时时间特别短(10ms)来快速判断该端口是否在监听。
虽然端口设置的范围比较大,但一般情况下前面十多个就够用了,从55000开始listen,bind失败换下一个端口。
根据上面的介绍,我们连接Unity Profiler的思路就是,通过短超时连接扫描端口,在能够连接的情况下既确定了监听端口,也连接了Profiler。
连接上Profiler之后,需要给Profiler发送消息,告诉它你要采集信息,它就开始发送茫茫多的消息过来。
把这些消息处理或者直接转发给Web端(根据不同需求定夺)。直接转发数据量巨大,一个正常的游戏峰值可以达到10+M/s的传输。优点是不需要客户端进行处理,不会给CPU带来额外负担,缺点是对发热,耗电,都带来不小的影响,对公司网速也带来不小的影响,单个服务器能够承受的客户端连接也少的可怜。
消息解析
消息头(伪代码),消息头我们都不陌生,一般都包含消息id,消息体大小,这个只是比平常的多了一个magicnumber
struct MessageHeader
{
UINT32 MagicNumber
UGUID MessageId
UINT32 Size
}
MagicNumber 值为 0x67A54E8F
MesasgeId 是一个结构体, 4个INT长,可转化为字符串 ,举个例子 c58d77184f4b4b59b3fffc6f800ae10e
struct UGUID
{
UINT32 data[4]
}
Size 接下来消息体的大小
下面是一个简单的消息体的结构
LogMessage MessageId "394ada038ba04f26b0011a6cdeb05a62"
struct LogMessage
{
UINT32 logType
BYTE[] message
}
Log的消息体比较简单,只包含了log类型,和字符串数组,这两部分就组成了一个完整的消息,log并不是我们关心的消息,接下来就是一个比较复杂的消息了
ProfilerDataMessage MessageId "c58d77184f4b4b59b3fffc6f800ae10e"
[Header] [[BlockHeader[Message ... Message]BlockFooter] ... [BlockHeader[Message ... Message]BlockFooter]]
这是一个消息体的结构,比较复杂,对涉及到的结构进行简要说明
struct Header
{
UINT32 signature
UINT8 isLittleEndian
UINT8 isAlignedMemoryAccess
UINT16 platform
UINT32 version
UINT64 timeNumerator
UINT64 timeDenominator
UINT64 mainThreadId
}
该Head不要和MessgeHead搞混淆,它是属于 ProfilerDataMessage的,并且它在一次连接中,只出现一次,附带了一些初始的信息,其中isLittleEndian与isAlignedMemoryAccess比较重要,对我们后续内容的解析影响比较大。
接下来是Block,Block里又包含了BlockHeader,BlockFooter,在中间又包含了多个Message,这些Message就不包含头和尾了,只包含了消息类型和内容 而处理这些message就是我们的重点了
struct BlockHeader
{
UINT32 signature
UINT32 blockId
UINT64 threadId
UINT32 length
}
struct BlockFooter
{
UINT32 nextBlockId
UINT32 signature
}
这些结构的成员通过命名我们就能理解,这里提一个比较有意思的地方,就是BlockHeader和BlockFooter的signature
BlockHeader 的 signature为0xB10C7EAD
BlockFooter的signature为0xB10CF007
不知道大家看出来了么
threadId,用于标识这个消息是关于哪个线程的,blockid和nextBlockid,是为了判断是否出错的,比如说某一个特定线程上次处理的blockdi 为1,这次处理blockid为3,那说明中间有丢失,就无法继续处理了。
Block中的Message
enum MessageType
{
...
MarkerInfo = 1,
...
GlobalMessagesCount = 32,
ThreadInfo = GlobalMessagesCount + 1,
Frame = GlobalMessagesCount + 2,
...
BeginSample = GlobalMessagesCount + 4,
EndSample = GlobalMessagesCount + 5,
Sample = GlobalMessagesCount + 6,
...
GCAlloc = GlobalMessagesCount + 20,
...
}
由于消息数量比较多,列举了一些比较重要的,其他的就省略了,真实的情况要比下面讲的要复杂的多
- Session数据解析
BlockHeader的threadId表明了该消息所属的线程,其中有一个GlobalThread,id为-1,它并不是真实纯在的线程,而是标识该数据包含了全局数据。 这些数据对应的消息主要类型有 MarkerInfo , ThreadInfo 等
struct MarkerInfo
{
UINT16 messageType
UINT32 samplerId;
...
STRING name;
...
}
name该采样的名字,一般情况下就是函数名,每个名字对应一个id,方便后面只发送id,不发送名字来减少包体大小。
struct ThreadInfo
{
UINT16 messageType
UINT32 flags;
STRING group;
STRING name;
ULONG startTime;
ULONG threadID;
}
线程的信息,主要有Main Thread, Render Thread 等,这些都是全局信息,解析后保存在字典中,方便后面的检索
- Thread数据解析
全局信息发送完之后就是真实线程的消息了,主要讲解BeginSample和EndSample
struct Sample
{
UINT16 messageType
INT8 flags;
UINT32 id;
ULONG Time;
}
BeginSample和EndSample的结构是一样的,通过messageType来判断是Begin还是End,id就是MarkerInfo的sampleId,可以通过该id查找到函数名,Time是该sample采样的时间。
举一个简单的例子,假设我们有如下函数调用
fun A // sampleId 100
B()
end
fun B // sampleId 101
end
A() // main Thread id 1000000
收到BlockerHeader threadid 为 -1的消息,进入Session数据处理阶段,其中包含MarkerInfo, ThreadInfo
struct MarkerInfo
{
UINT16 messageType 1
UINT32 samplerId; 100
...
STRING name; A
...
}
struct MarkerInfo
{
UINT16 messageType 1
UINT32 samplerId; 101
...
STRING name; B
...
}
struct ThreadInfo
{
UINT16 messageType 33
UINT32 flags;
STRING group;
STRING name; Main Thread
ULONG startTime;
ULONG threadID; 1000000
}
收到BlockHeader threadid 为 1000000的消息,这是一个主线程消息进入线程消息处理阶段,其中包含BeginSample,EndSample
struct Sample
{
UINT16 messageType 36
INT8 flags;
UINT32 id; 100
ULONG Time; 0
}
struct Sample
{
UINT16 messageType 36
INT8 flags;
UINT32 id; 101
ULONG Time; 10
}
struct Sample
{
UINT16 messageType 37
INT8 flags;
UINT32 id; 101
ULONG Time; 20
}
struct Sample
{
UINT16 messageType 37
INT8 flags;
UINT32 id; 100
ULONG Time; 30
}
上面消息的意思是 A 开始,时间0,B开始,时间10, B结束,时间20,A结束 时间30
那我们就可以计算函数的调用时间了,B耗时10 = 20 -10, A耗时 30 = 30-0, A self 耗时 20 = 30(A耗时) - 10(B耗时)
总结
上面简单介绍下性能工具如何通过Profiler获取数据,希望起到抛砖引砖的作用。 通过该工具,在测试人员跑游戏的同时,无感的上传性能数据,
并通过Web的形式展现出来,使得我们能够实时了解游戏的性能状态,及时发现问题,及时解决问题。
欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com