作者:李卓立 仲凯宁

背景介绍

《字节跳动 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/api.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!

字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案的更多相关文章

  1. mpvue 生成字节跳动小程序的问题!!

    初始化项目文件 $ vue init mpvue/mpvue-quickstart fuck $ cd fuck $ npm install 这个时候就初始化好了,接下来 $ npm run dev: ...

  2. 字节跳动已经10万人了?渣本双非Android程序员怎么上车?

    字节跳动已经 10 万人了? 是的,在 2020 年字节跳动的员工总数从 6 万蹿到 10 万,平均每个工作日就有 150 人在办理入职,加入字节跳动全球超过 240 个办公点. 更有统计,在总部北京 ...

  3. 去了字节跳动,才知道年薪40W的测试有这么多?

    最近脉脉职言区有一条讨论火了: 哪家互联网公司薪资最'厉害'? 下面的评论多为字节跳动,还炸出了很多年薪40W的测试工程师   我只想问一句,现在的测试都这么有钱了吗? 前几天还有朋友说,从腾讯跳槽去 ...

  4. HC32L110(三) HC32L110的GCC工具链和VSCode开发环境

    目录 HC32L110(一) HC32L110芯片介绍和Win10下的烧录 HC32L110(二) HC32L110在Ubuntu下的烧录 HC32L110(三) HC32L110的GCC工具链和VS ...

  5. 字节跳动端智能工程链路 Pitaya 的架构设计

    Client AI 是字节跳动产研架构下属的端智能团队,负责端智能 AI 框架和平台的建设,也负责模型和算法的研发,为字节跳动开拓端上智能新场景.本文介绍的 Pitaya 是由字节跳动的 Client ...

  6. Linux简单程序实例(GNU工具链,进程,线程,无名管道pipe,基于fd的文件操作,信号,scoket)

    一, GNU工具链简介: (1)编译代码步骤: 预处理 -> 编译 -> 汇编 -> 链接: 预处理:去掉注释,进行宏替换,头文件包含等工作: gcc -E test.c -o te ...

  7. javassist:字节码编辑器工具

    简介: javassist是一款可以在运行时生成字节码的工具,可以通过它来构造一个新的class对象.method对象,这个class是运行时生成的.可以通过简短的几行代码就可以生成一个新的class ...

  8. 【嵌入式开发】 嵌入式开发工具简介 (裸板调试示例 | 交叉工具链 | Makefile | 链接器脚本 | eclipse JLink 调试环境)

    作者 : 韩曙亮 博客地址 : http://blog.csdn.net/shulianghan/article/details/42239705  参考博客 : [嵌入式开发]嵌入式 开发环境 (远 ...

  9. Android NDK 工具链的使用方法(Standalone Toolchain)

    转载:http://blog.csdn.net/smfwuxiao/article/details/6587709 首先需要确定目标机器的指令集. 如果是 x86 的机器,用 x86-4.4.3 版本 ...

随机推荐

  1. SAP setting and releasing locks

    REPORT demo_transaction_enqueue MESSAGE-ID sabapdocu. TABLES sflight. DATA text(8) TYPE c. DATA ok_c ...

  2. Django【执行查询】(二)

    官方Django3.2 文档:https://docs.djangoproject.com/en/3.2/topics/db/queries/ 本文大部分内容参考官方3.2版本文档撰写,仅供学习使用 ...

  3. CSS 盒子模型(一)

    CSS 盒子模型(一) 本人在校学生,主学后端,后来发现前端的基础都忘得差不多了才想着写文章回来复习!欢迎留言交流. 什么是盒子呢? 拿下举例,我们可以把每个红框都比作一个盒子,他们可以是任意的 HT ...

  4. 如何等待ajax完成再执行相应操作

    ajax广泛应用于异步请求,对于大多数业务来说,这是十分方便的,但对于一些特殊的业务,ajax的异步性会起到相反的作用. 例如在ajax请求成功后,后续的操作需要依赖ajax执行成功后的相应操作. / ...

  5. JUC源码学习笔记1——AQS和ReentrantLock

    笔记主要参考<Java并发编程的艺术>并且基于JDK1.8的源码进行的刨析,此篇只分析独占模式,后续在ReentrantReadWriteLock和 CountDownLatch中 会重点 ...

  6. 面试突击64:了解 HTTP 协议吗?

    HTTP(Hyper Text Transfer Protocol)超文本传输协议,下文简称 HTTP,它的作用是用于实现服务器端和客户端的数据传输的.它可以传输任意的数据类型,如文本.HTML.图片 ...

  7. java geteway 手机返回数据

    import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.c ...

  8. 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 ...

  9. 零基础学Java(8)数组

    数组 数组存储相同类型值的序列. 声明数组 数组是一种数据结构,用来存储同一类型值的集合.通过一个整型下标(index,或称索引)可以访问数组中的每一个值.例如,如果a是一个整型数组,a[i]就是数组 ...

  10. Running Median_via牛客网

    题目 链接:https://ac.nowcoder.com/acm/contest/28886/1002 来源:牛客网 时间限制:C/C++ 5秒,其他语言10秒 空间限制:C/C++ 65536K, ...