作者:李卓立 仲凯宁

背景介绍

在《字节跳动 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

注入动态库

字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案

正常流程中,Xcode开始调试时会启动一个lldb-rpc-server的进程,这个进程会加载Xcode默认东西链,或指定东西链中的LLDB.framework,而且经过这个动态库中暴露出的Script Bridge API调用LLDB的各功用。

字节跳动 DanceCC 工具链系列之Xcode 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指令

字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案

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的变量显示耗时,优化了接近一个数量级。

字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案
字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案

极点耗时场景仓库搜集

除了根本的耗时时刻搜集以外,咱们还希望可以及时发现新增的极点耗时场景和新问题,因而规划了一套极点耗时情况下的调试器仓库搜集机制,现在只需发现,展现变量场景和输入expr指令耗时超过10秒种,则会记载LLDB.framework的当时调用仓库的每个函数耗时,并将数据上签到后台进行核算和人工分析。仓库搜集运用了log timers dump所产出的仓库和耗时信息,本质上是LLDB代码中经过LLDB_SCOPED_TIMER()宏记载的函数,其会运用编译器的__PRETTY_FUNCTION__才能来在运行时得到一个用于人类可读的函数名。在获取到调用前和调用后的两条仓库后,咱们会对每个函数进行Diff核算和排序,将最耗时的前10条进行了采样记载,运用字符串一同上传到核算后台中。

字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案

总结

无论是App仍是东西链,在做功用优化的同时,数据目标建造是必不可少的。这篇文章讲述的监控计划,在后续迭代DanceCC东西链的时分,可以清晰相关的优化对实践的调试体验有所帮助,能防止了主观和片面的测试来评价调试器的可用性。除了调试器之外,DanceCC东西链还包含诸如链接器,编译器,LLVM子东西(如dsymutil)等相关优化,系列文章也会进一步进行相关的共享,敬请期待。

引证链接

  1. mp.weixin.qq.com/s/MTt3Igy7f…
  2. reviews.llvm.org/rG4272cc7d4…
  3. lldb.llvm.org/design/sbap…
  4. github.com/facebook/fi…
  5. discourse.llvm.org/t/rfc-lldb-…

关于字节终端技能团队

字节跳动终端技能团队 (Client Infrastructure) 是大前端根底技能的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),担任整个字节跳动的大前端根底设施建造,提高公司全产品线的功用、稳定性和工程功率;支撑的产品包含但不限于抖音、今天头条、西瓜视频、飞书、瓜瓜龙等,在移动端、Web、Desktop等各终端都有深入研究。

参加咱们

咱们是字节的 Client Infrastructure 部门下的编译器东西链团队,团队成员由编译器专家及构建体系专家组成,咱们基于开源的 LLVM/Swift 项目供给深度定制的 clang/swift 编译器、链接器、lldb 调试器和言语根底库等东西及优化计划,掩盖构建功用优化应用功用稳定性优化等场景,并在事务研发功率和应用质量提高方面取得了明显的效果,同时,在实践的过程中咱们也看到了很多令人兴奋的新机会,希望有更多对编译东西链技能感兴趣的同学参加咱们一同探索。

工作地址

深圳、北京

职位描述

  1. 规划与完成高效的编译器/链接器/调试器优化
  2. 自定义 LLVM 东西链的保护和开发
  3. 提高Client Infrastructure编译东西链的功用及稳定性
  4. 协同事务团队推进技能计划的落地

职位要求

  1. 至少熟练掌握 C++/Objective-C/Swift 其间一门言语,了解言语特性的完成细节
  2. 了解编程言语的完成技能,如解释器、编译器、内存办理方面的完成
  3. 了解某个构建体系 (CMake/Bazel/Gradle/XCBuild 等)
  4. 有编译器、链接器、调试器等东西的开发和优化经验优先,有 LLVM、GCC 等项目项目开发经历优先
  5. 有移动端技能栈开发经验优先

职位链接

点击链接投递简历:job.toutiao.com/s/FBS9cLk!