433 lines
23 KiB
Markdown
433 lines
23 KiB
Markdown
好的,收到您的任务。作为一名资深Android性能优化专家,我将为您撰写一份详尽、可操作的《Android游戏整机性能卡顿与丢帧根因分析与优化指南》。本指南将严格遵循您提出的技术要求、分析维度和交付标准。
|
||
|
||
---
|
||
|
||
### **Android游戏整机性能卡顿与丢帧根因分析与优化指南**
|
||
|
||
**版本:** 1.0
|
||
**适用对象:** Android游戏性能优化工程师、系统工程师、高级开发人员
|
||
**核心目标:** 建立一套从现象到根因,再到优化的系统性分析方法,根治Android游戏中出现的卡顿与丢帧问题。
|
||
|
||
---
|
||
|
||
### **第一部分:问题表征与数据收集**
|
||
|
||
在开始任何优化之前,必须能够准确、一致地复现和捕获问题数据。本部分旨在建立性能基线和标准化的数据收集流程。
|
||
|
||
#### **1.1 关键性能指标(KPI)定义与基线建立**
|
||
|
||
不要只看平均帧率,那会掩盖严重的卡顿问题。请建立以下多维度的KPI体系:
|
||
|
||
* **平均帧率(Average FPS):** 宏观性能指标,反映整体流畅度,但不够敏感。
|
||
* **帧时间(Frame Time):** **核心指标**。记录每帧渲染所消耗的时间(毫秒)。对于60fps的目标,帧时间应为16.6ms;120fps则为8.3ms。
|
||
* **百分位帧时间(P99 / P95 Frame Time):** **卡顿敏感指标**。P99帧时间表示99%的帧都在此时间内完成渲染。如果P99帧时间远高于16.6ms,说明有严重的尾部延迟,即卡顿。例如,“平均55fps,但P99帧时间为48ms”才是问题的真实写照。
|
||
* **1% Low FPS:** 另一种表达尾部延迟的方式,指最低1%帧的平均帧率,数值越低,卡顿感越强。
|
||
* **帧时间直方图(Frame Time Histogram):** 可视化每一帧的时间分布,能直观地看出帧时间是否集中在16.6ms附近,还是有大量长尾帧。
|
||
|
||
**建立基线:** 在游戏的一个**稳定、简单场景**(如游戏主界面、单人静止场景)下运行5-10分钟,记录上述所有KPI,作为后续对比的“黄金基线”。
|
||
|
||
#### **1.2 捕获高质量的 `Perfetto` 跟踪文件**
|
||
|
||
`Perfetto` 是系统级性能分析的基石。一次成功的跟踪捕获,是成功分析的一半。
|
||
|
||
**目标:** 捕获一个从卡顿发生前2秒到发生后3秒,包含完整系统信息的跟踪文件。
|
||
|
||
**推荐配置(`game_performance.cfg`):**
|
||
```protobuf
|
||
# game_performance.cfg
|
||
buffers {
|
||
size_kb: 204800 # 200MB 缓冲区,足够捕获几秒钟的详细数据
|
||
fill_policy: DISCARD
|
||
}
|
||
buffers {
|
||
size_kb: 4096
|
||
fill_policy: DISCARD
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "linux.process_stats"
|
||
target_buffer: 1
|
||
process_stats_config {
|
||
scan_all_processes_on_start: true
|
||
proc_stats_poll_ms: 100 # 每100ms采样一次进程CPU/内存
|
||
}
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.log"
|
||
android_log_config {
|
||
log_ids: LID_DEFAULT
|
||
log_ids: LID_SYSTEM
|
||
log_ids: LID_CRASH
|
||
}
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.game_intervention_listener" # 捕获游戏模式相关的系统干预
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.surfaceflinger" # 捕获SurfaceFlinger的合成过程和VSync信息
|
||
surfaceflinger_config {
|
||
trace_messages: true
|
||
}
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.hwcomposer" # 捕获硬件合成信息
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.gpu.memory" # GPU内存信息
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.gpu.frequency" # GPU频率信息
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "linux.sys_stats" # 系统整体CPU/磁盘/内存统计
|
||
sys_stats_config {
|
||
meminfo_period_ms: 100
|
||
vmstat_period_ms: 100
|
||
stat_period_ms: 100
|
||
}
|
||
}
|
||
}
|
||
data_sources {
|
||
config {
|
||
name: "android.heapprofd" # 用于Native内存分析,可按需启用
|
||
heapprofd_config {
|
||
sampling_interval_bytes: 4096
|
||
}
|
||
}
|
||
}
|
||
# 核心数据源:包含所有关键tag
|
||
data_sources {
|
||
config {
|
||
name: "linux.ftrace"
|
||
ftrace_config {
|
||
ftrace_events: "power/*" # CPU频率、进入退出idle
|
||
ftrace_events: "sched/*" # 调度器事件:任务迁移、唤醒、切换
|
||
ftrace_events: "kmem/*" # 内核内存分配活动 (可能会产生大量数据,慎用)
|
||
ftrace_events: "mm_vmscan/*" # 内存回收事件
|
||
ftrace_events: "ion/*" # ION缓冲区分配/释放
|
||
ftrace_events: "f2fs/*" # 文件系统事件
|
||
ftrace_events: "ext4/*" # 文件系统事件
|
||
ftrace_events: "sync/*" # 同步原语
|
||
ftrace_events: "workqueue/*" # 工作队列
|
||
ftrace_events: "gpu_mem/*" # GPU内存事件
|
||
ftrace_events: "camera/*" # 可能的后台干扰
|
||
# Android 特定事件
|
||
atrace_apps: "*" # 跟踪所有应用的自定义atrace标记
|
||
atrace_categories: "gfx" # 图形系统 (Choreographer, 渲染)
|
||
atrace_categories: "input" # 输入事件
|
||
atrace_categories: "view" # 视图系统
|
||
atrace_categories: "webview" # WebView
|
||
atrace_categories: "wm" # 窗口管理器
|
||
atrace_categories: "am" # 活动管理器 (Activity启动)
|
||
atrace_categories: "sm" # 同步管理器
|
||
atrace_categories: "audio" # 音频
|
||
atrace_categories: "video" # 视频
|
||
atrace_categories: "camera" # 相机
|
||
atrace_categories: "hal" # 硬件抽象层
|
||
atrace_categories: "res" # 资源加载
|
||
atrace_categories: "dalvik" # Dalvik/ART (包含GC)
|
||
atrace_categories: "bionic" # bionic库调用
|
||
atrace_categories: "power" # 电源管理
|
||
atrace_categories: "pm" # 电源管理
|
||
atrace_categories: "ss" # 系统服务
|
||
atrace_categories: "database" # 数据库
|
||
atrace_categories: "network" # 网络
|
||
atrace_categories: "adb" # ADB
|
||
atrace_categories: "vibrator" # 振动器
|
||
atrace_categories: "aidl" # AIDL调用
|
||
}
|
||
}
|
||
}
|
||
duration_ms: 10000 # 跟踪持续10秒,可根据需要调整
|
||
```
|
||
|
||
**捕获命令:**
|
||
```bash
|
||
# 1. 推送配置到设备
|
||
adb push game_performance.cfg /data/local/tmp/
|
||
|
||
# 2. 开始跟踪(在游戏运行到目标场景前执行)
|
||
adb shell perfetto -c /data/local/tmp/game_performance.cfg --txt -o /data/misc/perfetto-traces/trace.perfetto-trace
|
||
|
||
# 3. 在游戏中进行复现卡顿的操作
|
||
|
||
# 4. 停止跟踪(Ctrl+C 或等待duration_ms超时),然后拉取文件
|
||
adb pull /data/misc/perfetto-traces/trace.perfetto-trace .
|
||
```
|
||
|
||
#### **1.3 辅助数据收集**
|
||
|
||
* **`dumpsys gfxinfo`:** 在卡顿发生后立即执行,获取最近128帧的详细帧时间直方图。
|
||
```bash
|
||
adb shell dumpsys gfxinfo <游戏包名>
|
||
```
|
||
重点关注 `Total frames rendered` 和 `frames` 部分的时间分布。
|
||
* **`dumpsys SurfaceFlinger`:** 获取显示系统的合成信息、BufferQueue状态、HWC信息。
|
||
```bash
|
||
adb shell dumpsys SurfaceFlinger
|
||
```
|
||
查看 `[包名/Activity]` 对应的Layer,关注 `queue` 和 `buffer count` 状态。
|
||
* **`top` / `vmstat`:** 实时监控系统整体负载。虽然Perfetto已有,但可做快速现场检查。
|
||
```bash
|
||
adb shell top -d 1 -n 10 | grep <游戏包名> # 监控游戏进程CPU占用
|
||
adb shell vmstat 1 10 # 监控系统级CPU、内存、IO情况
|
||
```
|
||
|
||
---
|
||
|
||
### **第二部分:分层诊断流程**
|
||
|
||
拿到 `perfetto` 跟踪文件后,打开 [Perfetto UI](https://ui.perfetto.dev/),加载文件,开始以下分层诊断。
|
||
|
||
#### **步骤一:应用层分析**
|
||
|
||
**目标:** 确定卡顿是否由应用进程自身(主线程/渲染线程)的逻辑耗时引起。
|
||
|
||
1. **识别卡顿帧:**
|
||
* 在 `SurfaceFlinger` 或 `gfx` 轨道下找到应用的 `FrameLifecycle` (或 `DrawFrame`) 切片。如果一个帧的开始到结束时间超过了16.6ms(对于60fps),或者相邻帧之间有巨大空隙,则标记为卡顿帧。
|
||
|
||
2. **分析主线程(Main Thread,通常为 `[包名]`):**
|
||
* 找到卡顿帧对应时间点的主线程切片。
|
||
* **查找长耗时函数:** 寻找宽度明显大于其他区域的切片。常见元凶:
|
||
* **`Choreographer#doFrame`:** 这是处理输入、动画、绘制入口的核心。如果它延迟启动,说明主线程之前被阻塞了。
|
||
* **GC 事件:** 搜索 `art::gc::` 相关切片,如 `ConcurrentMarking`, `Pause`。一次几十毫秒的GC暂停会直接导致丢帧。**量化:** 一次GC暂停了24ms,导致 `doFrame` 延迟。
|
||
* **锁竞争(Lock Contention):** 切片上出现 `Lock` 或 `Mutex` 字样,颜色通常是红色或黄色。点击它可以看到是哪个线程持有锁(`LockOwner`)。**量化:** 主线程在 `android.view.View#invalidate` 上等待某锁释放15ms,该锁被一个后台线程持有。
|
||
* **Binder 调用:** 切片以 `binder transaction` 开头。这表明主线程在等待系统服务响应,如 `PackageManager`, `WindowManager`。**量化:** 主线程调用 `IActivityManager` 的Binder接口,等待了8ms。
|
||
* **文件I/O:** 切片以 `open`, `read`, `write`, `fopen` 开头。意味着主线程直接进行磁盘操作。
|
||
|
||
3. **分析渲染线程(RenderThread):**
|
||
* 在 `RenderThread` (通常为 `[包名]:GPU` 或 `[包名]:RenderThread`) 轨道上分析。
|
||
* **查找长耗时函数:**
|
||
* **`eglSwapBuffers` 或 `queueBuffer`:** 这是一个关键点,它标志着CPU侧渲染工作已完成,等待Buffer被SurfaceFlinger消费。
|
||
* 如果 `eglSwapBuffers` 阻塞时间很长,说明**GPU负载过高**或 **BufferQueue 满了**(后面会分析)。
|
||
* **`flush commands`:** 向GPU提交绘制命令的开销。
|
||
* **`draw` 相关的调用:** 如 `glDrawElements`, 大量耗时可能意味着Draw Call过多。
|
||
* **检查渲染线程优先级:** 查看其调度切片,是否经常被其他线程抢占。
|
||
|
||
#### **步骤二:系统层分析**
|
||
|
||
**目标:** 确定卡顿是否由系统服务(如SurfaceFlinger)或系统资源管理(CPU调度、内存)引起。
|
||
|
||
1. **分析显示系统(SurfaceFlinger):**
|
||
* 找到 `SurfaceFlinger` 进程的主线程轨道。
|
||
* **检查合成周期:** `onMessageReceived` 或 `doComposition` 切片。如果这些切片的执行时间超过了VSync间隔,会导致Buffer不能及时被消费,从而阻塞游戏的生产者线程(如RenderThread的 `eglSwapBuffers`)。
|
||
* **检查VSync信号:** `VSync-app` 和 `VSync-sf` 轨道。如果VSync信号本身出现抖动或丢失,`Choreographer` 就无法准时被唤醒,导致应用错过帧。这通常与系统负载高或中断处理延迟有关。
|
||
* **检查BufferQueue状态:** 通过 `dumpsys SurfaceFlinger` 或在Perfetto中搜索相关切片。如果 `BufferQueue` 的 `dequeueBuffer` 或 `queueBuffer` 出现阻塞,可能是因为 `SurfaceFlinger` 来不及消费,导致队列满(例如,三缓冲都满了)。
|
||
|
||
2. **分析CPU调度器(`sched` 轨道):**
|
||
* 这是诊断CPU瓶颈的核心。切换到 `Scheduling` 跟踪视图。
|
||
* **检查关键线程的调度:**
|
||
* 找到游戏的主线程和渲染线程。
|
||
* **CPU迁移:** 在卡顿发生瞬间,这两个线程是否被迁移到了**小核(LITTLE core)** 上运行?由于小核性能差,这会导致计算能力突然下降。**量化:** 卡顿时刻,主线程从大核(CPU4)被迁移到小核(CPU0)上运行了15ms。
|
||
* **被抢占(Preempted):** 查看是否有更高优先级的系统线程(如 `kswapd`, `irq`, `cfinteractive`)长时间抢占了游戏线程。
|
||
* **就绪态等待(Runnable):** 线程状态为 `R`(绿色)但未运行,表示它在等待CPU。如果等待时间过长,说明CPU核心不足或调度器决策不当。
|
||
|
||
3. **分析内存回收压力(`memreclaim` 和 `mm_vmscan` 事件):**
|
||
* 在Perfetto的搜索框中输入 `kswapd` 或 `direct reclaim`。
|
||
* **`kswapd` 唤醒:** 如果 `kswapd` (内核交换守护进程)被频繁唤醒且运行时间较长,说明系统内存压力大,正在后台回收页面。
|
||
* **直接内存回收(Direct Reclaim):** **这是严重卡顿的信号**。当 `kswapd` 回收速度跟不上时,分配内存的线程(如游戏主/渲染线程)会自己进入内存回收流程。这会导致该线程被阻塞几十甚至上百毫秒。搜索 `mm_vmscan:direct_reclaim_begin` 和 `_end` 切片。**量化:** 渲染线程在分配一个纹理时触发了直接内存回收,被阻塞了35ms。
|
||
|
||
#### **步骤三:内核/硬件层分析**
|
||
|
||
**目标:** 确定卡顿是否由硬件频率、温度限制或特定硬件单元瓶颈引起。
|
||
|
||
1. **分析CPU频率(`power/cpu_frequency`):**
|
||
* 在Perfetto的 `Frequency` 视图中,查看大核和中核的CPU频率曲线。
|
||
* **热限频(Thermal Throttling):** 在卡顿时刻,CPU频率是否出现**断崖式下跌**?如果所有核心频率突然降到最低,很可能是触发了温控。可以结合 `thermal` 事件或 `temperature` 传感器数据确认。
|
||
* **频率提升不足:** 在进入团战等高负载场景时,CPU频率是否及时拉升到最高?如果拉升缓慢,会导致性能不足。
|
||
|
||
2. **分析GPU频率与负载(需要厂商工具或Perfetto的`gpu`事件):**
|
||
* 使用高通Snapdragon Profiler或ARM Streamline可以获取GPU的硬件计数器。
|
||
* **GPU频率过低:** 类似CPU,检查GPU频率是否在高负载场景下未提升。
|
||
* **着色器核心(Shader Core)瓶颈:** 计数器显示ALUs(算术逻辑单元)占用率100%,而纹理单元(TMU)空闲,说明是计算瓶颈,可能Shader太复杂。
|
||
* **内存带宽瓶颈:** 计数器显示带宽接近极限,而核心利用率不高,说明GPU在等待从内存获取数据,通常由大量纹理采样或复杂帧缓冲操作引起。
|
||
|
||
3. **分析I/O与存储:**
|
||
* 在Perfetto中搜索 `f2fs_read`, `ext4_read`, `ion allocation`。
|
||
* **资源加载卡顿:** 当游戏进入新场景时,主线程或加载线程是否在等待 `read` 调用完成?如果是,说明磁盘I/O是瓶颈,可能需要优化资源加载策略或使用异步加载。
|
||
* **Shader编译卡顿:** 搜索 `ShaderCompiler` 或 `ProgramCache` 相关切片。首次运行游戏或更新后,GPU驱动编译着色器是一个非常耗时的操作,经常导致卡顿。
|
||
|
||
#### **步骤四:关联分析——构建根因逻辑链**
|
||
|
||
这是最重要的步骤,将以上各层的信息串联成一个完整的故事。
|
||
|
||
**逻辑链示例:**
|
||
|
||
1. **现象(Perfetto中观察):** 在时间戳T,游戏帧 `FrameLifecycle` 切片显示该帧耗时48ms。随后,`SurfaceFlinger` 因为没有新帧而进行了重复合成(`idle`)。
|
||
2. **应用层分析(主线程):** 放大T时刻主线程切片,发现 `Choreographer#doFrame` 直到T+20ms才开始。在它之前,有一个非常宽的 `art::gc::ConcurrentMarking` 暂停切片,持续了20ms。
|
||
3. **系统层分析(调度/内存):** 观察GC暂停期间的调度。GC线程(`HeapTaskDaemon`)正在运行,而主线程处于`Uninterruptible Sleep` (D状态) 等待。同时,内存统计数据显示系统`free memory`极低,`kswapd` 正在高频运行。
|
||
4. **内核/硬件层分析(频率):** 在GC发生前,CPU大核由于持续高负载温度上升,在GC开始的瞬间触发了温控,频率从2.4GHz骤降至1.0GHz,导致GC执行变慢,进一步拉长了暂停时间。
|
||
|
||
**最终根因结论:**
|
||
**由于系统内存压力过大,触发了垃圾回收(GC),而此时CPU因热限频导致性能下降,GC暂停时间被拉长到20ms,直接阻塞了游戏主线程,使其错过了随后的VSync信号,最终导致一帧耗时48ms,用户感知为一次严重卡顿。**
|
||
|
||
优化方向也随之清晰:减少应用内存占用以减少GC频率;优化GC参数;优化温控策略。
|
||
|
||
---
|
||
|
||
### **第三部分:优化策略与建议**
|
||
|
||
针对已识别的根因,提供具体的、分层的优化建议。
|
||
|
||
#### **3.1 应用层优化**
|
||
|
||
* **针对GC卡顿:**
|
||
* **内存优化:** 使用对象池减少频繁的对象分配;避免在每帧的循环中创建新对象;使用`SparseArray`替代`HashMap`;优化数据结构。
|
||
* **内存泄漏检测:** 使用LeakCanary定期检测并修复Java内存泄漏。
|
||
* **Native层内存分配:** 对于大型数据(如纹理、音频),尽量使用Native堆(`malloc`, `new`),避免Java堆压力。使用 `ByteBuffer.allocateDirect()` 创建直接缓冲区。
|
||
* **针对锁竞争:**
|
||
* **最小化锁粒度:** 只锁必要的代码块,而不是整个方法。
|
||
* **读写锁分离:** 对于读多写少的场景,使用 `ReentrantReadWriteLock`。
|
||
* **避免在临界区中执行耗时操作。**
|
||
* **针对渲染过载:**
|
||
* **LOD技术:** 根据距离使用不同精细度的模型。
|
||
* **遮挡剔除:** 不渲染玩家看不到的物体。
|
||
* **减少Overdraw:** 使用工具检查并减少重复绘制的像素。
|
||
* **优化Shader:** 减少复杂数学运算,合并材质。
|
||
* **合批渲染:** 减少Draw Call数量。
|
||
* **针对I/O卡顿:**
|
||
* **资源预加载:** 在进入新场景前,提前在后台线程加载必要的资源。
|
||
* **分帧加载:** 将大型资源的加载分摊到多帧中,避免单帧阻塞。
|
||
* **异步加载:** 使用`AssetManager`的异步接口或在加载线程中进行所有I/O操作。
|
||
* **针对Shader编译卡顿:**
|
||
* **使用Pipelines库:** Vulkan API使用Pipeline Cache,OpenGL ES使用Program Binary,在首次编译后保存,后续启动时直接加载。
|
||
* **后台编译:** 如果引擎支持,让Shader编译在加载屏幕或非关键路径上完成。
|
||
|
||
#### **3.2 系统层优化(需要与OEM/芯片厂商合作或利用游戏模式API)**
|
||
|
||
* **针对CPU调度不当:**
|
||
* **设置关键线程优先级:** 使用 `android.os.Process.setThreadPriority()` 和 `setThreadGroupAndCpuset()` 将主线程、渲染线程绑定到大核组,并赋予高优先级。
|
||
* **使用游戏模式API:** Google Play Console允许配置游戏模式,向系统申请性能资源。
|
||
* **与OEM合作:** 在系统服务中为特定游戏配置白名单,避免后台服务抢占CPU资源。
|
||
* **针对内存压力:**
|
||
* **使用 `ActivityManager` 的 `setMemoryClass()` 或 `setLargeHeapClass()` 请求更多堆内存。**
|
||
* **监控 `onTrimMemory()` 回调:** 当收到 `TRIM_MEMORY_MODERATE` 或更高级别时,主动释放缓存资源(如纹理、缓存数据)。
|
||
* **针对SurfaceFlinger阻塞:**
|
||
* **启用三重缓冲:** 在应用代码中请求三缓冲 `mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT)` 等操作有时会触发,但更可靠的是在引擎层配置。
|
||
* **使用Vulkan API:** Vulkan提供了对BufferQueue和渲染流水线更精细的控制,可以减少对SurfaceFlinger的依赖。
|
||
|
||
#### **3.3 内核/硬件层优化**
|
||
|
||
* **针对CPU/GPU频率/热限频:**
|
||
* **功耗优化:** 应用层的性能优化也意味着功耗降低,能有效延缓温控触发。
|
||
* **了解温控策略:** 与OEM团队沟通,了解温控阈值,并确保游戏在温控触发前能将热点任务迁移回大核。
|
||
* **选择目标GPU频率:** 在某些厂商工具中,可以针对特定游戏设置最小的GPU频率,防止因频率切换造成的瞬时性能下降。
|
||
* **针对I/O瓶颈:**
|
||
* **文件系统对齐:** 确保资源文件的存储对齐方式与文件系统(如F2FS)一致。
|
||
* **使用异步I/O接口:** 内核层面确保I/O操作不会阻塞关键线程。
|
||
|
||
---
|
||
|
||
### **第四部分:工具与脚本**
|
||
|
||
#### **4.1 自动化数据收集脚本 (Python示例)**
|
||
|
||
```python
|
||
# collect_game_perf.py
|
||
import subprocess
|
||
import time
|
||
import sys
|
||
import os
|
||
|
||
PACKAGE_NAME = sys.argv[1] # 从命令行传入包名
|
||
TRACE_TIME = 10 # 跟踪时间(秒)
|
||
PERFETTO_CONFIG = "/data/local/tmp/game_performance.cfg"
|
||
OUTPUT_DIR = "./perf_traces"
|
||
|
||
def run_adb_command(cmd):
|
||
result = subprocess.run(f"adb shell {cmd}", shell=True, capture_output=True, text=True)
|
||
return result.stdout.strip()
|
||
|
||
def main():
|
||
if not os.path.exists(OUTPUT_DIR):
|
||
os.makedirs(OUTPUT_DIR)
|
||
|
||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||
trace_file = f"{OUTPUT_DIR}/trace_{PACKAGE_NAME}_{timestamp}.perfetto-trace"
|
||
|
||
print(f"1. 确保 {PACKAGE_NAME} 正在前台运行...")
|
||
input("按 Enter 键继续...")
|
||
|
||
print("2. 启动 Perfetto 跟踪...")
|
||
run_adb_command(f"perfetto -c {PERFETTO_CONFIG} --txt -o /data/misc/perfetto-traces/trace_tmp.perfetto-trace &")
|
||
|
||
print(f"3. 请在 {TRACE_TIME} 秒内执行卡顿复现操作...")
|
||
time.sleep(TRACE_TIME)
|
||
|
||
print("4. 停止跟踪...")
|
||
run_adb_command("killall -SIGINT perfetto")
|
||
time.sleep(2) # 等待写入完成
|
||
|
||
print("5. 拉取跟踪文件...")
|
||
run_adb_command(f"pull /data/misc/perfetto-traces/trace_tmp.perfetto-trace {trace_file}")
|
||
run_adb_command("rm /data/misc/perfetto-traces/trace_tmp.perfetto-trace")
|
||
|
||
print(f"6. 获取 dumpsys gfxinfo...")
|
||
gfx_info = run_adb_command(f"dumpsys gfxinfo {PACKAGE_NAME}")
|
||
with open(f"{OUTPUT_DIR}/gfxinfo_{PACKAGE_NAME}_{timestamp}.txt", "w") as f:
|
||
f.write(gfx_info)
|
||
|
||
print(f"完成!跟踪文件已保存至: {trace_file}")
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) != 2:
|
||
print("用法: python collect_game_perf.py <游戏包名>")
|
||
sys.exit(1)
|
||
main()
|
||
```
|
||
|
||
#### **4.2 推荐 `Perfetto` UI 配置**
|
||
|
||
在Perfetto UI中,保存以下配置为 `game_config.json`,通过 `Load Config` 加载,可以快速显示关键轨道。
|
||
|
||
```json
|
||
{
|
||
"config": {
|
||
"cpu": true,
|
||
"ftrace": true,
|
||
"gpu": true,
|
||
"power": true,
|
||
"mem": true,
|
||
"sched": true
|
||
},
|
||
"pinnedCpus": [],
|
||
"visibleColumns": ["name", "utid", "dur", "avg_cpu"],
|
||
"traceProcessorConfig": {
|
||
"http": {
|
||
"port": 9001,
|
||
"browserUrl": "http://localhost:9001/"
|
||
}
|
||
},
|
||
"displayConfig": {
|
||
"showThreadNames": true,
|
||
"showCpuTimes": true,
|
||
"showCpuFreq": true,
|
||
"showMemUsage": true,
|
||
"showGpuFreq": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**在Perfetto UI中的快速过滤清单:**
|
||
* 搜索游戏包名,快速定位其所有线程。
|
||
* 搜索 `GC` 或 `art` 查看垃圾回收。
|
||
* 搜索 `SurfaceFlinger` 查看合成。
|
||
* 搜索 `kswapd` 和 `direct_reclaim` 查看内存压力。
|
||
* 搜索 `cpu_frequency` 查看降频情况。
|
||
* 关注状态为 `Runnable` (绿色) 但长时间未运行的线程。
|
||
|
||
通过遵循本指南,您将有能力系统性地诊断和解决Android游戏中最棘手的卡顿问题,为用户提供极致流畅的游戏体验。 |