Mobile Development 26 min read

How to Pinpoint Android App Power Drain with Custom ASM Instrumentation

This article explains a comprehensive approach to detecting and analyzing Android app power consumption by combining official power profiling, custom metrics, and bytecode instrumentation to capture API usage, caller information, and timing for modules such as Bluetooth, CPU, location, sensors, wakelocks, and alarms.

Huolala Tech
Huolala Tech
Huolala Tech
How to Pinpoint Android App Power Drain with Custom ASM Instrumentation

Background

Usually app power consumption receives less attention than crashes or ANRs. Power consumption is a hidden performance issue that varies with device hardware, usage duration, and user habits, making it difficult to standardize. Google provides the Battery Historian tool for objective measurement, while manufacturers use custom metrics. This article proposes a solution that combines official power calculation with custom detection.

Power Consumption Calculation

Android requires device manufacturers to include a component power profile file at /frameworks/base/core/res/res/power_profile.xml, which declares the power usage of each component.

Obtaining the Power Profile File

The power_profile.xml file is typically packaged inside /system/framework/framework-res.apk. Use adb pull /system/framework/framework-res.apk ./ to retrieve the APK, then decompile it with tools like apktool or jadx to locate /res/xml/power_profile.xml.

System Power Consumption

After obtaining the power profile, Android calculates consumption in BatteryStatsHelper. Most modules inherit from PowerCalculator and implement calculatorApp to compute duration‑based power usage. The refreshStats method shows the list of active calculators, such as CPU, Wi‑Fi, GPS, etc.

mPowerCalculators = new ArrayList<>();
// Add calculators in order
mPowerCalculators.add(new CpuPowerCalculator(mPowerProfile));
mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));
// ... other calculators ...

Power Consumption Detection

Google’s Battery Historian provides module‑level data, but custom detection is often needed. Starting with Android P, the Android Vitals project adds backend power monitoring.

Solution 1: Current Meter Test

Use an external current meter to measure the app’s power draw. By reading power_profile.xml, you can map current to individual modules. This method is accurate but requires expensive hardware and cannot pinpoint the exact code responsible for the consumption.

Solution 2: Battery Historian

Battery Historian is an official Google tool that, after simple configuration, reports per‑module power usage. It is easy to set up and provides fairly precise data, but it cannot identify which part of the code caused the consumption.

Solution 3: Instrumentation

By inserting bytecode instrumentation into power‑related APIs, we can collect detailed usage data and the caller class, enabling precise code‑level diagnosis. Although the quantification is less exact than a current meter, it allows us to locate problematic calls.

Choosing a Solution

Based on research, we selected Solution 3 (instrumentation). For example, in a freight‑driver app, location fetching dominates power usage; instrumentation helps identify unreasonable location calls without affecting business logic.

We reference Battery‑metrics for custom detection and Android Vitals for rule definitions.

Power API Selection

Bluetooth

Scanning is the main power consumer. Use

BluetoothManager.getAdapter().bluetoothLeScanner.startScan(...)

and stopScan. Record the scan mode (low power, balanced, low latency, opportunistic) to calculate power gradients.

val bluetooth = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val scanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // continuous scan
    .build()
val scanner = bluetooth.adapter.bluetoothLeScanner
scanner.startScan(null, scanSettings, callback)

SCAN_MODE_LOW_POWER – low power, default when app is background.

SCAN_MODE_BALANCED – balanced frequency.

SCAN_MODE_LOW_LATENCY – high power, recommended for foreground.

SCAN_MODE_OPPORTUNISTIC – listens to other apps' scans.

CPU

Read /proc/self/stat to obtain user and kernel time (fields 14‑17). Convert ticks to seconds using Os.sysconf(OsConstants._SC_CLK_TCK).

24010 R 24007 24010 24007 34817 24010 4210688 493 0 0 0 1 0 0 0 20 0 1 0 42056617 ...

Fields 14‑17 correspond to user time, kernel time, user‑wait time, kernel‑wait time.

Location

Use requestLocationUpdates for continuous tracking and requestSingleUpdate for one‑time fixes. Continuous tracking consumes more power, especially if minTime or minDistance are too small.

fun requestLocationUpdates(provider: String, minTime: Long, minDistance: Float, listener: LocationListener) { ... }

Cancel with removeUpdates. Third‑party SDKs often wrap these calls.

Sensor

Register listeners with

registerListener(listener, sensor, samplingPeriodUs, maxReportLatencyUs)

. Larger samplingPeriodUs reduces power; maxReportLatencyUs allows batching.

public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs, int maxReportLatencyUs) { ... }

Check if a sensor is wake‑up with isWakeUpSensor(). Use unregisterListener to stop.

Wakelock

Both PowerManager.WakeLock and WifiManager.WifiLock keep the CPU awake. Use acquire(), optionally with a timeout, and release(). Reference counting can be disabled with setReferenceCounted(false).

val mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.canonicalName)
mWakeLock.setReferenceCounted(false)
mWakeLock.acquire()

Timeout version:

public void acquire(long timeout) { synchronized (mToken) { acquireLocked(); mHandler.postDelayed(mReleaser, timeout); } }

Alarm

Alarms can be set with setAlarmClock, setExactAndAllowWhileIdle, or setExact. Precise alarms (e.g., setExact) break low‑power idle scheduling and increase consumption.

public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) { setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation, null, null, null); }
public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis, PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation, null, null, null); }

Power Consumption Statistics

Obtain the current battery level via the sticky broadcast ACTION_BATTERY_CHANGED. Record the level and scale, then compute the delta over a time interval.

val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1

Listen for charging state changes with ACTION_POWER_CONNECTED and ACTION_POWER_DISCONNECTED.

override fun onReceive(context: Context?, intent: Intent?) {
    when (intent?.action) {
        Intent.ACTION_POWER_CONNECTED -> receive.invoke(SystemClock.elapsedRealtime(), Intent.ACTION_POWER_CONNECTED)
        Intent.ACTION_POWER_DISCONNECTED -> receive.invoke(SystemClock.elapsedRealtime(), Intent.ACTION_POWER_DISCONNECTED)
    }
}

ASM Instrumentation Implementation

To replace calls like wifiLock.acquire() with a custom hook, we keep the original object on the stack, push the caller class name using LDC, and invoke a static method in a hook class. The original INVOKEVIRTUAL is changed to INVOKESTATIC while preserving the stack.

Custom Hook Class (WifiWakeLock)

@Keep
object WifiWakeLockHook {
    @Synchronized @JvmStatic
    fun setReferenceCounted(wakeLock: WifiLock, value: Boolean) { /* record */ }

    @JvmStatic
    fun acquire(wifiLock: WifiLock, acquireClass: String) { /* record start time, caller */ wifiLock.acquire() }

    @JvmStatic
    fun release(wifiLock: WifiLock, releaseClass: String) { /* record end time, caller */ wifiLock.release() }
}

ASM Hook Helper

public class HookHelper {
    static public void replaceNode(MethodInsnNode node, ClassNode klass, MethodNode method, String owner) {
        // Insert LDC with caller class name
        LdcInsnNode ldc = new LdcInsnNode(klass.name);
        method.instructions.insertBefore(node, ldc);
        // Change to static call
        node.setOpcode(Opcodes.INVOKESTATIC);
        // Adjust descriptor to add a String parameter
        int anchor = node.desc.indexOf(')');
        String subDesc = node.desc.substring(anchor);
        String origin = node.desc.substring(1, anchor);
        node.desc = "(L" + node.owner + ";" + origin + "Ljava/lang/String;" + subDesc;
        node.owner = owner;
    }
}

Data Layer

Collected data is stored in a map keyed by the object's hashcode to avoid memory leaks. Each entry records acquire/release counts, timestamps, reference‑counting flag, and caller class names.

class WakeLockData {
    var acquireTime: Int = 0
    var releaseTime: Int = 0
    var heldTime: Long = 0L
    var startHoldTime: Long = 0L
    var isRefCounted = true
    var autoReleaseByTimeOver: Long = 0L
    var autoReleaseTime: Int = 0
    var holdClassName: String = ""
    var releaseClassName: String = ""
    fun isRelease(): Boolean { /* logic */ }
    override fun toString() = "WakeLockData(acquireTime=$acquireTime, releaseTime=$releaseTime, heldTime=$heldTime, startHoldTime=$startHoldTime, isRefCounted=$isRefCounted, autoReleaseByTimeOver=$autoReleaseByTimeOver, autoReleaseTime=$autoReleaseTime, holdClassName='$holdClassName', releaseClassName='$releaseClassName')"
}

The data layer exposes interfaces for other components to query specific metrics, and a debug UI can display the collected information in real time.

Future Enhancements

By parsing power_profile.xml for device‑specific module power values, we can compute actual energy consumption as energy = Σ (moduleTime × modulePower), providing a low‑cost quantitative metric for business use.

Conclusion

Custom ASM instrumentation offers a non‑intrusive, flexible way to monitor Android app power usage at the API level, allowing precise identification of inefficient code while keeping overhead low.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

InstrumentationlocationWakeLockSensorpower consumptionbattery
Huolala Tech
Written by

Huolala Tech

Technology reshapes logistics

0 followers
Reader feedback

How this landed with the community

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.