Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4e17d257 authored by Dmitri Plotnikov's avatar Dmitri Plotnikov
Browse files

Limit the number of UidBatteryConsumers included in the BatteryUsageStats atom

Bug: 189225426
Test: atest BatteryUsageStatsProtoTests:BatteryUsageStatsPulledTest
Change-Id: Ic9866962330f0b51727e5202560d97ffac0b8980
parent c00c253b
Loading
Loading
Loading
Loading
+66 −8
Original line number Diff line number Diff line
@@ -110,6 +110,8 @@ public final class BatteryUsageStats implements Parcelable {
    static final String XML_ATTR_TIME_IN_FOREGROUND = "time_in_foreground";
    static final String XML_ATTR_TIME_IN_BACKGROUND = "time_in_background";

    private static final int STATSD_PULL_ATOM_MAX_BYTES = 45000;

    private final int mDischargePercentage;
    private final double mBatteryCapacityMah;
    private final long mStatsStartTimestampMs;
@@ -400,30 +402,60 @@ public final class BatteryUsageStats implements Parcelable {

    /** Returns a proto (as used for atoms.proto) corresponding to this BatteryUsageStats. */
    public byte[] getStatsProto() {
        // ProtoOutputStream.getRawSize() returns the buffer size before compaction.
        // BatteryUsageStats contains a lot of integers, so compaction of integers to
        // varint reduces the size of the proto buffer by as much as 50%.
        int maxRawSize = (int) (STATSD_PULL_ATOM_MAX_BYTES * 1.75);
        // Limit the number of attempts in order to prevent an infinite loop
        for (int i = 0; i < 3; i++) {
            final ProtoOutputStream proto = new ProtoOutputStream();
            writeStatsProto(proto, maxRawSize);

            final int rawSize = proto.getRawSize();
            final byte[] protoOutput = proto.getBytes();

            if (protoOutput.length <= STATSD_PULL_ATOM_MAX_BYTES) {
                return protoOutput;
            }

            // Adjust maxRawSize proportionately and try again.
            maxRawSize =
                    (int) ((long) STATSD_PULL_ATOM_MAX_BYTES * rawSize / protoOutput.length - 1024);
        }

        // Fallback: if we have failed to generate a proto smaller than STATSD_PULL_ATOM_MAX_BYTES,
        // just generate a proto with the _rawSize_ of STATSD_PULL_ATOM_MAX_BYTES, which is
        // guaranteed to produce a compacted proto (significantly) smaller than
        // STATSD_PULL_ATOM_MAX_BYTES.
        final ProtoOutputStream proto = new ProtoOutputStream();
        writeStatsProto(proto, STATSD_PULL_ATOM_MAX_BYTES);
        return proto.getBytes();
    }

    @NonNull
    private void writeStatsProto(ProtoOutputStream proto, int maxRawSize) {
        final BatteryConsumer deviceBatteryConsumer = getAggregateBatteryConsumer(
                AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);

        final ProtoOutputStream proto = new ProtoOutputStream();
        proto.write(BatteryUsageStatsAtomsProto.SESSION_START_MILLIS, getStatsStartTimestamp());
        proto.write(BatteryUsageStatsAtomsProto.SESSION_END_MILLIS, getStatsEndTimestamp());
        proto.write(BatteryUsageStatsAtomsProto.SESSION_DURATION_MILLIS, getStatsDuration());
        deviceBatteryConsumer.writeStatsProto(proto,
                BatteryUsageStatsAtomsProto.DEVICE_BATTERY_CONSUMER);
        writeUidBatteryConsumersProto(proto);
        proto.write(BatteryUsageStatsAtomsProto.SESSION_DISCHARGE_PERCENTAGE,
                getDischargePercentage());
        return proto.getBytes();
        deviceBatteryConsumer.writeStatsProto(proto,
                BatteryUsageStatsAtomsProto.DEVICE_BATTERY_CONSUMER);
        writeUidBatteryConsumersProto(proto, maxRawSize);
    }

    /**
     * Writes the UidBatteryConsumers data, held by this BatteryUsageStats, to the proto (as used
     * for atoms.proto).
     */
    private void writeUidBatteryConsumersProto(ProtoOutputStream proto) {
    private void writeUidBatteryConsumersProto(ProtoOutputStream proto, int maxRawSize) {
        final List<UidBatteryConsumer> consumers = getUidBatteryConsumers();
        // Order consumers by descending weight (a combination of consumed power and usage time)
        consumers.sort(Comparator.comparingDouble(this::getUidBatteryConsumerWeight).reversed());

        // TODO(b/189225426): Sort the list by power consumption. If during the for,
        //                    proto.getRawSize() > 45kb, truncate the remainder of the list.
        final int size = consumers.size();
        for (int i = 0; i < size; i++) {
            final UidBatteryConsumer consumer = consumers.get(i);
@@ -451,8 +483,34 @@ public final class BatteryUsageStats implements Parcelable {
                    BatteryUsageStatsAtomsProto.UidBatteryConsumer.TIME_IN_BACKGROUND_MILLIS,
                    bgMs);
            proto.end(token);

            if (proto.getRawSize() >= maxRawSize) {
                break;
            }
        }
    }

    private static final double WEIGHT_CONSUMED_POWER = 1;
    // Weight one hour in foreground the same as 100 mAh of power drain
    private static final double WEIGHT_FOREGROUND_STATE = 100.0 / (1 * 60 * 60 * 1000);
    // Weight one hour in background the same as 300 mAh of power drain
    private static final double WEIGHT_BACKGROUND_STATE = 300.0 / (1 * 60 * 60 * 1000);

    /**
     * Computes the weight associated with a UidBatteryConsumer, which is used for sorting.
     * We want applications with the largest consumed power as well as applications
     * with the highest usage time to be included in the statsd atom.
     */
    private double getUidBatteryConsumerWeight(UidBatteryConsumer uidBatteryConsumer) {
        final double consumedPower = uidBatteryConsumer.getConsumedPower();
        final long timeInForeground =
                uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND);
        final long timeInBackground =
                uidBatteryConsumer.getTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND);
        return consumedPower * WEIGHT_CONSUMED_POWER
                + timeInForeground * WEIGHT_FOREGROUND_STATE
                + timeInBackground * WEIGHT_BACKGROUND_STATE;
    }

    /**
     * Prints the stats in a human-readable format.
+2 −0
Original line number Diff line number Diff line
@@ -14,9 +14,11 @@ android_test {
    static_libs: [
        "androidx.test.rules",
        "junit",
        "mockito-target-minus-junit4",
        "platform-test-annotations",
        "platformprotosnano",
        "statsdprotolite",
        "truth-prebuilt",
    ],

    libs: ["android.test.runner"],
+75 −1
Original line number Diff line number Diff line
@@ -17,10 +17,14 @@ package com.android.internal.os;

import static android.os.BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.os.BatteryConsumer;
import android.os.BatteryUsageStats;
@@ -37,7 +41,6 @@ import java.util.Arrays;
import java.util.List;



@SmallTest
public class BatteryUsageStatsPulledTest {

@@ -225,4 +228,75 @@ public class BatteryUsageStatsPulledTest {

        return builder.build();
    }

    @Test
    public void testLargeAtomTruncated() {
        final BatteryUsageStats.Builder builder =
                new BatteryUsageStats.Builder(new String[0]);
        // If not truncated, this BatteryUsageStats object would generate a proto buffer
        // larger than 70 Kb
        for (int i = 0; i < 20000; i++) {
            BatteryStatsImpl.Uid mockUid = mock(BatteryStatsImpl.Uid.class);
            when(mockUid.getUid()).thenReturn(i);
            builder.getOrCreateUidBatteryConsumerBuilder(mockUid)
                    .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND, 1 * 60 * 1000)
                    .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND, 2 * 60 * 1000)
                    .setConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN, 30)
                    .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 40);
        }

        // Add a UID with much larger battery footprint
        final int largeConsumerUid = 20001;
        BatteryStatsImpl.Uid largeConsumerMockUid = mock(BatteryStatsImpl.Uid.class);
        when(largeConsumerMockUid.getUid()).thenReturn(largeConsumerUid);
        builder.getOrCreateUidBatteryConsumerBuilder(largeConsumerMockUid)
                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND, 10 * 60 * 1000)
                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND, 20 * 60 * 1000)
                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN, 300)
                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 400);

        // Add a UID with much larger usage duration
        final int highUsageUid = 20002;
        BatteryStatsImpl.Uid highUsageMockUid = mock(BatteryStatsImpl.Uid.class);
        when(highUsageMockUid.getUid()).thenReturn(highUsageUid);
        builder.getOrCreateUidBatteryConsumerBuilder(highUsageMockUid)
                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND, 60 * 60 * 1000)
                .setTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND, 120 * 60 * 1000)
                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_SCREEN, 3)
                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 4);

        BatteryUsageStats batteryUsageStats = builder.build();
        final byte[] bytes = batteryUsageStats.getStatsProto();
        assertThat(bytes.length).isGreaterThan(40000);
        assertThat(bytes.length).isLessThan(50000);

        BatteryUsageStatsAtomsProto proto;
        try {
            proto = BatteryUsageStatsAtomsProto.parseFrom(bytes);
        } catch (InvalidProtocolBufferNanoException e) {
            fail("Invalid proto: " + e);
            return;
        }

        boolean largeConsumerIncluded = false;
        boolean highUsageAppIncluded = false;
        for (int i = 0; i < proto.uidBatteryConsumers.length; i++) {
            if (proto.uidBatteryConsumers[i].uid == largeConsumerUid) {
                largeConsumerIncluded = true;
                BatteryUsageStatsAtomsProto.BatteryConsumerData consumerData =
                        proto.uidBatteryConsumers[i].batteryConsumerData;
                assertThat(consumerData.totalConsumedPowerDeciCoulombs / 36)
                        .isEqualTo(300 + 400);
            } else if (proto.uidBatteryConsumers[i].uid == highUsageUid) {
                highUsageAppIncluded = true;
                BatteryUsageStatsAtomsProto.BatteryConsumerData consumerData =
                        proto.uidBatteryConsumers[i].batteryConsumerData;
                assertThat(consumerData.totalConsumedPowerDeciCoulombs / 36)
                        .isEqualTo(3 + 4);
            }
        }

        assertThat(largeConsumerIncluded).isTrue();
        assertThat(highUsageAppIncluded).isTrue();
    }
}