压测跑了 2 小时,CPU 一直稳定在 45%,看起来很健康。 但用户反馈"越用越卡"。查了才发现:设备早就热降频到 408MHz 了,45% 的算力只有正常模式的四分之一。 CPU% 没变,性能已经崩了。
一个真实场景
做长时间稳定性压测时,我们经常看到这样的 CPU 曲线:
时间 CPU% 体感
0min 40% 流畅
30min 42% 流畅
60min 45% 开始卡顿
90min 44% 明显卡顿
120min 43% 严重卡顿
CPU% 几乎没变,但卡顿越来越严重。如果只看 CPU 使用率,根本发现不了问题。
查 CPU 频率才发现真相:
时间 CPU% 频率 等效算力
0min 40% 1800MHz 40.0%
30min 42% 1400MHz 32.7%
60min 45% 800MHz 20.0%
90min 44% 408MHz 10.0%
120min 43% 408MHz 9.7%
60 分钟后设备因为发热触发了热降频,CPU 频率从 1800MHz 一路降到 408MHz。虽然 CPU% 还是 45%(占了 45% 的时间片),但每个时间片能做的运算量只有原来的四分之一。
这就是 CPU 采集中最容易误导人的坑:CPU% 只衡量"忙不忙",不衡量"干了多少活"。
为什么会降频:DVFS 机制
Android 设备通过 DVFS(Dynamic Voltage and Frequency Scaling,动态电压频率调节) 来平衡性能和功耗。内核中的 CPU 调频器(governor)根据负载动态调整频率。
常见的调频器:
| 调频器 | 策略 | 典型场景 |
|---|---|---|
| performance | 锁定最高频率 | 跑分/性能测试 |
| powersave | 锁定最低频率 | 息屏待机 |
| schedutil | 根据调度器负载动态调 | Android 默认(8.0+) |
| interactive | 根据 CPU 忙碌度快速升频 | 旧版 Android 默认 |
除了 governor 调频,还有热降频(thermal throttling)——设备温度超过阈值后,内核强制把频率压低。这是长时间压测中最常见的降频原因。
查看当前频率:
# 各核当前频率(单位 KHz)
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
# → 1800000(1.8GHz)或 408000(408MHz)
# 各核最高频率
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
# 各核最低频率
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
# 可用频率档位
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
# → 408000 600000 816000 1008000 1200000 1416000 1608000 1800000
查看热降频状态:
# 当前温度(不同设备路径不同)
adb shell cat /sys/class/thermal/thermal_zone0/temp
# → 65000(即 65°C)
# 当前热限制策略
adb shell cat /sys/class/thermal/thermal_zone0/policy
CPU% 的本质:时间片占比,不是算力占比
理解降频问题的关键,是搞清楚 CPU% 到底在度量什么。
/proc/stat 记录的是**时间片(tick)**的分配。CONFIG_HZ=100 时,每秒产生 100 个 tick,每个 tick 10ms。CPU% = 45% 意味着"在这些 tick 中,有 45% 的时间 CPU 在执行任务"。
但这些 tick 不关心频率。无论 CPU 跑在 408MHz 还是 1800MHz,1 个 tick 就是 1 个 tick。区别在于:
- 1800MHz 时,1 个 tick(10ms)能执行约 1800 万条指令
- 408MHz 时,1 个 tick(10ms)只能执行约 408 万条指令
同样是"用了 45 个 tick",前者完成的计算量是后者的 4.4 倍。
用一个类比:CPU% 就像"一天中有多少小时在工作",频率就像"每小时的工作效率"。一个人每天工作 8 小时(CPU 占用率不变),但如果他从精力充沛(高频)变成了困倦迷糊(低频),实际产出可能差好几倍。
Normalized CPU%:让使用率反映真实算力
解决思路很直接——把频率因素加进去。行业里这个指标叫 Normalized CPU%(归一化 CPU 使用率),PerfDog 等工具也是这么做的。
单核场景的公式:
Normalized CPU% = Raw CPU% × (当前频率 / 最高频率)
场景 A:Raw CPU 50% @ 1800MHz
Normalized = 50% × (1800/1800) = 50.0%
场景 B:Raw CPU 50% @ 408MHz
Normalized = 50% × (408/1800) = 11.3%
场景 B 虽然占了 50% 的时间片,但实际算力只有满血状态的 11.3%。
多核场景:实际设备中各核频率经常不同,需要用"频率比"来统一衡量:
频率比 = Σ(各在线核当前频率) / Σ(各核最高频率)
Normalized CPU% = Raw CPU% × 频率比
比如 4 核设备,cpu0~cpu3 最高均为 1800MHz,当前分别跑在 1200/408/1800/1800MHz:
频率比 = (1200 + 408 + 1800 + 1800) / (1800 × 4) = 0.723
Raw CPU 60% → Normalized = 60% × 0.723 = 43.4%
虽然 CPU 忙碌度有 60%,但因为部分核心降频了,实际算力打了七折。
关于分母"各核最高频率"——和第 6 篇不矛盾
第 6 篇说 Raw CPU% "无需知道核数"——因为
/proc/stat汇总行只含在线核 tick,Δuse/Δtotal 在时间维度上天然正确。这里 Normalized CPU% 的分母用了全部核心的最高频率(含关掉的核),是故意的——它度量的是"设备满血状态下,你用了多少算力"。如果 4 核关了 2 核,Raw CPU% = 100%(可用资源耗尽),而 Normalized CPU% = 50%(设备只发挥了一半潜力)。两个指标度量不同维度,一个看"忙不忙",一个看"产出了多少"。
频率比的完整数学推导(包含关核场景),见下篇第 8 篇:CPU 单核分析。与 PerfDog / Linux PELT 的公式对齐,见第 9 篇:CPU 采集落地方案。
大小核的隐患:频率相同,算力不同
上面的 Normalized CPU% 只考虑了频率维度。但现代 Android 几乎都是 big.LITTLE 架构:
小核 (LITTLE): cpu0~cpu3, 最高 1800MHz, Cortex-A55
大核 (big): cpu4~cpu5, 最高 2400MHz, Cortex-A76
超大核: cpu6~cpu7, 最高 3000MHz, Cortex-X2
关键问题:同样跑在最高频率,大核的 IPC(每时钟周期指令数)是小核的 2~3 倍。
也就是说:
- 进程跑在小核 cpu0 上,CPU% = 80%,实际性能可能只相当于大核 cpu6 的 20~25%
- 同一个解码任务,调度到小核可能卡顿,调度到大核完全流畅
纯频率加权解决了"降频导致数据失真"的问题,但无法区分大小核的微架构差异。要精确衡量,需要引入核心性能系数(IPC 系数)——这部分涉及 per-cpu 时间分摊、性能系数标定、CV 负载均衡度等,在下篇 第 8 篇:CPU 单核分析 中详细展开。
对于大部分测试场景,频率加权已经覆盖了最主要的误差来源(降频导致的数据失真)。大小核 IPC 差异属于进一步精细化的范畴。
频率驻留时间:time_in_state
除了实时频率,还有一个重要数据源——频率驻留时间统计。
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state
# 频率(KHz) 驻留时间(10ms为单位)
# 408000 52341
# 600000 1203
# 816000 892
# 1008000 2451
# 1200000 5623
# 1416000 3892
# 1608000 2341
# 1800000 8923
这告诉你从开机到现在,cpu0 在每个频率档位分别待了多长时间。做两次差值就能得到"这段测试时间内,CPU 在各频率档位的分布"。
典型应用场景:
场景 1:热降频严重程度量化
正常运行(前 30 分钟):
1800MHz: 70% 1200MHz: 20% 408MHz: 10%
热降频后(60~90 分钟):
1800MHz: 5% 1200MHz: 15% 408MHz: 80%
一目了然:80% 的时间被压在最低频率,性能衰减非常严重。
场景 2:不同版本/配置的功耗对比
功耗大致正比于 频率² × 电压(DVFS 调频的同时也在调电压)。频率差 4 倍时,功耗差可达 10~16 倍。
通过 time_in_state 可以算出"加权平均频率",比直接看瞬时频率更能反映整体能效:
def weighted_avg_freq(time_in_state_delta):
"""计算加权平均频率"""
total_time = sum(time_in_state_delta.values())
if total_time == 0:
return 0
weighted = sum(freq * duration for freq, duration in time_in_state_delta.items())
return weighted / total_time
# 结果:
# 正常运行:avg_freq ≈ 1500MHz
# 热降频后:avg_freq ≈ 550MHz
真实案例:CPU 45% 却严重卡顿的排查
回到开头的场景,完整的排查思路:
第一步:发现问题
压测 2 小时后用户反馈"越来越卡",但 CPU 监控曲线一直平稳在 45% 左右。
第二步:叠加频率数据
同时采集 CPU% 和各核频率,发现:
0~30min: Raw 40% × freq_ratio 0.95 = Normalized 38% → 正常
30~60min: Raw 42% × freq_ratio 0.72 = Normalized 30% → 开始衰减
60~90min: Raw 45% × freq_ratio 0.35 = Normalized 16% → 严重衰减
90~120min: Raw 44% × freq_ratio 0.23 = Normalized 10% → 几乎不可用
Normalized CPU 从 38% 一路降到 10%——算力跌了 74%。
第三步:确认降频原因
adb shell cat /sys/class/thermal/thermal_zone0/temp
# → 78000(78°C,超过降频阈值 70°C)
设备在高负载下持续发热,温度超过 70°C 后触发热降频,CPU 频率被强制压到最低档。
第四步:定位根因
- 不是 CPU 算法问题,是散热/热设计问题
- 解决方案:优化散热(加散热片/风扇)、降低持续负载、或在热降频前主动降低画质/帧率
如果只看 Raw CPU%,这个问题永远发现不了。 CPU% 会告诉你"一切正常",但用户已经卡到无法使用。
落地建议:三条线同时看
把 CPU 使用率和频率放在一起采集,每轮同时输出三个指标:
Raw CPU% → 衡量"CPU 有多忙"(时间片维度)
频率比 → 衡量"CPU 跑得多快"(频率维度)
Normalized CPU% → 衡量"CPU 做了多少活"(算力维度)
三个指标同时上报到图表,叠加展示:
- Raw CPU% 曲线发现"是否忙"
- 频率比曲线发现"是否降频"
- Normalized CPU% 曲线发现"实际算力变化"
三条线同时看,任何异常都藏不住。
单核粒度的归一化方案见第 8 篇,完整的采集代码和平台落地方案见第 9 篇。
小结
| 维度 | Raw CPU% | Normalized CPU% |
|---|---|---|
| 度量含义 | 时间片占比 | 等效算力占比 |
| 降频场景 | 看不出问题 | 直接反映算力衰减 |
| 大小核 IPC 差异 | 不区分 | 本篇不处理(见第 8、9 篇) |
| 功耗关联 | 弱 | 强(功耗 ∝ f² × V) |
| 实现成本 | 零 | 每轮多读几个 sysfs 文件 |
一句话总结:Raw CPU% 只告诉你"忙不忙",加上频率做 Normalized 才能告诉你"干了多少活"。
长时间压测、功耗分析、热性能评估——只要涉及频率变化的场景,Normalized CPU% 都比 Raw 值有意义得多。
下篇预告
CPU 单核分析:均值是最大的谎言
频率加权解决了降频失真,但整机均值掩盖了单核瓶颈——8 核平均 45%,其中一个小核已经 95% 满载。 下篇从数学推导出发,搞清楚整机 CPU% 和各核的真实关系,引入 CV 负载均衡度和 IPC 性能系数,补上空间维度的分析。
系列目录
- 第 1 篇:内存泄漏自动检测(上)——采集层设计
- 第 2 篇:内存泄漏自动检测(中)——检测层设计
- 第 3 篇:内存泄漏自动检测(下)——响应层设计
- 第 4 篇:Android 内存采集避坑指南
- 第 5 篇:Android CPU 使用率采集入门
- 第 6 篇:CPU 采集的 8 个坑
- 第 7 篇(本篇):CPU 降频了,你的采集数据还准吗?
- 第 8 篇(下一篇):CPU 单核分析:均值是最大的谎言
- 第 9 篇:CPU 采集落地:从公式到平台的完整方案
我是测试工坊,专注 Android 系统级性能工程。 如果你也遇到过"CPU 不高但就是卡"的场景,欢迎评论区交流 👇 关注我,后续更新不迷路。