客户端的性能是用户体验的一个非常重要的方面。在需求设计时要考虑到可能的性能问题;在不断的迭代开发新功能的同时要保持性能的稳定;在运营过程中要能追踪并解决历史遗留的性能问题。工欲善其事,必先利其器。虽然有不少现成工具可以使用,但自己设计性能工具更灵活,更有针对性。性能工具的主要功能是监控目标程序获取相关数据,组织数据并对其加以分析,帮助我们定位问题、找出性能瓶颈。
需要哪些数据
常见的性能问题主要有:界面卡、反应慢、CPU高、内存大等几个方面。我们针对这些情况来考虑需要收集哪些数据,以及如何来记录这些数据(针对windows客户端)。
1、界面卡、反应慢
界面卡、反应慢主要是指程序的响应速度过慢,包括:界面对鼠标键盘输入的响应速度;一些比较耗时的系统API的处理速度,比如读写文件等;以及目标程序的一些关键路径的执行速度。
界面响应速度,这里我们可以用每次消息循环的执行的时间点以及时间消耗来判断。对于大多数windows程序来说hook
GetMessage并记录下来每次调用GetMessage的时间点就可以了。但也有些客户端自己实现的消息循环,这里我们就需要hook对应的函数(比如PeekMessage)或是在消息循环里加上log。另外我们也可以向目标程序发送WM_NULL消息跟据其响应时间来判断响应速度。在这里,我推荐以两次消息循环的时间间隔或是对WM_NULL消息的响应速度小于50ms做为一个判断标准,大于这个值则认为“卡”。
系统API执行速度,用hook的方法记录调用的线程ID、时间点、消耗时间以及一些必要的参数(比如读写的是哪个文件)。通常需要关注的系统API有:CreateFile、ReadFile、WriteFile、BitBlt和StrenchBlt等等。
对于一些关键路径,我们需要记录下来执行时的线程ID、时间点和时间消耗。这一点针对性比较强,直接在目标程序加入log就可以了。当然也可以用hook的方法,但不推荐。因为Hook的实现相对比较复杂,需要实现一个跳板函数,在跳板函数中记录相关信息。Hook更适应于记录一些调用次数频繁的系统级API。而一些关键路径往往与目标程序逻辑密切相关,整个过程只有几次调用,甚至可能只有一次或完全不调用。这样就完全没有必要用hook的方法。
系统API和关键路径(函数)的耗时,推荐记录开始时间和结束时间两个数据。这样在有嵌套的情况下,比单纯记录消耗时长,更直观,更便于分析问题。
怎么hook?这里推荐参考微软的detours库。detours2.1版本之前是开源的。其基本原理是这样的:1.
把目标函数的前几行指令拷贝到一个地方,然后在原来的位置替换成一条跳转指令。2. 通过跳转指令跳转到自己实现的跳板函数,在跳板函数就可加入自己的代码了。如下:
2、CPU高、内存大
针对CPU高、内存大,我们按时间顺序以固定的时间间隔记录下来目标进程的CPU和内存的消耗就可以了。在CPU占用方面我们也可以更进一步记录下每个线程的CPU消耗
3、有了上面的数据就够了吗?
不够。比如,当我们收集到一组数据,分析后发现目标进程在启动后35秒-40秒的这段时间对WM_NULL消息无响应(界面卡住)。然后呢?然后我们仍然没有办法知道为什么会卡,以及卡在哪里。所以我们还需要目标的调用堆栈。同样按时间顺序以固定的时间间隔记录下堆栈信息。这样当我们发现问题的时间点后,便能得到当时一个堆栈执行情况。接下来就可以通过review代码来解决问题。
至于获取调用堆栈。微软提供的 StackWalk64 就可以了,具体请查阅MSDN。当然如果你喜欢自己通过堆来还原调用堆栈,那么可以使用NtQueryInformationThread在取得线程信息后,根据ebp
esp来回溯吧。
4、还需要哪些数据?
任何你关注的数据都可以记录下来。假如你关注IO对性能的影响,那么就记录下IO量和页面错误的数据;你怀疑是不是有其它模块的注入影响了你的程序,那么你也可以记录下来目标进程的模块信息……
组织分析数据
把数据初步整理,以图形的方式直观的表现出来。下面举几个例子。
用树表示关键函数的执行所花的时间。
用线形图来表示CPU占用
纵向表示CPU使用率,横向表示时间。用颜色来区分用户态CPU和内核态CPU。类似这样的图还可以用来表示无响应时间,内存占用等。
另外我们也可以自动分析堆栈数据,找出一段时间内连续出现在堆栈中的函数,在线形图上醒目标出。因为这种情况很有可能是卡住或阻塞了。如下图红线所占有的时间段内表示主线程堆栈中都有USER32.dll
77D18816调用,再配合PDB就知道调用的是哪个函数了。这样可以方便的找出性能瓶颈。
还有其它各种表现形式,我们完全可以跟据自己的需要来实现。
性能工具本身的影响
性能工具在监听目标进程收集性能数据,本身就会有性能的开销。比如增加log,注入后Hook相关函数等等。这些负面影响不可避免的,我们尽量将其影响减少。比如Log写入文件会带来IO开销,我们就采用先将log写入内存,当数据采集停止后一次性写入文件等,并且在记录内存开销时减掉log所占用的内存。
总结
总之,开发自己的性能工具,首先要明确自己关注哪些性能点,然后在此基础上确定所需要的数据;然后确定性能工具需要哪些功能,接着一步一步实现它就好了。最后别忘了跟据自己的需要持续改进。
|