飞书增量编译优化实践总结—transform 优化

一、上下文

时间大概发生在1月的上旬的时间,飞书在进行了对主仓库的源码模块拆分后从构建耗时上统计看出增量的水平仍然在大概在一个较高的范围水平波动,并未达到因源码模块大量减少所应有的对编译速度上的优化预期。

这里曾经猜测过有可能是因为老分支数据的影响带来的收益明没有来的那么及时,会产生一定的延迟满足效益,但是后来经过对Hummer(字节内部编译监控平台)的数据、图表分析,发现拆仓效果其实很快就有了在生产环境中的体现,依图可见我们的源码模块数量是在拆仓之后呈现骤减趋势的。

那么不禁令人怀疑,到底是哪里出了一些问题?

二、线上数据分析

这里我们随便的通过这段期间的编译列表,抓取一份增量在2min左右(有真实代码修改)的编译数据样本具体分析。

样本:

根据样本数据我们可以看到几个信息:

  1. 在接入FarSeer Optimizer优化后,我们的壳工程(顶部Module)对于R文件的javac任务其实已经被合理优化掉了。
  2. 参与增量编译的模块并不多,其BuildCache状态、执行耗时也符合预期。
  3. 有几个较为明显的耗时任务,除本来就很慢的Kapt之外,在mergeExtDex、pangaTransform、manisTransform任务上耗时尤为的突出。

同时我们也观察了一些同期内的线上数据样本,加上一些本地的测试数据基本都能保持相同的结果,所以这几个任务就是我们重点攻略的对象,其中必有妖孽。

三、线下问题分析

在有了初步的线上数据基础,问题研究方向后我们就需要逐一的去处理,通过之前的问题简析我们可以看到不论是我们自定义的一些字节码操作还是Gradle原生的DexMergingTask上感觉问题都是围绕着 transform 流程在发生。

那我们再简单的介绍一下transform的机制:

Android gradle transform增量构建优化

一个Transform就是一个处理构建产物的过程。一次完整的apk构建拥有多个Transform,这些Transform被串行执行。前一个Transform的输出会作为后一个Transform的输入。

那么,Transform的增量是怎么实现的呢?gradle里最小的执行单位是task,Transform被注册到构建流程里时会成为一个单独的TransformTask。而gradle task的incremental机制其实是通过检测Input和output的文件变化来实现增量构建的。当检测出Input有文件changed时,gradle会把它们放到changelist里,task内部根据changelist做增量输出;当检测出output被删除了,会触发该task的全量输出。在gradle的configuration完成之后,所有的task的input和output都已经被规划好了。如果taskA依赖taskB,那么taskB的output会成为taskA的input,Transform的增量也是基于这样的机制来实现。

在有了基础的transform知识之后我们大概有几点是明确的:

  1. transform的设计本身是个chain,先注册的transform task将先拿到整个构建输入,同时将自身逻辑加工后执行输出到下一个transform task上,gradle原生的transform task将会最后执行,每个环节都将直接影响到下一个transform task。
  2. transform在声明注册时将会声明该任务是否支持增量编译,而若其中一环遭到非增量破坏,将会影响整个chain的执行效率。
  3. transform大家在写的时候可以发现,获取输入源很简单,就是transformInvocation.inputs拿到一个Collection(真实实现为ImmutableList)对象,遍历即可拿到所有输入源。直观的感觉,够简单但是不够健壮。

所以我们按照上述几点探寻和排查确认即可发现一些隐藏的问题真实在影响着编译速度,也是恰巧下文中即将提到的三个问题面向了不同的三个方向,感觉具有一定的通用性,希望给大家一些启发,对大家在排查优化手上工程项目时提供一些思路。

3.1 未实现增量编译、重复的功能实现 — Panga

Panga是什么?据线下调研了解到,panga是飞书小程序所引入的一套DI框架,这里我们没有找到源码直接扒了下maven上的jar,发现panga每次稳定跑10于秒的原因正是其未能实现增量编译处理,在自身执行效率低下的基础上,继而又导致每次都在全量执行时也同时影响着后续注册执行的transform。

一般而言,对于这样的task我们往往进行优化其逻辑使其能够完成合理的增量处理即可,但是这里我们发现了可以顺手推动优化的另一个点,即:重复作用的transform框架引入。

在飞书中我们发现类似DI、SPI的框架有三个之多且其功能基本可以相互满足,最终我们采取了统一方案移除了Panga和ServiceManager。毕竟冗余的实现在全量编译时一个大型项目每个transform task基本都会带来10s+的耗时开销。

这里也是推崇一个更进一步的思想和理念就是尽可能的将我们自研、可控的transform进行收敛,避免重复过多的io开销。

使用ByteX框架收敛并提升android 项目中的 transform 编译速度

github.com/bytedance/B…

3.2 增量的实现是否足够达标 — Manis

简单介绍一下,Manis是飞书的一套IPC框架基本流程逻辑为在字节码阶段收集所有标注了Manis注解的实现类将其具体的执行进程信息进行收集,最终将收集好的信息数据插入到Manis框架内部的映射表中,提供runtime阶段的服务进程查找及映射。

同样这里我们找出两份样本数据来比对:

样本A 全量:

样本B 增量:

这里全量时该transform的耗时为17.259s,增量耗时为6.542s,同期大盘数据上看增量和全量的耗时也基本在这个水平上进行波动,这里增量减少的耗时基本为对未变更inputs的io减少而带来;同时我们横行进行对比比如ServiceManager、Claymore等插件,其增量的数据水平基本为1s上下波动,这里存在着明显的性能差异。

而我们分析其实现原理发现虽然增量处理了intpus相关的io行为,但是在对注解的收集上以及对Manis目标插入Jar的查询每次都是全量执行遍历的,这里涉及到对JarInput中的所有Class文件执行装载和分析,导致无论增量与否恒定耗时约5~7s的时间。

于是乎我们借助一个对于transform执行任务的潜规则,即:保持自身输出顺序的恒定,避免带来对后续任务的影响。

也经过测试保证了前序任务输出的稳定性后,我们将全量时收集到的注解及目标注入Jar的文件路径进行缓存,增量时仅处理变更inputs的注解收集,完善了这里对增量的耗时优化。优化后我们也是同样经过本地的测试及线上采样验证:

论证样本A :

论证样本B:

最终确认完成了其有效性,稳定减少增量部分开销约7s,由7~9s减少至0.5~1.5s的水平。

3.3 在任务中使用不够安全的多线程进行操作 — VCXP

我们发现前文提到的mergeExtDex的耗时在经过上述的优化操作之后,仍然不定时的会产生较为严重的缓存命中问题,导致任务重跑。在不断的进行线下的测试和确认后,我们发现最终的影响因素为VCXP — 一个飞书的视频会议业务引入的 IPC + DI 框架。

问题样本:

我们通过分析发现框架本身的执行逻辑并无明显的问题,其任务本身耗时并不严重且还算优秀,增量也是能够稳定在约1s上下的范围内波动,直观上感觉并无大碍。不过在仔细研究其实现原理时,我们发现在该框架内在实现inputs写操作时是通过多线程的方式进行的。

难道说transform中只能单线程操作不能借助多线程提高执行效率吗?其实并不是的,问题是出在transform本身的设计上。

File dest = outputProvider.

    getContentLocation(jarInput.getFile().getAbsolutePath(),

        jarInput.getContentTypes(),

        jarInput.getScopes(), Format.JAR);
复制代码

上述代码是一段我们非常熟悉的逻辑,主要是用于通过outputProvider 及输入的jarInput对象获取一个目标jar输出的File对象。其生成的路径即为我们非常熟悉的在build – intermediates – transforms – xxx目录下的xxx.jar,同时通过跟进实现我们发现 0.jar、1.jar、xxx.jar的生成真的是……超乎想象的简陋,调用顺序将直接决定输出目标文件的文件名及后续一个transform task的inputs list,同时在vcxp的实现中对output dest jar file的获取又是在子线程中执行调用的,这也就不可避免的造成了对于输出的jar file的顺序存在着不确定性,引发了后续任务的缓存策略失败。

问题定位后,其实也很好解决,我们依旧可以使用多线程来优化我们的io执行逻辑,只需要保持对于destJarFile的同步单线程调用固定顺序即可。同样也是通过线上样本分析,最终验证了我们的修改结果,在增量编译时拿掉了对mergeExtDex的意外耗时,正确命中buildCache。

论证样本A :

四、优化成效及小结

通过对现有问题的充分排查修复之后,我们也是在月末的时候完成了这一阶段对增量编译的优化。从最初的平均增量耗时2m波动基本优化到了1.1m波动的水平,甚至状态好的时候可以在pct50分位达到一分钟以下的水准,可见transform若存在坑的话对编译的效率影响也是极其严重的。

通过此次摸排整改也是大致总结出了以下几个方向,也是供大家进行参考:

  1. 由于transform设计上不够鲁棒,因此需要在引入transform plugin的时候需要格外的小心和注意,切莫轻易准入,需要经过充分的测试和验证。
  2. 对于类似功能实现的transform plugin最好能够推动业务方进行收敛融合,避免过多轮子存在在工程中,带来无谓的编译、运行时性能开销、亦会带来在基础组件架构上的不一致。
  3. 对于transform需要有一个明确的判定依据和标准,全量、增量的水平到底是什么样,对耗时严重的任务要敢于质疑,一个好的增量实现耗时影响应当是非常之低的。
  4. 合理借助基建工具,对于线上大盘的编译数据多观察勤研究,及早的发现问题避免出现研发效能的无端浪费。

加入我们

飞书-字节跳动旗下企业协作平台,集视频会议、在线文档、移动办公、协同软件的一站式企业沟通协作平台。目前飞书业务正在飞速发展中,在北京、深圳等城市都有研发中心,前端、移动端、Rust、服务端、测试、产品等职位都有足够的 HC,期待你的加入,和我们一起做有挑战的事情(请戳链接:future.feishu.cn/recruit

我们也欢迎和飞书的同学一起进行技术问题的交流,有兴趣的同学请点击 飞书技术交流群 入群交流。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享