字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案
作者:李卓立 仲凯宁
背景介绍
在《字节跳动 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接口:
namespace lldb {
bool PluginInitialize(SBDebugger debugger);
}
这种Plugin,需要用户在脚本中手动按需加载,并常驻在内存中:
plugin load /path/to/plugin.dylib
- 老Plugin接口:
extern "C" bool LLDBPluginInitialize(void);
extern "C" void LLDBPluginTerminate(void);
将编译的动态库放入以下两个目录,即可自动被加载,无法手动控制时机,在当前调试Session结束时卸载:
/path/to/LLDB.framework/Resources/Plugins
~/Library/Application Support/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文件中可找到)。
///
/// Hook a SB API using the stub method defined with the macros above
///
#define LLDB_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((struct rebinding[1]){{#MANGLED, (void *) hook_##MANGLED, (void **) & ptr_##MANGLED.pvoid }}, 1) < 0) { \
Logger::Log(dlerror()); \
return; \
} \
Logger::Log("Hook "#CLASS"::"#METHOD" succeed!");
C++的成员函数的函数指针第一个应该是this指针,这里用self命名。也可以调用原实现先获取结果,再根据结果进行相关的统计逻辑。
///
/// Call the original implementation for member function
///
#define LLDB_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, int foo, double bar) {
return LLDB_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)等相关优化,系列文章也会进一步进行相关的分享,敬请期待。
引用链接
- https://mp.weixin.qq.com/s/MTt3Igy7fu7hU0ooE8vZog
- https://reviews.llvm.org/rG4272cc7d4c1e1a8cb39595cfe691e2d6985f7161
- https://lldb.llvm.org/design/api.html
- https://github.com/facebook/fishhook
- https://discourse.llvm.org/t/rfc-lldb-telemetry-metrics/64588
关于字节终端技术团队
字节跳动终端技术团队 (Client Infrastructure) 是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、瓜瓜龙等,在移动端、Web、Desktop等各终端都有深入研究。
加入我们
我们是字节的 Client Infrastructure 部门下的编译器工具链团队,团队成员由编译器专家及构建系统专家组成,我们基于开源的 LLVM/Swift 项目提供深度定制的 clang/swift 编译器、链接器、lldb 调试器和语言基础库等工具及优化方案,覆盖构建性能优化及应用性能稳定性优化等场景,并在业务研发效率和应用品质提升方面取得了显著的效果,同时,在实践的过程中我们也看到了很多令人兴奋的新机会,希望有更多对编译工具链技术感兴趣的同学加入我们一起探索。
工作地点
深圳、北京
职位描述
- 设计与实现高效的编译器/链接器/调试器优化
- 自定义 LLVM 工具链的维护和开发
- 提升Client Infrastructure编译工具链的性能及稳定性
- 协同业务团队推动技术方案的落地
职位要求
- 至少熟练掌握 C++/Objective-C/Swift 其中一门语言,熟悉语言特性的实现细节
- 熟悉编程语言的实现技术,如解释器、编译器、内存管理方面的实现
- 熟悉某个构建系统 (CMake/Bazel/Gradle/XCBuild 等)
- 有编译器、链接器、调试器等工具的开发和优化经验优先,有 LLVM、GCC 等项目项目开发经历优先
- 有移动端技术栈开发经验优先
职位链接
点击链接投递简历:https://job.toutiao.com/s/FBS9cLk!
字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案的更多相关文章
- mpvue 生成字节跳动小程序的问题!!
初始化项目文件 $ vue init mpvue/mpvue-quickstart fuck $ cd fuck $ npm install 这个时候就初始化好了,接下来 $ npm run dev: ...
- 字节跳动已经10万人了?渣本双非Android程序员怎么上车?
字节跳动已经 10 万人了? 是的,在 2020 年字节跳动的员工总数从 6 万蹿到 10 万,平均每个工作日就有 150 人在办理入职,加入字节跳动全球超过 240 个办公点. 更有统计,在总部北京 ...
- 去了字节跳动,才知道年薪40W的测试有这么多?
最近脉脉职言区有一条讨论火了: 哪家互联网公司薪资最'厉害'? 下面的评论多为字节跳动,还炸出了很多年薪40W的测试工程师 我只想问一句,现在的测试都这么有钱了吗? 前几天还有朋友说,从腾讯跳槽去 ...
- HC32L110(三) HC32L110的GCC工具链和VSCode开发环境
目录 HC32L110(一) HC32L110芯片介绍和Win10下的烧录 HC32L110(二) HC32L110在Ubuntu下的烧录 HC32L110(三) HC32L110的GCC工具链和VS ...
- 字节跳动端智能工程链路 Pitaya 的架构设计
Client AI 是字节跳动产研架构下属的端智能团队,负责端智能 AI 框架和平台的建设,也负责模型和算法的研发,为字节跳动开拓端上智能新场景.本文介绍的 Pitaya 是由字节跳动的 Client ...
- Linux简单程序实例(GNU工具链,进程,线程,无名管道pipe,基于fd的文件操作,信号,scoket)
一, GNU工具链简介: (1)编译代码步骤: 预处理 -> 编译 -> 汇编 -> 链接: 预处理:去掉注释,进行宏替换,头文件包含等工作: gcc -E test.c -o te ...
- javassist:字节码编辑器工具
简介: javassist是一款可以在运行时生成字节码的工具,可以通过它来构造一个新的class对象.method对象,这个class是运行时生成的.可以通过简短的几行代码就可以生成一个新的class ...
- 【嵌入式开发】 嵌入式开发工具简介 (裸板调试示例 | 交叉工具链 | Makefile | 链接器脚本 | eclipse JLink 调试环境)
作者 : 韩曙亮 博客地址 : http://blog.csdn.net/shulianghan/article/details/42239705 参考博客 : [嵌入式开发]嵌入式 开发环境 (远 ...
- Android NDK 工具链的使用方法(Standalone Toolchain)
转载:http://blog.csdn.net/smfwuxiao/article/details/6587709 首先需要确定目标机器的指令集. 如果是 x86 的机器,用 x86-4.4.3 版本 ...
随机推荐
- SAP setting and releasing locks
REPORT demo_transaction_enqueue MESSAGE-ID sabapdocu. TABLES sflight. DATA text(8) TYPE c. DATA ok_c ...
- Django【执行查询】(二)
官方Django3.2 文档:https://docs.djangoproject.com/en/3.2/topics/db/queries/ 本文大部分内容参考官方3.2版本文档撰写,仅供学习使用 ...
- CSS 盒子模型(一)
CSS 盒子模型(一) 本人在校学生,主学后端,后来发现前端的基础都忘得差不多了才想着写文章回来复习!欢迎留言交流. 什么是盒子呢? 拿下举例,我们可以把每个红框都比作一个盒子,他们可以是任意的 HT ...
- 如何等待ajax完成再执行相应操作
ajax广泛应用于异步请求,对于大多数业务来说,这是十分方便的,但对于一些特殊的业务,ajax的异步性会起到相反的作用. 例如在ajax请求成功后,后续的操作需要依赖ajax执行成功后的相应操作. / ...
- JUC源码学习笔记1——AQS和ReentrantLock
笔记主要参考<Java并发编程的艺术>并且基于JDK1.8的源码进行的刨析,此篇只分析独占模式,后续在ReentrantReadWriteLock和 CountDownLatch中 会重点 ...
- 面试突击64:了解 HTTP 协议吗?
HTTP(Hyper Text Transfer Protocol)超文本传输协议,下文简称 HTTP,它的作用是用于实现服务器端和客户端的数据传输的.它可以传输任意的数据类型,如文本.HTML.图片 ...
- java geteway 手机返回数据
import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.c ...
- DENIED Redis is running in protected mode because protected mode is enabled
DENIED Redis is running in protected mode because protected mode is enabled redisson连接错误 Unable to i ...
- 零基础学Java(8)数组
数组 数组存储相同类型值的序列. 声明数组 数组是一种数据结构,用来存储同一类型值的集合.通过一个整型下标(index,或称索引)可以访问数组中的每一个值.例如,如果a是一个整型数组,a[i]就是数组 ...
- Running Median_via牛客网
题目 链接:https://ac.nowcoder.com/acm/contest/28886/1002 来源:牛客网 时间限制:C/C++ 5秒,其他语言10秒 空间限制:C/C++ 65536K, ...