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

一文读懂字节跳动自研移动研发工具链 MBox MBox 是字节跳动抖音基础技术、Client Infra-DevOps根据移动端研发出现的现状与问题,结合移动端研发工具相关实践经验,自研的一款面向移动端开发者的研发工具链产品。目前,M... 阅读详情

作者:李卓立 仲凯宁

背景介绍

《字节跳动 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)等相关优化,系列文章也会进一步进行相关的分享,敬请期待。

引用链接

  1. https://mp.weixin.qq.com/s/MTt3Igy7fu7hU0ooE8vZog
  2. https://reviews.llvm.org/rG4272cc7d4c1e1a8cb39595cfe691e2d6985f7161
  3. https://lldb.llvm.org/design/sbapi.html
  4. https://github.com/facebook/fishhook
  5. https://discourse.llvm.org/t/rfc-lldb-telemetry-metrics/64588

关于字节终端技术团队

字节跳动终端技术团队 (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. 有移动端技术栈开发经验优先

职位链接

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

基于Spring AOP实现对外接口的耗时监控 AOP是Spring的核心,Spring不但自身对多种框架的集成是基于AOP,并且以非常方便的形式暴露给普通使用者。以前用AOP不多,主要是因为它以横截面的方式插入到主流程中,担心导致主流程代码不够清晰,定位问题不够方便,而在计费二期的项目里需要一个很适合用AOP来做的功能,就是要把对外接口和所调用的外部接口的耗时时间给记录下来,这个需求主要来自于计费一期的联调,常常发生系统间交互不够顺畅的情况, 阅读详情

相关推荐

Xcode中Playground运行代码无响应的极简解决方法

大多数童鞋可能对Xcode中的Playground又爱又恨,我完全可以体会你们的感受… Playground遇到比较多的一种情况就是:执行代码挂起! 就是点击那个小三角运行按钮,等到天荒地老却此情可待成追忆的赶脚… 这时你把Xcode彻底的完全的关闭,但仍然没有什么卵用… 难道抓狂的你只有重启Mac么??? 答案当然是:NO! 一个非常简单的解决办法是: 1.打开活动监视器(就是Windows...

享受开发,颠倒银河 2878

Xcode 系统崩溃问题01

Xcode 崩溃

ws1836300的博客 2155

java8源码-P18020-Test-SB-Api:P18020-Test-SB-Api

java8 源码 P18020-Test-SB-Api 项目介绍 实验平台后端Api,技术:Java8+SpringBoot2+JPA+Mysql 软件架构 软件架构说明 安装教程 xxxx xxxx xxxx 使用说明 xxxx xxxx xxxx 参与贡献 Fork 本项目 新建 Feat_xxx 分支 提交代码 新建 Pull Request 码云特技 使用 Readme_XXX.md 来支持不同的语言,例如 Readme_en.md, Readme_zh.md 码云官方博客 你可以 这个地址来了解码云上的优秀开源项目 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目 码云官方提供的使用手册 码云封面人物是一档用来展示码云会员风采的栏目

调试器:Xcode已终止LLDB RPC服务器,以允许调试器与进程分离。您可能需要手动终止进程

远程调试的一个常见原因是,如果手机上没有加载到二进制文件中的系统库的“主机端”副本。第一个问题涉及代码签名问题,在解决了这个问题之后,我现在遇到了这个调试问题。我以前从未遇到过这样的问题,但当我通过iPhone升级到15.6. 1时,这些问题就开始出现了。下一次运行Xcode时,您应该会看到一些关于“为调试准备设备”的消息,即复制这些文件。Xcode通过将设备上的系统库复制到主机Mac上的缓存中,并将它们放在lldb知道要查找它们的位置来解决这个问题。每次看到一个新操作系统的设备时,它都必须这样做。

weixin_42610770的博客 2668

xcode使用playground运行程序报 LLDB RPC server crashed

问题 在第一次打开xcode的时候编写代码,然后运行的时候报这个错误 The LLDB RPC server has crashed. The crash log is located in ~/Library/Logs/DiagnosticReports and has a prefix 'lldb-rpc-server'. Please file a bug and attach the mo...

风烟 3580

字节跳动的前端工程化实践

大厂技术高级前端Node进阶点击上方程序员成长指北,关注公众号回复1,加入高级Node交流群前言首先分析了当前前端开发领域的趋势和所面临的新挑战,包括涉及平台的增多、业务复杂度的增加以及前端团队规模的增大等。接着,分享了字节跳动针对这些挑战采取的新实践,包括 Monorepo 工具的使用、自研的 Bundler 和 Build System 工具的建设以及微前端的工程化实践。最后,介绍了...

程序员成长指北 214

Android 耗时函数 监控

函数相关视频讲解:微积分基本想法Android 耗时函数监控:提升应用性能的关键 在Android应用开发过程中,性能优化是一个永恒的话题。其中,监控和优化耗时函数是提升应用性能的关键步骤之一。本文将介绍如何通过代码示例和状态图,对Android中的耗时函数进行监控和管理。 耗时函数的影响 耗时函数,顾名思义,是指执行时...

weixin_34943809的博客 189

LLDB与debugserver

配置debugserver默认情况下iOS上并没有安装debugserver,因此需要设备链接一次Xcode,并在Window->Devices菜单中增加此设备后。会被安装到IOS的Developer/usr/bin/目录下。 瘦身 接下来我们将当前设备进行从IOS拷贝到我们的mac系统上,进行对debugserver进行瘦身 lipo -thin armv7s ~/debugserver -ou

细心程度决定你的成败 2220

iOS 菜鸟逆向学习 (二)----iOS debugserver + lldb的安装调试

今天准备试试想试试--debugserver + lldb,是根据大神的帖子: http://bbs.iosre.com/forum.php?mod=viewthread&tid=52&highlight=debugserver 虽然很详细 但是还是遇到了很多问题,在这里记录一下。 首先导出debugserver,导出的路径ios--Developer/usr/bin/debugserve

think_ma 8814

Mac Xcode崩溃 (打开ios项目引起崩溃)

bug:每次打开此工程都会导致Xcode崩溃。其他工程没有问题。解决办法:1.确定本地跟服务器没有需要更新和提交的代码2.把本地工程移到废纸篓3.从新check out工程4.新工程完美运行(这样没法解决的话,就问你慌不慌,哈哈哈哈哈哈哈)(一般来说,不会存在你写的代码会导致xcode崩溃。我的崩溃原因是打开多个项目的时候发生的。)以下是Xcode提供的崩溃日志:Process:         ...

wxdtan的专栏 1万+

一步一步用debugserver + lldb代替gdb进行动态调试(整理与补充)

因为Apple已经弃gdb投lldb,所以随着我动态调试的次数越来越频繁,gdb上一个接一个的bug经常会让人很恼火。既然苹果打算建立自己的调试器王国,也投入了钱力精力,那我们干脆也上手lldb玩玩,看看lldb是不是比gdb要更好用(以下操作在iPhone 5,iOS 7.0.4上测试,应该也适用于arm64,如果不行,请参照iphonedevwiki) 一、定制lldb 1. 下载一个

zhangmiaoping23的专栏 1万+

LLDB+debugserver动态调试

LLDB(Low Level Debugger) 内置于Xcode中的动态调试工具,通吃C,C++,OC,全盘支持OSX,iOS,iOS模拟器 在指定的条件下启动程序; 在指定的条件下停止程序; 在程序停止的时候检查程序内部发生的事; 在程序停止的时候对程序进行改动,观察程序的执行过程有什么变化 LLDB是运行在OSX中的,要想调试iOS,需要和debugserver进行配合. debugs

Esirnus 5734

cmake编译文件

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34159110的博客 320

LLDB 动态调试Android 应用

1. 获取 llddb_server: NDK/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/14.0.7/lib/linux/aarch64/lldb-server。4. 在手机开启lldb_server ./lldb-server p --server --listen unix-abstract:///data/local/tmp/debug.sock。9. 查看某个调试so的基地址:image list -o -f libxxx.so。

明风的博客 1629

三种Timer使用

System.Windows.Forms.Timer,System.Threading.Timer,System.Timer,三种Timer使用如下 第一种:System.Windows.Forms.Timer使用 [DllImport("User32.dll", CharSet = CharSet.Auto)] public static extern int SetWindowTe...

LongtengGensSupreme博客 522

计算程序运行时间的封装

  只能在linux下使用: #include &lt;sys/time.h&gt; class timer { public: timer(int _timerType = CLOCK_PROCESS_CPUTIME_ID) : timerType(_timerType) { clock_gettime(ti...

weixin_34248258的博客 118

brpc源代码分析-定时器Timer

源代码地址:https://github.com/apache/incubator-brpc/blob/master/src/bthread/timer_thread.h 阅读源代码,不仅可以了解到功能的具体实现方法,还能够学习大神们的思考和设计思路。所以在未来一段时间,通过阅读分析源代码的方式,研究brpc的实现和设计思路。 本节首先从定时器开始。 定时器相关文档:https://github.com/apache/incubator-brpc/blob/master/docs/cn/timer_keep

baidu_41692782的博客 793
上一篇: Introduction to ByteDance Pitaya
下一篇: 中心化决议管理——云端分析
字节跳动终端技术
博客等级 码龄7年 206粉丝 52原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值