作者:李卓立 仲凯宁
背景介绍
在《字节跳动 DanceCC 东西链系列之Swift 调试功用的优化计划》[1]一文中,咱们介绍了怎么运用自定义的东西链,来针对性优化调试器的功用,处理大型Swift项目的调试痛点。
在经过内部项目的接入以及一段时刻的试用之后,为了准确测量经过优化后的LLDB调试Xcode项目功率提高效果,衡量项目收益,需求开发一套可以同时获取Xcode官方东西链与DanceCC东西链调试耗时的耗时监控计划。
一般来说,LLDB内置的工作耗时,可以经过输入log timers dump
来获取粗略的累计耗时,可是这个耗时只包含了源代码中插入了LLDB_SCOPED_TIMER()
宏的函数,并不代表完好的实在耗时。而且这个耗时核算需求用户手动触发,假如要单独获取某次操作的耗时还需求先进行reset操作清空之前的耗时记载;对于咱们现在的需求而言不行准确也不行自动。
因而DanceCC提出了一套专门的计划。计划原理基于LLDB Plugin[2],利用Fishhook[3],从LLDB的Script Bridge API[4]层面阻拦Xcode对LLDB调用,以此来进行耗时监控核算。
注:LLDB论坛也有贡献者,讨论另一套内置的LLDB metries计划[5],可是目标侧重点和咱们略有不同,而且到发稿日未有完好的定论,因而仅在引证链接提及供读者延伸阅览。
计划原理
LLDB Plugin
Apple在其LLDB和前期Xcode集成中,为了不侵入一些容易改动的上层逻辑,引入了LLDB Plugin的规划和支撑。
每个Plugin是一个动态链接库,需求完成特定的C++/C入口函数,由LLDB主进程在运行时经过dladdr
找到函数入口并加载进内存。现在有两种Plugin的接口方法(网上常见第一种)
- 新Plugin接口:
namespacelldb{
boolPluginInitialize(SBDebuggerdebugger);
}
这种Plugin,需求用户在脚本中手动按需加载,并常驻在内存中:
pluginload/path/to/plugin.dylib
- 老Plugin接口:
extern"C"boolLLDBPluginInitialize(void);
extern"C"voidLLDBPluginTerminate(void);
将编译的动态库放入以下两个目录,即可自动被加载,无法手动控制时机,在当时调试Session结束时卸载:
/path/to/LLDB.framework/Resources/Plugins
~/Library/ApplicationSupport/LLDB/PlugIns
注入动态库
正常流程中,Xcode开始调试时会启动一个lldb-rpc-server的进程,这个进程会加载Xcode默认东西链,或指定东西链中的LLDB.framework,而且经过这个动态库中暴露出的Script Bridge API调用LLDB的各功用。
监控流程中,咱们向lldbinit文件中添加了command script import ~/.dancecc/dancecc_lldb.py
,用于在LLDB启动时加载脚本,脚本内会履行plugin load ~/.dancecc/libLLDBStatistics.dylib
,加载监控动态库。
监控动态库在被加载时,因为被加载的动态库和LLDB.framework不在一个MachO Image中,咱们可以经过Fishhook计划,对LLDB.framework暴露出的咱们关心的Script Bridge API进行hook。
hook成功之后,每次Xcode对Script Bridge API进行调用都会先进入咱们的监控逻辑。此刻咱们记载时刻戳来计时,然后再进入LLDB.framework中的逻辑,获取成果后回来给lldb-rpc-server,并在Xcode的GUI中展现。
Hook SB API
Hook SB API时,需求一份含有要部署的LLDB.framework的头文件(Xcode并未内置)。因为上述的流程运用了动态链接的LLDB.framework,咱们挑选了Swift 5.6的产品,并tbd化防止库房胀大。
因为LLDB Script Bridge API相对稳定,因而可以运用一个动态库完成,经过运行时来应对不同版别的API改变(很少呈现,截止发文调研5.5~5.7之间Xcode并没有改变调用接口)。
对于hook C++函数的方法,这儿借用了Fishhook进行替换。原C++的函数地址,可经过dlsym调用得到。留意C++函数名运用mangled后的名称(在tbd文件中可找到)。
///
///HookaSBAPIusingthestubmethoddefinedwiththemacrosabove
///
#defineLLDB_HOOK_METHOD(MANGLED,CLASS,METHOD)\
Logger::Log("Hook"#CLASS"::"#METHOD"started!");\
ptr_##MANGLED.pvoid=dlsym(RTLD_DEFAULT,#MANGLED);\
if(!ptr_##MANGLED.pvoid){\
Logger::Log(dlerror());\
return;\
}\
if(rebind_symbols((structrebinding[1]){{#MANGLED,(void*)hook_##MANGLED,(void**)&ptr_##MANGLED.pvoid}},1)<0){\
Logger::Log(dlerror());\
return;\
}\
Logger::Log("Hook"#CLASS"::"#METHOD"succeed!");
C++的成员函数的函数指针第一个应该是this指针,这儿用self命名。也可以调用原完成先获取成果,再根据成果进行相关的核算逻辑。
///
///Calltheoriginalimplementationformemberfunction
///
#defineLLDB_CALL_HOOKED_METHOD(MANGLED,SELF,...)(SELF->*(ptr_##MANGLED.pmember))(__VA_ARGS__)
最终全体代码中Hook一个API就可以写为:
//假定希望Hook办法为:char * ClassA::MethodB(int foo, double bar)
//这儿写被Hook的办法完成
LLDB_GEN_HOOKED_METHOD(mangled,char*,ClassA,MethodB,intfoo,doublebar){
returnLLDB_CALL_HOOKED_METHOD(mangled,self,1,2.0);
}
//这儿是履行Hook(只履行一次)
LLDB_HOOK_METHOD(mangled,ClassA,MethodB);
耗时监控场景
现在耗时监控包含下列场景:
- 展现frame变量
- 展开变量的子变量
- 输入expr指令(p, po指令也是expr指令的alias)
- Attach进程耗时
- Launch进程耗时
展现frame变量场景
经过调查,咱们发现当在Xcode中进入断点,GUI显示当时frame的变量时,lldb-rpc-server调用SB API的流程为先调用SBFrame::GetVariables
办法,回来一个表明当时frame中所有变量的SBValueList
目标,然后再调用一系列办法获取它们的详细信息,最终调用SBListener::GetNextEvent
等候下一个event呈现。因而咱们核算展现frame变量的流程为,当SBFrame::GetVariables
办法被调用时记载当时时刻戳,等候直至SBListener::GetNextEvent
办法被调用,再记载此刻时刻戳算出耗时。
展现子变量场景
经过调查,咱们发现当在Xcode中展开变量,需求显示当时变量的子变量时,lldb-rpc-server调用SB API的流程为先调用SBValue::GetNumChildren
办法,回来表明当时变量中子变量的数目,然后再调用SBValue::GetChildAtIndex
获取这些子变量以及它们的的详细信息,最终调用SBListener::GetNextEvent
等候下一个event呈现。因而咱们核算展现frame变量的流程为,当SBValue::GetNumChildren
办法被调用时记载当时时刻戳,等候直至SBListener::GetNextEvent
办法被调用,再记载此刻时刻戳算出耗时。
输入expr指令场景
Xcode中用户直接从debug console中输入LLDB指令的方法是不走SB API的,因而无法直接经过hook的方法获取耗时。咱们发现大多数开发者,都习气在debug console中运用po/expr等指令而不是GUI点击输入框。因而咱们专门做了支撑,经过SB API的OverrideCallback办法进行了阻拦。
LLDB.framework暴露了一个用于注册在LLDB指令前调用自定义callback的接口:SBCommandInterpreter::SetCommandOverrideCallback
;咱们利用了这个接口注册了一个用于阻拦并获取用户输入指令的callback函数,这个callback会记载当时耗时,然后调用SBDebugger::HandleCommand
来处理用户输入的指令。可是当SBDebugger::HandleCommand
被调用时,咱们注册的callback相同会收效,并再次进入咱们阻拦的callback流程中。
为了处理这个递归调用自己的问题,咱们经过一个static bool isTrapped
变量表明当时进入的expr指令是否被OverrideCallback阻拦过。假如未被阻拦,将isTrapped置true表明expr指令现已被阻拦,则调用HandleCommand办法重新处理expr指令,此刻进入的HandleCommand办法同样会被OverrideCallback阻拦到,可是此刻isTrapped现已被置true,因而callback回来false不再进入阻拦分支,而是走原有逻辑正常履行expr指令
Attach进程场景
Attach进程时,lldb-rpc-server会调用SBTarget::Attach
办法,常见于真机调试的场景。这儿在调用前后记载时刻戳,核算出耗时即可。
Launch进程场景
Launch进程时,lldb-rpc-server会调用SBTarget::Launch
办法,常见于模拟器启动并调试的场景。这儿在调用前后记载时刻戳,核算出耗时即可。
上报部分
数据上报
为了进一步复原耗时的细节,除了符号场景的类型以外,咱们还会一致记载这些非敏感信息:
- 正在调试的进程名,用于区别多调试Session并存的场景
- 正在调试的App的Bundle ID
- 当时断点方位在哪个文件
- 当时断点方位在哪一行
- 当时断点方位在哪个函数
- 当时断点方位在哪个Module
- 表明当时运用的东西链是Xcode的仍是DanceCC的
- 表明当时运用的Swift版别(与Xcode版别一一对应)
在内网供给的版别中,也经过外部环境变量,得知对应的App的库房标识,用于在内网的数据核算平台上展现和区别。如图,这是内网大型Swift工程,飞书iOS App接入DanceCC东西链之后,某时刻的耗时数据,可以明显看出,DanceCC相比于Xcode的变量显示耗时,优化了接近一个数量级。
极点耗时场景仓库搜集
除了根本的耗时时刻搜集以外,咱们还希望可以及时发现新增的极点耗时场景和新问题,因而规划了一套极点耗时情况下的调试器仓库搜集机制,现在只需发现,展现变量场景和输入expr指令耗时超过10秒种,则会记载LLDB.framework的当时调用仓库的每个函数耗时,并将数据上签到后台进行核算和人工分析。仓库搜集运用了log timers dump
所产出的仓库和耗时信息,本质上是LLDB代码中经过LLDB_SCOPED_TIMER()
宏记载的函数,其会运用编译器的__PRETTY_FUNCTION__
才能来在运行时得到一个用于人类可读的函数名。在获取到调用前和调用后的两条仓库后,咱们会对每个函数进行Diff核算和排序,将最耗时的前10条进行了采样记载,运用字符串一同上传到核算后台中。
总结
无论是App仍是东西链,在做功用优化的同时,数据目标建造是必不可少的。这篇文章讲述的监控计划,在后续迭代DanceCC东西链的时分,可以清晰相关的优化对实践的调试体验有所帮助,能防止了主观和片面的测试来评价调试器的可用性。除了调试器之外,DanceCC东西链还包含诸如链接器,编译器,LLVM子东西(如dsymutil)等相关优化,系列文章也会进一步进行相关的共享,敬请期待。
引证链接
- mp.weixin.qq.com/s/MTt3Igy7f…
- reviews.llvm.org/rG4272cc7d4…
- lldb.llvm.org/design/sbap…
- github.com/facebook/fi…
- discourse.llvm.org/t/rfc-lldb-…
关于字节终端技能团队
字节跳动终端技能团队 (Client Infrastructure) 是大前端根底技能的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),担任整个字节跳动的大前端根底设施建造,提高公司全产品线的功用、稳定性和工程功率;支撑的产品包含但不限于抖音、今天头条、西瓜视频、飞书、瓜瓜龙等,在移动端、Web、Desktop等各终端都有深入研究。
参加咱们
咱们是字节的 Client Infrastructure 部门下的编译器东西链团队,团队成员由编译器专家及构建体系专家组成,咱们基于开源的 LLVM/Swift 项目供给深度定制的 clang/swift 编译器、链接器、lldb 调试器和言语根底库等东西及优化计划,掩盖构建功用优化及应用功用稳定性优化等场景,并在事务研发功率和应用质量提高方面取得了明显的效果,同时,在实践的过程中咱们也看到了很多令人兴奋的新机会,希望有更多对编译东西链技能感兴趣的同学参加咱们一同探索。
工作地址
深圳、北京
职位描述
- 规划与完成高效的编译器/链接器/调试器优化
- 自定义 LLVM 东西链的保护和开发
- 提高Client Infrastructure编译东西链的功用及稳定性
- 协同事务团队推进技能计划的落地
职位要求
- 至少熟练掌握 C++/Objective-C/Swift 其间一门言语,了解言语特性的完成细节
- 了解编程言语的完成技能,如解释器、编译器、内存办理方面的完成
- 了解某个构建体系 (CMake/Bazel/Gradle/XCBuild 等)
- 有编译器、链接器、调试器等东西的开发和优化经验优先,有 LLVM、GCC 等项目项目开发经历优先
- 有移动端技能栈开发经验优先
职位链接
点击链接投递简历:job.toutiao.com/s/FBS9cLk!