查看原文
科技

如何利用Perfetto自动化分析Android Camera性能

极客笔记圈 极客笔记圈 2023-08-24

Perfetto 是Google推出的性能分析工具,取代之前的systrace,功能非常强大,本文介绍如何利用Perfetto中的Trace Processor完成Android Camera性能的自动化分析。

文章目录如下:


Trace Processor是什么

Trace Processor是一个C++库

  • 输入:各种格式编码的Trace

  • 输出:SQL接口

  • 这个库被嵌入到很多Trace分析工具中

    • trace_processor 二进制文件

    • Perfetto UI, 作为一个WebAssembly模块

    • Android GPU Inspector

    • Android Studio

运行Trace Processor

可以到Github上下载基于Linux或Mac的可执行文件,单独运行:

# Download prebuilts (Linux and Mac only)
curl -LO https://get.perfetto.dev/trace_processor
chmod +x ./trace_processor

# Start the interactive shell
./trace_processor trace.perfetto-trace

示例:

也可以用本地的trace_processor替换掉Perfetto UI中的trace_processor,执行如下命令

# Start a local trace processor instance to replace wasm module in the UI
./trace_processor trace.perfetto-trace --httpd
  • 好处:

    • 使用本地的trace_processor, Perfetto UI通过TCP与之通信

    • 无WASM 的内存限制,特别适用于加载大文件的Trace

    • SQL 查询的性能更好

可以执行如下命令,运行Metrics

# 运行一个Metrics
./trace_processor --run-metrics android_cpu trace.perfetto-trace

# 同时运行多个Metrics
./trace_processor --run-metrics android_mem,android_cpu trace.perfetto-trace

# 输出binary格式(protobuf)
./trace_processor --run-metrics android_mem --metrics-output=binary trace.perfetto-trace

# 输出JSON格式
./trace_processor --run-metrics android_mem,android_cpu --metrics-output=json trace.perfetto-trace

Perfetto Trace Processor中的线程和进程标识符

为什么不能直接使用PID/TID

  • 在Android/Linux系统中pid/tid可能被重复使用,所以无法用pid/tid来唯一标识进程/线程

Trace Processor使用utid(unique tid))和upid(unique pid)来唯一标识线程和进程

Perfetto 查看所有的表/视图名字

执行如下语句

SELECT name from sqlite_master

示例输出:

Perfetto Trace Processor Tracks

可以把Perfetto UI显示的一行看作是一个Track,包括

  • Thread Track

  • Process Track

  • Counter track

比如下面是一个process_track

Track表的继承关系

查询当前Trace支持哪些类型的Track

SELECT type from track group by type

示例输出

Perfetto Trace Processor Event

可以把Trace理解为带时间戳的Event的集合,有两种类型的Event

  • Slices

    比如:CPU调度Slice、Android Atrace Slice等

  • Counters

    比如:CPU频率、Android Atrace Counter等

Perfetto Trace Processor Event与Track的关系

Event分为Slice和Counter两种

  • Slice与Track

    • slice.track_id与track.id匹配

  • Counter与counter_track

    • counter.track_id与counter_track.id匹配

Event和Track表又通过其他表建立的连接

Perfetto Trace Processor Event与Track的关系实战 - 查看某个Event所在的进程/线程名

例子1 – Slice的Track type为process_track

Trace Processor Event与Track的关系实战 – 查看某个Event所在的进程/线程名

步骤

  • Step1: 确定该Event的Track type

  • Step2: 根据Track type去thread_track或process_track里找到utid/upid

  • Step3: 根据utid/upid到thread/process里面去找到name

Step1: 根据Slice和Track表找到’frame capture’的track.type为process_track

Step2和Step3:结合process_track和process表找到进程名和进程pid

Step2和Step3也可以用USING语句写成(当两个要关联表的字段名是一样的,我们可以使用 USING, 可减少 SQL 语句的长度,JOIN USING简化了JOIN ON)

例子2 – Slice的Track type为thread_track

Step1:根据Slice和Track表找到’sendRequestsBatch’的track.type为thread_track

Step2和Step3:结合thread_track和thread表找到线程名和线程tid

Step2和Step3也可以用USING写成如下的语句

例子3 – Counter的Track type为process_counter_track

Step1:根据counters和Track表找到’cam2_frame’的track.type为process_counter_track

Step2和Step3:结合process_counter_track和process表找到进程名和进程pid

Step2和Step3也可以用USING写成如下的语句

Perfetto SQL Tables详解

查看某个SQL表中有哪些字段

pragma table_info(<table_name>)

示例输出

SQL 表字段官方说明: https://perfetto.dev/docs/analysis/sql-tables

Track Tables之间的关系

Event Tables之间的关系

Misc Tables之间的关系

SQL Tables详解

Track类

  • track

  • process_track

  • thread_track

  • process_counter_track

Event类

  • slice

  • counters

  • sched

Misc

  • process

  • thread

如何写SQL查询语句

SQL基本查询语句

查询所有列的数据

SELECT * FROM thread

查询指定列的数据

SELECT name AS NAME, tid AS TID FROM thread

限制查询结果数量

SELECT * FROM thread LIMIT 5

对查询结果进行分组

SELECT name FROM process GROUP BY name

对查询结果进行排序

SELECT * FROM process ORDER BY pid DESC
SELECT * FROM process ORDER BY pid ASC

SQL条件查询语句

基本条件查询

SELECT * FROM process WHERE name='/system/bin/cameraserver'
SELECT * FROM process WHERE name IN ('com.android.camera2', '/system/bin/cameraserver')

模糊条件查询

SELECT * FROM process WHERE name LIKE '%cam%'

SQL 条件运算符

SQL多表查询

语法:

SELECT column1, column2, ...
FROM tableA
JOIN tableB ON condition;

示例:

SELECT slice.name, process.pid, process.name
FROM slice
JOIN process_track ON slice.track_id = process_track.id
JOIN process USING(upid)
WHERE slice.name = 'frame capture'
GROUP BY process.name

等价于

SELECT slice.name, process.pid, process.name
FROM slice
JOIN process_track ON slice.track_id = process_track.id
JOIN process ON process_track.upid = process.upid
WHERE slice.name = 'frame capture'
GROUP BY process.name

在分析Camera性能时常用的SQL查询语句

条件查询时,忽略大小写

SELECT * FROM slice WHERE LOWER(name) LIKE "%cam%" LIMIT 50

Perfetto提供的常用查询语句

Trace最开始的5秒,每个进程的CPU Time

SELECT process.name, SUM(sched.dur)/1e9 AS cpu_sec
FROM sched
JOIN thread using(utid)
JOIN process using(upid)
WHERE ts <= (SELECT ts+5000000000 FROM sched ORDER BY ts LIMIT 1)
GROUP BY upid
ORDER BY cpu_sec DESC
LIMIT 100;

统计处理Camera CaptureRequest的帧率

SELECT COUNT(*)/((MAX(ts) - MIN(ts))/1e9) AS Request_FPS
FROM slice
WHERE name='frame capture'

输出:

统计某路Camera Stream的帧率(以预览为例)

SELECT COUNT(*)/((MAX(ts) - MIN(ts))/1e9) AS FPS
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread USING(utid)
WHERE slice.name LIKE '%queueBuffer%' AND thread.name like '%PreviewSpacer%'

输出:

统计某路Camera Stream相邻两帧的时间间隔,用于衡量抖动(以预览为例)

SELECT ((slice.ts) - LAG(slice.ts,1) OVER (ORDER BY (slice.ts) ASC))/1e6 AS diff_ms
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread USING(utid)
WHERE slice.name LIKE '%queueBuffer%' AND thread.name like '%PreviewSpacer%'
LIMIT -1 OFFSET 1 --LIMIT设置为-1表示选择所有剩余行,OFFSET为1表示跳过第一行

输出:

Python SDK自动化分析性能环境搭建

搭建Perfetto Python SDK环境

安装Perfetto库(使用Python3)

pip install perfetto

获取Trace Processor二进制文件(避免科学上网)

  • 下载脚本

    https://get.perfetto.dev/trace_processor
  • 根据Python SDK运行的操作系统环境选择Trace Processor二进制文件下载地址,以Windows 64版本为例

    https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/trace_processor_shell.exe

Perfetto Python SDK – Hello World

from perfetto.trace_processor import TraceProcessor
from perfetto.trace_processor import TraceProcessorConfig

tp = TraceProcessor(trace='geekcamera2_camera_launch.trace',
                   config=TraceProcessorConfig(
                       bin_path=r'trace_processor_shell_v3.7.exe',
                       verbose=False
                  ))

qr_it = tp.query('SELECT name,dur FROM slice WHERE name="connectDevice"')
for row in qr_it:
   print(row.name, row.dur / 1e6)

cpu_metrics = tp.metric(['android_startup'])
# print(cpu_metrics)

输出:

Camera 启动性能自动化分析

Camera 启动时间拆解

模块开始点结束点
App点击Camera App Icon开始调用connectDevice
HAL开始调用connectDevice调用connectDevice结束
App调用connectDevice结束开始调用endConfigure
HAL开始调用endConfigure调用endConfigure结束
App调用endConfigure结束调用submitRequestList
HAL调用submitRequestListCameraServer收到第一帧

Camera 启动时间自动化分析

from perfetto.trace_processor import TraceProcessor
from perfetto.trace_processor import TraceProcessorConfig

tp = TraceProcessor(trace='geekcamera2_camera_launch.trace',
                   config=TraceProcessorConfig(
                       bin_path=r'trace_processor_shell_v3.7.exe',
                       verbose=False
                  ))

### click up
click_app_icon = tp.query('SELECT (slice.ts+slice.dur)/1e6 '
                         'FROM slice '
                         'WHERE ts <(SELECT ts FROM slice WHERE name == "connectDevice" LIMIT 1) '
                         'AND name like "%deliverInputEvent%" '
                         'ORDER BY ts DESC '
                         'LIMIT 1')
click_app_icon_ms = click_app_icon.as_pandas_dataframe().values[0][0]

### Connect Device
open_session = tp.query('SELECT (slice.ts/1e6), (slice.dur/1e6) '
                       'FROM slice '
                       'WHERE slice.name like "%CameraHal::openSession%"')
open_session_df = open_session.as_pandas_dataframe()
open_session_begin_ms = open_session_df.values[0][0]
open_session_duration_ms = open_session_df.values[0][1]

### beginConfigure
begin_configure = tp.query('SELECT (slice.ts/1e6) '
                         'FROM slice '
                         'WHERE slice.name like "%beginConfigure%" '
                         'LIMIT 1')
begin_configure_df = begin_configure.as_pandas_dataframe()
begin_configure_ms = begin_configure_df.values[0][0]

### endConfigure
end_configure = tp.query('SELECT (slice.ts/1e6), (slice.dur/1e6) '
                         'FROM slice '
                         'WHERE slice.name like "%endConfigure%" '
                         'LIMIT 1')
end_configure_df = end_configure.as_pandas_dataframe()
end_configure_begin_ms = end_configure_df.values[0][0]
end_configure_duration_ms = end_configure_df.values[0][1]

### submitRequestList
submit_request_list = tp.query('SELECT (slice.ts/1e6) '
                         'FROM slice '
                         'WHERE slice.name like "%submitRequestList%" '
                         'ORDER BY slice.ts ASC '
                         'LIMIT 1')
submit_request_list_df = submit_request_list.as_pandas_dataframe()
submit_request_list_begin_ms = submit_request_list_df.values[0][0]

### CameraServer queue first frame
first_full_buffer = tp.query('SELECT (slice.ts/1e6), (slice.dur/1e6) '
                       'FROM slice '
                       'WHERE slice.name like "%first full buffer%"')
first_full_buffer_df = first_full_buffer.as_pandas_dataframe()
first_full_buffer_queued_ms = first_full_buffer_df.values[0][0] + first_full_buffer_df.values[0][1]

### print results
print("Total Launch time:" + str(round((first_full_buffer_queued_ms - click_app_icon_ms), 2)) + " ms, break down as following:")
print("   [App] Click                     -->     CameraHal::openSession: " +
     str(round((open_session_begin_ms - click_app_icon_ms), 2)) + " ms")
print("   [HAL]                                   CameraHal::openSession: " +
     str(round((open_session_duration_ms), 2)) + " ms")
print("   [App] CameraHal::openSession   -->             beginConfigure: " +
     str(round((begin_configure_ms - open_session_duration_ms - open_session_begin_ms), 2)) + " ms")
print("   [HAL] beginConfigure           -->               endConfigure: " +
     str(round((end_configure_begin_ms + end_configure_duration_ms - begin_configure_ms), 2)) + " ms")
print("   [App] endConfigure             -->           submitRequestList: " +
     str(round((submit_request_list_begin_ms - end_configure_begin_ms - end_configure_duration_ms), 2)) + " ms")
print("   [HAL] submitRequestList         --> Stream x: first full buffer: " +
     str(round((first_full_buffer_queued_ms - submit_request_list_begin_ms), 2)) + " ms")

输出:

Camera 前后摄切换性能自动化分析

Camera前后摄切换性能拆解

 模块    开始点    结束点  
 App    点击Camera  Switch Icon    开始调用disconnect  
 HAL    开始调用disconnect    调用disconnect结束  
 App    调用disconnect结束    开始调用connectDevice  
 HAL    开始调用connectDevice    调用connectDevice结束  
 App    调用connectDevice结束    开始调用endConfigure  
 HAL    开始调用endConfigure    调用endConfigure结束  
 App    调用endConfigure结束    调用submitRequestList  
 HAL    调用submitRequestList    CameraServer收到第一帧  

Camera 前后摄切换性能自动化分析代码

#!/usr/bin/env python3
from perfetto.trace_processor import TraceProcessor
from perfetto.trace_processor import TraceProcessorConfig

tp = TraceProcessor(trace=r'switch_camera.perfetto-trace', config=TraceProcessorConfig(
   bin_path=r'trace_processor_shell_v3.7.exe',
   verbose=False))

### Click switch camera icon
click_app_icon = tp.query('SELECT (slice.ts+slice.dur)/1e6 '
                         'FROM slice '
                         'WHERE ts <(SELECT ts FROM slice WHERE name == "connectDevice" LIMIT 1) '
                         'AND name like "%deliverInputEvent%" '
                         'ORDER BY ts DESC '
                         'LIMIT 1')
click_app_icon_ms = click_app_icon.as_pandas_dataframe().values[0][0]

### closCamera
closecamera_sql = """SELECT ts/1e6, dur/1e6 FROM
              slice
              JOIN thread_track ON slice.track_id = thread_track.id
              JOIN thread USING(utid)
              JOIN process USING(upid)
              WHERE slice.name='detachDevice' AND process.name='/system/bin/cameraserver'
              LIMIT 1"""
close_camera_df = tp.query(closecamera_sql).as_pandas_dataframe()
close_camera_begin = close_camera_df.values[0][0]
close_camera_dur = close_camera_df.values[0][1]

### openCamera
open_session = tp.query('SELECT (slice.ts/1e6), (slice.dur/1e6) '
                       'FROM slice '
                       'WHERE slice.name like "%CameraHal::openSession%"')
open_session_df = open_session.as_pandas_dataframe()
open_session_begin_ms = open_session_df.values[0][0]
open_session_duration_ms = open_session_df.values[0][1]

### beginConfigure
begin_configure = tp.query('SELECT (slice.ts/1e6) '
                         'FROM slice '
                         'WHERE slice.name like "%beginConfigure%" '
                         'LIMIT 1')
begin_configure_df = begin_configure.as_pandas_dataframe()
begin_configure_ms = begin_configure_df.values[0][0]

### endConfigure
end_configure = tp.query('SELECT (slice.ts/1e6), (slice.dur/1e6) '
                         'FROM slice '
                         'WHERE slice.name like "%endConfigure%" '
                         'LIMIT 1')
end_configure_df = end_configure.as_pandas_dataframe()
end_configure_begin_ms = end_configure_df.values[0][0]
end_configure_duration_ms = end_configure_df.values[0][1]

### submitRequestList
submit_request_list = tp.query('SELECT (slice.ts/1e6) '
                         'FROM slice '
                         'WHERE ts >(SELECT ts FROM slice WHERE name == "endConfigure" LIMIT 1) '
                         'AND slice.name like "%submitRequestList%" '
                         'ORDER BY slice.ts ASC '
                         'LIMIT 1')
submit_request_list_df = submit_request_list.as_pandas_dataframe()
submit_request_list_begin_ms = submit_request_list_df.values[0][0]

### CameraServer queue first frame
first_full_buffer = tp.query('SELECT (slice.ts/1e6), (slice.dur/1e6) '
                       'FROM slice '
                       'WHERE slice.name like "%first full buffer%"')
first_full_buffer_df = first_full_buffer.as_pandas_dataframe()
first_full_buffer_queued_ms = first_full_buffer_df.values[0][0] + first_full_buffer_df.values[0][1]

### print results
print("Total Switch Camera time:" + str(round((first_full_buffer_queued_ms - click_app_icon_ms), 2)) + " ms, break down as following:")
print("   [App] Click                     -->                 closCamera: " +
     str(round((close_camera_begin - click_app_icon_ms), 2)) + " ms")
print("   [HAL]                           -->                 closCamera: " +
     str(round((close_camera_dur), 2)) + " ms")
print("   [App] closCamera               -->     CameraHal::openSession: " +
     str(round((open_session_begin_ms - close_camera_begin - close_camera_dur), 2)) + " ms")
print("   [HAL]                                   CameraHal::openSession: " +
     str(round((open_session_duration_ms), 2)) + " ms")
print("   [App] CameraHal::openSession   -->             beginConfigure: " +
     str(round((begin_configure_ms - open_session_duration_ms - open_session_begin_ms), 2)) + " ms")
print("   [HAL] beginConfigure           -->               endConfigure: " +
     str(round((end_configure_begin_ms + end_configure_duration_ms - begin_configure_ms), 2)) + " ms")
print("   [App] endConfigure             -->           submitRequestList: " +
     str(round((submit_request_list_begin_ms - end_configure_begin_ms - end_configure_duration_ms), 2)) + " ms")
print("   [HAL] submitRequestList         --> Stream x: first full buffer: " +
     str(round((first_full_buffer_queued_ms - submit_request_list_begin_ms), 2)) + " ms")

输出:

Camera 拍照性能自动化分析

Camera 拍照性能拆解

这里我们只拆解从单击拍照到收到拍照的图片,后续App的流程大家可以自己完善。

  1. 点击拍照->下拍照Request

  2. 处理完拍照Request

Camera 拍照性能自动化分析代码

#!/usr/bin/env python3
from perfetto.trace_processor import TraceProcessor
from perfetto.trace_processor import TraceProcessorConfig

tp = TraceProcessor(trace=r'capture.perfetto-trace', config=TraceProcessorConfig(
   bin_path=r'trace_processor_shell_v3.7.exe',
   verbose=False))

### Click shutter button icon
geekcamera_click_sql = """SELECT ts/1e6, dur/1e6 FROM
              slice
              JOIN process_track ON slice.track_id = process_track.id
              JOIN process USING(upid)
              WHERE slice.name='deliverInputEvent' AND process.name like '%geekcamera%'
              ORDER BY slice.ts DESC
              LIMIT 1"""
click_app_icon = tp.query(geekcamera_click_sql)
click_app_icon_ms = click_app_icon.as_pandas_dataframe().values[0][0]

### capture submitrequest
still_capture = tp.query('SELECT ts/1e6,dur/1e6 FROM slice where name = "still capture" LIMIT 1').as_pandas_dataframe()
still_capture_begin_ms = still_capture.values[0][0]
still_capture_dur_ms = still_capture.values[0][1]

print("Total Camera Capture time:" + str(round((still_capture_begin_ms + still_capture_dur_ms - click_app_icon_ms), 2)) + " ms, break down as following:")
print("   [App] Click                     -->                 submitRequestList: " +
     str(round((still_capture_begin_ms - click_app_icon_ms), 2)) + " ms")
print("   [HAL] submitRequestList         -->                 capture end: " +
     str(round((still_capture_dur_ms), 2)) + " ms")

输出:

Camera 录像帧率自动化分析

Camera 录像帧率自动化分析代码:

下面的代码我们会统计平均帧率和每两帧之间的时间间隔,通过时间间隔我们可以衡量抖动。

from perfetto.trace_processor import TraceProcessor
from perfetto.trace_processor import TraceProcessorConfig
import matplotlib.pyplot as plt

tp = TraceProcessor(trace=r'Codec2_MediaRecorder_getSurface_Android12', config=TraceProcessorConfig(
   bin_path=r'trace_processor_shell_v3.7.exe',
   verbose=False))

android12_surfaceview_fps_sql = """
SELECT ts/1e6
FROM counters
WHERE name like '%BufferTX - SurfaceView%' AND value=1
ORDER BY counters.ts ASC"""
surfaceview_fps_counter = tp.query(android12_surfaceview_fps_sql).as_pandas_dataframe()
if surfaceview_fps_counter.empty:
   pass
else:
   surfaceview_fps_gap = surfaceview_fps_counter.diff().iloc[1:]
   avg_gap_ms = surfaceview_fps_gap['ts/1e6'].mean()
   surfaceview_fps = round(1000 / avg_gap_ms, 2)

   print('FPS:' + str(surfaceview_fps))
   print('Gap:' + str(surfaceview_fps_gap))
   plt.subplot(211)
   plt.plot(surfaceview_fps_gap.index, surfaceview_fps_gap['ts/1e6'])
   plt.xlabel('Index')
   plt.ylabel('Gap(ms)')
   plt.title('Camera Preview - SurfaceView Frame Duration(' + str(surfaceview_fps) + " FPS)")
   plt.axhline(avg_gap_ms, color='r', linestyle='--', label='Average')
   plt.annotate(f'{round(avg_gap_ms,2)}', xy=(0, avg_gap_ms), color='black')
   # plt.show()

android12_codec2_mediaserver_fps_sql = """
SELECT ts/1e6, dur/1e6 FROM
              slice
              JOIN thread_track ON slice.track_id = thread_track.id
              JOIN thread USING(utid)
              JOIN process USING(upid)
              WHERE slice.name='queueBuffer' AND process.name='/system/bin/mediaserver'
              ORDER BY slice.ts ASC
"""
mediaserver_fps_counter = tp.query(android12_codec2_mediaserver_fps_sql).as_pandas_dataframe()
if mediaserver_fps_counter.empty:
   print("No MediaServer FPS Data.")
else:
   mediaserver_fps_gap = mediaserver_fps_counter.diff().iloc[1:]
   avg_gap_ms = mediaserver_fps_gap['ts/1e6'].mean()
   mediaserver_fps = round(1000 / avg_gap_ms, 2)

   print('FPS:' + str(mediaserver_fps))
   print('Gap:' + str(mediaserver_fps_gap))
   plt.subplot(212)
   plt.plot(mediaserver_fps_gap.index, mediaserver_fps_gap['ts/1e6'])
   plt.xlabel('Index')
   plt.ylabel('Gap(ms)')
   plt.title('Camera Recording - MediaServer Frame Duration(' + str(mediaserver_fps) + " FPS)")
   plt.axhline(avg_gap_ms, color='r', linestyle='--', label='Average')
   plt.annotate(f'{round(avg_gap_ms, 2)}', xy=(0, avg_gap_ms), color='black')
   plt.show()

输出:

FPS:29.99
Gap:       ts/1e6
1    30.025521
2    38.370052
3    30.871041
4     30.58724
5    34.343021
..         ...
164  30.254323
165  39.080417
166  23.453958
167      39.89
168  35.142969

FPS:29.98
Gap:       ts/1e6   dur/1e6
1    30.157083 -0.152187
2    39.070729 -0.134792
3    31.485886 -0.188438
4    30.791146   0.10724
5    33.955572  0.028282
..         ...       ...
164  33.912448  -0.15625
165  38.380521  0.176197
166  25.375365 -0.233177
167  39.878177   0.17526
168  34.983385  0.187761

[168 rows x 2 columns]


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存