使用Perfetto进行流畅度分析

1,346 阅读20分钟

一 、分享目标

本质旨在让大家可以在看完本篇文档后可以落地最基本的流畅度分析和优化,时间关系,获取Trace,简单能分析Trace。

  1. 可以通过Perfetto抓Trace
  2. 了解基本绘制流程
  3. 可以通过Perfetto简单分析Trace
  4. 让Perfetto可以在日常开发中使用起来提升效率

分析之后,配合:Perfetto分析Trace后,如何定位问题?

二、 Perfetto - 流畅对工具最优解

下一代工具,Google持续维护,Chrome支持拥有强大的可视化能力

目前市面上主流的流畅度工具有三个,分别是

  • Perfetto

  • SystemTrace

    • Trace 类型的可视化分析工具,是第一代系统级性能分析工具,与 Log 相比,Systrace 的图像化方式更为直观
  • AS Profiler

    • Android Studio内置集成,多种性能分析工具于一体,本质调用的还是上面的工具,仅仅做了一个包装
    • 4 类性能分析工具: CPU、Memory、Network、Battery

从实践经验来看最常用的工具有 SystracePerfetto 与 Android Studio 中的 Profiler 工具,对于我们日常的简单业务分析Perfetto 是一个最优解

三、如何使用Perfetto抓trace

  1. 下载或更新脚本文件

      gitlab.tx2.898311.xyz/zbc/record_…

      当然你也可以通过perfetto官方去下载最新的脚本,我的脚本也是通过官方地址下载的,不过增加了自己的一些改动。

  2. 配置并抓取Trace文件

文末有完整的抓取trace的配置。

官方参考:developer.android.com/tools/perfe… 进行参数配置。

进阶

实际使用开发中我们可以使用自定义的标签完成快速勾选,这个非常实用,下面是一个使用的示例

四、Perfetto分析卡顿

4.1 准备

4.1.1 Chrome浏览器

4.1.2 trace文件

4.1.3 一个竖着摆放的显示屏(如果有是最好的)

将Chrome 浏览器放在竖屏显示器部分。打开网址 ui.perfetto.dev/#!/。将侧边菜单栏收…

4.2 界面介绍

Perfetto UI 地址 ui.perfetto.dev/ ,chrome中打开,建议收藏

从上到下依次是

4.2.1 搜索栏

可对Trace中的关键字搜索,输入 > in the (empty) search boxSwitch to command mode进入命令模式

CTRL+O 进入 Run query模式

CTRL+ S退出query模式

4.2.2 时间轴

时间越深,CPU越繁忙

4.2.3 CPU区

这部分主要是看CPU的状态是否正常,包括

  • 是不是少核

  • 频率是不是正常

  • CPU是不是饱和了

4.2.4 进程信息区

色块越密集,表示执行的任务越密集

分析的时候从这里找对应进程,进程下找对应线程,比如这里的 com.mi.android.globallauncher [19398]

4.2.5 详细信息区

选中任意一个slice ,底部展示此 slice 对应的具体信息。

4.3 快捷键介绍

W放大
S缩小
D右移
A左移
F精确定位需要选中一个格子,这个区域会滚动到屏幕中间区域
M框选需要选中一个格子,或者在时间轴拖动一个范围。下面有一处会具体介绍。
ClickSelect event
Ctrl + Scroll wheel缩放同快捷键w/s
Click + Drag选择区域计算时间一般在需要计算时间时候使用
Shift + Click + Drag左右移动同快捷键 a/d
?显示快捷键

可以尝试一下各个操作。很简单吧。

其他快捷键里面用的比较多的:

4.3.1 f 是放大选中

4.3.2 m 是临时 Mark 一段区域

(与 Systrace 一样), 用来上下看时间、看其他进程信息等。临时的意思就是你如果按 m 去 mark 另外一个区域,那么上一个用 m mark 出来的 Mark 区域就会消失。退出临时选中:esc ,或者选择其他的 Slice 按 m,当前这个 Slice 的选中效果就会消失

4.3.3 shift + m 是持续 Mark 一段区域

(如果你不点,他就不会消失),主要是用来长时间 Mark 住一段信息,比如你把一份 Trace 中所有的掉帧点都 Mark 出来,就可以用 shift + m,这样就不会丢失。

点击小旗子,就可以看到这段区间内的执行信息

4.3.4 删除持续 Mark

  1. 点击你选中的那个 Slice 的最上面那个三角
  2. 下面选择 Tab:Current Selection
  3. 点击最后边的 Remove ,就可以把他 Remove 掉了

4.3.5 q :隐藏和显示信息 Tab,由于 Perfetto 非常占屏幕,熟练使用 q 键很重要,看的时候快速打开,看完后快速关闭。

4.3.6 插旗子:

Perfetto 还可以通过插旗子的方法来在 Trace 上做标记,Perfetto 支持你把鼠标放到 Trace 最上面,就会出现一个旗子,点击左键即可插一个旗子在上面,方便我们标记某个事件发生,或者某个时间点

4.3.7 取消插的旗子

与退出持续选中一样,点击旗子,右下角有个 Remove ,点击就可以把这个旗子干掉了,就不插图了

4.4 置顶关键行

  在进程信息区找到所需要的行。并勾选pin to top 图标。如下图。pin后这一行会置顶。

下面就是具体需要找的内容。都是先找到进程,再找具体的行。括号里是这个进程需要找几行。这里以桌面为例子

  1. system_server(1)

  1. 桌面(6)

    1.   这里基本是属于你自己应用的代码。比如第一行第二行就是主线程。第三行第四行就是渲染线程。aq那一行可以简单的理解为接收到的输入事件。

  1. Surfaceflinger(6)

    1.   这里是SF的区域。其中VSYNC-APP是控制APP的出帧的。有这个信号才会触发doFrame的出帧。VSYNC-SF是SF的同步信号。接收到这个信号的时候SF会从应用的帧队列的尝试拿一帧用于合成。
    2.   bufferTX就是对应应用的帧队列的。在VSYNC-APP 期间准备好的帧会进入这个队列。当

VSYNC-SF到来时,又会从这里面取出来一个去用于合成。

其中最后两个需要找自己的应用。最后一个不一定有,没有就算了。

4.5 CPU Info 区域 Task 高亮

在 CPU Info 区域,鼠标放到某一个 Task 上,就会这个 Task 对应的 Thread 的其他 Task 都会高亮。

我们经常会用这个方法来初步看某些 Thread 的摆核信息

4.6 了解绘制流程

简单看一个正常的上屏流程

每一个Vsync-sf都可以获取到一个buffer,获取不到就丢帧了

4.7 大体定位丢帧原因

4.7.1 调整标签顺序

上面的标签基本全了,重新调整下每一个行的位置如下,这个就是出帧位置了

4.7.2 放大定位原因

像下面这个SF因为没有buffer所以导致隔帧上帧问题

这个基本就定位好了,就是应用图标隔帧出帧

4.8 增加自定义Trace-进一步定位

参考官方文档 GoogleTrace

两个点

  • Trace.beginSection Trace.endSection() 成对出现、同一线程

  • sectionName 1~127字符且不能为null

4.8.1 Trace.beginSection Trace.endSection() 同线程成对出现添加标签

Trace.beginSection("需要增加的TAG");
// 中间是需要统计的代码
Trace.endSection();

注意一个Trace.beginSection 一定对应有一个唯一的 Trace.endSection(); ,并且要求开始和结束必须存在同一个线程中

排查耗时问题的时候我们可以添加在具体怀疑耗时的位置,如下排查最近任务卡片图标绘制耗时

抓取Trace后显示如下

4.8.2 嵌套TAG

Trace.beginSection("需要增加的外部TAG-A");
// 中间是需要统计的代码

Trace.beginSection("需要增加的内部TAG-B");
// 中间是需要统计的代码
// 结束TAG-B
Trace.endSection();
// 结束TAG-A
Trace.endSection();

Trace如下,Trace表现为包含关系

发散一下,当发现一段函数耗时,咱是不是就可以逐步的通过增加Trace来缩小代码范围,如上图我们就可以通过draw:FloatingIconView入手,在FloatingIconView的draw函数进一步添加Trace定位,定位当下一个函数drawBackground后,进一步增加Trace,进一步定位到 mBackground.draw(canvas); 直到耗时的调用或者函数被发现,我们再找出可行的解决方案

4.9 Perfetto + Log 结合,可视化定位日志信息,提高效率

Perfetto 是可以支持可视化的方式分析Log的,这一点在调试动画的时候会特别好用,可以结合绘制流程和当前的整机日志来系统的分析当前系统的运行状态是什,为什么没有出现图标不显示、卡顿、耗时、UI不符合预期的问题

下面图是一个Trace+Log结合的示例

在信息栏上切换到 Android Logs 这个 Tab,鼠标放倒某一行上,Perfetto 就会把对应的 Timeline 拉一条直线,可以看到这个 Log 所对应的时间:

一些问题

  1. MainThread和RenderThread是如何协调工作的? RenderThread 运行在应用程序的同一进程中,而不是独立的进程。它是由系统在应用进程内创建的一个专门用于处理绘制和合成任务的线程,与 MainThread 线程并行工作,从而提高渲染效率。
  • 并行运行
    MainThread(UI 线程)和 RenderThread(渲染线程)各自负责不同的任务,通常在同一个 16 毫秒(60fps)左右的帧周期内完成各自的工作,但它们是并行运行的,不是严格同步在同一个 CPU 时钟周期内执行。

  • 异步协调
    主线程负责更新 UI 状态、处理事件等,而渲染线程负责将绘制命令提交给 GPU。它们之间通过 vsync 同步,以保证每一帧在截止时间内都能完成各自的任务。

  • 任务依赖与调度
    虽然整体上需要在同一帧周期内完成,但如果主线程耗时过长,渲染线程可能也无法按时完成绘制,因此系统会通过调度机制来协调两者,以确保流畅性。

  1. 为什么Trace 里边 有些vsync-app 或者是 vsync-sf会很长?

在 Trace 中看到某些 vsync-appvsync-sf 时间很长,通常表明在某一帧的处理过程中出现了延迟。主要原因可能包括:

  • 应用侧耗时
    如果应用在处理 vsync 信号后开始绘制时,主线程执行了大量耗时操作(例如复杂的布局计算、繁重的计算任务或频繁的 GC),那么 vsync-app 的时间会延长,因为它反映了应用从收到 vsync 到开始渲染的延迟。
  • 系统调度问题
    线程调度不及时、CPU 负载过高、或者其他系统任务干扰也会导致 vsync-appvsync-sf 的处理延迟。
  • SurfaceFlinger 合成瓶颈
    vsync-sf 代表系统合成线程处理帧的时间。如果 SurfaceFlinger 遇到合成复杂场景、GPU 处理压力大或者等待硬件同步,这部分延时也会被记录下来。
  • 硬件或同步等待
    有时硬件层面的同步等待(如等待 VSync 信号)也会造成这一现象。

完整的直接获取trace的参考配置

1. 脚本参数配置文件 perfetto.pbtx

 duration_ms: 4000 # 调整时间
flush_period_ms: 3000
incremental_state_config {
    clear_period_ms: 5000
}
buffers: {
    size_kb: 63488
    fill_policy: DISCARD
}
buffers: {
    size_kb: 2048
    fill_policy: DISCARD
}
data_sources: {
    config {
        name: "android.packages_list"
        target_buffer: 1
    }
}
data_sources: {
    config {
        name: "android.gpu.memory"
    }
}
data_sources: {
    config {
        name: "linux.process_stats"
        target_buffer: 1
        process_stats_config {
            scan_all_processes_on_start: true
        }
    }
}
data_sources: {
    config {
        name: "android.log"
        android_log_config {
        }
    }
}
data_sources: {
    config {
        name: "android.surfaceflinger.frametimeline"
    }
}
data_sources: {
    config {
        name: "linux.sys_stats"
        sys_stats_config {
            stat_period_ms: 1000
            stat_counters: STAT_CPU_TIMES
            stat_counters: STAT_FORK_COUNT
            cpufreq_period_ms: 1000
        }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "sched/sched_switch" # 使能Trace抓取CPU调度
            ftrace_events: "power/suspend_resume" # 使能Trace抓取亮灭屏
            ftrace_events: "sched/sched_wakeup" # 使能Trace抓取亮灭屏
            ftrace_events: "sched/sched_wakeup_new" # 使能Trace抓取唤醒信息
            ftrace_events: "sched/sched_waking" # 使能Trace抓取CPUd调度信息
            ftrace_events: "power/cpu_frequency" # 使能Trace抓取CPU频率等信息
            ftrace_events: "power/cpu_idle" # 使能Trace抓取CPU空闲
            ftrace_events: "power/gpu_frequency" # 使能Trace抓取CPU调度
            ftrace_events: "gpu_mem/gpu_mem_total" # 使能Trace抓取GPU内存
            ftrace_events: "sched/sched_process_exit" # 使能Trace抓取进程退出
            ftrace_events: "sched/sched_process_free" # 使能Trace抓取进程空闲
            ftrace_events: "task/task_newtask" # 使能Trace抓取任务创建
            ftrace_events: "task/task_rename" # 使能Trace抓取任务信息
            atrace_categories: "am" # 使能Trace抓取AMS Trace
            atrace_categories: "aidl" # 使能Trace抓取aidl
            atrace_categories: "binder_lock" # 使能Trace抓取binder_lock
            atrace_categories: "binder_driver" # 使能Trace抓取binder调度关系
            atrace_categories: "camera" # 使能Trace抓取相机
            atrace_categories: "database" # 使能Trace抓取数据库
            atrace_categories: "gfx" # 使能Trace抓取gfx层
            atrace_categories: "hal" # 使能Trace抓取hal层
            atrace_categories: "input" # 使能Trace抓取input事件
            atrace_categories: "pm"  
            atrace_categories: "rs" 
            atrace_categories: "res" 
            atrace_categories: "rro"  
            atrace_categories: "sm" 
            atrace_categories: "ss"
            atrace_categories: "video"
            atrace_categories: "view"
            atrace_categories: "wm"
            atrace_categories: "dalvik"
            atrace_apps: "*"
        }
    }
}

2. Windows获取Trace脚本getTrace.bat

python3 record_android_trace.py -c perfetto.pbtx -o trace_file.perfetto-trace
pause

3.perfetto.sh配置文件:

adb root;
adb shell "echo 0 > sys/kernel/tracing/tracing_on";
adb shell setprop persist.sys.perfdebug.monitor.enable true;
adb shell setprop persist.sys.perfdebug.monitor.catalog  all;
adb shell setprop persist.sys.hwui.skia_atrace_enabled true;
cat<<EOF>config.pbtx
duration_ms: 2000
flush_period_ms: 1800
incremental_state_config {
    clear_period_ms: 5000
}

buffers: {
    size_kb: 126488
    fill_policy: RING_BUFFER
}
buffers: {
    size_kb: 2048
    fill_policy: RING_BUFFER
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "sched/sched_switch"
            ftrace_events: "power/suspend_resume"
            ftrace_events: "sched/sched_wakeup"
            ftrace_events: "sched/sched_wakeup_new"
            ftrace_events: "sched/sched_waking"
            ftrace_events: "power/cpu_frequency"
            ftrace_events: "power/cpu_idle"
            ftrace_events: "power/gpu_frequency"
            ftrace_events: "gpu_mem/gpu_mem_total"
            ftrace_events: "sched/sched_process_exit"
            ftrace_events: "sched/sched_process_free"
            ftrace_events: "task/task_newtask"
            ftrace_events: "task/task_rename"
            atrace_categories: "am"
            atrace_categories: "aidl"
            atrace_categories: "binder_lock"
            atrace_categories: "binder_driver"
            atrace_categories: "camera"
            atrace_categories: "database"
            atrace_categories: "gfx"
            atrace_categories: "hal"
            atrace_categories: "input"
            atrace_categories: "pm"
            atrace_categories: "rs"
            atrace_categories: "res"
            atrace_categories: "rro"
            atrace_categories: "sm"
            atrace_categories: "ss"
            atrace_categories: "video"
            atrace_categories: "view"
            atrace_categories: "wm"
            atrace_apps: "com.miui.home"
            atrace_apps: "com.miui.miwallpaper"
            atrace_apps: "com.miui.aod:keyguardeditor"
            atrace_apps: "com.android.systemui"
        }
    }
}

data_sources: {
    config {
        name: "android.log"
        android_log_config {
        }
    }
}

data_sources: {
    config {
        name: "linux.process_stats"
        target_buffer: 1
        process_stats_config {
            scan_all_processes_on_start: true
        }
    }
}
EOF
if [ ! -d "result" ];then
  mkdir result
fi

python3 record_android_trace.py -c config.pbtx -o ./trace/result/perfetto_$(date +%Y%m%d%H%M%S)

4. 获取trace并自动有Google浏览器打开:

#!/usr/bin/env python3
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# DO NOT EDIT. Auto-generated by tools/gen_amalgamated_python_tools
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

import atexit
import argparse
import datetime
import hashlib
import http.server
import os
import re
import shutil
import socketserver
import subprocess
import sys
import time
import webbrowser


# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
# This file has been generated by: tools/roll-prebuilts v46.0
TRACEBOX_MANIFEST = [{
    'arch':
        'mac-amd64',
    'file_name':
        'tracebox',
    'file_size':
        1597432,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/mac-amd64/tracebox',
    'sha256':
        'fda9aa1a57fc6bd85a7f332a436ae0ba8629eac81f5fd0e21a72fe3673b2d609',
    'platform':
        'darwin',
    'machine': ['x86_64']
}, {
    'arch':
        'mac-arm64',
    'file_name':
        'tracebox',
    'file_size':
        1459128,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/mac-arm64/tracebox',
    'sha256':
        '9a7ee198c0b2ca41edd73afc313193e6f643aaa1a88cafb1e515b76d6dcc47dd',
    'platform':
        'darwin',
    'machine': ['arm64']
}, {
    'arch':
        'linux-amd64',
    'file_name':
        'tracebox',
    'file_size':
        2333576,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/linux-amd64/tracebox',
    'sha256':
        '3567682e999c9bc36c9c757fe1fc56067963e226573377da21747b7686238012',
    'platform':
        'linux',
    'machine': ['x86_64']
}, {
    'arch':
        'linux-arm',
    'file_name':
        'tracebox',
    'file_size':
        1422204,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/linux-arm/tracebox',
    'sha256':
        '66890d26ab8f88241b3608ce099e45dee7179ddeca3966dab6e7c1ade78963e3',
    'platform':
        'linux',
    'machine': ['armv6l', 'armv7l', 'armv8l']
}, {
    'arch':
        'linux-arm64',
    'file_name':
        'tracebox',
    'file_size':
        2229176,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/linux-arm64/tracebox',
    'sha256':
        '425d7a8e88054b5b314bea3db884722a6646d9d7e8230b8ebebc044e57207739',
    'platform':
        'linux',
    'machine': ['aarch64']
}, {
    'arch':
        'android-arm',
    'file_name':
        'tracebox',
    'file_size':
        1314720,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/android-arm/tracebox',
    'sha256':
        'e3a6905bf8db5af2bd2b83512a745cfd3d86cf842fc81b1d1b3a05f4fb9f15d0'
}, {
    'arch':
        'android-arm64',
    'file_name':
        'tracebox',
    'file_size':
        2086288,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/android-arm64/tracebox',
    'sha256':
        'b506b076e19470afa4ca3f625d596f894a3778b3fe2fd7ad9f97f9e136f25542'
}, {
    'arch':
        'android-x86',
    'file_name':
        'tracebox',
    'file_size':
        2264088,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/android-x86/tracebox',
    'sha256':
        '953cb01053d8094a5e39e05acc2f5b81a73836e699e4e0a7469c0cfa7c820364'
}, {
    'arch':
        'android-x64',
    'file_name':
        'tracebox',
    'file_size':
        2114328,
    'url':
        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v46.0/android-x64/tracebox',
    'sha256':
        '570d475684bcc93ae8b6b8a5566887337d94aff91dea2270a0feb1c0bec00a8b'
}]

# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py

# ----- Amalgamator: begin of python/perfetto/prebuilts/perfetto_prebuilts.py
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Functions to fetch pre-pinned Perfetto prebuilts.

This function is used in different places:
- Into the //tools/{trace_processor, traceconv} scripts, which are just plain
  wrappers around executables.
- Into the //tools/{heap_profiler, record_android_trace} scripts, which contain
  some other hand-written python code.

The manifest argument looks as follows:
TRACECONV_MANIFEST = [
  {
    'arch': 'mac-amd64',
    'file_name': 'traceconv',
    'file_size': 7087080,
    'url': https://commondatastorage.googleapis.com/.../trace_to_text',
    'sha256': 7d957c005b0dc130f5bd855d6cec27e060d38841b320d04840afc569f9087490',
    'platform': 'darwin',
    'machine': 'x86_64'
  },
  ...
]

The intended usage is:

  from perfetto.prebuilts.manifests.traceconv import TRACECONV_MANIFEST
  bin_path = get_perfetto_prebuilt(TRACECONV_MANIFEST)
  subprocess.call(bin_path, ...)
"""

import hashlib
import os
import platform
import random
import subprocess
import sys


def download_or_get_cached(file_name, url, sha256):
  """ Downloads a prebuilt or returns a cached version

  The first time this is invoked, it downloads the |url| and caches it into
  ~/.local/share/perfetto/prebuilts/$tool_name. On subsequent invocations it
  just runs the cached version.
  """
  dir = os.path.join(
      os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
  os.makedirs(dir, exist_ok=True)
  bin_path = os.path.join(dir, file_name)
  sha256_path = os.path.join(dir, file_name + '.sha256')
  needs_download = True

  # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
  # download is cached into file_name.sha256, just check if that matches.
  if os.path.exists(bin_path) and os.path.exists(sha256_path):
    with open(sha256_path, 'rb') as f:
      digest = f.read().decode()
      if digest == sha256:
        needs_download = False

  if needs_download:  # The file doesn't exist or the SHA256 doesn't match.
    # Use a unique random file to guard against concurrent executions.
    # See https://github.com/google/perfetto/issues/786 .
    tmp_path = '%s.%d.tmp' % (bin_path, random.randint(0, 100000))
    print('Downloading ' + url)
    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
    with open(tmp_path, 'rb') as fd:
      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
    if actual_sha256 != sha256:
      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
                      (url, actual_sha256, sha256))
    os.chmod(tmp_path, 0o755)
    os.replace(tmp_path, bin_path)
    with open(tmp_path, 'w') as f:
      f.write(sha256)
    os.replace(tmp_path, sha256_path)
  return bin_path


def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
  plat = sys.platform.lower()
  machine = platform.machine().lower()
  manifest_entry = None
  for entry in manifest:
    # If the caller overrides the arch, just match that (for Android prebuilts).
    if arch:
      if entry.get('arch') == arch:
        manifest_entry = entry
        break
      continue
    # Otherwise guess the local machine arch.
    if entry.get('platform') == plat and machine in entry.get('machine', []):
      manifest_entry = entry
      break
  if manifest_entry is None:
    if soft_fail:
      return None
    raise Exception(
        ('No prebuilts available for %s-%s\n' % (plat, machine)) +
        'See https://perfetto.dev/docs/contributing/build-instructions')

  return download_or_get_cached(
      file_name=manifest_entry['file_name'],
      url=manifest_entry['url'],
      sha256=manifest_entry['sha256'])


def run_perfetto_prebuilt(manifest):
  bin_path = get_perfetto_prebuilt(manifest)
  if sys.platform.lower() == 'win32':
    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
  os.execv(bin_path, [bin_path] + sys.argv[1:])

# ----- Amalgamator: end of python/perfetto/prebuilts/perfetto_prebuilts.py

# ----- Amalgamator: begin of python/perfetto/common/repo_utils.py
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os


def repo_root():
  """ Finds the repo root by traversing up the hierarchy

  This is for use in scripts that get amalgamated, where _file_ can be either
  python/perfetto/... or tools/amalgamated_tool.
  """
  path = os.path.dirname(os.path.abspath(__file__))  # amalgamator:nocheck
  last_dir = ''
  while path and path != last_dir:
    if os.path.exists(os.path.join(path, 'perfetto.rc')):
      return path
    last_dir = path
    path = os.path.dirname(path)
  return None


def repo_dir(rel_path):
  return os.path.join(repo_root() or '', rel_path)

# ----- Amalgamator: end of python/perfetto/common/repo_utils.py

# This is not required. It's only used as a fallback if no adb is found on the
# PATH. It's fine if it doesn't exist so this script can be copied elsewhere.
HERMETIC_ADB_PATH = repo_dir('buildtools/android_sdk/platform-tools/adb')

# Translates the Android ro.product.cpu.abi into the GN's target_cpu.
ABI_TO_ARCH = {
    'armeabi-v7a': 'arm',
    'arm64-v8a': 'arm64',
    'x86': 'x86',
    'x86_64': 'x64',
}

MAX_ADB_FAILURES = 15  # 2 seconds between retries, 30 seconds total.

devnull = open(os.devnull, 'rb')
adb_path = None
procs = []


class ANSI:
  END = '\033[0m'
  BOLD = '\033[1m'
  RED = '\033[91m'
  BLACK = '\033[30m'
  BLUE = '\033[94m'
  BG_YELLOW = '\033[43m'
  BG_BLUE = '\033[44m'


# HTTP Server used to open the trace in the browser.
class HttpHandler(http.server.SimpleHTTPRequestHandler):

  def end_headers(self):
    self.send_header('Access-Control-Allow-Origin', self.server.allow_origin)
    self.send_header('Cache-Control', 'no-cache')
    super().end_headers()

  def do_GET(self):
    if self.path != '/' + self.server.expected_fname:
      self.send_error(404, "File not found")
      return

    self.server.fname_get_completed = True
    super().do_GET()

  def do_POST(self):
    self.send_error(404, "File not found")


def setup_arguments():
  atexit.register(kill_all_subprocs_on_exit)
  default_out_dir_str = '~/traces/'
  default_out_dir = os.path.expanduser(default_out_dir_str)

  examples = '\n'.join([
      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
      '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
      '  -c /path/to/full-textual-trace.config', '',
      ANSI.BOLD + 'Long traces' + ANSI.END,
      'If you want to record a hours long trace and stream it into a file ',
      'you need to pass a full trace config and set write_into_file = true.',
      'See https://perfetto.dev/docs/concepts/config#long-traces .'
  ])
  parser = argparse.ArgumentParser(
      epilog=examples, formatter_class=argparse.RawTextHelpFormatter)

  help = 'Output file or directory (default: %s)' % default_out_dir_str
  parser.add_argument('-o', '--out', default=default_out_dir, help=help)

  help = 'Don\'t open or serve the trace'
  parser.add_argument('-n', '--no-open', action='store_true', help=help)

  help = 'Don\'t open in browser, but still serve trace (good for remote use)'
  parser.add_argument('--no-open-browser', action='store_true', help=help)

  help = 'The web address used to open trace files'
  parser.add_argument('--origin', default='https://ui.perfetto.dev', help=help)

  help = 'Force the use of the sideloaded binaries rather than system daemons'
  parser.add_argument('--sideload', action='store_true', help=help)

  help = ('Sideload the given binary rather than downloading it. ' +
          'Implies --sideload')
  parser.add_argument('--sideload-path', default=None, help=help)

  help = 'Ignores any tracing guardrails which might be used'
  parser.add_argument('--no-guardrails', action='store_true', help=help)

  help = 'Don\'t run `adb root` run as user (only when sideloading)'
  parser.add_argument('-u', '--user', action='store_true', help=help)

  help = 'Specify the ADB device serial'
  parser.add_argument('--serial', '-s', default=None, help=help)

  grp = parser.add_argument_group(
      'Short options: (only when not using -c/--config)')

  help = 'Trace duration N[s,m,h] (default: trace until stopped)'
  grp.add_argument('-t', '--time', default='0s', help=help)

  help = 'Ring buffer size N[mb,gb] (default: 32mb)'
  grp.add_argument('-b', '--buffer', default='32mb', help=help)

  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
          'for all apps (without space between a and * or bash will expand it)')
  grp.add_argument(
      '-a',
      '--app',
      metavar='com.myapp',
      action='append',
      default=[],
      help=help)

  help = 'sched, gfx, am, wm (see --list)'
  grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)

  help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
  grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)

  help = 'Lists all the categories available'
  grp.add_argument('--list', action='store_true', help=help)

  help = 'Lists all the ftrace events available'
  grp.add_argument('--list-ftrace', action='store_true', help=help)

  section = ('Full trace config (only when not using short options)')
  grp = parser.add_argument_group(section)

  help = 'Can be generated with https://ui.perfetto.dev/#!/record'
  grp.add_argument('-c', '--config', default=None, help=help)

  help = 'Parse input from --config as binary proto (default: parse as text)'
  grp.add_argument('--bin', action='store_true', help=help)

  help = ('Pass the trace through the trace reporter API. Only works when '
          'using the full trace config (-c) with the reporter package name '
          "'android.perfetto.cts.reporter' and the reporter class name "
          "'android.perfetto.cts.reporter.PerfettoReportService' with the "
          'reporter installed on the device (see '
          'tools/install_test_reporter_app.py).')
  grp.add_argument('--reporter-api', action='store_true', help=help)

  args = parser.parse_args()
  args.sideload = args.sideload or args.sideload_path is not None

  if args.serial:
    os.environ["ANDROID_SERIAL"] = args.serial

  find_adb()

  if args.list:
    adb('shell', 'atrace', '--list_categories').wait()
    sys.exit(0)

  if args.list_ftrace:
    adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
    sys.exit(0)

  if args.config is not None and not os.path.exists(args.config):
    prt('Config file not found: %s' % args.config, ANSI.RED)
    sys.exit(1)

  if len(args.events) == 0 and args.config is None:
    prt('Must either pass short options (e.g. -t 10s sched) or a --config file',
        ANSI.RED)
    parser.print_help()
    sys.exit(1)

  if args.config is None and args.events and os.path.exists(args.events[0]):
    prt(('The passed event name "%s" is a local file. ' % args.events[0] +
         'Did you mean to pass -c / --config ?'), ANSI.RED)
    sys.exit(1)

  if args.reporter_api and not args.config:
    prt('Must pass --config when using --reporter-api', ANSI.RED)
    parser.print_help()
    sys.exit(1)

  return args


def start_trace(args, print_log=True):
  perfetto_cmd = 'perfetto'
  device_dir = '/data/misc/perfetto-traces/'

  # Check the version of android. If too old (< Q) sideload tracebox. Also use
  # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later.
  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
  lines = probe.communicate()[0].decode().strip().split('\n')
  lines = [x.strip() for x in lines]  # To strip \r(s) on Windows.
  if probe.returncode != 0:
    prt('ADB connection failed', ANSI.RED)
    sys.exit(1)
  api_level = int(lines[0])
  abi = lines[1]
  arch = ABI_TO_ARCH.get(abi)
  if arch is None:
    prt('Unsupported ABI: ' + abi)
    sys.exit(1)
  shell_user = lines[2]
  if api_level < 29 or args.sideload:  # 29: Android Q.
    tracebox_bin = args.sideload_path
    if tracebox_bin is None:
      tracebox_bin = get_perfetto_prebuilt(
          TRACEBOX_MANIFEST, arch='android-' + arch)
    perfetto_cmd = '/data/local/tmp/tracebox'
    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
    if exit_code != 0:
      prt('ADB push failed', ANSI.RED)
      sys.exit(1)
    device_dir = '/data/local/tmp/'
    if shell_user != 'root' and not args.user:
      # Run as root if possible as that will give access to more tracing
      # capabilities. Non-root still works, but some ftrace events might not be
      # available.
      adb('root').wait()

  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
  fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex())
  device_file = device_dir + fname

  cmd = [perfetto_cmd, '--background']
  if not args.bin:
    cmd.append('--txt')

  if args.no_guardrails:
    cmd.append('--no-guardrails')

  if args.reporter_api:
    # Remove all old reporter files to avoid polluting the file we will extract
    # later.
    adb('shell',
        'rm /sdcard/Android/data/android.perfetto.cts.reporter/files/*').wait()
    cmd.append('--upload')
  else:
    cmd.extend(['-o', device_file])

  on_device_config = None
  on_host_config = None
  if args.config is not None:
    cmd += ['-c', '-']
    if api_level < 24:
      # adb shell does not redirect stdin. Push the config on a temporary file
      # on the device.
      mktmp = adb(
          'shell',
          'mktemp',
          '--tmpdir',
          '/data/local/tmp',
          stdout=subprocess.PIPE)
      on_device_config = mktmp.communicate()[0].decode().strip().strip()
      if mktmp.returncode != 0:
        prt('Failed to create config on device', ANSI.RED)
        sys.exit(1)
      exit_code = adb('push', '--sync', args.config, on_device_config).wait()
      if exit_code != 0:
        prt('Failed to push config on device', ANSI.RED)
        sys.exit(1)
      cmd = ['cat', on_device_config, '|'] + cmd
    else:
      on_host_config = args.config
  else:
    cmd += ['-t', args.time, '-b', args.buffer]
    for app in args.app:
      cmd += ['--app', '\'' + app + '\'']
    cmd += args.events

  # Work out the output file or directory.
  if args.out.endswith('/') or os.path.isdir(args.out):
    host_dir = args.out
    host_file = os.path.join(args.out, fname)
  else:
    host_file = args.out
    host_dir = os.path.dirname(host_file)
    if host_dir == '':
      host_dir = '.'
      host_file = './' + host_file
  if not os.path.exists(host_dir):
    shutil.os.makedirs(host_dir)

  with open(on_host_config or os.devnull, 'rb') as f:
    if print_log:
      print('Running ' + ' '.join(cmd))
    proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE)
    proc_out = proc.communicate()[0].decode().strip()
    if on_device_config is not None:
      adb('shell', 'rm', on_device_config).wait()
    # On older versions of Android (x86_64 emulator running API 22) the output
    # looks like:
    #   WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ...
    #   WARNING: ... (other 2 WARNING: linker: lines)
    #   1234  <-- The actual pid we want.
    match = re.search(r'^(\d+)$', proc_out, re.M)
    if match is None:
      prt('Failed to read the pid from perfetto --background', ANSI.RED)
      prt(proc_out)
      sys.exit(1)
    bg_pid = match.group(1)
    exit_code = proc.wait()

  if exit_code != 0:
    prt('Perfetto invocation failed', ANSI.RED)
    sys.exit(1)

  prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE)
  log_level = "-v"
  if not print_log:
    log_level = "-e"
  logcat = adb('logcat', log_level, 'brief', '-s', 'perfetto', '-b', 'main',
               '-T', '1')

  ctrl_c_count = 0
  adb_failure_count = 0
  while ctrl_c_count < 2:
    try:
      # On older Android devices adbd doesn't propagate the exit code. Hence
      # the RUN/TERM parts.
      poll = adb(
          'shell',
          'test -d /proc/%s && echo RUN || echo TERM' % bg_pid,
          stdout=subprocess.PIPE)
      poll_res = poll.communicate()[0].decode().strip()
      if poll_res == 'TERM':
        break  # Process terminated
      if poll_res == 'RUN':
        # The 'perfetto' cmdline client is still running. If previously we had
        # an ADB error, tell the user now it's all right again.
        if adb_failure_count > 0:
          adb_failure_count = 0
          prt('ADB connection re-established, the trace is still ongoing',
              ANSI.BLUE)
        time.sleep(0.5)
        continue
      # Some ADB error happened. This can happen when tracing soon after boot,
      # before logging in, when adb gets restarted.
      adb_failure_count += 1
      if adb_failure_count >= MAX_ADB_FAILURES:
        prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
        sys.exit(1)
      time.sleep(2)
    except KeyboardInterrupt:
      sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
      ctrl_c_count += 1
      if print_log:
        prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW)
      adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()

  logcat.kill()
  logcat.wait()

  if args.reporter_api:
    if print_log:
      prt('Waiting a few seconds to allow reporter to copy trace')
    time.sleep(5)

    ret = adb(
        'shell',
        'cp /sdcard/Android/data/android.perfetto.cts.reporter/files/* ' +
        device_file).wait()
    if ret != 0:
      prt('Failed to extract reporter trace', ANSI.RED)
      sys.exit(1)

  if print_log:
    prt('\n')
    prt('Pulling into %s' % host_file, ANSI.BOLD)
  adb('pull', device_file, host_file).wait()
  adb('shell', 'rm -f ' + device_file).wait()

  if not args.no_open:
    if print_log:
      prt('\n')
      prt('Opening the trace (%s) in the browser' % host_file)
    open_browser = not args.no_open_browser
    open_trace_in_browser(host_file, open_browser, args.origin)

  return host_file


def main():
  args = setup_arguments()
  start_trace(args)


def prt(msg, colors=ANSI.END):
  print(colors + msg + ANSI.END)


def find_adb():
  """ Locate the "right" adb path

  If adb is in the PATH use that (likely what the user wants) otherwise use the
  hermetic one in our SDK copy.
  """
  global adb_path
  for path in ['adb', HERMETIC_ADB_PATH]:
    try:
      subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
      adb_path = path
      break
    except OSError:
      continue
  if adb_path is None:
    sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
    prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED)
    prt('You can download adb from %s' % sdk_url, ANSI.RED)
    sys.exit(1)


def open_trace_in_browser(path, open_browser, origin):
  # We reuse the HTTP+RPC port because it's the only one allowed by the CSP.
  PORT = 9001
  path = os.path.abspath(path)
  os.chdir(os.path.dirname(path))
  fname = os.path.basename(path)
  socketserver.TCPServer.allow_reuse_address = True
  with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
    address = f'{origin}/#!/?url=http://127.0.0.1:{PORT}/{fname}&referrer=record_android_trace'
    if open_browser:
      webbrowser.open_new_tab(address)
    else:
      print(f'Open URL in browser: {address}')

    httpd.expected_fname = fname
    httpd.fname_get_completed = None
    httpd.allow_origin = origin
    while httpd.fname_get_completed is None:
      httpd.handle_request()


def adb(*args, stdin=devnull, stdout=None, stderr=None):
  cmd = [adb_path, *args]
  setpgrp = None
  if os.name != 'nt':
    # On Linux/Mac, start a new process group so all child processes are killed
    # on exit. Unsupported on Windows.
    setpgrp = lambda: os.setpgrp()
  proc = subprocess.Popen(
      cmd, stdin=stdin, stdout=stdout, stderr=stderr, preexec_fn=setpgrp)
  procs.append(proc)
  return proc


def kill_all_subprocs_on_exit():
  for p in [p for p in procs if p.poll() is None]:
    p.kill()


def check_hash(file_name, sha_value):
  with open(file_name, 'rb') as fd:
    file_hash = hashlib.sha1(fd.read()).hexdigest()
    return file_hash == sha_value


if __name__ == '__main__':
  sys.exit(main())

image.png

整体文件的配置结构如上,在window下,直接运行getTrace就能抓到trace了。