Android应用功耗计算方法和埋点方案

627 阅读9分钟

Android应用功耗计算方法和埋点方案

功耗认知

电功率计算公式为W = U * I * t

U为电压值,I为电流值,t为运行时间

在移动设备上,U可认为是恒定不变的,因此可以忽略掉参数U,单独通过电流及时间即可表示电量

通过电流及时间即可估算出电量,以mAh为单位衡量:

模块电量 (mAh) = 模块电流 (mA) * 模块耗时 (h)

Android 是怎么通过计算App耗电的?

PowerProfile 和 power_profile

文件路径:

frameworks/base/core/res/res/xml/power_profile.xml

frameworks/base/core/java/com/android/internal/os/PowerProfile.java

文件功能:

● xml文件定义了不同模块的电流消耗值,不同厂商不同机型的文件配置都是不一样的,决定了耗电统计的准确性。(各项数据一般通过实验室测试得出)

● 电流的大小和模块的状态也有关系,例如屏幕在不同亮度和CPU在不同频率下的电流消耗都是不一样的。

● 配置参数包含待机、WiFi、蓝牙、CPU、GPU、闪光灯、数据网络、屏幕等资源使用时的电流值。

PowerProfile.java. 负责解析power_profile中的电流数据,然后提供给系统计算,非厂家应用只能通过反射获取,而且有的方法已经被隐藏,只能获取到部分数据

真正负责计算的是Android耗电服务

Android 耗电统计服务简介

● BatteryStats:

抽象类,定义耗电统计相关方法和内部类

● BatteryStatsImpl:

耗电统计服务的实现类

● BatteryStatsServices:

耗电服务,可通过ServiceManager获取

image.png

Android系统的耗电统计分为软件排行和硬件排行,分别统计App耗电和主要硬件耗电总量排行

● java用于获取配置电流数值

● 软件排行榜:processAppUsage()方法

● 硬件排行榜:processMiscUsage()方法

软件耗电排行的8个模块耗电计算类,都继承于PowerCalculator抽象类,processAppUsage()方法统一计算应用所有的软件耗电量

计算项Class文件
CPU功耗CpuPowerCalculator.java
Wakelock功耗WakelockPowerCalculator.java
数据网络功耗MobileRadioPowerCalculator.java
WIFI功耗WifiPowerCalculator.java
蓝牙功耗BluetoothPowerCalculator.java
Sensor功耗SensorPowerCalculator.java
相机功耗CameraPowerCalculator.java
闪光灯功耗FlashlightPowerCalculator.java
媒体使用功耗MediaPowerCalculator.java

image.png

耗电根据Uid进行统计,一般每个App都对应一个Uid,如果App签名和sharedUserId相同,则他们拥有相同Uid。对于系统应用uid为system的,则统一算入system应用的耗电量。

Uid_Power = process_1_Power + … + process_N_Power,其中所有进程都是属于同一个uid。 当同一uid下只有一个进程时,Uid_Power = process_Power;

process_Power = CPU功耗 + Wakelock功耗 + 数据网络功耗 + WIFI功耗 + 蓝牙功耗 + Sensor功耗 + 相机功耗 + 闪光灯功耗 + 媒体使用功耗

继续回顾我们的万能公式

模块电量 (mAh) = 模块电流 (mA) * 模块耗时 (h)

在Android原生代码中,设置——电池——耗电排行 中的数据就是这么来的,各厂商会根据实际需求进行差异化定制,比如vivo把 屏幕耗电(AOSP里属于硬件耗电)计算在了App耗电内,就会使得 前台使用应用的耗电在耗电排行内的占比非常大。

所以根据耗电排行来判断app是否耗电并不准确。(硬件电流是实验室测出,存在干扰,并且 厂商会进行差异化定制,把原本不属于app的耗电计算进来)

那么怎么做耗电埋点呢?

系统统计应用会统计9个软件耗电项,首先来分析App所使用的哪些资源(耗电项)。

● Cpu ——系统使用的CPU

● 网络 (mobile + wifi)——网络消耗

● Media (Audio + Media) ——除了CPU外的硬件消耗,DSP或者外放之类的,从测试数据来看,小米和vivo的这项电流都为0,意味着都没有单独统计进去

● Sensor ——传感器,统计传感器的耗电是反注册时长-注册时长,即使是非唤醒的低功耗传感器(比如计步器),如果长时间不反注册,在耗电排行上也会很耗电。

如果想要统计业务的耗电,那么只需要统计上述资源的使用时间,再乘对应的硬件工作电流,就能换算成对应的耗电量 mAH,需要确认的就是子项的 工作时间 和 电流。

App使用的总功耗 = CPU功耗 + 数据网络功耗 + WIFI功耗 + Sensor功耗 + 媒体使用功耗

先看怎么确定子项的电流?其中,CPU、网络、Media 的电流理论上都可以通过PowerProfile.java 反射获取。

这也是之前提出的一个解决方案。通过厂家提供的数据尽可能模拟真实数值。

在完成基础编码后测试的过程中,我们发现了如下的问题。

1、不同平台,CPU 簇数量、频率数量、对应的电流都不一样,当时参考的解决方案是取中间CPU簇,再取每个频率电流的取平均值。

耗电计算
            powerProfileClazz = Class.forName("com.android.internal.os.PowerProfile");
            Class[] argTypes = {Context.class};
            Constructor constructor = powerProfileClazz
                    .getDeclaredConstructor(argTypes);
            Object[] arguments = {this};
            Object powerProInstance = constructor.newInstance(arguments);

            Method getAveragePowerInt = powerProfileClazz.getMethod("getAveragePower", new Class[]{String.class, int.class});
            Method getNumSpeedStepsInCpuCluster = powerProfileClazz.getMethod("getNumSpeedStepsInCpuCluster", int.class);
            Method getNumCpuClusters = powerProfileClazz.getMethod("getNumCpuClusters", null);
            Method getAveragePower = powerProfileClazz.getMethod("getAveragePower", String.class);

            mNumCpuClusters = Integer.parseInt(getNumCpuClusters.invoke(powerProInstance, null).toString());//得到CPU簇数
            int tarClusters = (mNumCpuClusters - 1)/ 2;(对于簇数为2的平台取小核簇,簇数为3的平台取大核,避免大核和 超大核的干扰)
            mNumSpeedStepsInCpuCluster = Integer.parseInt(getNumSpeedStepsInCpuCluster.invoke(powerProInstance, tarClusters).toString());//得到目标CPU簇的频率数
            double tmp;
            for (int i = 0; i < mNumSpeedStepsInCpuCluster; i++) {
                tmp = Double.parseDouble(getAveragePowerInt.invoke(powerProInstance, CPU_CORE_POWER_PREFIX + tarClusters, i).toString());//反射获取每个频率对应的耗电
                mAveragePower += tmp;

            }
            mAveragePower /= mNumSpeedStepsInCpuCluster;//最终计算得到平均电流
            mCapacity = Float.parseFloat(getAveragePower.invoke(powerProInstance, POWER_BATTERY_CAPACITY).toString());

经过本地验证,发现了如下问题:

1、手机厂商只配置了CP U的电流数据,对于手机其他器件的电流很多都没配置 —— vivo 小米 就没有配置media, 小米Mix2S甚至没有配置 网络的电流

2、各个SOC平台、机型、器件差异很大,导致电流差异也很大,同样的场景,同样的使用时间,在不同的机型上的耗电数值差异可能会很大,将这个电流值统一上传到大盘会造成差异。—— 以CPU举例, 大核高频模式下CPU时间片消耗会显著低于 小核 低频率下的消耗

3、新机型或者新硬件的出现,可能会导致某个子项异常波动,从而影响大盘数据。(比如之前的5G通信,modem 在5G 和 4G 工作状态的下电流差异非常大,目前AOSP的耗电统计是没有区分的)

综上,根据机型真实数据模拟耗电排行的统计方法方案并不是上报统计的最佳方案,

功耗监控需要关心什么?

对于应用,我们关心的更应该是 业务场景 单位时间内资源的使用量,并不是反应在不同设备上不同的毫安值。

使用设备提供的电流计算毫安值反而不准确,可能因为新机型或者新硬件的出现,导致某个子项异常波动,波动影响到大盘数据。

所以建议客户端上报时只需要统计 资源使用量即可,如果需要统计具体耗电量,可以设定一个统一的阈值,通过乘这个阈值上报。这样就能减少机型差异的干扰。或者是将电流计算放在后台做,做成可调整参数的模式,方便灵活

那么如何统计资源使用量?

1、CPU,我们一般只需要统计CPU的使用时间片即可,

CPU时间片可以通过读取

/proc/pid/stat文件:

该文件包含了某一进程所有的活动的信息,该文件中的所有值都是从系统启动开始累计 到当前时刻。

Utime该任务在用户态运行的时间,单位为jiffies

Stime该任务在核心态运行的时间,单位为jiffies

Cutime所有已死线程在用户态运行的时间,单位为jiffies

Cstime所有已死在核心态运行的时间,单位为jiffies

在Linux的内核中,有一个全局变量:Jiffies。 Jiffies代表时间。它的单位随硬件平台的不同而不同。jiffies的单位就是 1/HZ。Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最小时间间隔了。每个CPU时间片,Jiffies都要加1。

2、Sensor使用的统计方法。

通过hook SensorManager的调用,添加监控逻辑即可。

3、Audio 和 Video的统计方法。

可以通过 监控使用语音 和 直播业务 的 播放状态来统计时间,这个受用户行为影响比较大

4、网络数据

网络比较特殊,原生的统计方法里分了分别 统计WIFI 和 MOBILE 两个子模块,然后通过计算硬件的工作时长来得到最终的电量。

代码开发过程中,我们会关注WIFI或者MOBILE场景,基于两者做差异化的逻辑。但是在数据监控中,我们无需关注这一点。

正常应该是通过监控硬件使用时间来计算网络。

但是有两个原因

● 网络统计本身就是个非常复杂的过程, 需要硬件的工作时长,比如modem一次拉起就会工作10s,厂商可能会把这段时间的电流全都计算到应用上,而且我们也获取不到硬件具体工作时长

● 业务在使用过程中,可以认为硬件是一直处于工作态。

● 某些厂商也是用数据量这个维度来计算网络消耗的电量权值,用以监控应用前台使用的耗电。

所以正常监控就可以通过 数据量 来定义网络耗电,那么上报监控的指标就变成了

● Cpu ——单位时间内的 时间片使用

● 网络 (mobile + wifi)—— 单位时间内的流量总量

● Media (Audio + Media) —— 单位时间内的使用时长

● Sensor —— 单位时间内的使用时长