You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

练习Sample跑起来 | 热点问题答疑第3期

你好我是孙鹏飞。又到了答疑的时间今天我将围绕卡顿优化这个主题和你探讨一下专栏第6期和补充篇的两个Sample的实现。

专栏第6期的Sample完全来自于Facebook的性能分析框架Profilo主要功能是收集线上用户的atrace日志。关于atrace相信我们都比较熟悉了平时经常使用的systrace工具就是封装了atrace命令来开启ftrace事件并读取ftrace缓冲区生成可视化的HTML日志。这里多说一句ftrace是Linux下常用的内核跟踪调试工具如果你不熟悉的话可以返回第6期文稿最后查看ftrace的介绍。Android下的atrace扩展了一些自己使用的categories和tag这个Sample获取的就是通过atrace的同步事件。

Sample的实现思路其实也很简单有两种方案。

第一种方案hook掉atrace写日志时的一系列方法。以Android 9.0的代码为例写入ftrace日志的代码在trace-dev.cpp里,由于每个版本的代码有些区别,所以需要根据系统版本做一些区分。

第二种方案也是Sample里所使用的方案由于所有的atrace event写入都是通过/sys/kernel/debug/tracing/trace_markeratrace在初始化的时候会将该路径fd的值写入atrace_marker_fd全局变量中我们可以通过dlsym轻易获取到这个fd的值。关于trace_maker这个文件我需要说明一下这个文件涉及ftrace的一些内容ftrace原来是内核的事件trace工具并且ftrace文档的开头已经写道

Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on inside the kernel.

从文档中可以看出来ftrace工具主要是用来探查outside of user-space的性能问题。不过在很多场景下我们需要知道user space的事件调用和kernel事件的一个先后关系所以ftrace也提供了一个解决方法也就是提供了一个文件trace_marker往该文件中写入内容可以产生一条ftrace记录这样我们的事件就可以和kernel的日志拼在一起。但是这样的设计有一个不好的地方在往文件写入内容的时候会发生system call调用有系统调用就会产生用户态到内核态的切换。这种方式虽然没有内核直接写入那么高效但在很多时候ftrace工具还是很有用处的。

由此可知用户态的事件数据都是通过trace_marker写入的更进一步说是通过write接口写入的那么我们只需要hook住write接口并过滤出写入这个fd下的内容就可以了。这个方案通用性比较高而且使用PLT Hook即可完成。

下一步会遇到的问题是想要获取atrace的日志就需要设置好atrace的category tag才能获取到。我们从源码中可以得知判断tag是否开启是通过atrace_enabled_tags & tag来计算的如果大于0则认为开启等于0则认为关闭。下面我贴出了部分atrace_tag的值你可以看到判定一个tag是否是开启的只需要tag值的左偏移数的位值和atrace_enabled_tags在相同偏移数的位值是否同为1。其实也就是说我将atrace_enabled_tags的所有位都设置为1那么在计算时候就能匹配到任何的atrace tag。

#define ATRACE_TAG_NEVER            0      
#define ATRACE_TAG_ALWAYS           (1<<0)  
#define ATRACE_TAG_GRAPHICS         (1<<1)
#define ATRACE_TAG_INPUT            (1<<2)
#define ATRACE_TAG_VIEW             (1<<3)
#define ATRACE_TAG_WEBVIEW          (1<<4)
#define ATRACE_TAG_WINDOW_MANAGER   (1<<5)
#define ATRACE_TAG_ACTIVITY_MANAGER (1<<6)
#define ATRACE_TAG_SYNC_MANAGER     (1<<7)
#define ATRACE_TAG_AUDIO            (1<<8)
#define ATRACE_TAG_VIDEO            (1<<9)
#define ATRACE_TAG_CAMERA           (1<<10)
#define ATRACE_TAG_HAL              (1<<11)
#define ATRACE_TAG_APP              (1<<12)

下面是我用atrace抓下来的部分日志。

看到这里有同学会问Begin和End是如何对应上的呢要回答这个问题首先要先了解一下这种记录产生的场景。这个日志在Java端是由Trace.traceBegin和Trace.traceEnd产生的在使用上有一些硬性要求这两个方法必须成对出现否则就会造成日志的异常。请看下面的系统代码示例。

void assignWindowLayers(boolean setLayoutNeeded) {
2401        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "assignWindowLayers");//关注此处事件开始代码
2402        assignChildLayers(getPendingTransaction());
2403        if (setLayoutNeeded) {
2404            setLayoutNeeded();
2405        }
2406
2411        scheduleAnimation();
2412        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);//事件结束
2413    }
2414

所以我们可以认为B下面紧跟的E就是事件的结束标志但很多情况下我们会遇到上面日志中所看到的两个B连在一起紧跟的两个E我们不知道分别对应哪个B。此时我们需要看一下产生事件的CPU是哪个并且看一下产生事件的task_pid是哪个也就是最前面的InputDispatcher-1944这样我们就可以对应出来了。

接下来我们一起来看看补充篇的Sample它的目的是希望让你练习一下如何监控线程创建并且打印出创建线程的Java方法。Sample的实现比较简单主要还是依赖PLT Hook来hook线程创建时使用的主要函数pthread_create。想要完成这个Sample你需要知道Java线程是如何创建出来的并且还要理解Java线程的执行方式。需要特别说明的是其实这个Sample也存在一个缺陷。从虚拟机的角度看线程其实又分为两种一种是Attached线程我习惯按照.Net的叫法称其为托管线程一种是Unattached线程为非托管线程。但底层都是依赖POSIX Thread来实现的从pthread_create里无法区分该线程是否是托管线程也有可能是Native直接开启的线程所以有可能并不能对应到创建线程时候的Java Stack。

关于线程我们在日常监控中可能并不太关心线程创建时候的状况而区分线程可以通过提前设置Thread Name来实现。举个例子比如在出现OOM时发现是发生在pthread_create执行的时候说明当前线程数可能过多一般我们会在OOM的时候采集当前线程数和线程堆栈信息可以看一下是哪个线程创建过多如果指定了线程名称则很快就能查找出问题所在。

对于移动端的线程来说我们大多时候更关心的是主线程的执行状态。因为主线程的任何耗时操作都会影响操作界面的流畅度所以我们经常把看起来比较耗时的操作统统都往子线程里面丢虽然这种操作虽然有时候可能很有效但还可能会产生一些我们平时很少遇到的异常情况。比如我曾经遇到过由于用户手机的I/O性能很低大量的线程都在wait io或者线程开启的太多导致线程Context switch过高又或者是一个方法执行过慢导致持有锁的时间过长其他线程无法获取到锁等一系列异常的情况

虽然线程的监控很不容易但并不是不能实现只是实现起来比较复杂并且要考虑兼容性。比如我们可能比较关心一个Lock当前有多少线程在等待锁释放就需要先获取到这个Object的MirrorObject然后构造一个MonitorInfo之后获取到waiters的列表而这个列表里就存储了等待锁释放的线程。你看其实过程也并不复杂只是在计算地址偏移量的时候需要做一些处理。

当然还有更细致的优化比如我们都知道Java里是有轻量级锁和重量级锁的一个转换过程在ART虚拟机里被称为ThinLocked和FatLocked而转换过程是通过Monitor::Inflate和Monitor::Deflate函数来实现的。此时我们可以监控Monitor::Inflate调用时monitor指向的Object来判断是哪段代码产生了“瘦锁”到“胖锁”转换的过程从而去做一些优化。接下来要做优化需要先知晓ART虚拟机锁转换的机制如果当前锁是瘦锁持有该锁的线程再一次获取这个锁只递增了lock count并未改变锁的状态。但是lock count超过4096则会产生瘦锁到胖锁的转换如果当前持有该锁的线程和进入MontorEnter的线程不是同一个的情况下就会产生锁争用的情况。ART虚拟机为了减少胖锁的产生做了一些优化虚拟机先通过sched_yield让出当前线程的执行权操作系统在后面的某个时间再次调度该线程执行从调用sched_yield到再次执行的时候计算时间差在这个时间差里占用该锁的线程可能会释放对锁的占用那么调用线程会再次尝试获取锁如果获取锁成功的话则会从 Unlocked状态直接转换为ThinLocked状态不会产生FatLocked状态。这个过程持续50次如果在50次循环内无法获取到锁则会将瘦锁转为胖锁。如果我们对某部分的多线程代码性能敏感则希望锁尽量持续在瘦锁的状态我们可以减少同步块代码的粒度尽量减少很多线程同时争抢锁可以监控Inflate函数调用情况来判断优化效果。

最后还有同学对在Crash状态下获取Java线程堆栈的方法比较感兴趣我在这里简单讲一下后面会有专门的文章介绍这部分内容。

一种方案是使用ThreadList::ForEach接口间接实现具体的逻辑可以看这里。另一种方案是 Profilo里的Unwinder机制,这种实现方式就是模拟StackVisitor的逻辑来实现。

这两期反馈的问题不多答疑的内容也可以算作对正文的补充如果有同学想多了解虚拟机的机制或者其他性能相关的问题欢迎你给我留言我也会在后面的文章和你聊聊这些话题比如有同学问到的ART下GC的详细逻辑之类的问题。

相关资料

福利彩蛋

今天为认真提交作业完成练习的同学,送出第二波“学习加油礼包”。@Seven同学提交了第5期的作业送出“极客周历”一本其他同学如果完成了练习千万别忘了通过Pull request提交哦。

欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。