29、线上性能监控:帧率采集与上报、功耗监控、内存监控、ANR与Crash监控、自定义Trace点埋点
线上性能监控,说白了就是给摄像头系统装个“黑匣子”。
我做了这么多年Camera HAL,最怕的不是功能出bug,而是用户说“卡”、“烫”、“闪退”。这些体验问题,你在实验室里根本复现不出来。所以,线上监控就是我们的眼睛和耳朵。
这一章,我把线上监控拆成五个核心模块来讲。每个模块我都踩过坑,咱们一个一个说。
核心观点:线上监控不是为了看数据,而是为了快速定位问题。数据采集得再漂亮,如果查不出根因,那就是白费功夫。
帧率采集与上报:别被“平均帧率”骗了
帧率监控,看起来简单吧?
很多团队的做法是:每帧打一个时间戳,算一下每秒多少帧。嗯,我以前也这么干。直到有一次,用户反馈“拍照后预览卡顿”,我一看线上数据,帧率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发生时,额外采集以下信息:
- 当前帧的Pipeline状态:是预览还是拍照?Request是否已提交?
- 最近10个Binder调用的耗时:这个可以从Binder线程的Trace中提取。
- 内存快照:包括Java Heap和Native Heap的占比。
- 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