Mobile Development 16 min read

Understanding the XNU "wakeup" Metric and Its Impact on iOS Battery Consumption

Apple’s XNU kernel introduced the “wakeup” metric in iOS 13 to count CPU core activations from low‑power states, and because high wake‑up rates increase dynamic power consumption, developers must identify and consolidate timers, avoid frequent polling, and pause background tasks to lower the metric and extend battery life.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Understanding the XNU "wakeup" Metric and Its Impact on iOS Battery Consumption

Apple added a new performance metric called wakeup to the XNU kernel in iOS 13. The metric is used to count how many times a CPU core is woken up from a low‑power state. Because the statistic is collected inside the kernel, it is difficult to locate problematic wake‑ups using ordinary logs, which motivates a deeper technical analysis.

Why is wakeup measured?

In XNU, performance metrics are grouped into CPU, memory and I/O. wakeup belongs to the CPU category, alongside CPU usage percentage. The default thresholds are defined as follows:

/*
 * Default parameters for CPU usage monitor.
 *
 * Default setting is 50% over 3 minutes.
 */
#define          DEFAULT_CPUMON_PERCENTAGE 50
#define          DEFAULT_CPUMON_INTERVAL   (3 * 60)

#define TASK_WAKEUPS_MONITOR_DEFAULT_LIMIT               150 /* wakeups per second */
#define TASK_WAKEUPS_MONITOR_DEFAULT_INTERVAL   300 /* in seconds. */

/*
 * Level (in terms of percentage of the limit) at which the wakeups monitor triggers telemetry.
 *
 * (ie when the task's wakeups rate exceeds 70% of the limit, start taking user
 *  stacktraces, aka micro‑stackshots)
 */
#define TASK_WAKEUPS_MONITOR_DEFAULT_USTACKSHOTS_TRIGGER        70

When the average CPU usage exceeds 50 % over a three‑minute window, the system flags excessive CPU usage. Similarly, if the average wakeup count exceeds 150 per 300 seconds, the kernel treats the task as having too many wake‑ups and starts additional monitoring at the 70 % water‑mark.

How does wakeup affect battery life?

ARM‑based devices are predominantly battery‑powered, so power‑usage optimisation is a primary design constraint. Energy consumption can be split into two components:

Energy use can be divided into two components:

- Static
Static power consumption, also often called leakage, occurs whenever the core logic or RAM blocks have power applied to them. In general terms, the leakage currents are proportional to the total silicon area, meaning that the bigger the chip, the higher the leakage. The proportion of power consumption from leakage gets significantly higher as you move to smaller fabrication geometries.

- Dynamic
Dynamic power consumption occurs because of transistor switching and is a function of the core clock speed and the numbers of transistors that change state per cycle. Clearly, higher clock speeds and more complex cores consume more power.

Static power is hardware‑determined and cannot be reduced by software, while dynamic power grows with clock frequency. Therefore, limiting CPU usage directly reduces battery drain, and a high wakeup rate indicates frequent transitions from low‑power states to active execution, which also incurs dynamic power.

What is wakeup?

ARM provides two low‑power instructions, WFI (Wait‑For‑Interrupt) and WFE (Wait‑For‑Event). Executing either shuts down most of the core’s clock, leaving only static leakage. The core is later woken up by an interrupt, which increments the wakeup counter.

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.

In XNU, the idle thread repeatedly executes the wfi instruction:

__NO_RETURN int arch_idle_thread_routine(void*)
{
  for (;;) {
    __asm__ volatile("wfi");
  }
}

The kernel models a CPU core as a state machine (shutdown → offline → start → running → idle → dispatching). Each transition from idle to running increments the wakeup counter.

/*
 *           -------------------- SHUTDOWN
 *          /                     ^     ^
 *      _/                     |      \
 *  OFF_LINE --> START --> RUNNING --> IDLE --> DISPATCHING
 *          \_________________^   ^ ^______/           /
 *                                 \__________________/
 */

How is wakeup counted?

The kernel records wake‑ups in the function thread_unblock , which is called when a blocked thread becomes runnable. The code checks whether the current context is an interrupt (using ml_at_interrupt_context ) and, if so, increments the wake‑up counter.

boolean_t thread_unblock(thread_t thread, wait_result_t wresult)
{
  // ...
  boolean_t aticontext, pidle;
  ml_get_power_state(&aticontext, &pidle);
  /* Obtain power‑relevant interrupt and "platform‑idle exit" statistics.
   * We also account for "double hop" thread signaling via
   * the thread callout infrastructure.
   */
  if (__improbable(aticontext /* ... */)) {
    // wakeup ++
  }
  // ...
}

boolean_t ml_at_interrupt_context(void)
{
  /* Do not use a stack‑based check here, as the top‑level exception handler
   * is free to use some other stack besides the per‑CPU interrupt stack.
   * Interrupts should always be disabled if we’re at interrupt context.
   */
  return !ml_get_interrupts_enabled() && (getCpuDatap()->cpu_int_state != NULL);
}

The interrupt state flag cpu_int_state is set in locore.S when an exception vector is entered:

str        x0, [x23, CPU_INT_STATE]            // Saved context in cpu_int_state

Exception vectors such as el1_sp0_irq_vector_long , el0_irq_vector_64_long , etc., are registered in the XNU exception table during boot.

.section __DATA_CONST,__const
.align 3
.globl EXT(exc_vectors_table)
LEXT(exc_vectors_table)
    /* Table of exception handlers.
         * These handlers sometimes contain deadloops.
         * It's nice to have symbols for them when debugging. */
    .quad el1_sp0_synchronous_vector_long
    .quad el1_sp0_irq_vector_long
    .quad el1_sp0_fiq_vector_long
    .quad el1_sp0_serror_vector_long
    .quad el1_sp1_synchronous_vector_long
    .quad el1_sp1_irq_vector_long
    .quad el1_sp1_fiq_vector_long
    .quad el1_sp1_serror_vector_long
    .quad el0_synchronous_vector_64_long
    .quad el0_irq_vector_64_long
    .quad el0_fiq_vector_64_long
    .quad el0_serror_vector_64_long

Because the counted wake‑ups are associated with hardware interrupts (IRQ/FIQ), software events such as system calls or page faults do not increase the wakeup metric.

At the user‑level, the metric can be retrieved via the task_power_info API:

#include
#include
BOOL GetSystemWakeup(NSInteger *interrupt_wakeup, NSInteger *timer_wakeup)
{
    struct task_power_info info = {0};
    mach_msg_type_number_t count = TASK_POWER_INFO_COUNT;
    kern_return_t ret = task_info(current_task(), TASK_POWER_INFO, (task_info_t)&info, &count);
    if (ret == KERN_SUCCESS) {
        if (interrupt_wakeup) *interrupt_wakeup = info.task_interrupt_wakeups;
        if (timer_wakeup) *timer_wakeup = info.task_timer_wakeups_bin_1 + info.task_timer_wakeups_bin_2;
        return true;
    } else {
        if (interrupt_wakeup) *interrupt_wakeup = 0;
        if (timer_wakeup) *timer_wakeup = 0;
        return false;
    }
}

Governance and mitigation

In practice, most excessive wakeup counts stem from timers or other periodic wake‑up sources. Common culprits include NSTimer , CADisplayLink , dispatch_semaphore_wait , and pthread_cond_timedwait . Recommendations:

Avoid creating duplicate timers on different threads; reuse a single timer when possible.

Pause or invalidate timers with a period shorter than 1 s when the app moves to the background.

Prefer event‑driven APIs over polling; if waiting is necessary, increase the wait interval or use non‑blocking variants.

Long‑term monitoring can be implemented by defining rules such as:

Timers with intervals < 1 s must be paused when the app is backgrounded.

Wait operations that fire more than ten times with a latency < 1 s should be reviewed for optimisation.

Conclusion

The wakeup metric is collected deep inside the XNU kernel, making direct debugging difficult. By understanding the low‑level mechanisms—how the CPU enters low‑power states, how hardware interrupts trigger wake‑ups, and how the kernel aggregates the counts—developers can adopt higher‑level strategies (timer consolidation, background‑task management, avoiding unnecessary wake‑ups) to reduce the metric and consequently improve battery life.

iOSCPU Power ManagementBattery Optimizationlow‑power stateswakeup metricXNU
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.