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

Commit 2a0460f7 authored by Dmitri Plotnikov's avatar Dmitri Plotnikov
Browse files

Implement BatteryUsageStatsStore

Bug: 187223764
Test: atest FrameworksCoreTests:BatteryUsageStatsStoreTest

Change-Id: I66bbb173f402e795c900ccf3bc82b8fb210142a4
parent 054e2db8
Loading
Loading
Loading
Loading
+21 −0
Original line number Diff line number Diff line
@@ -341,6 +341,19 @@ public class BatteryStatsImpl extends BatteryStats {
        }
    }
    /**
     * Listener for the battery stats reset.
     */
    public interface BatteryResetListener {
        /**
         * Callback invoked immediately prior to resetting battery stats.
         */
        void prepareForBatteryStatsReset();
    }
    private BatteryResetListener mBatteryResetListener;
    public interface BatteryCallback {
        public void batteryNeedsCpuUpdate();
        public void batteryPowerChanged(boolean onBattery);
@@ -11186,6 +11199,10 @@ public class BatteryStatsImpl extends BatteryStats {
        mDischargeCounter.reset(false, elapsedRealtimeUs);
    }
    public void setBatteryResetListener(BatteryResetListener batteryResetListener) {
        mBatteryResetListener = batteryResetListener;
    }
    public void resetAllStatsCmdLocked() {
        final long mSecUptime = mClocks.uptimeMillis();
        long uptimeUs = mSecUptime * 1000;
@@ -11221,6 +11238,10 @@ public class BatteryStatsImpl extends BatteryStats {
    }
    private void resetAllStatsLocked(long uptimeMillis, long elapsedRealtimeMillis) {
        if (mBatteryResetListener != null) {
            mBatteryResetListener.prepareForBatteryStatsReset();
        }
        final long uptimeUs = uptimeMillis * 1000;
        final long elapsedRealtimeUs = elapsedRealtimeMillis * 1000;
        mStartCount = 0;
+231 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

import android.annotation.Nullable;
import android.content.Context;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
import android.os.Handler;
import android.util.LongArray;
import android.util.Slog;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;

import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

/**
 * A storage mechanism for BatteryUsageStats snapshots.
 */
public class BatteryUsageStatsStore {
    private static final String TAG = "BatteryUsageStatsStore";

    private static final List<BatteryUsageStatsQuery> BATTERY_USAGE_STATS_QUERY = List.of(
            new BatteryUsageStatsQuery.Builder()
                    .setMaxStatsAgeMs(0)
                    .includePowerModels()
                    .build());
    private static final String BATTERY_USAGE_STATS_DIR = "battery-usage-stats";
    private static final String SNAPSHOT_FILE_EXTENSION = ".bus";
    private static final String DIR_LOCK_FILENAME = ".lock";
    private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 100 * 1024;

    private final Context mContext;
    private final BatteryStatsImpl mBatteryStats;
    private final File mStoreDir;
    private final File mLockFile;
    private final long mMaxStorageBytes;
    private final Handler mHandler;
    private final BatteryUsageStatsProvider mBatteryUsageStatsProvider;

    public BatteryUsageStatsStore(Context context, BatteryStatsImpl stats, File systemDir,
            Handler handler) {
        this(context, stats, systemDir, handler, MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
    }

    @VisibleForTesting
    public BatteryUsageStatsStore(Context context, BatteryStatsImpl batteryStats, File systemDir,
            Handler handler, long maxStorageBytes) {
        mContext = context;
        mBatteryStats = batteryStats;
        mStoreDir = new File(systemDir, BATTERY_USAGE_STATS_DIR);
        mLockFile = new File(mStoreDir, DIR_LOCK_FILENAME);
        mHandler = handler;
        mMaxStorageBytes = maxStorageBytes;
        mBatteryStats.setBatteryResetListener(this::prepareForBatteryStatsReset);
        mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(mContext, mBatteryStats);
    }

    private void prepareForBatteryStatsReset() {
        final List<BatteryUsageStats> stats =
                mBatteryUsageStatsProvider.getBatteryUsageStats(BATTERY_USAGE_STATS_QUERY);
        if (stats.isEmpty()) {
            Slog.wtf(TAG, "No battery usage stats generated");
            return;
        }

        mHandler.post(() -> storeBatteryUsageStats(stats.get(0)));
    }

    private void storeBatteryUsageStats(BatteryUsageStats stats) {
        try (FileLock lock = lockSnapshotDirectory()) {
            if (!mStoreDir.exists()) {
                if (!mStoreDir.mkdirs()) {
                    Slog.e(TAG,
                            "Could not create a directory for battery usage stats snapshots");
                    return;
                }
            }
            File file = makeSnapshotFilename(stats.getStatsEndTimestamp());
            try {
                writeXmlFileLocked(stats, file);
            } catch (Exception e) {
                Slog.e(TAG, "Cannot save battery usage stats", e);
            }

            removeOldSnapshotsLocked();
        } catch (IOException e) {
            Slog.e(TAG, "Cannot lock battery usage stats directory", e);
        }
    }

    /**
     * Returns the timestamps of the stored BatteryUsageStats snapshots. The timestamp corresponds
     * to the time the snapshot was taken {@link BatteryUsageStats#getStatsEndTimestamp()}.
     */
    public long[] listBatteryUsageStatsTimestamps() {
        LongArray timestamps = new LongArray(100);
        try (FileLock lock = lockSnapshotDirectory()) {
            for (File file : mStoreDir.listFiles()) {
                String fileName = file.getName();
                if (fileName.endsWith(SNAPSHOT_FILE_EXTENSION)) {
                    try {
                        String fileNameWithoutExtension = fileName.substring(0,
                                fileName.length() - SNAPSHOT_FILE_EXTENSION.length());
                        timestamps.add(Long.parseLong(fileNameWithoutExtension));
                    } catch (NumberFormatException e) {
                        Slog.wtf(TAG, "Invalid format of BatteryUsageStats snapshot file name: "
                                + fileName);
                    }
                }
            }
        } catch (IOException e) {
            Slog.e(TAG, "Cannot lock battery usage stats directory", e);
        }
        return timestamps.toArray();
    }

    /**
     * Reads the specified snapshot of BatteryUsageStats.  Returns null if the snapshot
     * does not exist.
     */
    @Nullable
    public BatteryUsageStats loadBatteryUsageStats(long timestamp) {
        try (FileLock lock = lockSnapshotDirectory()) {
            File file = makeSnapshotFilename(timestamp);
            try {
                return readXmlFileLocked(file);
            } catch (Exception e) {
                Slog.e(TAG, "Cannot read battery usage stats", e);
            }
        } catch (IOException e) {
            Slog.e(TAG, "Cannot lock battery usage stats directory", e);
        }
        return null;
    }

    private FileLock lockSnapshotDirectory() throws IOException {
        mLockFile.getParentFile().mkdirs();
        mLockFile.createNewFile();
        return FileChannel.open(mLockFile.toPath(), StandardOpenOption.WRITE).lock();
    }

    /**
     * Creates a file name by formatting the timestamp as 19-digit zero-padded number.
     * This ensures that sorted directory list follows the chronological order.
     */
    private File makeSnapshotFilename(long statsEndTimestamp) {
        return new File(mStoreDir, String.format(Locale.ENGLISH, "%019d", statsEndTimestamp)
                + SNAPSHOT_FILE_EXTENSION);
    }

    private void writeXmlFileLocked(BatteryUsageStats stats, File file) throws IOException {
        try (OutputStream out = new FileOutputStream(file)) {
            TypedXmlSerializer serializer = Xml.newBinarySerializer();
            serializer.setOutput(out, StandardCharsets.UTF_8.name());
            serializer.startDocument(null, true);
            stats.writeXml(serializer);
            serializer.endDocument();
        }
    }

    private BatteryUsageStats readXmlFileLocked(File file)
            throws IOException, XmlPullParserException {
        try (InputStream in = new FileInputStream(file)) {
            TypedXmlPullParser parser = Xml.newBinaryPullParser();
            parser.setInput(in, StandardCharsets.UTF_8.name());
            return BatteryUsageStats.createFromXml(parser);
        }
    }

    private void removeOldSnapshotsLocked() {
        // Read the directory list into a _sorted_ map.  The alphanumeric ordering
        // corresponds to the historical order of snapshots because the file names
        // are timestamps zero-padded to the same length.
        long totalSize = 0;
        TreeMap<File, Long> mFileSizes = new TreeMap<>();
        for (File file : mStoreDir.listFiles()) {
            final long fileSize = file.length();
            totalSize += fileSize;
            if (file.getName().endsWith(SNAPSHOT_FILE_EXTENSION)) {
                mFileSizes.put(file, fileSize);
            }
        }

        while (totalSize > mMaxStorageBytes) {
            final Map.Entry<File, Long> entry = mFileSizes.firstEntry();
            if (entry == null) {
                break;
            }

            File file = entry.getKey();
            if (!file.delete()) {
                Slog.e(TAG, "Cannot delete battery usage stats " + file);
            }
            totalSize -= entry.getValue();
            mFileSizes.remove(file);
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import org.junit.runners.Suite;
        BatteryStatsUidTest.class,
        BatteryUsageStatsProviderTest.class,
        BatteryUsageStatsTest.class,
        BatteryUsageStatsStoreTest.class,
        BatteryStatsUserLifecycleTests.class,
        BluetoothPowerCalculatorTest.class,
        BstatsCpuTimesValidationTest.class,
+183 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

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

import android.content.Context;
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.TypedXmlSerializer;
import android.util.Xml;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

@RunWith(AndroidJUnit4.class)
public class BatteryUsageStatsStoreTest {
    private static final long MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES = 2 * 1024;

    private final MockClocks mMockClocks = new MockClocks();
    private MockBatteryStatsImpl mBatteryStats;
    private BatteryUsageStatsStore mBatteryUsageStatsStore;
    private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
    private File mStoreDirectory;

    @Before
    public void setup() {
        mMockClocks.currentTime = 123;
        mBatteryStats = new MockBatteryStatsImpl(mMockClocks);
        mBatteryStats.setNoAutoReset(true);

        Context context = InstrumentationRegistry.getContext();

        mStoreDirectory = new File(context.getCacheDir(), "BatteryUsageStatsStoreTest");
        clearDirectory(mStoreDirectory);

        mBatteryUsageStatsStore = new BatteryUsageStatsStore(context, mBatteryStats,
                mStoreDirectory, new TestHandler(), MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);

        mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mBatteryStats);
    }

    @Test
    public void testStoreSnapshot() {
        mMockClocks.currentTime = 1_600_000;

        prepareBatteryStats();
        mBatteryStats.resetAllStatsCmdLocked();

        final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
        assertThat(timestamps).hasLength(1);
        assertThat(timestamps[0]).isEqualTo(1_600_000);

        final BatteryUsageStats batteryUsageStats = mBatteryUsageStatsStore.loadBatteryUsageStats(
                1_600_000);
        assertThat(batteryUsageStats.getStatsStartTimestamp()).isEqualTo(123);
        assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(1_600_000);
        assertThat(batteryUsageStats.getBatteryCapacity()).isEqualTo(4000);
        assertThat(batteryUsageStats.getDischargePercentage()).isEqualTo(5);
        assertThat(batteryUsageStats.getAggregateBatteryConsumer(
                BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE).getConsumedPower())
                .isEqualTo(600);  // (3_600_000 - 3_000_000) / 1000
    }

    @Test
    public void testGarbageCollectOldSnapshots() throws Exception {
        prepareBatteryStats();

        mMockClocks.realtime = 10_000_000;
        mMockClocks.uptime = 10_000_000;
        mMockClocks.currentTime = 10_000_000;

        final int snapshotFileSize = getSnapshotFileSize();
        final int numberOfSnapshots =
                (int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / snapshotFileSize);
        for (int i = 0; i < numberOfSnapshots + 2; i++) {
            mBatteryStats.resetAllStatsCmdLocked();

            mMockClocks.realtime += 10_000_000;
            mMockClocks.uptime += 10_000_000;
            mMockClocks.currentTime += 10_000_000;
            prepareBatteryStats();
        }

        final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps();
        Arrays.sort(timestamps);
        assertThat(timestamps).hasLength(numberOfSnapshots);
        // Two snapshots (10_000_000 and 20_000_000) should have been discarded
        assertThat(timestamps[0]).isEqualTo(30_000_000);
        assertThat(getDirectorySize(mStoreDirectory))
                .isAtMost(MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES);
    }

    private void prepareBatteryStats() {
        mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
                /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
                mMockClocks.realtime, mMockClocks.uptime, mMockClocks.currentTime);
        mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
                /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
                mMockClocks.realtime + 500_000, mMockClocks.uptime + 500_000,
                mMockClocks.currentTime + 500_000);
    }

    private void clearDirectory(File dir) {
        if (dir.exists()) {
            for (File child : dir.listFiles()) {
                if (child.isDirectory()) {
                    clearDirectory(child);
                }
                child.delete();
            }
        }
    }

    private long getDirectorySize(File dir) {
        long size = 0;
        if (dir.exists()) {
            for (File child : dir.listFiles()) {
                if (child.isDirectory()) {
                    size += getDirectorySize(child);
                } else {
                    size += child.length();
                }
            }
        }
        return size;
    }

    private int getSnapshotFileSize() throws IOException {
        BatteryUsageStats stats = mBatteryUsageStatsProvider.getBatteryUsageStats(
                new BatteryUsageStatsQuery.Builder()
                        .setMaxStatsAgeMs(0)
                        .includePowerModels()
                        .build());
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        TypedXmlSerializer serializer = Xml.newBinarySerializer();
        serializer.setOutput(out, StandardCharsets.UTF_8.name());
        serializer.startDocument(null, true);
        stats.writeXml(serializer);
        serializer.endDocument();
        return out.toByteArray().length;
    }

    private static class TestHandler extends Handler {
        TestHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
            msg.getCallback().run();
            return true;
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ import com.android.internal.os.BackgroundThread;
import com.android.internal.os.BatteryStatsHelper;
import com.android.internal.os.BatteryStatsImpl;
import com.android.internal.os.BatteryUsageStatsProvider;
import com.android.internal.os.BatteryUsageStatsStore;
import com.android.internal.os.BinderCallsStats;
import com.android.internal.os.PowerProfile;
import com.android.internal.os.RailStats;
@@ -124,10 +125,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub
        Watchdog.Monitor {
    static final String TAG = "BatteryStatsService";
    static final boolean DBG = false;
    private static final boolean BATTERY_USAGE_STORE_ENABLED = false;

    private static IBatteryStats sService;

    final BatteryStatsImpl mStats;
    private final BatteryUsageStatsStore mBatteryUsageStatsStore;
    private final BatteryStatsImpl.UserInfoProvider mUserManagerUserInfoProvider;
    private final Context mContext;
    private final BatteryExternalStatsWorker mWorker;
@@ -341,6 +344,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub

        mStats = new BatteryStatsImpl(systemDir, handler, this,
                this, mUserManagerUserInfoProvider);
        if (BATTERY_USAGE_STORE_ENABLED) {
            mBatteryUsageStatsStore =
                    new BatteryUsageStatsStore(context, mStats, systemDir, mHandler);
        } else {
            mBatteryUsageStatsStore = null;
        }
        mWorker = new BatteryExternalStatsWorker(context, mStats);
        mStats.setExternalStatsSyncLocked(mWorker);
        mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(