前置阅读:建议先读 [第 5 篇:CPU 采集入门]、[第 6 篇:8 个坑] 和 [第 7 篇:CPU 降频了,你的数据还准吗]。
整机 CPU 42%,报告写"正常"。90 分钟后 ANR。 事后查数据:有一个小核跑到 95%、降频到 408MHz、进程就卡在这个核上。 42% 是 8 个核的平均值。平均值把瓶颈藏起来了。
一、从一次 ANR 说起
24 小时稳定性压测,整机 CPU% 曲线平稳在 35~45%。第 60 分钟用户反馈卡顿,第 90 分钟 ANR。
只看整机数据,完全看不出问题:
时间 整机 Raw% 整机 Norm% 看起来
0min 32% 30% 正常
30min 38% 32% 正常
60min 42% 22% ← Normalized 开始掉了
90min 40% 15% ← ANR 发生
拿到各核数据后,真相浮出水面:
时间 cpu0(小核) cpu4(大核) 进程所在核 cpu0频率
0min 35% 28% cpu4 1800MHz
30min 52% 22% cpu4 1800MHz
60min 90% 12% cpu0 ← 1200MHz ←
90min 95% 8% cpu0 408MHz ←
三个关键变化:
- 第 60 分钟:进程从大核 cpu4 迁移到了小核 cpu0
- cpu0 使用率从 35% 飙到 95%:小核算力弱,同样的任务占满了整个核
- cpu0 频率从 1800MHz 降到 408MHz:发热后被热降频,雪上加霜
整机 42% 是被其他 7 个空闲核"平均"出来的。真正的瓶颈在一个降频的小核上,整机均值完全看不到。
二、实操:三条命令看清各核状态
命令 1:各核 CPU 使用率
adb shell cat /proc/stat
输出(截取前几行):
cpu 128891 3842 52341 882234 12453 0 5819 0 0 0 ← 汇总行
cpu0 18234 512 8123 98234 2341 0 1203 0 0 0 ← cpu0 单独
cpu1 15672 483 6234 112345 1892 0 892 0 0 0 ← cpu1 单独
cpu2 17891 501 7456 105678 1456 0 1045 0 0 0
cpu3 16234 462 5892 118234 2134 0 834 0 0 0
cpu4 21345 612 9234 89012 1823 0 923 0 0 0
cpu5 19876 578 8345 95234 1567 0 712 0 0 0
cpu6 12345 398 4123 145678 892 0 145 0 0 0
cpu7 7294 296 2934 117819 348 0 65 0 0 0
两次采样做差值,就能算出各核 CPU%。/proc/stat 第一行是汇总,后面每行是一个核——同一次读取,零额外开销。
命令 2:各核当前频率
adb shell "for i in 0 1 2 3 4 5 6 7; do \
echo -n \"cpu\$i: \"; \
cat /sys/devices/system/cpu/cpu\$i/cpufreq/scaling_cur_freq 2>/dev/null || echo 'offline'; \
done"
输出:
cpu0: 408000
cpu1: 408000
cpu2: 1200000
cpu3: 408000
cpu4: 2400000
cpu5: 1800000
cpu6: offline
cpu7: offline
一眼看出:小核 cpu0/1/3 被降频到最低档 408MHz,cpu6/7 已关闭。
命令 3:进程跑在哪个核
adb shell cat /proc/12345/stat
输出(关键字段):
12345 (com.example.app) S 1 12345 12345 0 -1 ... 5200 1300 ... 0 0 0 0 2 0 ...
^utime ^stime ^processor
field14 field15 field39
第 39 个字段 processor(从 1 开始计数):告诉你进程此刻正在哪个核上运行。
用
)锚点解析更安全(第 6 篇坑 7):tail = raw.split(')')[1].strip().split() utime = int(tail[11]) # field 14 stime = int(tail[12]) # field 15 processor = int(tail[36]) # field 39
这个字段是判断"进程是不是被调度到了小核"的核心证据,但很多采集工具都没采。
三、把数据拼在一起:一次完整的快照
指标命名约定(避免混淆)
名称 公式 含义 整机 Raw CPU% (Δtotal−Δidle−Δiowait)/Δtotal 整机时间片占比 整机 Normalized CPU% Raw CPU% × 频率比 整机等效算力占比(#7 引入) 各核 Raw CPU% 各核行独立差值 单核时间片占比 单核算力占比 各核CPU% × (各核频率/各核最高频率) × IPC系数 单核的等效算力贡献(本篇引入) "整机 Normalized" 看整体降频程度;"单核算力占比" 看每个核的真实产出。
三条命令的数据拼在一起,就得到一个完整的 CPU 快照:
── 采集时刻:90min(ANR 发生时) ──
整机 Raw CPU%: 42.0%
频率比: (408+408+1200+408+2400+1800+0+0) / (1800×4+2400×2+3000×2)
= 6624 / 18000 = 0.368
整机 Norm CPU%: 42.0% × 0.368 = 15.5%
在线核: 6 / 8
各核状态:
核心 类型 CPU% 频率 计算过程 单核算力占比
cpu0 小核 95% 408MHz 95%×(408/1800)×1.0 = 21.5% ← 满载+降频
cpu1 小核 8% 408MHz 8%×(408/1800)×1.0 = 1.8%
cpu2 小核 88% 1200MHz 88%×(1200/1800)×1.0 = 58.7%
cpu3 小核 5% 408MHz 5%×(408/1800)×1.0 = 1.1%
cpu4 大核 12% 2400MHz 12%×(2400/2400)×2.0 = 24.0%
cpu5 大核 15% 1800MHz 15%×(1800/2400)×2.0 = 22.5%
cpu6 超大 关闭 — — —
cpu7 超大 关闭 — — —
(公式:单核算力占比 = CPU% × (当前频率/最高频率) × IPC系数)
CV: 104% → 负载严重不均衡(计算过程见第四节)
最高负载核: cpu0 (95%)
进程所在核: cpu0 (小核, 408MHz)
这份数据能回答三个问题:
| 问题 | 数据 | 结论 |
|---|---|---|
| CPU 忙不忙? | Raw 42% | 整机看不忙 |
| 实际干了多少活? | Normalized 15.5% | 算力只有满血的 15.5% |
| 瓶颈在哪? | cpu0: 95% @ 408MHz,进程就在 cpu0 | 小核满载 + 降频 |
四、CV:一个数字判断均值是否可信
各核 CPU% 列表很长,不直观。变异系数(CV) 把离散程度压缩成一个数字:
CV = 标准差 / 均值
| CV 范围 | 含义 | 整机 CPU% 可信吗 |
|---|---|---|
| < 20% | 各核负载均匀 | 可信 |
| 20~50% | 有不均衡 | 基本可信,建议同时看单核 |
| > 50% | 严重不均衡 | 不可信 |
用上面案例的数据(仅在线核)逐步算一遍:
各核 CPU%: [95%, 8%, 88%, 5%, 12%, 15%](6 核在线)
Step 1: 均值
mean = (95 + 8 + 88 + 5 + 12 + 15) / 6 = 223 / 6 = 37.2%
Step 2: 各核偏差²
(95−37.2)² = 3341 ← cpu0 偏离极大
( 8−37.2)² = 852
(88−37.2)² = 2581
( 5−37.2)² = 1036
(12−37.2)² = 635
(15−37.2)² = 492
Step 3: 标准差
方差 = (3341+852+2581+1036+635+492) / 6 = 8937 / 6 = 1489.5
标准差 = √1489.5 = 38.6%
Step 4: CV
CV = 标准差 / 均值 = 38.6 / 37.2 = 1.04 ≈ 104%
CV > 50% → 整机 42% 这个数字毫无参考价值,必须下钻到单核。
对比一个正常场景:
各核 CPU%: [35%, 38%, 32%, 40%, 28%, 33%]
mean = 34.3%
标准差 = √[((35−34.3)²+(38−34.3)²+...+(33−34.3)²)/6] = 3.9%
CV = 3.9 / 34.3 = 0.11 = 11%
CV = 11% < 20% → 整机 34% 有代表性,可以直接用。
五、大小核 IPC:同频不同力
现代 Android 几乎都是 big.LITTLE 架构:
小核 (A55): cpu0~cpu3, 最高 1800MHz
大核 (A76): cpu4~cpu5, 最高 2400MHz
超大核 (X2): cpu6~cpu7, 最高 3000MHz
同频下,大核 IPC 是小核的 2~3 倍。 对应 Linux 内核 PELT 的 r_cpu 参数:
| 核心类型 | 微架构 | 性能系数 |
|---|---|---|
| 小核 | Cortex-A55 | 1.0 |
| 大核 | Cortex-A76 | 2.0 |
| 超大核 | Cortex-X2 | 3.0 |
单核归一化算力 = CPU% × (当前频率 / 最高频率) × 性能系数
cpu0(小核):95% × (408/1800) × 1.0 = 21.5%
cpu4(大核):12% × (2400/2400) × 2.0 = 24.0%
cpu4 的 CPU% 只有 12%,但实际算力贡献比 95% 满载的 cpu0 还高。如果进程从 cpu0 迁到 cpu4,同样的计算只需占 cpu4 约 5% 的使用率。
性能系数是近似值,精确值需在目标设备上做 benchmark。第一版只做频率归一化(不乘性能系数)就够用,已经比 Raw CPU% 准确得多。
六、完整案例:CPU 42% 为什么 ANR
把上面的知识串起来,走一遍完整的排查流程。
Step 1:看整机数据,发现 Raw 和 Normalized 分叉
时间 Raw% 频率比 Norm%(=Raw×频率比) 判断
0min 32% 0.94 32%×0.94=30.1% 两线接近,正常
60min 42% 0.52 42%×0.52=21.8% 两线分叉,在降频
90min 40% 0.368 40%×0.368=14.7% 分叉加大,严重降频
Raw 稳定但 Normalized 一路掉 → 设备在降频。
Step 2:查各核 CPU%,发现负载不均衡
90min 各核 CPU%: [95%, 8%, 88%, 5%, 12%, 15%, 关, 关]
CV = 104%
CV >> 50% → 整机 42% 不可信。cpu0 = 95%,已满载。
Step 3:查进程所在核,发现被困在小核
cat /proc/12345/stat → processor = 0
cpu0 类型:小核 A55
cpu0 频率:408MHz(最高 1800MHz)
进程跑在 cpu0(小核),这个核还被降频到了最低档。
Step 4:算实际算力
进程所在核的实际算力 = 95% × (408/1800) × 1.0 = 21.5%
如果跑在 cpu4(大核满频):同任务只需约 5% 使用率
Step 5:汇总分析
┌─────────────────────────────────────────────────┐
│ ANR 时刻数据 │
│ │
│ 整机 Raw: 42% ← 看起来正常 │
│ 整机 Norm: 15.5% ← 42%×0.368 │
│ CV: 104% ← 整机值不可信 │
│ 进程所在核: cpu0 (小核, 95%, 408MHz) │
│ 大核状态: cpu4 12% @ 2400MHz (大量空闲) │
│ │
│ 瓶颈链: │
│ 进程困在小核 → 小核满载 → 温度升高 │
│ → 热降频到 408MHz → 算力暴跌 → ANR │
└─────────────────────────────────────────────────┘
七、根因在哪一层
单核瓶颈的根因分布在不同层级。看数据特征判断:
| 层级 | 数据特征 | 谁来修 |
|---|---|---|
| App 层 | 进程 CPU% 持续 > 85%;主线程有明显耗时操作 | App 开发(拆线程、减计算) |
| 调度层 | 进程 processor 持续指向小核;大核空闲但不迁移 | 系统团队(查 cpuset/affinity) |
| 热设计层 | 频率比持续下降;温度 > 70°C;time_in_state 集中在低档 | 硬件/散热(改散热方案) |
| 系统负载层 | 最高核不是你的进程,而是 SystemServer 等系统进程 | ROM 团队(优化后台) |
用上面的案例验证:
进程 CPU% = 89% → App 层有关(主线程计算量大)
processor = 0 且 cpu4 空闲 → 调度层有关(小核困住了)
cpu0 频率 408MHz,温度 78°C → 热设计层有关(降频严重)
cpu0 最高使用者就是目标进程 → 不是系统负载问题
结论:App 层 + 调度层 + 热设计层的叠加问题。 优先级:
- 查 cpuset 策略,为什么进程会从大核迁到小核
- 优化主线程计算量,减少对单核的依赖
- 长时间压测场景加散热或降低持续负载
系列指标层级递进
Layer 1 (#5 入门) Layer 2 (#6 避坑)
Raw CPU% 排除 iowait
= (Δtotal−Δidle)/Δtotal = (Δtotal−Δidle−Δiowait)/Δtotal
→ 知道"忙不忙" → 把 Raw 算对
│ │
▼ ▼
Layer 3 (#7 降频) Layer 4 (#8 单核 = 本篇)
+ 频率比 + 各核 CPU% → CV
Normalized = Raw × freq_ratio + processor → 进程在哪个核
→ 知道"实际干了多少活" + IPC 系数 → 单核算力占比
│ → 知道"瓶颈在哪个核"
▼ │
Layer 5 (#9 落地) ▼
完整输出 JSON + 采集代码 判断逻辑:
+ 平台展示 + 压测报告 CV > 50%? → 整机值不可信
→ 从公式到可交付方案 进程在小核? → 调度异常
频率比 < 0.4? → 严重降频
每一层指标都建立在前一层之上,只有全部叠加起来才能完整诊断 CPU 问题。
小结
| 指标 | 公式 | 数据来源 | 回答什么 |
|---|---|---|---|
| 各核 CPU% | 各核行 (Δtotal−Δidle−Δiowait)/Δtotal | /proc/stat 各核行 | 哪个核是瓶颈 |
| CV | 各核CPU%的标准差/均值 | 各核 CPU% 计算 | 整机均值是否可信 |
| processor | 直接读取 | /proc/{pid}/stat 第 39 字段 | 进程跑在哪个核 |
| 各核频率 | 直接读取 | scaling_cur_freq | 哪个核被降频 |
| 单核算力占比 | CPU% × (频率/最高频率) × IPC系数 | 上述数据组合 | 各核真实产出 |
什么时候必须看单核
| 信号 | 阈值 | 数据来源 | 说明 |
|---|---|---|---|
| CV 过高 | > 50% | 各核 CPU% 计算 | 负载不均衡,整机值不可信 |
| 频率比过低 | < 0.5 | scaling_cur_freq | 算力已腰斩,需看哪些核被压了 |
| 在线核不足 | < 总核的 75% | /proc/stat 行数 | 核被关了,均值被拉低 |
| 整机值"正常"但体感卡 | Raw < 50% 但有卡顿 | 用户反馈 + Raw CPU% | 最典型的均值陷阱 |
| 进程在小核 | processor 指向 LITTLE 核 | /proc/{pid}/stat | 大核空闲但进程被困小核 |
任何一条命中,整机 CPU% 就不能独立使用——必须下钻到单核。
下篇预告
CPU 采集落地:从公式到平台的完整方案
本篇讲清了"看什么数据、怎么判断"。下篇给出完整的采集代码、一轮采集的具体输出、2 小时压测的数据全貌、平台展示方案和最终的压测报告模板。 系列终篇,数据说话,拿走就能用。
系列目录
- 第 1 篇:内存泄漏自动检测(上)——采集层设计
- 第 2 篇:内存泄漏自动检测(中)——检测层设计
- 第 3 篇:内存泄漏自动检测(下)——响应层设计
- 第 4 篇:Android 内存采集避坑指南
- 第 5 篇:Android CPU 使用率采集入门
- 第 6 篇:CPU 采集的 8 个坑
- 第 7 篇:CPU 降频了,你的采集数据还准吗?
- 第 8 篇(本篇):CPU 单核分析:均值是最大的谎言
- 第 9 篇(下一篇):CPU 采集落地:从公式到平台的完整方案
我是测试工坊,专注 Android 系统级性能工程。 如果你也遇到过"整机 CPU 不高但就是卡"的场景,欢迎评论区交流 👇 关注我,后续更新不迷路。