4、Profiling工具链:端侧Profiling工具介绍

做端侧AI ISP优化,最怕什么?

怕的是你改了一堆代码,跑起来一看——帧率没变,功耗还涨了。更怕的是,你根本不知道瓶颈在哪。

我刚开始做ISP调试那会儿,就吃过这个亏。对着代码调了三天,结果发现是DDR带宽被某个DMA占死了。嗯,从那以后,我养成了一个习惯:动手优化之前,先上Profiling工具。

这一章,我就把端侧最常用的几把“尺子”给你捋一遍。它们分别是:perf、simpleperf、systrace、atrace,以及我们自己写的自定义计时器。

核心观点:没有Profile就没有优化。工具不是用来炫技的,是用来回答“时间花在哪了”这个问题的。

端侧 Profiling 工具链全景 端侧 AI ISP 性能瓶颈 内核级 用户态 系统跟踪 自定义 perf simpleperf systrace / atrace 自定义计时器 输出:热点函数 / 调度延迟 / 带宽瓶颈 / 帧耗时 指导优化方向

4.1 perf:内核级的“手术刀”

perf 是Linux内核自带的性能剖析工具。它直接读取PMU(性能监控单元),精度高,开销低。

我一般在定位CPU热点函数或者cache miss的时候用它。比如,你的ISP算法里有个循环特别慢,perf能告诉你——是计算瓶颈,还是内存访问瓶颈。

我的习惯:先用 perf top 看一眼全局热点,再用 perf record 抓具体进程。别一上来就全量采样,数据太多反而看不清。

常用命令示例:

# 查看全局热点(实时)
perf top

# 对指定进程采样10秒
perf record -p <PID> -g -- sleep 10

# 生成报告
perf report -g graph

-g 参数打开调用链,这个很重要。只看函数名不看谁调了它,等于只看到症状没看到病因。

注意:Android 标准内核可能裁剪了 perf 的某些功能。如果遇到 perf_event_open 失败,检查内核配置 CONFIG_PERF_EVENTS 是否开启。

4.2 simpleperf:Android上的“亲儿子”

simpleperf 是Google为Android打造的perf替代品。它解决了两个痛点:一是Android内核碎片化,perf行为不一致;二是它支持Java + C++ 混合调用链。

我记得有一次,一个AI ISP的预处理线程CPU占用异常高。用perf只能看到native层的memcpy热点,但不知道是谁调的。换成simpleperf,调用链直接追溯到Java层的Bitmap处理逻辑——原来是格式转换没走硬件加速。

基本用法:

# 记录采样数据
simpleperf record -p <PID> -g --duration 10 -o perf.data

# 生成报告
simpleperf report -i perf.data --full-call-graph
特性 perf simpleperf
平台 通用Linux Android专用
调用链支持 Native only Java + Native
符号解析 依赖debug符号 自动解析(需带symbol的so)
开销 低 低

避坑指南:我曾经在64位进程上用了32位的simpleperf,结果符号全解不出来。记住:simpleperf的位数必须跟目标进程一致。

4.3 systrace / atrace:系统级的“录像机”

perf和simpleperf擅长看“哪里慢”,但看不透“为什么慢”。比如,你的ISP pipeline帧率突然掉到15fps,perf告诉你CPU空闲——那问题在哪?

答案往往是:在等。等GPU渲染、等DMA传输、等VSync信号。这时候就需要systrace出场了。

systrace 基于 atrace 内核机制,可以同时抓取CPU调度、进程状态、内核事件、硬件计数器。它把时间轴画出来,你一眼就能看到哪个时间段是“空的”。

抓取命令:

# 抓取10秒,包含sched、gfx、input等标签
python systrace.py -t 10 sched gfx input view dalvik

在Android 10之后,Google主推 Perfetto 替代systrace。但底层机制一样,都是atrace。我个人建议新项目直接上Perfetto,功能更强,UI也更现代。

怎么看systrace?我一般先看CPU Frequence行,看频率有没有被限制;再看SurfaceFlinger,看合成有没有掉帧;最后看你的ISP进程状态——是Running还是Uninterruptible Sleep。如果是后者,大概率在等内核驱动。

4.4 自定义计时器:最后的“底牌”

工具再强,也有覆盖不到的地方。比如,你想精确测量某个ISP算法模块从输入到输出的延迟,或者你想在硬件IP内部打点——这时候,就得自己动手了。

我常用的自定义计时器方案有三种:

  • clock_gettime + CLOCK_MONOTONIC:精度ns级,适合用户态函数计时。
  • RDTSSC指令:x86平台读取CPU周期计数器,精度最高,但要注意乱序执行和频率变化。
  • 硬件timer寄存器:在驱动或固件里直接读硬件cycle counter,适合测量硬件IP处理时间。

一个简单的C++计时器封装:

#include <chrono>

class ScopedTimer {
public:
    ScopedTimer(const char* name) : name_(name) {
        start_ = std::chrono::steady_clock::now();
    }
    ~ScopedTimer() {
        auto end = std::chrono::steady_clock::now();
        auto us = std::chrono::duration_cast<std::chrono::microseconds>(end - start_).count();
        ALOGD("[Timer] %s: %ld us", name_, us);
    }
private:
    const char* name_;
    std::chrono::time_point<std::chrono::steady_clock> start_;
};

// 用法:在函数入口定义即可
void ISPProcessFrame() {
    ScopedTimer t("ISPProcessFrame");
    // ... 你的处理逻辑
}

注意:自定义计时器会引入测量开销。对于微秒级的函数,多次测量取平均值,去掉首尾异常值。我曾经在1ms的ISP pipeline里插了20个计时器,结果计时器本身占了5%的开销——这就本末倒置了。

4.5 工具选型:什么时候用哪个?

你可能会问:这么多工具,我到底该用哪个?

我的经验是:先粗后细,先系统后局部。

  • 第一步:用 systrace/Perfetto 看全局,确认瓶颈在CPU、GPU、DDR还是IO。
  • 第二步:如果瓶颈在CPU,用 simpleperf 定位热点函数。
  • 第三步:如果热点在某个模块内部,用 自定义计时器 精确测量子函数耗时。
  • 第四步:如果怀疑内核或硬件问题,用 perf 抓内核事件和PMU计数器。

一句话总结:systrace看调度,perf看CPU,simpleperf看调用链,自定义计时器看细节。四把尺子配合使用,没有定位不了的瓶颈。