Add getThermalHeadroomThresholds API
Test: atest ThermalManagerServiceTest
Bug: 288119641
Change-Id: I06b1f389dfb6805c7aa07599b888438a2eb17993
diff --git a/core/api/current.txt b/core/api/current.txt
index d8ea721..d641fba 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -33039,6 +33039,7 @@
method public int getCurrentThermalStatus();
method public int getLocationPowerSaveMode();
method public float getThermalHeadroom(@IntRange(from=0, to=60) int);
+ method @FlaggedApi("android.os.allow_thermal_headroom_thresholds") @NonNull public java.util.Map<java.lang.Integer,java.lang.Float> getThermalHeadroomThresholds();
method public boolean isAllowedInLowPowerStandby(int);
method public boolean isAllowedInLowPowerStandby(@NonNull String);
method public boolean isBatteryDischargePredictionPersonalized();
diff --git a/core/java/android/os/IThermalService.aidl b/core/java/android/os/IThermalService.aidl
index c6c8adc..bcffa45 100644
--- a/core/java/android/os/IThermalService.aidl
+++ b/core/java/android/os/IThermalService.aidl
@@ -111,4 +111,9 @@
* occur; returns NaN if the headroom or forecast is unavailable
*/
float getThermalHeadroom(int forecastSeconds);
+
+ /**
+ * @return thermal headroom for each thermal status
+ */
+ float[] getThermalHeadroomThresholds();
}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index fce715a..0511cfd 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -19,6 +19,7 @@
import android.Manifest.permission;
import android.annotation.CallbackExecutor;
import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -41,14 +42,17 @@
import com.android.internal.util.Preconditions;
+import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -1179,6 +1183,8 @@
private final ArrayMap<OnThermalStatusChangedListener, IThermalStatusListener>
mListenerMap = new ArrayMap<>();
+ private final Object mThermalHeadroomThresholdsLock = new Object();
+ private float[] mThermalHeadroomThresholds = null;
/**
* {@hide}
@@ -2634,6 +2640,7 @@
public static final int THERMAL_STATUS_SHUTDOWN = Temperature.THROTTLING_SHUTDOWN;
/** @hide */
+ @Target(ElementType.TYPE_USE)
@IntDef(prefix = { "THERMAL_STATUS_" }, value = {
THERMAL_STATUS_NONE,
THERMAL_STATUS_LIGHT,
@@ -2798,6 +2805,63 @@
}
/**
+ * Gets the thermal headroom thresholds for all available thermal throttling status above
+ * {@link #THERMAL_STATUS_NONE}.
+ * <p>
+ * A thermal status key in the returned map is only set if the device manufacturer has the
+ * corresponding threshold defined for at least one of its sensors. If it's set, one should
+ * expect to see that from {@link #getCurrentThermalStatus()} or
+ * {@link OnThermalStatusChangedListener#onThermalStatusChanged(int)}.
+ * <p>
+ * The headroom threshold is used to interpret the possible thermal throttling status based on
+ * the headroom prediction. For example, if the headroom threshold for
+ * {@link #THERMAL_STATUS_LIGHT} is 0.7, and a headroom prediction in 10s returns 0.75
+ * (or {@code getThermalHeadroom(10)=0.75}), one can expect that in 10 seconds the system could
+ * be in lightly throttled state if the workload remains the same. The app can consider
+ * taking actions according to the nearest throttling status the difference between the headroom
+ * and the threshold.
+ * <p>
+ * For new devices it's guaranteed to have a single sensor, but for older devices with multiple
+ * sensors reporting different threshold values, the minimum threshold is taken to be
+ * conservative on predictions. Thus, when reading real-time headroom, it's not guaranteed that
+ * a real-time value of 0.75 (or {@code getThermalHeadroom(0)}=0.75) exceeding the threshold of
+ * 0.7 above will always come with lightly throttled state
+ * (or {@code getCurrentThermalStatus()=THERMAL_STATUS_LIGHT}) but it can be lower
+ * (or {@code getCurrentThermalStatus()=THERMAL_STATUS_NONE}). While it's always guaranteed that
+ * the device won't be throttled heavier than the unmet threshold's state, so a real-time
+ * headroom of 0.75 will never come with {@link #THERMAL_STATUS_MODERATE} but lower, and 0.65
+ * will never come with {@link #THERMAL_STATUS_LIGHT} but {@link #THERMAL_STATUS_NONE}.
+ * <p>
+ * The returned map of thresholds will not change between calls to this function, so it's
+ * best to call this once on initialization. Modifying the result will not change the thresholds
+ * cached by the system, and a new call to the API will get a new copy.
+ *
+ * @return map from each thermal status to its thermal headroom
+ * @throws IllegalStateException if the thermal service is not ready
+ * @throws UnsupportedOperationException if the feature is not enabled
+ */
+ @FlaggedApi(Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS)
+ public @NonNull Map<@ThermalStatus Integer, Float> getThermalHeadroomThresholds() {
+ try {
+ synchronized (mThermalHeadroomThresholdsLock) {
+ if (mThermalHeadroomThresholds == null) {
+ mThermalHeadroomThresholds = mThermalService.getThermalHeadroomThresholds();
+ }
+ final ArrayMap<Integer, Float> ret = new ArrayMap<>(THERMAL_STATUS_SHUTDOWN);
+ for (int status = THERMAL_STATUS_LIGHT; status <= THERMAL_STATUS_SHUTDOWN;
+ status++) {
+ if (!Float.isNaN(mThermalHeadroomThresholds[status])) {
+ ret.put(status, mThermalHeadroomThresholds[status]);
+ }
+ }
+ return ret;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* If true, the doze component is not started until after the screen has been
* turned off and the screen off animation has been performed.
* @hide
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index c4521c0..7aac9ef 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -22,6 +22,13 @@
}
flag {
+ name: "allow_thermal_headroom_thresholds"
+ namespace: "game"
+ description: "Enable thermal headroom thresholds API"
+ bug: "288119641"
+}
+
+flag {
name: "allow_private_profile"
namespace: "profile_experiences"
description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 99064bc..d17207b 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -28,6 +28,7 @@
import android.hardware.thermal.V1_1.IThermalCallback;
import android.os.Binder;
import android.os.CoolingDevice;
+import android.os.Flags;
import android.os.Handler;
import android.os.HwBinder;
import android.os.IBinder;
@@ -181,7 +182,7 @@
onTemperatureChanged(temperatures.get(i), false);
}
onTemperatureMapChangedLocked();
- mTemperatureWatcher.updateSevereThresholds();
+ mTemperatureWatcher.updateThresholds();
mHalReady.set(true);
}
}
@@ -506,6 +507,20 @@
}
@Override
+ public float[] getThermalHeadroomThresholds() {
+ if (!mHalReady.get()) {
+ throw new IllegalStateException("Thermal HAL connection is not initialized");
+ }
+ if (!Flags.allowThermalHeadroomThresholds()) {
+ throw new UnsupportedOperationException("Thermal headroom thresholds not enabled");
+ }
+ synchronized (mTemperatureWatcher.mSamples) {
+ return Arrays.copyOf(mTemperatureWatcher.mHeadroomThresholds,
+ mTemperatureWatcher.mHeadroomThresholds.length);
+ }
+ }
+
+ @Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
dumpInternal(fd, pw, args);
}
@@ -580,6 +595,12 @@
mHalWrapper.getTemperatureThresholds(false, 0));
}
}
+ if (Flags.allowThermalHeadroomThresholds()) {
+ synchronized (mTemperatureWatcher.mSamples) {
+ pw.println("Temperature headroom thresholds:");
+ pw.println(Arrays.toString(mTemperatureWatcher.mHeadroomThresholds));
+ }
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -964,7 +985,14 @@
connectToHal();
}
if (mInstance != null) {
- Slog.i(TAG, "Thermal HAL AIDL service connected.");
+ try {
+ Slog.i(TAG, "Thermal HAL AIDL service connected with version "
+ + mInstance.getInterfaceVersion());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to read interface version from Thermal HAL", e);
+ connectToHal();
+ return;
+ }
registerThermalChangedCallback();
}
}
@@ -1440,26 +1468,55 @@
ArrayMap<String, Float> mSevereThresholds = new ArrayMap<>();
@GuardedBy("mSamples")
+ float[] mHeadroomThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1];
+ @GuardedBy("mSamples")
private long mLastForecastCallTimeMillis = 0;
private static final int INACTIVITY_THRESHOLD_MILLIS = 10000;
@VisibleForTesting
long mInactivityThresholdMillis = INACTIVITY_THRESHOLD_MILLIS;
- void updateSevereThresholds() {
+ void updateThresholds() {
synchronized (mSamples) {
List<TemperatureThreshold> thresholds =
mHalWrapper.getTemperatureThresholds(true, Temperature.TYPE_SKIN);
+ if (Flags.allowThermalHeadroomThresholds()) {
+ Arrays.fill(mHeadroomThresholds, Float.NaN);
+ }
for (int t = 0; t < thresholds.size(); ++t) {
TemperatureThreshold threshold = thresholds.get(t);
if (threshold.hotThrottlingThresholds.length <= ThrottlingSeverity.SEVERE) {
continue;
}
- float temperature =
+ float severeThreshold =
threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE];
- if (!Float.isNaN(temperature)) {
- mSevereThresholds.put(threshold.name,
- threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE]);
+ if (!Float.isNaN(severeThreshold)) {
+ mSevereThresholds.put(threshold.name, severeThreshold);
+ for (int severity = ThrottlingSeverity.LIGHT;
+ severity <= ThrottlingSeverity.SHUTDOWN; severity++) {
+ if (Flags.allowThermalHeadroomThresholds()
+ && threshold.hotThrottlingThresholds.length > severity) {
+ updateHeadroomThreshold(severity,
+ threshold.hotThrottlingThresholds[severity],
+ severeThreshold);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // For a older device with multiple SKIN sensors, we will set a severity's headroom
+ // threshold based on the minimum value of all as a workaround.
+ void updateHeadroomThreshold(int severity, float threshold, float severeThreshold) {
+ if (!Float.isNaN(threshold)) {
+ synchronized (mSamples) {
+ float headroom = normalizeTemperature(threshold, severeThreshold);
+ if (Float.isNaN(mHeadroomThresholds[severity])) {
+ mHeadroomThresholds[severity] = headroom;
+ } else {
+ float lastHeadroom = mHeadroomThresholds[severity];
+ mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom);
}
}
}
@@ -1541,15 +1598,13 @@
private static final float DEGREES_BETWEEN_ZERO_AND_ONE = 30.0f;
@VisibleForTesting
- float normalizeTemperature(float temperature, float severeThreshold) {
- synchronized (mSamples) {
- float zeroNormalized = severeThreshold - DEGREES_BETWEEN_ZERO_AND_ONE;
- if (temperature <= zeroNormalized) {
- return 0.0f;
- }
- float delta = temperature - zeroNormalized;
- return delta / DEGREES_BETWEEN_ZERO_AND_ONE;
+ static float normalizeTemperature(float temperature, float severeThreshold) {
+ float zeroNormalized = severeThreshold - DEGREES_BETWEEN_ZERO_AND_ONE;
+ if (temperature <= zeroNormalized) {
+ return 0.0f;
}
+ float delta = temperature - zeroNormalized;
+ return delta / DEGREES_BETWEEN_ZERO_AND_ONE;
}
private static final int MINIMUM_SAMPLE_COUNT = 3;
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index 13c011a..44dad59 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -18,9 +18,11 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -28,6 +30,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -49,6 +52,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.server.SystemService;
+import com.android.server.power.ThermalManagerService.TemperatureWatcher;
import com.android.server.power.ThermalManagerService.ThermalHalWrapper;
import org.junit.Before;
@@ -65,6 +69,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
/**
* atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server
@@ -415,9 +420,9 @@
@Test
public void testTemperatureWatcherUpdateSevereThresholds() throws RemoteException {
- ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+ TemperatureWatcher watcher = mService.mTemperatureWatcher;
watcher.mSevereThresholds.erase();
- watcher.updateSevereThresholds();
+ watcher.updateThresholds();
assertEquals(1, watcher.mSevereThresholds.size());
assertEquals("skin1", watcher.mSevereThresholds.keyAt(0));
Float threshold = watcher.mSevereThresholds.get("skin1");
@@ -426,9 +431,60 @@
}
@Test
+ public void testTemperatureWatcherUpdateHeadroomThreshold() {
+ TemperatureWatcher watcher = mService.mTemperatureWatcher;
+ synchronized (watcher.mSamples) {
+ Arrays.fill(watcher.mHeadroomThresholds, Float.NaN);
+ }
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 40, 49);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 49);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 49, 49);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 49);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 70, 49);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 79, 49);
+ synchronized (watcher.mSamples) {
+ assertArrayEquals(new float[]{Float.NaN, 0.7f, 0.9f, 1.0f, 1.5f, 1.7f, 2.0f},
+ watcher.mHeadroomThresholds, 0.01f);
+ }
+
+ // when another sensor reports different threshold, we expect to see smaller one to be used
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 37, 52);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 52);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 52, 52);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 52);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 100, 52);
+ watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 200, 52);
+ synchronized (watcher.mSamples) {
+ assertArrayEquals(new float[]{Float.NaN, 0.5f, 0.8f, 1.0f, 1.4f, 1.7f, 2.0f},
+ watcher.mHeadroomThresholds, 0.01f);
+ }
+ }
+
+ @Test
+ public void testGetThermalHeadroomThresholdsOnlyReadOnce() throws Exception {
+ float[] expected = new float[]{Float.NaN, 0.1f, 0.2f, 0.3f, 0.4f, Float.NaN, 0.6f};
+ when(mIThermalServiceMock.getThermalHeadroomThresholds()).thenReturn(expected);
+ Map<Integer, Float> thresholds1 = mPowerManager.getThermalHeadroomThresholds();
+ verify(mIThermalServiceMock, times(1)).getThermalHeadroomThresholds();
+ for (int status = PowerManager.THERMAL_STATUS_LIGHT;
+ status <= PowerManager.THERMAL_STATUS_SHUTDOWN; status++) {
+ if (Float.isNaN(expected[status])) {
+ assertFalse(thresholds1.containsKey(status));
+ } else {
+ assertEquals(expected[status], thresholds1.get(status), 0.01f);
+ }
+ }
+ reset(mIThermalServiceMock);
+ Map<Integer, Float> thresholds2 = mPowerManager.getThermalHeadroomThresholds();
+ verify(mIThermalServiceMock, times(0)).getThermalHeadroomThresholds();
+ assertNotSame(thresholds1, thresholds2);
+ assertEquals(thresholds1, thresholds2);
+ }
+
+ @Test
public void testTemperatureWatcherGetSlopeOf() throws RemoteException {
- ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
- List<ThermalManagerService.TemperatureWatcher.Sample> samples = new ArrayList<>();
+ TemperatureWatcher watcher = mService.mTemperatureWatcher;
+ List<TemperatureWatcher.Sample> samples = new ArrayList<>();
for (int i = 0; i < 30; ++i) {
samples.add(watcher.createSampleForTesting(i, (float) (i / 2 * 2)));
}
@@ -437,21 +493,23 @@
@Test
public void testTemperatureWatcherNormalizeTemperature() throws RemoteException {
- ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
- assertEquals(0.5f, watcher.normalizeTemperature(25.0f, 40.0f), 0.0f);
+ assertEquals(0.5f,
+ TemperatureWatcher.normalizeTemperature(25.0f, 40.0f), 0.0f);
// Temperatures more than 30 degrees below the SEVERE threshold should be clamped to 0.0f
- assertEquals(0.0f, watcher.normalizeTemperature(0.0f, 40.0f), 0.0f);
+ assertEquals(0.0f,
+ TemperatureWatcher.normalizeTemperature(0.0f, 40.0f), 0.0f);
// Temperatures above the SEVERE threshold should not be clamped
- assertEquals(2.0f, watcher.normalizeTemperature(70.0f, 40.0f), 0.0f);
+ assertEquals(2.0f,
+ TemperatureWatcher.normalizeTemperature(70.0f, 40.0f), 0.0f);
}
@Test
public void testTemperatureWatcherGetForecast() throws RemoteException {
- ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+ TemperatureWatcher watcher = mService.mTemperatureWatcher;
- ArrayList<ThermalManagerService.TemperatureWatcher.Sample> samples = new ArrayList<>();
+ ArrayList<TemperatureWatcher.Sample> samples = new ArrayList<>();
// Add a single sample
samples.add(watcher.createSampleForTesting(0, 25.0f));
@@ -478,7 +536,7 @@
@Test
public void testTemperatureWatcherGetForecastUpdate() throws Exception {
- ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
+ TemperatureWatcher watcher = mService.mTemperatureWatcher;
// Reduce the inactivity threshold to speed up testing
watcher.mInactivityThresholdMillis = 2000;
@@ -499,7 +557,7 @@
}
// Helper function to hold mSamples lock, avoid GuardedBy lint errors
- private boolean isWatcherSamplesEmpty(ThermalManagerService.TemperatureWatcher watcher) {
+ private boolean isWatcherSamplesEmpty(TemperatureWatcher watcher) {
synchronized (watcher.mSamples) {
return watcher.mSamples.isEmpty();
}