Skip to content

车端性能采集体系:从无符号到函数级火焰图 ​

车端性能问题最难的不是优化,是拿到能看的证据。这套体系把车端 arm64 火焰图从「一堆十六进制地址」做到「函数级调用栈」:改一个打包参数、替换一份 678MB 的 IL2CPP 符号表、用 simpleperf 的 DWARF 调用栈采样,再沉淀成 trace.ps1 一键工具和两条免打包的实时调试通道。

符号表体积678MB
采集通道4 条
打包要求5.9.x 起免重打包

背景 ​

6.0 路测阶段,一张图(Unity IL2CPP 应用,arm64 车机)持续有性能课题:GPU 占用、帧率、卡顿都要拿数据说话。方法论层面我们已经有了 GPU 80% 红线,但红线只能告诉你「超了」,不能告诉你「谁在烧」。要在车端回答这个问题,必须能抓到函数级火焰图——而默认状态下抓到的 perf 数据全是地址,没有符号。

问题 ​

问题根因
火焰图全是地址apk 里的 so 被 strip,不带符号
IL2CPP 函数名全丢C# 经 IL2CPP 编成 native,符号只在打包产物的 libil2cpp.so 符号表里
so 文件不在文件系统上默认 extractNativeLibs=false,so 直接从 apk 内映射加载,没法替换
每次采集都要出包5.9 之前抓取版本必须带 -Pdebuggable=true 单独打包
Windows 解符号失败解符号脚本依赖的网络组件在 Windows 环境超时,需换 Linux/Mac

方案 ​

mermaid
flowchart LR
    A["打包修改<br>extractNativeLibs=true"] --> B["安装 apk<br>pm path 定位 so 目录"]
    B --> C["adb push 替换<br>678MB libil2cpp.so"]
    C --> D["simpleperf record<br>--call-graph dwarf"]
    D --> E["adb pull<br>perf.data"]
    E --> F["解符号<br>gecko_profile_generator --symfs"]
    F --> G["火焰图 HTML<br>/ Firefox Profiler"]

核心思路一句话:让带符号的 so 出现在采样时进程加载的路径上。extractNativeLibs=true 让 so 释放到 lib/arm64 目录,adb push 把带符号的 libil2cpp.so 覆盖进去,simpleperf 采样时自然就能读到符号。Unity 引擎层的符号(2022.3.29f1 release 版)和 IL2CPP 的符号分开准备,解符号时用 --symfs 指向同一个目录即可。

关键实现 ​

1. extractNativeLibs 打包修改 + 符号表替换 ​

打包时把 AndroidManifest 里的 android:extractNativeLibs 改为 true(我基于一笔 cherry-pick 出包)。安装后定位 so 目录并替换:

bash
adb shell pm path com.xiaopeng.montecarlo   # 拿到 base.apk 路径,同级 lib/arm64
adb root && adb disable-verity               # 首次需 reboot
adb remount
adb push libil2cpp.so <安装路径>/lib/arm64   # 678MB,约 6 秒
adb shell killall com.xiaopeng.montecarlo    # 重启进程生效

符号表按 ROM 版本号反查打包路径获取,6.0.0 版本的 libil2cpp.so 符号表 678MB,早期 5.8.0 的版本约 580MB——必须和车上跑的版本严格对应,否则符号错位比没有符号更误导。

2. simpleperf DWARF 调用栈 ​

bash
adb shell simpleperf record --app com.xiaopeng.montecarlo \
    --duration 100 -o /data/local/tmp/perf_1.data --call-graph dwarf
adb pull /data/local/tmp/perf_1.data .

关键是 --call-graph dwarf:帧指针在 release 构建里经常被优化掉,DWARF 调用栈才能可靠还原跨 so 的调用链。转换用 NDK 自带的 simpleperf 脚本(不用单独下载):report_html.py 直接出 HTML 火焰图,gecko_profile_generator.py 出 Firefox Profiler 格式。转换日志里的 failed to read symbols from [wlan] 这类警告是内核/第三方 so 没符号,不影响主链路,忽略即可。

3. IL2CPP 解符号 ​

bash
python3.9 gecko_profile_generator.py --symfs ./libs/arm64-v8a \
    -i perf.data > perf-back.json.gz

--symfs 指到存放所有带符号 so 的文件夹(不是单个 so),所有需要解析的 so 放同一目录。Windows 上脚本会因网络组件超时解符号失败,换 Linux 或 Mac 执行,其余步骤不变。

4. trace.ps1 一键工具化 ​

5.9.x 和 6.0 之后的版本不再需要 -Pdebuggable=true 单独打包,抓取前打开调试开关即可:

powershell
adb root; adb disable-verity; adb remount
adb shell setprop persist.xp.debug_app true; adb reboot
adb root; adb remount; adb shell setenforce 0
.\trace.ps1 <包名> <时长秒> <设备序列号> <保存文件名>

一次执行产出 4 类数据:perf.data(约 80MB 原始采样)、profile.json.gz(Profiler 格式)、studio.trace、thread_info_per.txt,直接打包给研发。SR 进程采集前多一步:把对应版本的 libil2cpp.so push 到 /data/local/tmp/native_lib/。Windows 上脚本跑不起来通常是 PowerShell 执行策略问题,Set-ExecutionPolicy RemoteSigned 解决。

5. 采集前置 SOP(抓火焰图流程) ​

性能数据可比的前提是工况一致:先确认帧率锁在 30 帧(广播打开 Unity 水印或用 adbdebug 广播下发 DebugAttribute:性能:fps显示);确认录制回放开关关闭;车辆出地库、开始导航、进入车道级之后再开抓——地库和出库段的负载画像和车道级完全不同,混在一起采样结论会偏。

6. 两条免打包的实时调试通道 ​

火焰图是事后分析,实时调试另有通道:

  • 实车 Unity Profiler:用 userdebug aar 出 apk,笔记本开 Unity 空工程 Ctrl+7,adb forward tcp:55002 localabstract:Unity-com.xiaopeng.montecarlo 后即可连上实车进程抓 Profiler 数据。
  • 浏览器实时调试(Proxima Runtime Inspector):不用 Debug 包、不用联网。广播下发 Proxima:Start:0 启动插件,adb forward tcp:7759 tcp:7759,浏览器打开 127.0.0.1:7759?page=inspector 就能实时查看和修改运行中的 3D 场景——效果类问题排查不用出包重刷。

指标 ​

指标改造前改造后
火焰图可读性十六进制地址函数级调用栈(含 IL2CPP C# 函数)
采集打包要求5.9 之前必须 -Pdebuggable=true 单独打包5.9.x / 6.0 起免重新打包
单次采集产出手工多条命令、零散文件trace.ps1 一条命令 4 类数据
实时调试必须出 Debug 包adb forward + 浏览器直达

复盘 ​

  1. 符号问题是版本问题。 火焰图失真最多的场景不是没符号,而是符号表和车上版本对不上。按 ROM 版本号反查打包产物这一步写进了 SOP,从源头杜绝错位。
  2. 把一次性操作沉淀成工具。 extractNativeLibs、替换符号表、DWARF 采样这些步骤单独看都不难,难的是每个人都做对。trace.ps1 把命令序列固化后,测试同学也能独立产出可用的数据包。
  3. 事后分析和实时调试是互补的两条腿。 火焰图回答「CPU 花在哪」,Proxima 实时 Inspector 回答「场景现在长什么样」,前者抓性能后者抓效果,两条通道都要免打包才有人用。
  4. 这套采集体系后来和 回放工具 75 倍提速 形成闭环:车端抓火焰图定位瓶颈,Windows 回放复现验证优化效果。

罗子初 · AI 座舱专家 · 8年端侧研发

Lv.1