苹果在iOS13的时候,在内核中加入了一个新的性能衡量指标wakeup
,同时由于这个指标而被系统杀死的应用数不胜数,其中也包括我们常用的微信淘宝等。而这个指标完全是由XNU内核统计的,所以我们很难通过日志等普通手段去准确的定位问题,所以这里通过另一种思路去解决这个问题。
为什么要统计 wakeup
要定位这个问题,首先我们需要知道这个指标的目的是什么。
XNU中,对性能的指标有CPU、内存、IO,而wakeup
属于CPU的性能指标,同时属于CPU指标的还有CPU使用率,下面是XNU中对其限制的定义。
1 | /* |
1 |
|
总结来说,当CPU使用率在3分钟内均值超过50%,就认为过度使用CPU,当wakeup
在300秒内均值超过150次,则认为唤起次数过多,同时在阈值的70%水位内核会开启监控。
CPU使用率我们很容易理解,使用率越高,电池寿命越低,而且并不是线性增加的。那么wakeup
又是如何影响电池寿命的呢?
首先我们需要看看ARM架构中对于CPU功耗问题的描述:
1 | Many ARM systems are mobile devices and powered by batteries. In such systems, optimization of power use, and total energy use, is a key design constraint. Programmers often spend significant amounts of time trying to save battery life in such systems. |
由于ARM被大量使用与低功耗设备,而这些设备往往会由电池来作为驱动,所以ARM在硬件层面就对功耗这个问题进行了优化设计。
1 | Energy use can be divided into two components: |
功耗可以分为2种类型,即静态功耗与动态功耗。
静态功耗指的是只要CPU通上电,由于芯片无法保证绝对绝缘,所以会存在“漏电”的情况,而且越大的芯片这种问题越严重,这也是芯片厂家为什么拼命的研究更小尺寸芯片的原因。这部分功耗由于是硬件本身决定的,所以我们无法去控制,而这种类型功耗占比不大。
动态功耗指的是CPU运行期间,接通时钟后,执行指令所带来的额外开销,而这个开销会和时钟周期频率相关,频率越高,耗电量越大。这也就说明了苹果为什么会控制CPU使用率,而相关研究(Facebook也做过)也表明,CPU在20以下和20以上的能耗几乎是成倍的增加。
CPU使用率已经能够从一定程度上限制电池损耗问题了,那么wakeup
又是什么指标呢?
wakeup 是什么
要了解wakeup
是什么,首先要知道ARM低功耗模式的2个重要指令WFI
和WFE
。
1 | ARM assembly language includes instructions that can be used to place the core in a low-power state. The architecture defines these instructions as hints, meaning that the core is not required to take any specific action when it executes them. In the Cortex-A processor family, however, these instructions are implemented in a way that shuts down the clock to almost all parts of the core. This means that the power consumption of the core is significantly reduced so that only static leakage currents are drawn, and there is no dynamic power consumption. |
通过这2个指令进入低功耗模式后,时钟将会被关闭,这个CPU将不会再执行任何指令,这样这个CPU的动态能耗就没有了。这个能力的实现是由和CPU核心强绑定的空转线程idle thread
实现的,有意思的是XNU中的实现较为复杂,而Zircon
中则非常直接暴力:
1 | __NO_RETURN int arch_idle_thread_routine(void*) { |
在XNU中,一个CPU核心的工作流程被概括为如下状态机:
1 | /* |
而wakeup
则表示的是,从低功耗模式唤起进入运行模式的次数。
wakeup 如何统计的
ARM异常系统
CPU时钟被关闭了,那么又要怎么唤起呢?这就涉及到CPU的异常系统。
在ARM中,异常和中断的概念比较模糊,他把所有会引起CPU执行状态变更的事件都称为异常,其中包括软中断,debug中断,硬件中断等。
从触发时机上可以区分为同步异常与异步异常。这里指的同步异步并不是应用程序的概念,这里同步指的是拥有明确的触发时机,比如系统调用,缺页中断等,都会发生在明确的时机,而异步中断,则完全无视指令的逻辑,会强行打断指令执行,比如FIQ和IRQ,这里比较典型的是定时器中断。
异常系统有很多能力,其中一个重要的能力就是内核态与用户态切换。ARM的执行权限分为4个等级,EL0,EL1,EL2,EL3。其中EL0代表用户态,而EL1代表内核态,当用户态想要切换至内核态的时候,必须通过异常系统进行切换,而且异常系统只能向同等或更高等级权限进行切换。
那么这么多类型的异常,又是如何响应的呢?这里就涉及到一个异常处理表(exception table),在系统启动的时候,需要首先就去注册这个表,在XNU中,这个表如下:
1 | .section __DATA_CONST,__const |
wakeup 计数
那么我们回过头来看看wakeup
计数的地方:
1 | /* |
而这里的aticontext
则是通过ml_at_interrupt_context
获取的,其含义则是是否处于中断上下文中。
1 | /* |
那么cpu_int_state
标记又是在什么时候设置上去的呢?只有在locore.S
中,才会更新该标记:1
str x0, [x23, CPU_INT_STATE] // Saved context in cpu_int_state
同时发现如下几个方法会配置这个标记:
1 | el1_sp0_irq_vector_long |
结合上述的异常处理表的注册位置,与ARM官方文档的位置进行对比,可以发现:
这几个中断类型均为FIQ或者IRQ,也就是硬中断。由此我们可以判断,wakeup
必然是由硬中断引起的,而像系统调用,线程切换,缺页中断这种并不会引起wakeup
。
进程统计
由上可以看出,wakeup
其实是对CPU核心唤起次数的统计,和应用层的线程与进程似乎毫不相干。但从程序执行的角度思考,如果一个程序一直在运行,就不会进入等待状态,而从等待状态唤醒,肯定是因为某些异常中断,比如网络,vsync等。
在CPU核心被唤醒后,在当前CPU核心执行的线程会进行wakeup++
,而系统统计维度是应用维度,也就是进程维度,所以会累计该进程下面的所有线程的wakeup
计数。
1 | queue_iterate(&task->threads, thread, thread_t, task_threads) { |
所以在我们代码中,如果在2个不同线程启用用同样的定时器,wakeup
是同一个线程起2个定时器的2倍(同样的定时器在底层其实是一颗树,注册同样的定时器实际只注册了一个)。
用户层获取该统计值则可以通过如下方式:
1 |
|
wakeup 治理
从以上分析来看,我们只需要排查各种硬件相关事件即可。
从实际排查结果来看,目前只有定时器或者拥有定时能力的类型是最普遍的场景。
比如NSTimer
,CADisplayLink
,dispatch_semaphore_wait
,pthread_cond_timedwait
等。
关于定时器,我们尽量复用其能力,避免在不同线程去创建同样的定时能力,同时在回到后台的时候,关闭不需要的定时器,因为大部分定时器都是UI相关的,关闭定时器也是一种标准的做法。
关于wait类型的能力,从方案选择上避免轮询的方案,或者增加轮询间隔时间,比如可以通过try_wait,runloop或者EventKit等能力进行优化。
监控与防劣化
一旦我们知道了问题原因,那么对问题的治理比较简单,而后续我们需要建立持续的管控等长效措施才可以。
在此我们可以简单的定义一些规则,并且嵌入线下监控能力中:
- 定时器时间周期小于1s的,在进入后台需要进行暂停
- wait类型延迟小于1s,并且持续使用10次以上的情况需要进行优化
总结
wakeup
由于是XNU内核统计数据,所以在问题定位排查方面特别困难,所以从另一个角度去解决这个问题反而是一种更好的方式。
同时从XNU中对CPU功耗的控制粒度可以看出,苹果在极致的优化方面做的很好,在自身的软件生态中要求也比价高。电量问题在短时间内应该不会有技术上的突破,所以我们自身也需要多思考如何减少电池损耗。