如何利用ReleaseApk辅助排查性能问题

本文约 2100 字,阅读需 4 分钟。

背景

最近收到反馈,同样的场景,Android打开明显比iOS卡顿,实际打开也确实如此。故此要进一步排查这个问题。

我们的业务工程非常庞大,之前全量编一次竟然花了快2h。

20240430164344

此外,我负责的是底层渲染模块,此类问题虽然一般先找到我,但耗时的根源可能是底层框架的问题,也可能是上层业务的问题。底层框架虽然使用wolfpld/tracy: Frame profiler做了打点,但也有两个问题:

  • 是手动打点,不一定覆盖到了耗时点。以Build为例,只能看到Build耗时,但具体是哪个业务组件的Build耗时不得而知
  • 易用性不高,无论是导出trace文件还是用Server连接,由于平时很少用(而且一般只有Mac平台用),很难在业务工程中用起来

此外,框架自带PerfOverlay,能指出Raster/UI的总耗时,但无法准确定位。

如果用业务的Debug-Apk排查,则Debug相关的代码噪音会比较大。而业务工程只支持Debug/Release,并无Profile构建(使用场景少?没人维护?)。

总的来说,第一个挑战就是如何在业务开发环境限制比较大的情况下有一套高效的定位此类问题的方法:

  • 要和线上包一样(即Release),避开Debug的噪音
  • 因为不知道哪些耗时,手动埋点再重编显然效率不行

尝试

我的想法是直接利用流水线的Relase包,先改成Debuggable,这样就可以用AS进行Profile了:

 sh ./apktool.sh d -f *********.apk -o tda
 # 修改AndroidManifest.xml: android:debuggable="true",再重新打包
 sh ./apktool.sh b -f tda -o tda.apk
 # 生成一个临时签名,重新签名
 keytool -genkey -v -keystore keystore.jks -alias my_alias -keyalg RSA -keysize 2048 -validity 10000
 ~/Library/Android/sdk/build-tools/30.0.2/apksigner sign --ks keystore.jks --out signed_debuggable_release.apk tda.apk

接下来,就可以用Android Stduio Profiler的CPU-Profiler进行Callstack Sample Recording,便可以拿到火焰图。

20240430171547

发现耗时点,由于是Release包,需要结合符号表进行还原,便可以拿到对应的耗时点:

~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line -f -e  ~/Downloads/symbols/libbinding_arm64-v8a.so 0x4495ea0 --verbose -C

这样的好处是接到问题就能立即开始排查,无需编译和埋点。但目前最大的问题是火焰图只能显示方法的偏移地址,需要用addr2line翻译,一是麻烦,二是不直观

探索

基于还有些不便之处,尝试彻底解决。但网上相关资料比较少,只能自己探索。主要的参考资料

  • ~/Library/Android/sdk/ndk/25.1.8937393/simpleperf目录下的脚本和文档 -
  • Simpleperf

AS的Profiler的这个采样其实就是封装了simpleperf,如果自己从命令行也可以使用,但比较麻烦。

AS的Profiler导出的是一个.trace结尾的文件,用chome://tracing和perfetto都无法打开,也查不到这个.trace文件的格式。

后面查看了simpleperf的脚本和源码,才知道是一个基于ProtoBuf的二进制文件,自己解析处理目前看比较麻烦。

发现一个annotate.py的脚本,感觉可以用:


python3 ../extras-refs_heads_main-simpleperf/scripts/annotate.py -i cpu-simpleperf-20240429T182407.trace -s ../symbols/ --ndk_path ~/Library/Android/sdk/ndk/25.1.8937393 --dso /data/app/~~lFdDs3aoPJ7LrNU_vCU51g==/com.*******.****s-dXOmKCUGifMhUrBehyo7bA==/lib/arm64/libbinding.so --log debug
14:35:49,884 [DEBUG] (simpleperf_utils.py:567) Can't find dso /data/app/~~lFdDs3aoPJ7LrNU_vCU51g==/com.*******.****s-dXOmKCUGifMhUrBehyo7bA==/lib/arm64/libbinding.so
14:35:54,421 [INFO] (annotate.py:485) annotate finish successfully, please check result in annotated_files/.

从日志这就不行,感觉是这个脚本设计的问题,不支持指定符号表,看样子要自己写一个了:

import logging
from simpleperf_report_lib import GetReportLib

def main():
    print('start process')
    lib = GetReportLib('cpu-simpleperf-20240429T182407.trace')
    while True:
        sample = lib.GetNextSample()
        if sample is None:
            lib.Close()
            break
        symbols = []
        addr_list = set()
        symbols.append(lib.GetSymbolOfCurrentSample())
        callchain = lib.GetCallChainOfCurrentSample()
        for i in range(callchain.nr):
            symbols.append(callchain.entries[i].symbol)
        for symbol in symbols:
            if (symbol.dso_name.endswith("libbinding.so")):
                addr_list.add(symbol.vaddr_in_file)

        for addr in addr_list:
            print(hex(addr))

if __name__ == '__main__':
    main()

输出的是这个.trace文件包含的所有函数地址,再用一个脚本处理下:

cat addr.txt | while read line
do
   ~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line -f -e  ~/Downloads/symbols/libbinding_arm64-v8a.so -C $line
done

结果发现速度巨慢(因为我们的工程非常大,又打包成了一个so),每一个查找要3s,导出的有28w个地址。

看来目前,只能发现有问题的指针,再按需还原了….

需要注意的是,simpleperf的anotate.py之前只支持解析.data的数据格式,.trace的格式看起来只有AS Profiler支持,NDK的simpleperf还没有这个能力,但看最新的源码,是去年底提交的。

20240430163006

节前时间比较松,有点精力研究下,先用起来,至于更深度的使用,节后继续尝试。

参考


2024/05/09更新

  • 由于逐个地址还原还是太麻烦了,于是想着能不能编一个Profile的版本(应该就是CMake的RelWithDebInfo)
  • 研究了我们的项目之后,发现,当前的流程是用-g -O2编了一个带调试信息的release的so(3.2G),然后直接归档,并将strip过后的so上传maven(60M),给主流水线用
  • 这个3.2G的so其实就是我需要的,于是我直接在修改AndroidManifest.xml那一步直接把so也替换了,再重新打包
  • 用AS Profiler抓取调用栈,就都是有符号的了
  • 这个问题就阶段性解决了,这样会不会对运行耗时产生影响?有待研究
总阅读量次。