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