1. CPU 核心架构
在深入分析前,必须了解现代手机 SoC 的 CPU 核心架构。目前主流的移动处理器普遍采用 big.LITTLE 异构多核架构,或其变种,如 big.Medium.LITTLE(大中小核)。
- 小核 (LITTLE cores) : 针对低功耗设计,频率较低,用于处理后台任务、轻量级计算,以保证续航。
- 大核 (big cores) : 针对高性能设计,频率更高,功耗也更大,用于处理用户交互、游戏、应用启动等重负载场景。
- 超大核 (Prime Core) : 部分旗舰芯片会有一个频率极高的超大核,用于应对最严苛的单核性能挑战。
在 Perfetto 的 CPU 轨道中,CPU 通常从 0 开始编号。例如,在一个典型的八核处理器中,CPU 0-3 可能为小核,CPU 4-6 为大核,CPU 7 为超大核。识别核类型对性能分析很有帮助:一个计算密集型任务如果长时间运行在小核上,其耗时必然远超预期。分析时,需要将线程的运行 CPU 与其任务属性进行匹配,以判断调度器(Scheduler)的行为是否符合预期。
下面是一个典型的 4+4 的 CPU :
下面是一个典型的 4+3+1 的 CPU ( MTK 的天玑 9500、9400 ,以及高通骁龙高端系列):
下面是一个 5+2 的 CPU (高通 8Elite 1 阉割版,8Elite 1 的标准版 0-5 是小核,6-7 是大核,这里就不放图了)
一般通过查阅 CPU 的 spec 就可以知道他的大中小核心的架构,或者 cat 对应的 CPU 节点也可以,这里就不再赘述了。
2. CPU 调度
CPU Scheduling 轨道是最常用且最重要的部分,它可视化了 Linux 内核调度器的决策过程。其数据来源于内核 ftrace 中的 sched/sched_switch 事件。
每个 CPU 核心对应一行独立的轨迹。轨道上的不同色块,代表了在该时间片上,特定线程正在该 CPU 核心上运行。
- UI 细节:点击 CPU 切片,详情面板会显示该次调度的
cpu、end_state、priority、所属process/thread等;向下展开进程还能看到每个线程的独立轨道,便于跟踪单个线程的状态演化(参考 官方文档)。
3. 线程状态分析
理解 Linux 的线程状态是进行性能优化的前提。在 Perfetto 中,选中一个线程,下方的 Current State 面板会显示其当前状态。这些状态信息来源于 Perfetto 解析得到的 thread_state/thread_state_slice 表。
Running (绿色)
状态定义:绿色代表线程正在 CPU 上执行代码。这是唯一实际消耗 CPU 资源进行计算的状态。
分析要点:
- 执行时长:过长的
Running状态,尤其是在关键线程上,通常意味着密集的计算任务,例如复杂的算法或循环。这会增加任务耗时,并可能阻塞其他线程的执行。 - 运行核心:结合 CPU 的核心架构(例如 big.LITTLE)进行分析。一个计算密集型任务是否被调度到了预期的性能核心(大核)上,是评估调度策略是否合理的重要依据。
- 运行频率:线程的实际执行速度也受 CPU 频率影响。即使线程运行在大核上,如果因为温控等原因导致降频,其性能表现也会下降。因此需要结合
CPU Frequency轨道进行综合分析。
R (Runnable / 可运行)
状态定义:线程已具备所有运行条件,正在等待调度器分配 CPU 核心。在 Perfetto 的线程私有轨道中,Runnable 状态通常以浅绿色或白色条显示。
分析要点:
Runnable与卡顿:对于 UI 线程这类对响应时间敏感的线程,长时间处于Runnable状态是造成卡顿的直接原因。它意味着线程无法及时获得 CPU 时间来处理任务(如 UI 绘制),从而导致掉帧。
Runnable 的三种类型:
仔细观察会发现,线程进入 Runnable 状态的“前身”各不相同。根据内核的调度时机,我们可以将其分为三种情况:
- 从睡眠中唤醒-Wake-up:这是最常见的一种。线程因等待的资源(如锁、I/O、Binder 回复)已经就绪,从
S或D状态被唤醒,进入Runnable状态,等待被调度器选中执行。 - 用户抢占-User Preemption:指线程的运行时间片用完,或出现更高优先级的任务,导致调度器在从内核态返回用户态时(如系统调用、中断返回后)决定换下当前线程。此时,被换下的线程从
Running变为Runnable。在底层的sched_switchtrace 中,它的prev_state标记为R。 - 内核抢占-Kernel Preemption:指一个更高优先级的任务或中断,在当前线程正在执行内核态代码期间,就“强行”将其打断,使其让出 CPU。这种情况通常意味着一次更紧急的调度。此时,被换下的线程从
Running变为Runnable (Preempted)。在底层 trace 中,它的prev_state标记为R+,Perfetto 据此进行了解析和展示。
理解这三种类型的区别,有助于更精细地判断调度延迟的原因。例如,大量的 Runnable (Preempted) 可能暗示着系统中存在频繁的、高优先级的唤醒源,导致关键线程在内核态中被频繁打断,或者当时的 CPU 已经满载了(比较常见的情况),这时候优先级比较低的 Task 就很容易被其他优先级高的 Task 抢占,被迫让出 CPU 。如果你的关键 Task 总是被抢占,那就需要调整优先级。
S (Sleep / 可中断睡眠)
状态定义:线程因等待某个事件而进入睡眠,可以被信号中断。这是最常见的睡眠状态,通常情况下是良性的,因为它在等待期间不消耗 CPU 资源。
分析要点:
- 等待的资源:如果关键线程(如 UI 线程)睡眠时间过长,同样会引发性能问题。常见的等待原因包括:
- 锁竞争:等待获取一个
mutex(Java 锁或 native futex)。 - Binder 通信:等待另一个进程通过 Binder 调用返回结果。
- I/O 操作:等待网络 socket 数据 (
epoll_wait)。 - 显式休眠:代码中调用了
Thread.sleep()或Object.wait()。 - 依赖分析:在 Perfetto 中,选中 CPU 区域被唤醒正在 Running 的 Task,就会有 UI 标识他是被哪个 CPU 上的哪个 Task 唤醒的,这有助于快速定位线程间的依赖关系。结合函数调用栈,可以进一步定位导致睡眠的具体代码。
D (Uninterruptible Sleep / 不可中断睡眠)
状态定义:线程在等待硬件 I/O 操作完成,期间不能被任何信号中断。此状态旨在保护进程与设备交互过程中的数据一致性。在 Perfetto 中,该状态通常显示为橙色或红色,是需要重点关注的信号。
分析要点:
-
严重的性能瓶颈:长时间的
D状态意味着线程被完全阻塞,无法响应任何事件。如果发生在 UI 线程,极易导致 ANR。 -
常见原因:
- 磁盘 I/O:频繁或单次大量的文件读写操作。
- 内存压力:系统物理内存不足,导致频繁的页面换入/换出 (swap),其本质是高频的磁盘 I/O。
- 内核驱动问题:部分内核驱动的实现缺陷也可能导致线程陷入
D状态。
-
排查方向:在
Current State面板中,如果D状态伴有(iowait)标记,则明确表示在等待 I/O。需要检查应用的 I/O 模式,评估其合理性,例如是否将耗时 I/O 操作置于主线程,或是否存在可优化的空间(如减少 I/O 次数和数据量)。
4. 唤醒关系
线程间的依赖关系是性能分析中的一个难点。一个线程长时间 Sleep,关键在于找出它在“等谁”。Perfetto 提供了强大的唤醒关系可视化功能。
- UI 操作:在 Perfetto 的 CPU 区域中,用鼠标左键单击选中一个处于
Running状态的线程 Task 。Perfetto 会自动绘制一条从“唤醒者”到“被唤醒者”的依赖箭头,并高亮显示唤醒源所在的线程切片。
- 底层原理:该功能依赖于内核的
sched_wakeupftrace 事件。当线程 T1 释放了某个资源(如解锁、完成 Binder 调用),而线程 T2 正在等待该资源时,内核会将 T2 标记为Runnable状态,并记录下 T1 -> T2 的这次唤醒事件。Perfetto 解析这些事件,从而构建出线程间的依赖链。
通过唤醒分析,可以清晰地追踪复杂的调用链,例如:UI 线程等待一个 Binder 调用 -> Binder 线程执行任务 -> Binder 线程等待另一个锁 -> 持锁线程释放锁并唤醒 Binder 线程 -> Binder 线程完成任务并唤醒 UI 线程。整个过程中的瓶颈点将一目了然。