当前位置: 首页 > 后端技术 > Java

systrace统计方式比较耗时

时间:2023-04-01 20:36:54 Java

本文作者:Fangx3Android是单线程模型,用户的按键事件、屏幕触摸、UI绘制都在UI线程中处理。单线程意味着串行执行。如果某个操作需要时间,则后续操作将不得不等待。这时候用户的第一感觉就是卡住了。因此,解决滞后问题的最简单方法之一就是找出耗时的方法。统计方法耗时多少?开发时统计一个方法的耗时最简单的方法就是在方法的开始和结束处分别打上时间戳,两个时间戳相减就是方法的耗时。funtake(){valstart=System.currentTimeMillis()//..service.take();//...valend=System.currentTimeMillis()valconst=end-start}上面的方法可以统计我们应用代码的耗时,但是不能统计Android系统方法的耗时。其实Android系统已经在一些关键环节上嵌入了一些点,但是它的实现并不是像我们一样嵌入时间戳,而是通过Trace类来实现的,而且Trace类也支持我们应用层调用插入的方式来自定义点,使用Android提供的systrace工具,对Trace类的点信息进行抓取和处理。最后生成一个Html文件,通过Chrome可以直观的查看一个完整链接的耗时情况。Systrace在开发阶段确实是一个强大的调优工具,但是有两个明显的局限性阻碍了这个工具的上线使用:需要连接PC,通过执行命令开启Trace功能,开发者需要手动添加Trace.beginSection和Trace.endSection,这就变成了需要开发手动添加Trace函数来预测耗时的位置,但是线上环境无法预测到哪里会耗时。所以如果能解决以上两个问题,就可以使用systrace工具进行在线排查。远离PC端运行systrace下面简单画一下systrace的工作原理:从上图可以看出,systrace抓取的数据可以分为两类:Java层发生的函数调用信息和Native层,以及内核态的事件信息。Java层Native层的函数调用信息是调用Trace类的方法收集的信息(也是我们这次需要关心的数据),数据信息会记录在trace_marker中;内核态的时间信息由LinuxFunction提供的ftrace提供,通过激活不同的事件节点,根据内核运行时的节点启用状态,将事件记录在ftrace缓冲区中。最终systrace通过检索以上两个数据整合生成一个Html文件。从上图中我们可以看出,systrace是通过Atrace来设置Tag的。如果能找到需要抓取的类型信息对应的Tag,直接在终端设置,提取trace_marker中的数据后就可以摆脱PC端的限制。.设置标签publicstaticvoidbeginSection(@NonNullStringsectionName){if(isTagEnabled(TRACE_TAG_APP)){if(sectionName.length()>MAX_SECTION_NAME_LEN){thrownewIllegalArgumentException("sectionNameistoolong");}nativeTraceBegin(TRACE_TAG_APPsectionName);}}以上是系统Trace类中的beginSecion方法。它会先判断对应的Tag是否可用,然后调用native层的TraceBegin方法写入数据。isTagEnabled的实现如下:publicstaticbooleanisTagEnabled(longtraceTag){longtags=sEnabledTags;if(tags==TRACE_TAG_NOT_READY){tags=cacheEnabledTags();}return(tags&traceTag)!=0;}dprivatestaticlongTagcaches(){longtags=nativeGetEnabledTags();sEnabledTags=标签;returntags;}看到这里,想知道是否可以通过反射修改sEnabledTags的值来开启Trace功能?通过实践可以发现,仅仅通过修改sEnabledTags是无法开启Trace功能的,所以可以大致猜测native层应该也有类似的判断。具体native代码在/system/core/libcutils/trace-dev.c(AndroidO版本代码)文件staticinlinevoidatrace_begin(uint64_ttag,constchar*name){if(CC_UNLIKELY(atrace_is_tag_enabled(tag))){voidatrace_begin_body(constchar*);atrace_begin_body(名称);可以看到这里的逻辑和Java中的处理是类似的。也是判断Tag是否可用。如果可用,则执行写入数据的逻辑。继续看atrace_is_tag_enabled的实现,staticinlineuint64_tatrace_is_tag_enabled(uint64_ttag){returnatrace_get_enabled_tags()&tag;){atrace_init();返回一个trace_enabled_tags;}可以看到获取并操作了atrace_enabled_tags字段的值,Trace类中的sEnabledTags也是通过nativeGetEnabledTags方法获取的。因此,我们应该修改下层native层的值来开启Trace功能。这里参考Facebook的profilo方案,通过dlopen获取libcuitls.so对应的句柄,从对应的symbol中找到atrace_enabled_tags的指针,设置atrace_enabled_tags开启Trace功能。std::stringlib_name("libcutils.so");std::stringenabled_tags_sym("atrace_enabled_tags");if(sdk<18){lib_name="libutils.so";enabled_tags_sym="_ZN7android6Tracer12sEnabledTagsE";}if(sdk<21){handle=dlopen(lib_name.c_str(),RTLD_LOCAL);}else{handle=dlopen(nullptr,RTLD_GLOBAL);}atrace_enabled_tags=reinterpret_cast*>(dlsym(handle,enabled_tags_sym.c_str()));atrace_enabled_tags在不同版本有不同的符号名,所以这里需要区分下版本。可以使用objdump工具查询具体符号名称。在Mac上可以使用binutils工具提供的gobjdump工具查看。像上面的atrace_enabled_tags这样的symbol是Android18以下的,我们可以直接通过gobjdump工具查看:但是有时候一个symbol的名字可能是mangle,查看的时候不是很直观。确认是否是我们需要的符号名,可以通过c++filt工具demangle这个符号,得到一个更直观的符号名,方便我们确认。这里我们获取到atrace_enabled_tags符号对应的指针,修改为具体对应的Tag值,同时通过反射同步修改Trace类中的sEnabledTags值,开启Trace功能。这里要设置的Tag,可以详细参考系统提供的Trace类,里面定义了所有的Tag值。我们可以通过对这些Tag值进行OR运算得到一个需要设置的int类型的值。数据恢复经过以上步骤,我们在PC端不用执行systrace脚本就可以开启Trace功能了,但是从上面的实现示意图可以看出,数据最终是写在trace_marker中的,而这个是在内核中模式。无法直接读取应用层。在为Trace寻找对应Tag的过程中,可以看到native代码中有这样的定义:intatrace_marker_fd=-1;通过查看代码可以发现,这个字段就是对应trace_marker的文件描述符。而当我们调用Trace.beginSection进行写入时,最终会调用到native层的atrace_begin_body方法intlen=snprintf(buf,sizeof(buf),"B|%d|%s",getpid(),name);if(len>=(int)sizeof(buf)){ALOGW("%s中的截断名称:%s\n",__FUNCTION__,name);len=sizeof(buf)-1;}write(atrace_marker_fd,buf,len);}可以看到最后的写入过程其实是调用write方法实现的。我们可以和上面的atrace_enabled_tags一样获取trace_marker的文件描述符对应的指针,这样有了文件描述符,通过hook系统的write方法,在write方法中,通过文件描述符来判断是否写入trace_marker如果是,可以直接将内容保存到我们定制的文件中,实现数据恢复。但是从上面的代码我们可以看出,trace_marker中写入的最终内容是“B|pid|name”这样的一串数据。如果只是这样一串数据,是无法被systrace工具识别和解析的,所以我们还需要按照下面的格式进行数据补全。-[000]...1:tracing_mark_write:||数据保存文件导出后,可以提供通过systrace工具的--from-file参数将文件转换成Html文件,可以通过Chrome打开查看。最新的SDKPlatformTools去掉了systrace工具,这里可以直接通过Perfetto打开导出的文件,无需转换文件。在终端开启Trace后,抓取数据的最终效果如下。可以看到Android系统埋点和我们自己添加的点的数据都可以抓取到。预测耗时?systrace的另一个痛点是需要手动插入Trace.beginSection和Trace.endSection方法,这意味着开发预测where的函数非常耗时。但在大多数情况下,你可能并不知道哪里会发生耗时,尤其是在线环境下,根本无法判断哪里会发生耗时。既然无法预测,那就全部增加,但这对于大项目来说工作量很大,不实用。因此,Trace方法通过函数检测添加到每个方法中。如果只是在插入存根时设置方法名,生成的文件可读性差,不利于数据分析。但是如果插入方法的全限定名,Trace.beginSection方法中sectionName是有长度限制的,所以这里参考腾讯的martix的实现生成一个methodId,插入的时候插入一个methodId,从而避开beginSection方法的长度限制。最终效果如上图。多次操作后,你会发现最终生成的文件中会有一些DidnotFinish的数据。分析前后数据,发现这些地方都抛出异常,走异常流程,导致Trace数据没有关闭。因此,也需要在插入存根时抛出异常的catch代码块中插入Trace.endSection,完成数据的关闭。总结借助系统提供的Trace功能,我们可以生成一个完整的调用链路信息,对开发和排错都有帮助;在端开启systrace功能摆脱了PC端的限制,方便我们在各种环境下抓取数据,也能帮助我们发现一些偶发或隐藏的卡顿问题。参考资料https://github.com/facebookin...https://github.com/Tencent/ma...https://linux.cn/article-9838...https://segmentfault.com//11...本文由网易云音乐技术团队发布,未经授权禁止转载。我们常年招聘各种技术岗位。如果你要跳槽,又恰好喜欢云音乐,那就加入我们吧grp.music-fe(at)corp.netease.com!