如何利用Perfetto自动化分析Android Camera性能
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 DESCSELECT * 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.nameFROM 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.nameFROM 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 50Perfetto提供的常用查询语句
Trace最开始的5秒,每个进程的CPU Time
SELECT process.name, SUM(sched.dur)/1e9 AS cpu_secFROM 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_FPSFROM slice
WHERE name='frame capture'
输出:
统计某路Camera Stream的帧率(以预览为例)
SELECT COUNT(*)/((MAX(ts) - MIN(ts))/1e9) AS FPSFROM 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_msFROM 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 TraceProcessorfrom 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 | 调用submitRequestList | CameraServer收到第一帧 |
Camera 启动时间自动化分析
from perfetto.trace_processor import TraceProcessorfrom 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 python3from 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的流程大家可以自己完善。
点击拍照->下拍照Request
处理完拍照Request
Camera 拍照性能自动化分析代码
#!/usr/bin/env python3from 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 TraceProcessorfrom 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.99Gap: 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]