29、线上性能监控:帧率采集与上报、功耗监控、内存监控、ANR与Crash监控、自定义Trace点埋点

线上性能监控,说白了就是给摄像头系统装个“黑匣子”。

我做了这么多年Camera HAL,最怕的不是功能出bug,而是用户说“卡”、“烫”、“闪退”。这些体验问题,你在实验室里根本复现不出来。所以,线上监控就是我们的眼睛和耳朵。

这一章,我把线上监控拆成五个核心模块来讲。每个模块我都踩过坑,咱们一个一个说。

核心观点:线上监控不是为了看数据,而是为了快速定位问题。数据采集得再漂亮,如果查不出根因,那就是白费功夫。

线上性能监控体系 线上监控引擎 帧率采集与上报 功耗监控 内存监控 ANR与Crash监控 自定义Trace点埋点 五大模块协同工作,覆盖摄像头性能全链路 数据采集 → 本地缓存 → 异步上报 → 后台聚合分析

帧率采集与上报:别被“平均帧率”骗了

帧率监控,看起来简单吧?

很多团队的做法是:每帧打一个时间戳,算一下每秒多少帧。嗯,我以前也这么干。直到有一次,用户反馈“拍照后预览卡顿”,我一看线上数据,帧率30fps,稳稳的。但用户就是觉得卡。

为什么会这样?因为平均帧率掩盖了“掉帧”的真相。

我的建议:不要只上报平均帧率。要采集“帧间隔”的分布。比如,统计每100帧中,帧间隔超过33ms(即低于30fps)的帧有多少。这才是真正的卡顿率。

具体做法,我一般这样搞:

  • 逐帧打点:在HAL的processCaptureRequest返回时,记录时间戳。
  • 滑动窗口统计:维护一个最近60帧的窗口,计算P95帧间隔。
  • 分级上报:正常帧率(>28fps)不上报,减少带宽消耗。只有掉帧严重(<20fps)才立即上报。
// 伪代码:帧间隔采集
class FrameRateMonitor {
    val frameIntervals = CircularBuffer<Long>(60) // 滑动窗口
    
    fun onFrameCompleted(timestamp: Long) {
        val lastTs = frameIntervals.lastOrNull()
        if (lastTs != null) {
            val interval = timestamp - lastTs
            frameIntervals.add(interval)
            
            // 计算掉帧率
            val dropCount = frameIntervals.count { it > 33_000_000 } // >33ms
            val dropRate = dropCount.toFloat() / frameIntervals.size
            
            if (dropRate > 0.1f) { // 掉帧率超过10%
                reportDropFrame(dropRate, frameIntervals.average())
            }
        }
        frameIntervals.add(timestamp)
    }
}

注意:线上采集一定要轻量。不要在每一帧里做复杂的计算,否则监控本身就会导致卡顿。我习惯把计算逻辑放到一个后台线程,用异步方式处理。

功耗监控:电流数据比温度更敏感

功耗问题,是Camera的老大难。

我记得有一次,某款手机在视频通话时发热严重。我们一开始盯着CPU频率和温度看,但数据一直不温不火。后来加了电流监控,才发现ISP的功耗异常高——某个场景下,ISP一直跑在最高频,根本没有降频策略。

所以,功耗监控我建议关注三个维度:

监控维度 采集方式 告警阈值
瞬时电流 通过PowerProfile或BatteryStats 单场景 > 800mA
平均功耗 统计5分钟内的平均电流 持续 > 500mA
ISP频率 从HAL驱动读取 最高频占比 > 80%

这里有个坑:电流数据在部分低端机上不准。我遇到过某款手机,读出来的电流永远是0。所以,我一般会加一个“数据有效性校验”,如果连续10次读到0,就降级为温度监控。

内存监控:别等OOM了才报警

内存泄漏,是Camera的老毛病。特别是预览流和拍照流切换时,很容易把Surface对象泄漏掉。

我的做法是分层监控:

  • Java Heap:通过Runtime.getRuntime().totalMemory()采集,每30秒一次。
  • Native Heap:通过Debug.getNativeHeapSize()采集。这个很关键,因为HAL层很多内存是Native的。
  • Graphic Buffer:通过GraphicBufferMapper查询。这个最容易泄漏,而且一泄漏就是几百MB。

避坑指南:我曾经遇到过一个问题:线上内存数据一直正常,但用户还是闪退。后来发现,是因为我们只采集了“当前内存”,没有采集“内存增长速率”。一个缓慢泄漏,可能半小时才涨10MB,但积累下来就会OOM。所以,我建议加上“内存增长趋势”的监控,比如连续5次采集都呈上升趋势,就触发告警。

ANR与Crash监控:要的是“现场”

ANR和Crash,是线上监控的底线。

但很多团队只采集了堆栈,这远远不够。你想想看,一个ANR发生,你只知道主线程卡住了,但不知道卡在哪个Binder调用上,有什么用?

我习惯在Crash和ANR发生时,额外采集以下信息:

  1. 当前帧的Pipeline状态:是预览还是拍照?Request是否已提交?
  2. 最近10个Binder调用的耗时:这个可以从Binder线程的Trace中提取。
  3. 内存快照:包括Java Heap和Native Heap的占比。
  4. CPU负载:特别是Camera相关线程的CPU占用。
// ANR采集示例
class AnrMonitor {
    fun onAppNotResponding(anrInfo: AnrInfo) {
        val extraInfo = mapOf(
            "pipeline_state" to getCurrentPipelineState(),
            "binder_calls" to getRecentBinderCalls(),
            "heap_usage" to getHeapSnapshot(),
            "camera_thread_cpu" to getCameraThreadCpuUsage()
        )
        reportAnr(anrInfo, extraInfo)
    }
}

注意:ANR发生时,系统资源已经很紧张了。不要在ANR回调里做太多事情,否则会二次ANR。我一般只采集关键信息,然后异步上报。

自定义Trace点埋点:把“黑盒”变成“白盒”

系统自带的Trace,只能看到Framework层。但Camera的性能瓶颈,往往在HAL和驱动层。

所以,我强烈建议在关键路径上埋自定义Trace点。比如:

  • 打开相机:从App调用openCamera到HAL返回第一个预览帧。
  • 拍照:从capture到jpeg回调。
  • 切换场景:从低光切换到HDR模式。

埋点的方式,我推荐用Android的Trace API,配合atrace工具。但线上环境,atrace不一定能开。所以,我一般自己实现一个轻量级的Trace框架:

// 自定义Trace埋点
object CameraTrace {
    private val traceEvents = ConcurrentLinkedQueue<TraceEvent>()
    
    fun beginSection(name: String) {
        traceEvents.add(TraceEvent(name, System.nanoTime(), TraceEventType.BEGIN))
    }
    
    fun endSection() {
        traceEvents.add(TraceEvent("", System.nanoTime(), TraceEventType.END))
    }
    
    fun dumpAndReport() {
        // 将traceEvents序列化后上报
        val report = buildTraceReport(traceEvents)
        uploadTrace(report)
        traceEvents.clear()
    }
}

我的经验:埋点不是越多越好。每个Trace点都会带来额外的性能开销。我一般只在“用户可感知”的路径上埋点,比如启动、拍照、切换模式。那些内部循环里的细粒度操作,就别埋了,否则监控本身就会拖慢性能。

嗯,线上监控这块,说白了就是“数据驱动优化”。没有数据,你连问题在哪都不知道。但有了数据,你还要会看——别被平均数骗了,别只看堆栈不看现场,别为了监控而监控。

我见过太多团队,线上监控系统做得花里胡哨,但出了问题还是两眼一抹黑。为什么?因为数据采集了,但没有和“现场”关联起来。所以,记住一句话:监控的终极目标,是还原问题发生的“第一现场”。


公众号:蓝海资料掘金营,微信deep3321