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

Commit 2bd72480 authored by Makoto Onuki's avatar Makoto Onuki Committed by Android (Google) Code Review
Browse files

Merge "Extreme battery saver: Cap max cpu frequency."

parents ed760eff ce643a30
Loading
Loading
Loading
Loading
+20 −33
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.power.batterysaver.CpuFrequencies;

import java.io.PrintWriter;
import java.util.ArrayList;
@@ -40,15 +41,14 @@ import java.util.List;
/**
 * Class to decide whether to turn on battery saver mode for specific service
 *
 * TODO: We should probably make {@link #mFilesForInteractive} and {@link #mFilesForNoninteractive}
 * less flexible and just take a list of "CPU number - frequency" pairs. Being able to write
 * anything under /sys/ and /proc/ is too loose.
 *
 * Test: atest BatterySaverPolicyTest
 * Test:
 atest ${ANDROID_BUILD_TOP}/frameworks/base/services/tests/servicestests/src/com/android/server/power/BatterySaverPolicyTest.java
 */
public class BatterySaverPolicy extends ContentObserver {
    private static final String TAG = "BatterySaverPolicy";

    public static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE.

    // Value of batterySaverGpsMode such that GPS isn't affected by battery saver mode.
    public static final int GPS_MODE_NO_CHANGE = 0;
    // Value of batterySaverGpsMode such that GPS is disabled when battery saver mode
@@ -70,8 +70,8 @@ public class BatterySaverPolicy extends ContentObserver {
    private static final String KEY_FORCE_ALL_APPS_STANDBY = "force_all_apps_standby";
    private static final String KEY_OPTIONAL_SENSORS_DISABLED = "optional_sensors_disabled";

    private static final String KEY_FILE_FOR_INTERACTIVE_PREFIX = "file-on:";
    private static final String KEY_FILE_FOR_NONINTERACTIVE_PREFIX = "file-off:";
    private static final String KEY_CPU_FREQ_INTERACTIVE = "cpufreq-i";
    private static final String KEY_CPU_FREQ_NONINTERACTIVE = "cpufreq-n";

    private static String mSettings;
    private static String mDeviceSpecificSettings;
@@ -273,6 +273,11 @@ public class BatterySaverPolicy extends ContentObserver {
        mSettings = setting;
        mDeviceSpecificSettings = deviceSpecificSetting;

        if (DEBUG) {
            Slog.i(TAG, "mSettings=" + mSettings);
            Slog.i(TAG, "mDeviceSpecificSettings=" + mDeviceSpecificSettings);
        }

        final KeyValueListParser parser = new KeyValueListParser(',');

        // Non-device-specific parameters.
@@ -307,29 +312,11 @@ public class BatterySaverPolicy extends ContentObserver {
                    + deviceSpecificSetting);
        }

        mFilesForInteractive = collectParams(parser, KEY_FILE_FOR_INTERACTIVE_PREFIX);
        mFilesForNoninteractive = collectParams(parser, KEY_FILE_FOR_NONINTERACTIVE_PREFIX);
    }

    private static ArrayMap<String, String> collectParams(
            KeyValueListParser parser, String prefix) {
        final ArrayMap<String, String> ret = new ArrayMap<>();
        mFilesForInteractive = (new CpuFrequencies()).parseString(
                parser.getString(KEY_CPU_FREQ_INTERACTIVE, "")).toSysFileMap();

        for (int i = parser.size() - 1; i >= 0; i--) {
            final String key = parser.keyAt(i);
            if (!key.startsWith(prefix)) {
                continue;
            }
            final String path = key.substring(prefix.length());

            if (!(path.startsWith("/sys/") || path.startsWith("/proc/"))) {
                Slog.wtf(TAG, "Invalid path: " + path);
                continue;
            }

            ret.put(path, parser.getString(key, ""));
        }
        return ret;
        mFilesForNoninteractive = (new CpuFrequencies()).parseString(
                parser.getString(KEY_CPU_FREQ_NONINTERACTIVE, "")).toSysFileMap();
    }

    /**
@@ -399,9 +386,9 @@ public class BatterySaverPolicy extends ContentObserver {
        synchronized (mLock) {
            pw.println();
            pw.println("Battery saver policy");
            pw.println("  Settings " + Settings.Global.BATTERY_SAVER_CONSTANTS);
            pw.println("  Settings: " + Settings.Global.BATTERY_SAVER_CONSTANTS);
            pw.println("    value: " + mSettings);
            pw.println("  Settings " + mDeviceSpecificSettingsSource);
            pw.println("  Settings: " + mDeviceSpecificSettingsSource);
            pw.println("    value: " + mDeviceSpecificSettings);

            pw.println();
+1 −1
Original line number Diff line number Diff line
@@ -49,7 +49,7 @@ import java.util.ArrayList;
public class BatterySaverController implements BatterySaverPolicyListener {
    static final String TAG = "BatterySaverController";

    static final boolean DEBUG = false; // DO NOT MERGE WITH TRUE
    static final boolean DEBUG = BatterySaverPolicy.DEBUG;

    private final Object mLock = new Object();
    private final Context mContext;
+101 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.server.power.batterysaver;

import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

import java.util.Map;


/**
 * Helper to parse a list of "core-number:frequency" pairs concatenated with / as a separator,
 * and convert them into a map of "filename -> value" that should be written to
 * /sys/.../scaling_max_freq.
 *
 * Example input: "0:1900800/4:2500000", which will be converted into:
 *   "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq" "1900800"
 *   "/sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq" "2500000"
 *
 * Test:
 atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/CpuFrequenciesTest.java
 */
public class CpuFrequencies {
    private static final String TAG = "CpuFrequencies";

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private final ArrayMap<Integer, Long> mCoreAndFrequencies = new ArrayMap<>();

    public CpuFrequencies() {
    }

    /**
     * Parse a string.
     */
    public CpuFrequencies parseString(String cpuNumberAndFrequencies) {
        synchronized (mLock) {
            mCoreAndFrequencies.clear();
            try {
                for (String pair : cpuNumberAndFrequencies.split("/")) {
                    final String[] coreAndFreq = pair.split(":", 2);

                    if (coreAndFreq.length != 2) {
                        throw new IllegalArgumentException("Wrong format");
                    }
                    final int core = Integer.parseInt(coreAndFreq[0]);
                    final long freq = Long.parseLong(coreAndFreq[1]);

                    mCoreAndFrequencies.put(core, freq);
                }
            } catch (IllegalArgumentException e) {
                Slog.wtf(TAG, "Invalid configuration: " + cpuNumberAndFrequencies, e);
            }
        }
        return this;
    }

    /**
     * Return a new map containing the filename-value pairs.
     */
    public ArrayMap<String, String> toSysFileMap() {
        final ArrayMap<String, String> map = new ArrayMap<>();
        addToSysFileMap(map);
        return map;
    }

    /**
     * Add the filename-value pairs to an existing map.
     */
    public void addToSysFileMap(Map<String, String> map) {
        synchronized (mLock) {
            final int size = mCoreAndFrequencies.size();

            for (int i = 0; i < size; i++) {
                final int core = mCoreAndFrequencies.keyAt(i);
                final long freq = mCoreAndFrequencies.valueAt(i);

                final String file = "/sys/devices/system/cpu/cpu" + Integer.toString(core) +
                        "/cpufreq/scaling_max_freq";

                map.put(file, Long.toString(freq));
            }
        }
    }
}
+227 −8
Original line number Diff line number Diff line
@@ -16,40 +16,259 @@
package com.android.server.power.batterysaver;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.IoThread;

import libcore.io.IoUtils;

import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;

/**
 * Used by {@link BatterySaverController} to write values to /sys/ (and possibly /proc/ too) files
 * with retry and to restore the original values.
 * with retries. It also support restoring to the file original values.
 *
 * TODO Implement it
 * Retries are needed because writing to "/sys/.../scaling_max_freq" returns EIO when the current
 * frequency happens to be above the new max frequency.
 *
 * Test:
 atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/FileUpdaterTest.java
 */
public class FileUpdater {
    private static final String TAG = BatterySaverController.TAG;

    private static final boolean DEBUG = BatterySaverController.DEBUG;

    // Don't do disk access with this lock held.
    private final Object mLock = new Object();

    private final Context mContext;

    private final Handler mHandler;

    /**
     * Filename -> value map that holds pending writes.
     */
    @GuardedBy("mLock")
    private final ArrayMap<String, String> mPendingWrites = new ArrayMap<>();

    /**
     * Filename -> value that holds the original value of each file.
     */
    @GuardedBy("mLock")
    private final ArrayMap<String, String> mDefaultValues = new ArrayMap<>();

    /** Number of retries. We give up on writing after {@link #MAX_RETRIES} retries. */
    @GuardedBy("mLock")
    private int mRetries = 0;

    private final int MAX_RETRIES;

    private final long RETRY_INTERVAL_MS;

    /**
     * "Official" constructor. Don't use the other constructor in the production code.
     */
    public FileUpdater(Context context) {
        this(context, IoThread.get().getLooper(), 10, 5000);
    }

    /**
     * Constructor for test.
     */
    @VisibleForTesting
    FileUpdater(Context context, Looper looper, int maxRetries, int retryIntervalMs) {
        mContext = context;
        mHandler = new Handler(looper);

        MAX_RETRIES = maxRetries;
        RETRY_INTERVAL_MS = retryIntervalMs;
    }

    /**
     * Write values to files. (Note the actual writes happen ASAP but asynchronously.)
     */
    public void writeFiles(ArrayMap<String, String> fileValues) {
        synchronized (mLock) {
            for (int i = fileValues.size() - 1; i >= 0; i--) {
                final String file = fileValues.keyAt(i);
                final String value = fileValues.valueAt(i);

                if (DEBUG) {
            final int size = fileValues.size();
            for (int i = 0; i < size; i++) {
                Slog.d(TAG, "Writing '" + fileValues.valueAt(i)
                        + "' to '" + fileValues.keyAt(i) + "'");
                    Slog.d(TAG, "Scheduling write: '" + value + "' to '" + file + "'");
                }

                mPendingWrites.put(file, value);

            }
            mRetries = 0;

            mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable);
            mHandler.post(mHandleWriteOnHandlerRunnable);
        }
    }

    /**
     * Restore the default values.
     */
    public void restoreDefault() {
        synchronized (mLock) {
            if (DEBUG) {
                Slog.d(TAG, "Resetting file default values.");
            }
            mPendingWrites.clear();

            writeFiles(mDefaultValues);
        }
    }

    private Runnable mHandleWriteOnHandlerRunnable = () -> handleWriteOnHandler();

    /** Convert map keys into a single string for debug messages. */
    private String getKeysString(Map<String, String> source) {
        return new ArrayList<>(source.keySet()).toString();
    }

    /** Clone an ArrayMap. */
    private ArrayMap<String, String> cloneMap(ArrayMap<String, String> source) {
        return new ArrayMap<>(source);
    }

    /**
     * Called on the handler and writes {@link #mPendingWrites} to the disk.
     *
     * When it about to write to each file for the first time, it'll read the file and store
     * the original value in {@link #mDefaultValues}.
     */
    private void handleWriteOnHandler() {
        // We don't want to access the disk with the lock held, so copy the pending writes to
        // a local map.
        final ArrayMap<String, String> writes;
        synchronized (mLock) {
            if (mPendingWrites.size() == 0) {
                return;
            }

            if (DEBUG) {
                Slog.d(TAG, "Writing files: (# retries=" + mRetries + ") " +
                        getKeysString(mPendingWrites));
            }

            writes = cloneMap(mPendingWrites);
        }

        // Then write.

        boolean needRetry = false;

        final int size = writes.size();
        for (int i = 0; i < size; i++) {
            final String file = writes.keyAt(i);
            final String value = writes.valueAt(i);

            // Make sure the default value is loaded.
            if (!ensureDefaultLoaded(file)) {
                continue;
            }

            // Write to the file. When succeeded, remove it from the pending list.
            // Otherwise, schedule a retry.
            try {
                injectWriteToFile(file, value);

                removePendingWrite(file);
            } catch (IOException e) {
                needRetry = true;
            }
        }
        if (needRetry) {
            scheduleRetry();
        }
    }

    private void removePendingWrite(String file) {
        synchronized (mLock) {
            mPendingWrites.remove(file);
        }
    }

    private void scheduleRetry() {
        synchronized (mLock) {
            if (mPendingWrites.size() == 0) {
                return; // Shouldn't happen but just in case.
            }

            mRetries++;
            if (mRetries > MAX_RETRIES) {
                doWtf("Gave up writing files: " + getKeysString(mPendingWrites));
                return;
            }

            mHandler.removeCallbacks(mHandleWriteOnHandlerRunnable);
            mHandler.postDelayed(mHandleWriteOnHandlerRunnable, RETRY_INTERVAL_MS);
        }
    }

    /**
     * Make sure {@link #mDefaultValues} has the default value loaded for {@code file}.
     *
     * @return true if the default value is loaded. false if the file cannot be read.
     */
    private boolean ensureDefaultLoaded(String file) {
        // Has the default already?
        synchronized (mLock) {
            if (mDefaultValues.containsKey(file)) {
                return true;
            }
        }
        final String originalValue;
        try {
            originalValue = injectReadFromFileTrimmed(file);
        } catch (IOException e) {
            // If the file is not readable, assume can't write too.
            injectWtf("Unable to read from file", e);

            removePendingWrite(file);
            return false;
        }
        synchronized (mLock) {
            mDefaultValues.put(file, originalValue);
        }
        return true;
    }

    @VisibleForTesting
    String injectReadFromFileTrimmed(String file) throws IOException {
        return IoUtils.readFileAsString(file).trim();
    }

    @VisibleForTesting
    void injectWriteToFile(String file, String value) throws IOException {
        if (DEBUG) {
            Slog.d(TAG, "Resetting file default values");
            Slog.d(TAG, "Writing: '" + value + "' to '" + file + "'");
        }
        try (FileWriter out = new FileWriter(file)) {
            out.write(value);
        } catch (IOException e) {
            Slog.w(TAG, "Failed writing '" + value + "' to '" + file + "': " + e.getMessage());
            throw e;
        }
    }

    private void doWtf(String message) {
        injectWtf(message, null);
    }

    @VisibleForTesting
    void injectWtf(String message, Throwable e) {
        Slog.wtf(TAG, message, e);
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -30,6 +30,6 @@
    <string name="test_account_type2">com.android.server.accounts.account_manager_service_test.account.type2</string>

    <string name="config_batterySaverDeviceSpecificConfig_1"></string>
    <string name="config_batterySaverDeviceSpecificConfig_2">file-off:/sys/a=1,file-off:/sys/b=2</string>
    <string name="config_batterySaverDeviceSpecificConfig_3">file-off:/sys/a=3,file-on:/proc/c=4,/abc=3</string>
    <string name="config_batterySaverDeviceSpecificConfig_2">cpufreq-n=1:123/2:456</string>
    <string name="config_batterySaverDeviceSpecificConfig_3">cpufreq-n=2:222,cpufreq-i=3:333/4:444</string>
</resources>
Loading