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

Commit 0ed84f12 authored by Hui Yu's avatar Hui Yu
Browse files

Better Handling of Battery Stats History Overflow

Previously battery history is kept in the in-memory mHistoryBuffer
with size of 512KB (96KB on low memory device). When the buffer is
close to full we drop certain types of history. When the buffer is
full the whole buffer is reset and we lost elder history. On a
device with long battery life this problem is more frequent.

This CL changes mHistoryBuffer to 128KB (64KB on low memory device).
When the buffer is full, it is saved to history file on file system.
By default we allow 32 history files (64 history files on low-memory
device) which gives us 4MB history (compare to 512KB today).

The MAX_HISTORY_BUFFER and MAX_HISTORY_FILES can be remote configured
through GServices or P/H.

In case of history exceeding 4MB, the oldest history file is deleted
and new history file is open.

This change increases battery history by using disk file and the
chance of losing history is greatly reduced.

Bug: 67297625
Test: adb shell dumpsys batterystats --history
Change-Id: Id9aafea761649d7323b97d1e44135f7880a95414
parent 458b7f74
Loading
Loading
Loading
Loading
+0 −6
Original line number Diff line number Diff line
@@ -7604,7 +7604,6 @@ Lcom/android/internal/os/BatteryStatsImpl$Uid;->getWakelockStats()Landroid/util/
Lcom/android/internal/os/BatteryStatsImpl$Uid;->getWifiRunningTime(JI)J
Lcom/android/internal/os/BatteryStatsImpl$Uid;->getWifiScanTime(JI)J
Lcom/android/internal/os/BatteryStatsImpl;-><init>(Landroid/os/Parcel;)V
Lcom/android/internal/os/BatteryStatsImpl;->commitPendingDataToDisk()V
Lcom/android/internal/os/BatteryStatsImpl;->computeBatteryRealtime(JI)J
Lcom/android/internal/os/BatteryStatsImpl;->computeBatteryTimeRemaining(J)J
Lcom/android/internal/os/BatteryStatsImpl;->computeBatteryUptime(JI)J
@@ -8357,11 +8356,6 @@ Lcom/android/internal/util/AsyncChannel;->sendMessageSynchronously(III)Landroid/
Lcom/android/internal/util/AsyncChannel;->sendMessageSynchronously(Landroid/os/Message;)Landroid/os/Message;
Lcom/android/internal/util/AsyncChannel;->STATUS_SUCCESSFUL:I
Lcom/android/internal/util/FastPrintWriter;-><init>(Ljava/io/OutputStream;)V
Lcom/android/internal/util/JournaledFile;-><init>(Ljava/io/File;Ljava/io/File;)V
Lcom/android/internal/util/JournaledFile;->chooseForRead()Ljava/io/File;
Lcom/android/internal/util/JournaledFile;->chooseForWrite()Ljava/io/File;
Lcom/android/internal/util/JournaledFile;->commit()V
Lcom/android/internal/util/JournaledFile;->rollback()V
Lcom/android/internal/util/XmlUtils;->convertValueToBoolean(Ljava/lang/CharSequence;Z)Z
Lcom/android/internal/util/XmlUtils;->convertValueToInt(Ljava/lang/CharSequence;I)I
Lcom/android/internal/util/XmlUtils;->readMapXml(Ljava/io/InputStream;)Ljava/util/HashMap;
+2 −0
Original line number Diff line number Diff line
@@ -10963,6 +10963,8 @@ public final class Settings {
         * proc_state_cpu_times_read_delay_ms (long)
         * external_stats_collection_rate_limit_ms (long)
         * battery_level_collection_delay_ms (long)
         * max_history_files (int)
         * max_history_buffer_kb (int)
         * </pre>
         *
         * <p>
+453 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.os.BatteryStats;
import android.os.Parcel;
import android.os.StatFs;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.util.ParseUtils;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * BatteryStatsHistory encapsulates battery history files.
 * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into
 * {@link #mActiveFile}.
 * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
 * current mActiveFile is closed and a new mActiveFile is open.
 * History files are under directory /data/system/battery-history/.
 * History files have name battery-history-<num>.bin. The file number <num> starts from zero and
 * grows sequentially.
 * The mActiveFile is always the highest numbered history file.
 * The lowest number file is always the oldest file.
 * The highest number file is always the newest file.
 * The file number grows sequentially and we never skip number.
 * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES},
 * the lowest numbered file is deleted and a new file is open.
 *
 * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by
 * locks on BatteryStatsImpl object.
 */
class BatteryStatsHistory {
    private static final boolean DEBUG = false;
    private static final String TAG = "BatteryStatsHistory";
    public static final String HISTORY_DIR = "battery-history";
    public static final String FILE_SUFFIX = ".bin";
    private static final int MIN_FREE_SPACE = 100 * 1024 * 1024;

    private final BatteryStatsImpl mStats;
    private final Parcel mHistoryBuffer;
    private final File mHistoryDir;
    /**
     * The active history file that the history buffer is backed up into.
     */
    private AtomicFile mActiveFile;
    /**
     * A list of history files with incremental indexes.
     */
    private final List<Integer> mFileNumbers = new ArrayList<>();

    /**
     * A list of small history parcels, used when BatteryStatsImpl object is created from
     * deserialization of a parcel, such as Settings app or checkin file.
     */
    private List<Parcel> mHistoryParcels = null;

    /**
     * When iterating history files, the current file index.
     */
    private int mCurrentFileIndex;
    /**
     * When iterating history files, the current file parcel.
     */
    private Parcel mCurrentParcel;
    /**
     * When iterating history file, the current parcel's Parcel.dataSize().
     */
    private int mCurrentParcelEnd;
    /**
     * When iterating history files, the current record count.
     */
    private int mRecordCount = 0;
    /**
     * Used when BatteryStatsImpl object is created from deserialization of a parcel,
     * such as Settings app or checkin file, to iterate over history parcels.
     */
    private int mParcelIndex = 0;

    /**
     * Constructor
     * @param stats BatteryStatsImpl object.
     * @param systemDir typically /data/system
     * @param historyBuffer The in-memory history buffer.
     */
    public BatteryStatsHistory(BatteryStatsImpl stats, File systemDir, Parcel historyBuffer) {
        mStats = stats;
        mHistoryBuffer = historyBuffer;
        mHistoryDir = new File(systemDir, HISTORY_DIR);
        mHistoryDir.mkdirs();
        if (!mHistoryDir.exists()) {
            Slog.wtf(TAG, "HistoryDir does not exist:" + mHistoryDir.getPath());
        }

        final Set<Integer> dedup = new ArraySet<>();
        // scan directory, fill mFileNumbers and mActiveFile.
        mHistoryDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                final int b = name.lastIndexOf(FILE_SUFFIX);
                if (b <= 0) {
                    return false;
                }
                final Integer c =
                        ParseUtils.parseInt(name.substring(0, b), -1);
                if (c != -1) {
                    dedup.add(c);
                    return true;
                } else {
                    return false;
                }
            }
        });
        if (!dedup.isEmpty()) {
            mFileNumbers.addAll(dedup);
            Collections.sort(mFileNumbers);
        } else {
            // No file found, default to have file 0.
            mFileNumbers.add(0);
        }
        createActiveFile();
    }

    /**
     * Used when BatteryStatsImpl object is created from deserialization of a parcel,
     * such as Settings app or checkin file.
     * @param stats BatteryStatsImpl object.
     * @param historyBuffer the history buffer inside BatteryStatsImpl
     */
    public BatteryStatsHistory(BatteryStatsImpl stats, Parcel historyBuffer) {
        mStats = stats;
        mHistoryDir = null;
        mHistoryBuffer = historyBuffer;
    }
    /**
     * The highest numbered history file is active file that mHistoryBuffer is backed up into.
     * If file does not exists, truncate() creates a empty file.
     */
    private void createActiveFile() {
        final AtomicFile file = getFile(mFileNumbers.get(mFileNumbers.size() - 1));
        if (DEBUG) {
            Slog.d(TAG, "activeHistoryFile:" + file.getBaseFile().getPath());
        }
        if (!file.exists()) {
            try {
                file.truncate();
            } catch (IOException e) {
                Slog.e(TAG, "Error creating history file "+ file.getBaseFile().getPath(), e);
            }
        }
        mActiveFile = file;
    }

    /**
     * Create history AtomicFile from file number.
     * @param num file number.
     * @return AtomicFile object.
     */
    private AtomicFile getFile(int num) {
        return new AtomicFile(
                new File(mHistoryDir,  num + FILE_SUFFIX));
    }

    /**
     * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER},
     * create next history file.
     */
    public void createNextFile() {
        if (mFileNumbers.isEmpty()) {
            Slog.wtf(TAG, "mFileNumbers should never be empty");
            return;
        }
        // The last number in mFileNumbers is the highest number. The next file number is highest
        // number plus one.
        final int next = mFileNumbers.get(mFileNumbers.size() - 1) + 1;
        mFileNumbers.add(next);
        createActiveFile();

        // if free disk space is less than 100MB, delete oldest history file.
        if (!hasFreeDiskSpace()) {
            int oldest = mFileNumbers.remove(0);
            getFile(oldest).delete();
        }

        // if there are more history files than allowed, delete oldest history files.
        // MAX_HISTORY_FILES can be updated by GService config at run time.
        while (mFileNumbers.size() > mStats.mConstants.MAX_HISTORY_FILES) {
            int oldest = mFileNumbers.get(0);
            getFile(oldest).delete();
            mFileNumbers.remove(0);
        }
    }

    /**
     * Delete all existing history files. Active history file start from number 0 again.
     */
    public void resetAllFiles() {
        for (Integer i : mFileNumbers) {
            getFile(i).delete();
        }
        mFileNumbers.clear();
        mFileNumbers.add(0);
        createActiveFile();
    }

    /**
     * Start iterating history files and history buffer.
     * @return always return true.
     */
    public boolean startIteratingHistory() {
        mRecordCount = 0;
        mCurrentFileIndex = 0;
        mCurrentParcel = null;
        mCurrentParcelEnd = 0;
        mParcelIndex = 0;
        return true;
    }

    /**
     * Finish iterating history files and history buffer.
     */
    public void finishIteratingHistory() {
        // setDataPosition so mHistoryBuffer Parcel can be written.
        mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
        if (DEBUG) {
            Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
        }
    }

    /**
     * When iterating history files and history buffer, always start from the lowest numbered
     * history file, when reached the mActiveFile (highest numbered history file), do not read from
     * mActiveFile, read from history buffer instead because the buffer has more updated data.
     * @param out a history item.
     * @return The parcel that has next record. null if finished all history files and history
     *         buffer
     */
    public Parcel getNextParcel(BatteryStats.HistoryItem out) {
        if (mRecordCount == 0) {
            // reset out if it is the first record.
            out.clear();
        }
        ++mRecordCount;

        // First iterate through all records in current parcel.
        if (mCurrentParcel != null)
        {
            if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
                // There are more records in current parcel.
                return mCurrentParcel;
            } else if (mHistoryBuffer == mCurrentParcel) {
                // finished iterate through all history files and history buffer.
                return null;
            } else if (mHistoryParcels == null
                    || !mHistoryParcels.contains(mCurrentParcel)) {
                // current parcel is from history file.
                mCurrentParcel.recycle();
            }
        }

        // Try next available history file.
        // skip the last file because its data is in history buffer.
        while (mCurrentFileIndex < mFileNumbers.size() - 1) {
            mCurrentParcel = null;
            mCurrentParcelEnd = 0;
            final Parcel p = Parcel.obtain();
            AtomicFile file = getFile(mFileNumbers.get(mCurrentFileIndex++));
            if (readFileToParcel(p, file)) {
                int bufSize = p.readInt();
                int curPos = p.dataPosition();
                mCurrentParcelEnd = curPos + bufSize;
                mCurrentParcel = p;
                if (curPos < mCurrentParcelEnd) {
                    return mCurrentParcel;
                }
            } else {
                p.recycle();
            }
        }

        // mHistoryParcels is created when BatteryStatsImpl object is created from deserialization
        // of a parcel, such as Settings app or checkin file.
        if (mHistoryParcels != null) {
            while (mParcelIndex < mHistoryParcels.size()) {
                final Parcel p = mHistoryParcels.get(mParcelIndex++);
                if (!skipHead(p)) {
                    continue;
                }
                final int bufSize = p.readInt();
                final int curPos = p.dataPosition();
                mCurrentParcelEnd = curPos + bufSize;
                mCurrentParcel = p;
                if (curPos < mCurrentParcelEnd) {
                    return mCurrentParcel;
                }
            }
        }

        // finished iterator through history files (except the last one), now history buffer.
        if (mHistoryBuffer.dataSize() <= 0) {
            // buffer is empty.
            return null;
        }
        mHistoryBuffer.setDataPosition(0);
        mCurrentParcel = mHistoryBuffer;
        mCurrentParcelEnd = mCurrentParcel.dataSize();
        return mCurrentParcel;
    }

    /**
     * Read history file into a parcel.
     * @param out the Parcel read into.
     * @param file the File to read from.
     * @return true if success, false otherwise.
     */
    public boolean readFileToParcel(Parcel out, AtomicFile file) {
        byte[] raw = null;
        try {
            final long start = SystemClock.uptimeMillis();
            raw = file.readFully();
            if (DEBUG) {
                Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath()
                        + " duration ms:" + (SystemClock.uptimeMillis() - start));
            }
        } catch(Exception e) {
            Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
            return false;
        }
        out.unmarshall(raw, 0, raw.length);
        out.setDataPosition(0);
        return skipHead(out);
    }

    /**
     * Skip the header part of history parcel.
     * @param p history parcel to skip head.
     * @return true if version match, false if not.
     */
    private boolean skipHead(Parcel p) {
        p.setDataPosition(0);
        final int version = p.readInt();
        if (version != mStats.VERSION) {
            return false;
        }
        // skip historyBaseTime field.
        p.readLong();
        return true;
    }

    /**
     * Read all history files and serialize into a big Parcel. This is to send history files to
     * Settings app since Settings app can not access /data/system directory.
     * Checkin file also call this method.
     * @param out the output parcel
     */
    public void writeToParcel(Parcel out) {
        final long start = SystemClock.uptimeMillis();
        out.writeInt(mFileNumbers.size() - 1);
        for(int i = 0;  i < mFileNumbers.size() - 1; i++) {
            AtomicFile file = getFile(mFileNumbers.get(i));
            byte[] raw = new byte[0];
            try {
                raw = file.readFully();
            } catch(Exception e) {
                Slog.e(TAG, "Error reading file "+ file.getBaseFile().getPath(), e);
            }
            out.writeByteArray(raw);
        }
        if (DEBUG) {
            Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start));
        }
    }

    /**
     * This is for Settings app, when Settings app receives big history parcel, it call
     * this method to parse it into list of parcels.
     * Checkin file also call this method.
     * @param in the input parcel.
     */
    public void readFromParcel(Parcel in) {
        final long start = SystemClock.uptimeMillis();
        mHistoryParcels = new ArrayList<>();
        final int count = in.readInt();
        for(int i = 0; i < count; i++) {
            byte[] temp = in.createByteArray();
            if (temp.length == 0) {
                continue;
            }
            Parcel p = Parcel.obtain();
            p.unmarshall(temp, 0, temp.length);
            p.setDataPosition(0);
            mHistoryParcels.add(p);
        }
        if (DEBUG) {
            Slog.d(TAG, "readFromParcel duration ms:" + (SystemClock.uptimeMillis() - start));
        }
    }

    /**
     * @return true if there is more than 100MB free disk space left.
     */
    private boolean hasFreeDiskSpace() {
        final StatFs stats = new StatFs(mHistoryDir.getAbsolutePath());
        return stats.getAvailableBytes() > MIN_FREE_SPACE;
    }

    public List<Integer> getFilesNumbers() {
        return mFileNumbers;
    }

    public AtomicFile getActiveFile() {
        return mActiveFile;
    }

    /**
     * @return the total size of all history files and history buffer.
     */
    public int getHistoryUsedSize() {
        int ret = 0;
        for(int i = 0; i < mFileNumbers.size() - 1; i++) {
            ret += getFile(mFileNumbers.get(i)).getBaseFile().length();
        }
        ret += mHistoryBuffer.dataSize();
        if (mHistoryParcels != null) {
            for(int i = 0; i < mHistoryParcels.size(); i++) {
                ret += mHistoryParcels.get(i).dataSize();
            }
        }
        return ret;
    }
}
+248 −224

File changed.

Preview size limit exceeded, changes collapsed.

+145 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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 org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import static org.junit.Assert.assertEquals;

import android.content.Context;
import android.os.Parcel;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;

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

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Test BatteryStatsHistory.
 */
@RunWith(AndroidJUnit4.class)
public class BatteryStatsHistoryTest {
    private static final int MAX_HISTORY_FILES = 32;
    private final BatteryStatsImpl mBatteryStatsImpl = new MockBatteryStatsImpl();
    private final Parcel mHistoryBuffer = Parcel.obtain();
    private File mSystemDir;
    private File mHistoryDir;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        Context context = InstrumentationRegistry.getContext();
        mSystemDir = context.getDataDir();
        mHistoryDir = new File(mSystemDir, BatteryStatsHistory.HISTORY_DIR);
        mHistoryDir.delete();
    }

    @Test
    public void testConstruct() {
        BatteryStatsHistory history =
                new BatteryStatsHistory(mBatteryStatsImpl, mSystemDir, mHistoryBuffer);
        verifyFileNumbers(history, Arrays.asList(0));
        verifyActiveFile(history, "0.bin");
    }

    @Test
    public void testCreateNextFile() {
        BatteryStatsHistory history =
                new BatteryStatsHistory(mBatteryStatsImpl, mSystemDir, mHistoryBuffer);

        List<Integer> fileList = new ArrayList<>();
        fileList.add(0);

        // create file 1 to 31.
        for (int i = 1; i < MAX_HISTORY_FILES; i++) {
            fileList.add(i);
            history.createNextFile();
            verifyFileNumbers(history, fileList);
            verifyActiveFile(history, i + ".bin");
        }

        // create file 32
        history.createNextFile();
        fileList.add(32);
        fileList.remove(0);
        // verify file 0 is deleted.
        verifyFileDeleted("0.bin");
        verifyFileNumbers(history, fileList);
        verifyActiveFile(history, "32.bin");

        // create file 33
        history.createNextFile();
        // verify file 1 is deleted
        fileList.add(33);
        fileList.remove(0);
        verifyFileDeleted("1.bin");
        verifyFileNumbers(history, fileList);
        verifyActiveFile(history, "33.bin");

        assertEquals(0, history.getHistoryUsedSize());

        // create a new BatteryStatsHistory object, it will pick up existing history files.
        BatteryStatsHistory history2 =
                new BatteryStatsHistory(mBatteryStatsImpl, mSystemDir, mHistoryBuffer);
        // verify construct can pick up all files from file system.
        verifyFileNumbers(history2, fileList);
        verifyActiveFile(history2, "33.bin");

        history2.resetAllFiles();
        // verify all existing files are deleted.
        for (int i = 2; i < 33; ++i) {
            verifyFileDeleted(i + ".bin");
        }

        // verify file 0 is created
        verifyFileNumbers(history2, Arrays.asList(0));
        verifyActiveFile(history2, "0.bin");

        // create file 1.
        history2.createNextFile();
        verifyFileNumbers(history2, Arrays.asList(0, 1));
        verifyActiveFile(history2, "1.bin");
    }

    private void verifyActiveFile(BatteryStatsHistory history, String file) {
        final File expectedFile = new File(mHistoryDir, file);
        assertEquals(expectedFile.getPath(), history.getActiveFile().getBaseFile().getPath());
        assertTrue(expectedFile.exists());
    }

    private void verifyFileNumbers(BatteryStatsHistory history, List<Integer> fileList) {
        assertEquals(fileList.size(), history.getFilesNumbers().size());
        for (int i = 0; i < fileList.size(); i++) {
            assertEquals(fileList.get(i), history.getFilesNumbers().get(i));
            final File expectedFile =
                    new File(mHistoryDir, fileList.get(i) + ".bin");
            assertTrue(expectedFile.exists());
        }
    }

    private void verifyFileDeleted(String file) {
        assertFalse(new File(mHistoryDir, file).exists());
    }
}
 No newline at end of file
Loading