From 77491b99bdc624c4b1a9d4b042e58565b583d5e6 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Thu, 9 Mar 2023 18:22:01 -0800 Subject: [PATCH 1/4] Add API SystemHealthManager.getPowerMonitorReadings For now, the API is hidden. Bug: 273308894 Test: atest FrameworksServicesTests:PowerStatsServiceTest atest FrameworksCoreTests:SystemHealthManagerTest Change-Id: I99a14752ae7914375f9e8d51238a8c025e8ca34b --- .../android/app/SystemServiceRegistry.java | 7 +- core/java/android/os/IPowerStatsService.aidl | 39 +++ core/java/android/os/PowerMonitor.aidl | 20 ++ core/java/android/os/PowerMonitor.java | 106 ++++++ .../java/android/os/PowerMonitorReadings.java | 92 +++++ .../os/health/SystemHealthManager.java | 147 +++++++- .../os/health/SystemHealthManagerTest.java | 69 ++++ .../server/powerstats/PowerStatsService.java | 313 ++++++++++++++++-- .../powerstats/PowerStatsServiceTest.java | 217 +++++++++--- 9 files changed, 936 insertions(+), 74 deletions(-) create mode 100644 core/java/android/os/IPowerStatsService.aidl create mode 100644 core/java/android/os/PowerMonitor.aidl create mode 100644 core/java/android/os/PowerMonitor.java create mode 100644 core/java/android/os/PowerMonitorReadings.java create mode 100644 core/tests/coretests/src/android/os/health/SystemHealthManagerTest.java diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index dbba0c6f5e50a..bec12640e0baf 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -171,6 +171,7 @@ import android.os.IBinder; import android.os.IDumpstate; import android.os.IHardwarePropertiesManager; import android.os.IPowerManager; +import android.os.IPowerStatsService; import android.os.IRecoverySystem; import android.os.ISystemUpdateManager; import android.os.IThermalService; @@ -1117,8 +1118,10 @@ public final class SystemServiceRegistry { new CachedServiceFetcher() { @Override public SystemHealthManager createService(ContextImpl ctx) throws ServiceNotFoundException { - IBinder b = ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME); - return new SystemHealthManager(IBatteryStats.Stub.asInterface(b)); + IBinder batteryStats = ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME); + IBinder powerStats = ServiceManager.getServiceOrThrow(Context.POWER_STATS_SERVICE); + return new SystemHealthManager(IBatteryStats.Stub.asInterface(batteryStats), + IPowerStatsService.Stub.asInterface(powerStats)); }}); registerService(Context.CONTEXTHUB_SERVICE, ContextHubManager.class, diff --git a/core/java/android/os/IPowerStatsService.aidl b/core/java/android/os/IPowerStatsService.aidl new file mode 100644 index 0000000000000..a0c2262054603 --- /dev/null +++ b/core/java/android/os/IPowerStatsService.aidl @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 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 android.os; + +import android.os.ResultReceiver; + +/** @hide */ +interface IPowerStatsService { + /** @hide */ + const String KEY_MONITORS = "monitors"; + /** @hide */ + const String KEY_ENERGY = "energy"; + /** @hide */ + const String KEY_TIMESTAMPS = "timestamps"; + + /** @hide */ + const int RESULT_SUCCESS = 0; + /** @hide */ + const int RESULT_UNSUPPORTED_POWER_MONITOR = 1; + + /** {@hide} */ + oneway void getSupportedPowerMonitors(in ResultReceiver resultReceiver); + /** {@hide} */ + oneway void getPowerMonitorReadings(in int[] powerMonitorIndices, + in ResultReceiver resultReceiver); +} diff --git a/core/java/android/os/PowerMonitor.aidl b/core/java/android/os/PowerMonitor.aidl new file mode 100644 index 0000000000000..3f8943f136c77 --- /dev/null +++ b/core/java/android/os/PowerMonitor.aidl @@ -0,0 +1,20 @@ +/* + * Copyright 2023, 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 android.os; + +/** @hide */ +@JavaOnlyStableParcelable parcelable PowerMonitor; diff --git a/core/java/android/os/PowerMonitor.java b/core/java/android/os/PowerMonitor.java new file mode 100644 index 0000000000000..ebdd463d18f68 --- /dev/null +++ b/core/java/android/os/PowerMonitor.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 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 android.os; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @hide + */ +public final class PowerMonitor implements Parcelable { + + /** + * Power monitor corresponding to a subsystem. The energy value may be a direct pass-through + * power rail measurement, or modeled in some fashion. For example, an energy consumer may + * represent a combination of multiple rails or a portion of a rail shared between subsystems, + * e.g. WiFi and Bluetooth are often handled by the same chip, powered by a shared rail. + * Some consumer names are standardized (see android.hardware.power.stats.EnergyConsumerType), + * others are not. + */ + public static final int POWER_MONITOR_TYPE_CONSUMER = 0; + + /** + * Power monitor corresponding to a directly measured power rail. Rails are device-specific: + * no assumptions can be made about the source of those measurements across different devices, + * even if they have the same name. + */ + public static final int POWER_MONITOR_TYPE_MEASUREMENT = 1; + + /** @hide */ + @IntDef(flag = true, prefix = {"POWER_MONITOR_TYPE_"}, value = { + POWER_MONITOR_TYPE_CONSUMER, + POWER_MONITOR_TYPE_MEASUREMENT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PowerMonitorType { + } + + /** + * These indices are not guaranteed to be stable across reboots and should not + * be persisted. + * + * @hide + */ + public final int index; + @PowerMonitorType + public final int type; + @NonNull + public final String name; + + /** + * @hide + */ + public PowerMonitor(int index, int type, @NonNull String name) { + this.index = index; + this.type = type; + this.name = name; + } + + private PowerMonitor(Parcel in) { + index = in.readInt(); + type = in.readInt(); + name = in.readString(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(index); + dest.writeInt(type); + dest.writeString(name); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator<>() { + @Override + public PowerMonitor createFromParcel(@NonNull Parcel in) { + return new PowerMonitor(in); + } + + @Override + public PowerMonitor[] newArray(int size) { + return new PowerMonitor[size]; + } + }; +} diff --git a/core/java/android/os/PowerMonitorReadings.java b/core/java/android/os/PowerMonitorReadings.java new file mode 100644 index 0000000000000..3d7f859a7ed58 --- /dev/null +++ b/core/java/android/os/PowerMonitorReadings.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 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 android.os; + +import android.annotation.NonNull; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * A collection of energy measurements from Power Monitors. + * + * @hide + */ +public final class PowerMonitorReadings { + public static final int ENERGY_UNAVAILABLE = -1; + + @NonNull + private final PowerMonitor[] mPowerMonitors; + @NonNull + private final long[] mEnergyUws; + @NonNull + private final long[] mTimestampsMs; + + private static final Comparator POWER_MONITOR_COMPARATOR = + Comparator.comparingInt(pm -> pm.index); + + /** + * @param powerMonitors array of power monitor (ODPM) rails, sorted by PowerMonitor.index + * @hide + */ + public PowerMonitorReadings(PowerMonitor[] powerMonitors, + long[] energyUws, long[] timestampsMs) { + mPowerMonitors = powerMonitors; + mEnergyUws = energyUws; + mTimestampsMs = timestampsMs; + } + + /** + * Returns energy consumed by the specified power monitor since boot in microwatt-seconds. + * Does not persist across reboots. + * Represents total energy: both on-battery and plugged-in. + */ + public long getConsumedEnergyUws(PowerMonitor powerMonitor) { + int offset = Arrays.binarySearch(mPowerMonitors, powerMonitor, POWER_MONITOR_COMPARATOR); + if (offset >= 0) { + return mEnergyUws[offset]; + } + return ENERGY_UNAVAILABLE; + } + + /** + * Elapsed realtime when the snapshot was taken. + */ + public long getTimestampMs(PowerMonitor powerMonitor) { + int offset = Arrays.binarySearch(mPowerMonitors, powerMonitor, POWER_MONITOR_COMPARATOR); + if (offset >= 0) { + return mTimestampsMs[offset]; + } + return 0; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(" monitors: ["); + for (int i = 0; i < mPowerMonitors.length; i++) { + if (i != 0) { + sb.append(", "); + } + sb.append(mPowerMonitors[i].name) + .append(" = ").append(mEnergyUws[i]) + .append(" (").append(mTimestampsMs[i]).append(')'); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java index 8181911ec7831..d709334e850b4 100644 --- a/core/java/android/os/health/SystemHealthManager.java +++ b/core/java/android/os/health/SystemHealthManager.java @@ -16,17 +16,28 @@ package android.os.health; +import android.annotation.NonNull; import android.annotation.SystemService; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.os.BatteryStats; import android.os.Build; +import android.os.Bundle; +import android.os.IPowerStatsService; +import android.os.PowerMonitor; +import android.os.PowerMonitorReadings; import android.os.Process; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.ServiceManager; import com.android.internal.app.IBatteryStats; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + /** * Provides access to data about how various system resources are used by applications. * @more @@ -47,19 +58,25 @@ import com.android.internal.app.IBatteryStats; @SystemService(Context.SYSTEM_HEALTH_SERVICE) public class SystemHealthManager { private final IBatteryStats mBatteryStats; + private final IPowerStatsService mPowerStats; + private PowerMonitor[] mPowerMonitorsInfo; /** * Construct a new SystemHealthManager object. + * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public SystemHealthManager() { - this(IBatteryStats.Stub.asInterface(ServiceManager.getService(BatteryStats.SERVICE_NAME))); + this(IBatteryStats.Stub.asInterface(ServiceManager.getService(BatteryStats.SERVICE_NAME)), + IPowerStatsService.Stub.asInterface( + ServiceManager.getService(Context.POWER_STATS_SERVICE))); } /** {@hide} */ - public SystemHealthManager(IBatteryStats batteryStats) { + public SystemHealthManager(IBatteryStats batteryStats, IPowerStatsService powerStats) { mBatteryStats = batteryStats; + mPowerStats = powerStats; } /** @@ -69,22 +86,20 @@ public class SystemHealthManager { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) public static SystemHealthManager from(Context context) { - return (SystemHealthManager)context.getSystemService(Context.SYSTEM_HEALTH_SERVICE); + return (SystemHealthManager) context.getSystemService(Context.SYSTEM_HEALTH_SERVICE); } /** * Return a {@link HealthStats} object containing a snapshot of system health * metrics for the given uid (user-id, which in usually corresponds to application). - * @more - * - * An application must hold the {@link android.Manifest.permission#BATTERY_STATS - * android.permission.BATTERY_STATS} permission in order to retrieve any HealthStats - * other than its own. * * @param uid User ID for a given application. * @return A {@link HealthStats} object containing the metrics for the requested * application. The keys for this HealthStats object will be from the {@link UidHealthStats} * class. + * @more An application must hold the {@link android.Manifest.permission#BATTERY_STATS + * android.permission.BATTERY_STATS} permission in order to retrieve any HealthStats + * other than its own. * @see Process#myUid() Process.myUid() */ public HealthStats takeUidSnapshot(int uid) { @@ -111,23 +126,21 @@ public class SystemHealthManager { /** * Return a {@link HealthStats} object containing a snapshot of system health * metrics for the given uids (user-id, which in usually corresponds to application). - * @more - * - * An application must hold the {@link android.Manifest.permission#BATTERY_STATS - * android.permission.BATTERY_STATS} permission in order to retrieve any HealthStats - * other than its own. * * @param uids An array of User IDs to retrieve. * @return An array of {@link HealthStats} objects containing the metrics for each of * the requested uids. The keys for this HealthStats object will be from the * {@link UidHealthStats} class. + * @more An application must hold the {@link android.Manifest.permission#BATTERY_STATS + * android.permission.BATTERY_STATS} permission in order to retrieve any HealthStats + * other than its own. */ public HealthStats[] takeUidSnapshots(int[] uids) { try { final HealthStatsParceler[] parcelers = mBatteryStats.takeUidSnapshots(uids); final HealthStats[] results = new HealthStats[uids.length]; final int N = uids.length; - for (int i=0; i future = new CompletableFuture<>(); + getSupportedPowerMonitors(future); + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Retrieves a list of supported power monitors, see {@link #getSupportedPowerMonitors()} + * + * @hide + */ + public void getSupportedPowerMonitors(@NonNull CompletableFuture future) { + synchronized (this) { + if (mPowerMonitorsInfo != null) { + future.complete(mPowerMonitorsInfo); + return; + } + try { + mPowerStats.getSupportedPowerMonitors(new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + synchronized (this) { + mPowerMonitorsInfo = resultData.getParcelableArray( + IPowerStatsService.KEY_MONITORS, PowerMonitor.class); + } + future.complete(mPowerMonitorsInfo); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Retrieves the accumulated power consumption reported by the specified power monitors. + * + * @param powerMonitors power monitors to be returned. + * @hide + */ + @NonNull + public PowerMonitorReadings getPowerMonitorReadings(@NonNull PowerMonitor[] powerMonitors) { + CompletableFuture future = new CompletableFuture<>(); + getPowerMonitorReadings(powerMonitors, future); + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static final Comparator POWER_MONITOR_COMPARATOR = + Comparator.comparingInt(pm -> pm.index); + + /** + * @param powerMonitors power monitors to be retrieved. + * @hide + */ + public void getPowerMonitorReadings(@NonNull PowerMonitor[] powerMonitors, + @NonNull CompletableFuture future) { + Arrays.sort(powerMonitors, POWER_MONITOR_COMPARATOR); + int[] indices = new int[powerMonitors.length]; + for (int i = 0; i < powerMonitors.length; i++) { + indices[i] = powerMonitors[i].index; + } + try { + mPowerStats.getPowerMonitorReadings(indices, new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == IPowerStatsService.RESULT_SUCCESS) { + future.complete(new PowerMonitorReadings(powerMonitors, + resultData.getLongArray(IPowerStatsService.KEY_ENERGY), + resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS))); + } else if (resultCode == IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR) { + future.completeExceptionally( + new IllegalArgumentException("Unsupported power monitor")); + } else { + future.completeExceptionally( + new IllegalStateException( + "Unrecognized result code " + resultCode)); + } + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/tests/coretests/src/android/os/health/SystemHealthManagerTest.java b/core/tests/coretests/src/android/os/health/SystemHealthManagerTest.java new file mode 100644 index 0000000000000..2232e3a6ffa34 --- /dev/null +++ b/core/tests/coretests/src/android/os/health/SystemHealthManagerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 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 android.os.health; + +import static androidx.test.InstrumentationRegistry.getContext; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.PowerMonitor; +import android.os.PowerMonitorReadings; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class SystemHealthManagerTest { + + @Test + public void getPowerMonitors() { + SystemHealthManager shm = getContext().getSystemService(SystemHealthManager.class); + PowerMonitor[] powerMonitorInfo = shm.getSupportedPowerMonitors(); + assertThat(powerMonitorInfo).isNotNull(); + if (powerMonitorInfo.length == 0) { + // This device does not support PowerStats HAL + return; + } + + PowerMonitor consumerMonitor = null; + PowerMonitor measurementMonitor = null; + for (PowerMonitor pmi : powerMonitorInfo) { + if (pmi.type == PowerMonitor.POWER_MONITOR_TYPE_MEASUREMENT) { + measurementMonitor = pmi; + } else { + consumerMonitor = pmi; + } + } + + List pmis = new ArrayList<>(); + if (consumerMonitor != null) { + pmis.add(consumerMonitor); + } + if (measurementMonitor != null) { + pmis.add(measurementMonitor); + } + + PowerMonitor[] selectedMonitors = pmis.toArray(new PowerMonitor[0]); + PowerMonitorReadings readings = shm.getPowerMonitorReadings(selectedMonitors); + + for (PowerMonitor monitor : selectedMonitors) { + assertThat(readings.getConsumedEnergyUws(monitor)).isAtLeast(0); + assertThat(readings.getTimestampMs(monitor)).isGreaterThan(0); + } + } +} diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java index 2638f34fe7df4..994d36c97dcb6 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsService.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java @@ -16,27 +16,34 @@ package com.android.server.powerstats; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.power.stats.Channel; import android.hardware.power.stats.EnergyConsumer; import android.hardware.power.stats.EnergyConsumerResult; +import android.hardware.power.stats.EnergyConsumerType; import android.hardware.power.stats.EnergyMeasurement; import android.hardware.power.stats.PowerEntity; import android.hardware.power.stats.StateResidencyResult; -import android.os.Binder; +import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; +import android.os.IPowerStatsService; import android.os.Looper; +import android.os.PowerMonitor; +import android.os.PowerMonitorReadings; +import android.os.ResultReceiver; import android.os.UserHandle; import android.power.PowerStatsInternal; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; import com.android.internal.util.DumpUtils; -import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.SystemService; import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper; import com.android.server.powerstats.ProtoStreamUtils.ChannelUtils; @@ -46,6 +53,8 @@ import com.android.server.powerstats.ProtoStreamUtils.PowerEntityUtils; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -65,8 +74,10 @@ public class PowerStatsService extends SystemService { private static final String METER_CACHE_FILENAME = "meterCache"; private static final String MODEL_CACHE_FILENAME = "modelCache"; private static final String RESIDENCY_CACHE_FILENAME = "residencyCache"; + private static final long MAX_POWER_MONITOR_AGE_MILLIS = 30_000; private final Injector mInjector; + private final Clock mClock; private File mDataStoragePath; private Context mContext; @@ -83,15 +94,23 @@ public class PowerStatsService extends SystemService { @Nullable @GuardedBy("this") private Looper mLooper; + private Handler mHandler; @Nullable @GuardedBy("this") private EnergyConsumer[] mEnergyConsumers = null; + @Nullable + @GuardedBy("this") + private Channel[] mEnergyMeters = null; @VisibleForTesting static class Injector { @GuardedBy("this") private IPowerStatsHALWrapper mPowerStatsHALWrapper; + Clock getClock() { + return Clock.SYSTEM_CLOCK; + } + File createDataStoragePath() { return new File(Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM), DATA_STORAGE_SUBDIR); @@ -160,7 +179,18 @@ public class PowerStatsService extends SystemService { } } - private final class BinderService extends Binder { + private final IBinder mService = new IPowerStatsService.Stub() { + + @Override + public void getSupportedPowerMonitors(ResultReceiver resultReceiver) { + getHandler().post(() -> getSupportedPowerMonitorsImpl(resultReceiver)); + } + + @Override + public void getPowerMonitorReadings(int[] powerMonitorIds, ResultReceiver resultReceiver) { + getHandler().post(() -> getPowerMonitorReadingsImpl(powerMonitorIds, resultReceiver)); + } + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; @@ -191,7 +221,7 @@ public class PowerStatsService extends SystemService { } } } - } + }; @Override public void onBootPhase(int phase) { @@ -208,7 +238,7 @@ public class PowerStatsService extends SystemService { mPowerStatsInternal = new LocalService(); publishLocalService(PowerStatsInternal.class, mPowerStatsInternal); } - publishBinderService(Context.POWER_STATS_SERVICE, new BinderService()); + publishBinderService(Context.POWER_STATS_SERVICE, mService); } private void onSystemServicesReady() { @@ -263,6 +293,15 @@ public class PowerStatsService extends SystemService { } } + private Handler getHandler() { + synchronized (this) { + if (mHandler == null) { + mHandler = new Handler(getLooper()); + } + return mHandler; + } + } + private EnergyConsumer[] getEnergyConsumerInfo() { synchronized (this) { if (mEnergyConsumers == null) { @@ -272,6 +311,15 @@ public class PowerStatsService extends SystemService { } } + private Channel[] getEnergyMeterInfo() { + synchronized (this) { + if (mEnergyMeters == null) { + mEnergyMeters = getPowerStatsHal().getEnergyMeterInfo(); + } + return mEnergyMeters; + } + } + public PowerStatsService(Context context) { this(context, new Injector()); } @@ -281,15 +329,10 @@ public class PowerStatsService extends SystemService { super(context); mContext = context; mInjector = injector; + mClock = injector.getClock(); } private final class LocalService extends PowerStatsInternal { - private final Handler mHandler; - - LocalService() { - mHandler = new Handler(getLooper()); - } - @Override public EnergyConsumer[] getEnergyConsumerInfo() { @@ -300,9 +343,8 @@ public class PowerStatsService extends SystemService { public CompletableFuture getEnergyConsumedAsync( int[] energyConsumerIds) { final CompletableFuture future = new CompletableFuture<>(); - mHandler.sendMessage( - PooledLambda.obtainMessage(PowerStatsService.this::getEnergyConsumedAsync, - future, energyConsumerIds)); + getHandler().post( + () -> PowerStatsService.this.getEnergyConsumedAsync(future, energyConsumerIds)); return future; } @@ -315,9 +357,8 @@ public class PowerStatsService extends SystemService { public CompletableFuture getStateResidencyAsync( int[] powerEntityIds) { final CompletableFuture future = new CompletableFuture<>(); - mHandler.sendMessage( - PooledLambda.obtainMessage(PowerStatsService.this::getStateResidencyAsync, - future, powerEntityIds)); + getHandler().post( + () -> PowerStatsService.this.getStateResidencyAsync(future, powerEntityIds)); return future; } @@ -330,9 +371,8 @@ public class PowerStatsService extends SystemService { public CompletableFuture readEnergyMeterAsync( int[] channelIds) { final CompletableFuture future = new CompletableFuture<>(); - mHandler.sendMessage( - PooledLambda.obtainMessage(PowerStatsService.this::readEnergyMeterAsync, - future, channelIds)); + getHandler().post( + () -> PowerStatsService.this.readEnergyMeterAsync(future, channelIds)); return future; } } @@ -413,4 +453,237 @@ public class PowerStatsService extends SystemService { int[] channelIds) { future.complete(getPowerStatsHal().readEnergyMeter(channelIds)); } + + private static class PowerMonitorState { + public final PowerMonitor powerMonitor; + public final int id; + public long timestampMs; + public long energyUws = PowerMonitorReadings.ENERGY_UNAVAILABLE; + + private PowerMonitorState(PowerMonitor powerMonitor, int id) { + this.powerMonitor = powerMonitor; + this.id = id; + } + } + + private volatile PowerMonitor[] mPowerMonitors; + private volatile PowerMonitorState[] mPowerMonitorStates; + + private void ensurePowerMonitors() { + if (mPowerMonitors != null) { + return; + } + + synchronized (this) { + if (mPowerMonitors != null) { + return; + } + + List monitors = new ArrayList<>(); + List states = new ArrayList<>(); + + int index = 0; + + Channel[] channels = getEnergyMeterInfo(); + for (Channel channel : channels) { + PowerMonitor monitor = new PowerMonitor(index++, + PowerMonitor.POWER_MONITOR_TYPE_MEASUREMENT, + getChannelName(channel)); + monitors.add(monitor); + states.add(new PowerMonitorState(monitor, channel.id)); + } + + EnergyConsumer[] energyConsumers = getEnergyConsumerInfo(); + for (EnergyConsumer consumer : energyConsumers) { + PowerMonitor monitor = new PowerMonitor(index++, + PowerMonitor.POWER_MONITOR_TYPE_CONSUMER, + getEnergyConsumerName(consumer, energyConsumers)); + monitors.add(monitor); + states.add(new PowerMonitorState(monitor, consumer.id)); + } + + mPowerMonitors = monitors.toArray(new PowerMonitor[monitors.size()]); + mPowerMonitorStates = states.toArray(new PowerMonitorState[monitors.size()]); + } + } + + @NonNull + private String getChannelName(Channel c) { + StringBuilder sb = new StringBuilder(); + sb.append('[').append(c.name).append("]:"); + if (c.subsystem != null) { + sb.append(c.subsystem); + } + return sb.toString(); + } + + @NonNull + private String getEnergyConsumerName(EnergyConsumer consumer, + EnergyConsumer[] energyConsumers) { + if (consumer.type != EnergyConsumerType.OTHER) { + StringBuilder sb = new StringBuilder(); + sb.append(energyConsumerTypeToString(consumer.type)); + boolean hasOrdinal = consumer.ordinal != 0; + if (!hasOrdinal) { + // See if any other EnergyConsumer of the same type has an ordinal + for (EnergyConsumer aConsumer : energyConsumers) { + if (aConsumer.type == consumer.type && aConsumer.ordinal != 0) { + hasOrdinal = true; + break; + } + } + } + if (hasOrdinal) { + sb.append('/').append(consumer.ordinal); + } + return sb.toString(); + } else { + return consumer.name; + } + } + + private static String energyConsumerTypeToString(int type) { + switch(type) { + case EnergyConsumerType.BLUETOOTH: return "BLUETOOTH"; + case EnergyConsumerType.CPU_CLUSTER: return "CPU"; + case EnergyConsumerType.DISPLAY: return "DISPLAY"; + case EnergyConsumerType.GNSS: return "GNSS"; + case EnergyConsumerType.MOBILE_RADIO: return "MOBILE_RADIO"; + case EnergyConsumerType.WIFI: return "WIFI"; + case EnergyConsumerType.OTHER: return ""; + default: + throw new IllegalStateException("Unrecognized EnergyConsumerType: " + type); + } + } + + /** + * Returns names of supported power monitors, including Channels and EnergyConsumers. + */ + @VisibleForTesting + public void getSupportedPowerMonitorsImpl(ResultReceiver resultReceiver) { + ensurePowerMonitors(); + Bundle result = new Bundle(); + result.putParcelableArray(IPowerStatsService.KEY_MONITORS, mPowerMonitors); + resultReceiver.send(IPowerStatsService.RESULT_SUCCESS, result); + } + + /** + * Returns the latest readings for the specified power monitors. + */ + @VisibleForTesting + public void getPowerMonitorReadingsImpl(@NonNull int[] powerMonitorIndices, + ResultReceiver resultReceiver) { + ensurePowerMonitors(); + + long earliestTimestamp = Long.MAX_VALUE; + PowerMonitorState[] powerMonitorStates = new PowerMonitorState[powerMonitorIndices.length]; + for (int i = 0; i < powerMonitorIndices.length; i++) { + int index = powerMonitorIndices[i]; + if (index < 0 || index >= mPowerMonitorStates.length) { + resultReceiver.send(IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR, null); + return; + } + + powerMonitorStates[i] = mPowerMonitorStates[index]; + if (mPowerMonitorStates[index] != null + && mPowerMonitorStates[index].timestampMs < earliestTimestamp) { + earliestTimestamp = mPowerMonitorStates[index].timestampMs; + } + } + + if (earliestTimestamp == 0 + || mClock.elapsedRealtime() - earliestTimestamp > MAX_POWER_MONITOR_AGE_MILLIS) { + updateEnergyConsumers(powerMonitorStates); + updateEnergyMeasurements(powerMonitorStates); + } + + long[] energy = new long[powerMonitorStates.length]; + long[] timestamps = new long[powerMonitorStates.length]; + for (int i = 0; i < powerMonitorStates.length; i++) { + PowerMonitorState state = powerMonitorStates[i]; + + // TODO(273310268): add random noise + energy[i] = state.energyUws; + timestamps[i] = state.timestampMs; + } + + Bundle result = new Bundle(); + result.putLongArray(IPowerStatsService.KEY_ENERGY, energy); + result.putLongArray(IPowerStatsService.KEY_TIMESTAMPS, timestamps); + resultReceiver.send(IPowerStatsService.RESULT_SUCCESS, result); + } + + private void updateEnergyConsumers(PowerMonitorState[] powerMonitorStates) { + int[] ids = collectIds(powerMonitorStates, PowerMonitor.POWER_MONITOR_TYPE_CONSUMER); + if (ids == null) { + return; + } + + EnergyConsumerResult[] energyConsumerResults = getPowerStatsHal().getEnergyConsumed(ids); + if (energyConsumerResults == null) { + return; + } + + for (PowerMonitorState powerMonitorState : powerMonitorStates) { + if (powerMonitorState.powerMonitor.type + == PowerMonitor.POWER_MONITOR_TYPE_CONSUMER) { + for (EnergyConsumerResult energyConsumerResult : energyConsumerResults) { + if (energyConsumerResult.id == powerMonitorState.id) { + powerMonitorState.energyUws = energyConsumerResult.energyUWs; + powerMonitorState.timestampMs = energyConsumerResult.timestampMs; + break; + } + } + } + } + } + + private void updateEnergyMeasurements(PowerMonitorState[] powerMonitorStates) { + int[] ids = collectIds(powerMonitorStates, PowerMonitor.POWER_MONITOR_TYPE_MEASUREMENT); + if (ids == null) { + return; + } + + EnergyMeasurement[] energyMeasurements = getPowerStatsHal().readEnergyMeter(ids); + if (energyMeasurements == null) { + return; + } + + for (PowerMonitorState powerMonitorState : powerMonitorStates) { + if (powerMonitorState.powerMonitor.type + == PowerMonitor.POWER_MONITOR_TYPE_MEASUREMENT) { + for (EnergyMeasurement energyMeasurement : energyMeasurements) { + if (energyMeasurement.id == powerMonitorState.id) { + powerMonitorState.energyUws = energyMeasurement.energyUWs; + powerMonitorState.timestampMs = energyMeasurement.timestampMs; + break; + } + } + } + } + } + + @Nullable + private int[] collectIds(PowerMonitorState[] powerMonitorStates, + @PowerMonitor.PowerMonitorType int type) { + int count = 0; + for (PowerMonitorState monitorState : powerMonitorStates) { + if (monitorState.powerMonitor.type == type) { + count++; + } + } + + if (count == 0) { + return null; + } + + int[] ids = new int[count]; + int index = 0; + for (PowerMonitorState monitorState : powerMonitorStates) { + if (monitorState.powerMonitor.type == type) { + ids[index++] = monitorState.id; + } + } + return ids; + } } diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java index 304fe5a1c9c3e..e1da41b16cf4a 100644 --- a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.powerstats; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -30,10 +32,15 @@ import android.hardware.power.stats.PowerEntity; import android.hardware.power.stats.State; import android.hardware.power.stats.StateResidency; import android.hardware.power.stats.StateResidencyResult; +import android.os.Bundle; +import android.os.IPowerStatsService; import android.os.Looper; +import android.os.PowerMonitor; +import android.os.ResultReceiver; import androidx.test.InstrumentationRegistry; +import com.android.internal.os.Clock; import com.android.server.SystemService; import com.android.server.powerstats.PowerStatsHALWrapper.IPowerStatsHALWrapper; import com.android.server.powerstats.ProtoStreamUtils.ChannelUtils; @@ -58,13 +65,15 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; import java.util.Arrays; +import java.util.Map; import java.util.Random; +import java.util.stream.Collectors; /** * Tests for {@link com.android.server.powerstats.PowerStatsService}. * * Build/Install/Run: - * atest FrameworksServicesTests:PowerStatsServiceTest + * atest FrameworksServicesTests:PowerStatsServiceTest */ public class PowerStatsServiceTest { private static final String TAG = PowerStatsServiceTest.class.getSimpleName(); @@ -90,13 +99,29 @@ public class PowerStatsServiceTest { private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); private PowerStatsService mService; + private TestPowerStatsHALWrapper mPowerStatsHALWrapper = new TestPowerStatsHALWrapper(); private File mDataStorageDir; private TimerTrigger mTimerTrigger; private BatteryTrigger mBatteryTrigger; private PowerStatsLogger mPowerStatsLogger; + private MockClock mMockClock = new MockClock(); + + private class MockClock extends Clock { + public long realtime; + + @Override + public long elapsedRealtime() { + return realtime; + } + } private final PowerStatsService.Injector mInjector = new PowerStatsService.Injector() { - private TestPowerStatsHALWrapper mTestPowerStatsHALWrapper = new TestPowerStatsHALWrapper(); + + @Override + Clock getClock() { + return mMockClock; + } + @Override File createDataStoragePath() { if (mDataStorageDir == null) { @@ -142,7 +167,7 @@ public class PowerStatsServiceTest { @Override IPowerStatsHALWrapper getPowerStatsHALWrapperImpl() { - return mTestPowerStatsHALWrapper; + return mPowerStatsHALWrapper; } @Override @@ -152,29 +177,32 @@ public class PowerStatsServiceTest { String residencyFilename, String residencyCacheFilename, IPowerStatsHALWrapper powerStatsHALWrapper) { mPowerStatsLogger = new PowerStatsLogger(context, looper, dataStoragePath, - meterFilename, meterCacheFilename, - modelFilename, modelCacheFilename, - residencyFilename, residencyCacheFilename, - powerStatsHALWrapper); + meterFilename, meterCacheFilename, + modelFilename, modelCacheFilename, + residencyFilename, residencyCacheFilename, + powerStatsHALWrapper); return mPowerStatsLogger; } @Override BatteryTrigger createBatteryTrigger(Context context, PowerStatsLogger powerStatsLogger) { mBatteryTrigger = new BatteryTrigger(context, powerStatsLogger, - false /* trigger enabled */); + false /* trigger enabled */); return mBatteryTrigger; } @Override TimerTrigger createTimerTrigger(Context context, PowerStatsLogger powerStatsLogger) { mTimerTrigger = new TimerTrigger(context, powerStatsLogger, - false /* trigger enabled */); + false /* trigger enabled */); return mTimerTrigger; } }; public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper { + public EnergyConsumerResult[] energyConsumerResults; + public EnergyMeasurement[] energyMeasurements; + @Override public PowerEntity[] getPowerEntityInfo() { PowerEntity[] powerEntityList = new PowerEntity[POWER_ENTITY_COUNT]; @@ -195,12 +223,12 @@ public class PowerStatsServiceTest { @Override public StateResidencyResult[] getStateResidency(int[] powerEntityIds) { StateResidencyResult[] stateResidencyResultList = - new StateResidencyResult[POWER_ENTITY_COUNT]; + new StateResidencyResult[POWER_ENTITY_COUNT]; for (int i = 0; i < stateResidencyResultList.length; i++) { stateResidencyResultList[i] = new StateResidencyResult(); stateResidencyResultList[i].id = i; stateResidencyResultList[i].stateResidencyData = - new StateResidency[STATE_RESIDENCY_COUNT]; + new StateResidency[STATE_RESIDENCY_COUNT]; for (int j = 0; j < stateResidencyResultList[i].stateResidencyData.length; j++) { stateResidencyResultList[i].stateResidencyData[j] = new StateResidency(); stateResidencyResultList[i].stateResidencyData[j].id = j; @@ -226,24 +254,26 @@ public class PowerStatsServiceTest { return energyConsumerList; } - @Override - public EnergyConsumerResult[] getEnergyConsumed(int[] energyConsumerIds) { - EnergyConsumerResult[] energyConsumedList = - new EnergyConsumerResult[ENERGY_CONSUMER_COUNT]; - for (int i = 0; i < energyConsumedList.length; i++) { - energyConsumedList[i] = new EnergyConsumerResult(); - energyConsumedList[i].id = i; - energyConsumedList[i].timestampMs = i; - energyConsumedList[i].energyUWs = i; - energyConsumedList[i].attribution = - new EnergyConsumerAttribution[ENERGY_CONSUMER_ATTRIBUTION_COUNT]; - for (int j = 0; j < energyConsumedList[i].attribution.length; j++) { - energyConsumedList[i].attribution[j] = new EnergyConsumerAttribution(); - energyConsumedList[i].attribution[j].uid = j; - energyConsumedList[i].attribution[j].energyUWs = j; + private void buildEnergyConsumerResult() { + energyConsumerResults = new EnergyConsumerResult[ENERGY_CONSUMER_COUNT]; + for (int i = 0; i < energyConsumerResults.length; i++) { + energyConsumerResults[i] = new EnergyConsumerResult(); + energyConsumerResults[i].id = i; + energyConsumerResults[i].timestampMs = i; + energyConsumerResults[i].energyUWs = i; + energyConsumerResults[i].attribution = + new EnergyConsumerAttribution[ENERGY_CONSUMER_ATTRIBUTION_COUNT]; + for (int j = 0; j < energyConsumerResults[i].attribution.length; j++) { + energyConsumerResults[i].attribution[j] = new EnergyConsumerAttribution(); + energyConsumerResults[i].attribution[j].uid = j; + energyConsumerResults[i].attribution[j].energyUWs = j; } } - return energyConsumedList; + } + + @Override + public EnergyConsumerResult[] getEnergyConsumed(int[] energyConsumerIds) { + return energyConsumerResults; } @Override @@ -258,17 +288,20 @@ public class PowerStatsServiceTest { return energyMeterList; } + private void buildEnergyMeasurements() { + energyMeasurements = new EnergyMeasurement[ENERGY_METER_COUNT]; + for (int i = 0; i < energyMeasurements.length; i++) { + energyMeasurements[i] = new EnergyMeasurement(); + energyMeasurements[i].id = i; + energyMeasurements[i].timestampMs = i; + energyMeasurements[i].durationMs = i; + energyMeasurements[i].energyUWs = i; + } + } + @Override public EnergyMeasurement[] readEnergyMeter(int[] channelIds) { - EnergyMeasurement[] energyMeasurementList = new EnergyMeasurement[ENERGY_METER_COUNT]; - for (int i = 0; i < energyMeasurementList.length; i++) { - energyMeasurementList[i] = new EnergyMeasurement(); - energyMeasurementList[i].id = i; - energyMeasurementList[i].timestampMs = i; - energyMeasurementList[i].durationMs = i; - energyMeasurementList[i].energyUWs = i; - } - return energyMeasurementList; + return energyMeasurements; } @Override @@ -286,6 +319,7 @@ public class PowerStatsServiceTest { public void testWrittenMeterDataMatchesReadIncidentReportData() throws InterruptedException, IOException { mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); + mPowerStatsHALWrapper.buildEnergyMeasurements(); // Write data to on-device storage. mTimerTrigger.logPowerStatsData(PowerStatsLogger.MSG_LOG_TO_DATA_STORAGE_HIGH_FREQUENCY); @@ -331,6 +365,8 @@ public class PowerStatsServiceTest { throws InterruptedException, IOException { mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); + mPowerStatsHALWrapper.buildEnergyConsumerResult(); + // Write data to on-device storage. mTimerTrigger.logPowerStatsData(PowerStatsLogger.MSG_LOG_TO_DATA_STORAGE_LOW_FREQUENCY); @@ -368,7 +404,7 @@ public class PowerStatsServiceTest { == ENERGY_CONSUMER_ATTRIBUTION_COUNT); for (int j = 0; j < pssProto.energyConsumerResult[i].attribution.length; j++) { assertTrue(pssProto.energyConsumerResult[i].attribution[j].uid == j); - assertTrue(pssProto.energyConsumerResult[i].attribution[j].energyUws == j); + assertTrue(pssProto.energyConsumerResult[i].attribution[j].energyUws == j); } } } @@ -1008,4 +1044,111 @@ public class PowerStatsServiceTest { assertTrue(modelFile.exists()); assertTrue(residencyFile.exists()); } + + private static class GetSupportedPowerMonitorsResult extends ResultReceiver { + public PowerMonitor[] powerMonitors; + + GetSupportedPowerMonitorsResult() { + super(null); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + powerMonitors = resultData.getParcelableArray(IPowerStatsService.KEY_MONITORS, + PowerMonitor.class); + } + } + + @Test + public void getSupportedPowerMonitors() { + GetSupportedPowerMonitorsResult result = new GetSupportedPowerMonitorsResult(); + mService.getSupportedPowerMonitorsImpl(result); + assertThat(result.powerMonitors).isNotNull(); + assertThat(Arrays.stream(result.powerMonitors).map(pm -> pm.name).toList()) + .containsAtLeast( + "energyconsumer0", + "BLUETOOTH/1", + "[channelname0]:channelsubsystem0", + "[channelname1]:channelsubsystem1"); + } + + private static class GetPowerMonitorsResult extends ResultReceiver { + public long[] energyUws; + public long[] timestamps; + + GetPowerMonitorsResult() { + super(null); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + energyUws = resultData.getLongArray(IPowerStatsService.KEY_ENERGY); + timestamps = resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS); + } + } + + @Test + public void getPowerMonitors() { + mMockClock.realtime = 10 * 60_000; + + mPowerStatsHALWrapper.buildEnergyConsumerResult(); + EnergyConsumerResult[] energyConsumerResults = mPowerStatsHALWrapper.energyConsumerResults; + for (int i = 0; i < energyConsumerResults.length; i++) { + energyConsumerResults[i].energyUWs = 42 + 100 * i; + energyConsumerResults[i].timestampMs = mMockClock.realtime + 100 * i; + } + + mPowerStatsHALWrapper.buildEnergyMeasurements(); + EnergyMeasurement[] energyMeasurements = mPowerStatsHALWrapper.energyMeasurements; + for (int i = 0; i < energyMeasurements.length; i++) { + energyMeasurements[i].energyUWs = 314 + 200 * i; + energyMeasurements[i].timestampMs = mMockClock.realtime + 200 * i; + } + + GetSupportedPowerMonitorsResult supportedPowerMonitorsResult = + new GetSupportedPowerMonitorsResult(); + mService.getSupportedPowerMonitorsImpl(supportedPowerMonitorsResult); + Map map = + Arrays.stream(supportedPowerMonitorsResult.powerMonitors) + .collect(Collectors.toMap(pm -> pm.name, pm -> pm)); + PowerMonitor consumer1 = map.get("energyconsumer0"); + PowerMonitor consumer2 = map.get("BLUETOOTH/1"); + PowerMonitor measurement1 = map.get("[channelname0]:channelsubsystem0"); + PowerMonitor measurement2 = map.get("[channelname1]:channelsubsystem1"); + + GetPowerMonitorsResult result = new GetPowerMonitorsResult(); + mService.getPowerMonitorReadingsImpl( + new int[]{consumer1.index, consumer2.index, measurement1.index, + measurement2.index}, result); + + assertThat(result.energyUws).isEqualTo(new long[]{42, 142, 314, 514}); + assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_100, 600_000, 600_200}); + + // Test caching/throttling + mMockClock.realtime += 1; + + for (EnergyConsumerResult energyConsumerResult : energyConsumerResults) { + energyConsumerResult.energyUWs = 300; + energyConsumerResult.timestampMs = mMockClock.realtime + 300; + } + + for (EnergyMeasurement energyMeasurement : energyMeasurements) { + energyMeasurement.energyUWs = 400; + energyMeasurement.timestampMs = mMockClock.realtime + 400; + } + + mService.getPowerMonitorReadingsImpl(new int[]{consumer1.index, measurement1.index}, + result); + + assertThat(result.energyUws).isEqualTo(new long[]{42, 314}); + assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_000}); + + mMockClock.realtime += 10 * 60000; + + mService.getPowerMonitorReadingsImpl(new int[]{consumer1.index, measurement1.index}, + result); + + assertThat(result.energyUws).isEqualTo(new long[]{300, 400}); + assertThat(result.timestamps).isEqualTo(new long[]{600_301, 600_401}); + } } From 381754364732793b9a4f911a515dc44a59979058 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Fri, 31 Mar 2023 11:37:08 -0700 Subject: [PATCH 2/4] Add feature flag for PowerMonitor API Bug: 273308894 Test: atest FrameworksServicesTests:PowerStatsServiceTest Change-Id: I1b027c2f24cb6aeeba7e790fc279fc4d69d33ef8 --- .../server/powerstats/PowerStatsService.java | 49 +++++++++++++++++++ .../powerstats/PowerStatsServiceTest.java | 42 +++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java index 994d36c97dcb6..6d4c5a7891903 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsService.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java @@ -29,6 +29,7 @@ import android.hardware.power.stats.StateResidencyResult; import android.os.Bundle; import android.os.Environment; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.HandlerThread; import android.os.IBinder; import android.os.IPowerStatsService; @@ -38,6 +39,8 @@ import android.os.PowerMonitorReadings; import android.os.ResultReceiver; import android.os.UserHandle; import android.power.PowerStatsInternal; +import android.provider.DeviceConfig; +import android.provider.DeviceConfigInterface; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -56,6 +59,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; /** * This class provides a system service that estimates system power usage @@ -76,8 +80,12 @@ public class PowerStatsService extends SystemService { private static final String RESIDENCY_CACHE_FILENAME = "residencyCache"; private static final long MAX_POWER_MONITOR_AGE_MILLIS = 30_000; + static final String KEY_POWER_MONITOR_API_ENABLED = "power_monitor_api_enabled"; + private final Injector mInjector; private final Clock mClock; + private final DeviceConfigInterface mDeviceConfig; + private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener(); private File mDataStoragePath; private Context mContext; @@ -177,6 +185,10 @@ public class PowerStatsService extends SystemService { PowerStatsInternal powerStatsInternal) { return new StatsPullAtomCallbackImpl(context, powerStatsInternal); } + + DeviceConfigInterface getDeviceConfig() { + return DeviceConfigInterface.REAL; + } } private final IBinder mService = new IPowerStatsService.Stub() { @@ -223,6 +235,20 @@ public class PowerStatsService extends SystemService { } }; + private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener { + public Executor mExecutor = new HandlerExecutor(getHandler()); + + void startListening() { + mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_BATTERY_STATS, + mExecutor, this); + } + + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + refreshFlags(); + } + } + @Override public void onBootPhase(int phase) { if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { @@ -243,6 +269,8 @@ public class PowerStatsService extends SystemService { private void onSystemServicesReady() { mPullAtomCallback = mInjector.createStatsPullerImpl(mContext, mPowerStatsInternal); + mDeviceConfigListener.startListening(); + refreshFlags(); } @VisibleForTesting @@ -330,6 +358,12 @@ public class PowerStatsService extends SystemService { mContext = context; mInjector = injector; mClock = injector.getClock(); + mDeviceConfig = injector.getDeviceConfig(); + } + + void refreshFlags() { + setPowerMonitorApiEnabled(mDeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BATTERY_STATS, + KEY_POWER_MONITOR_API_ENABLED, true)); } private final class LocalService extends PowerStatsInternal { @@ -466,9 +500,18 @@ public class PowerStatsService extends SystemService { } } + private boolean mPowerMonitorApiEnabled = true; private volatile PowerMonitor[] mPowerMonitors; private volatile PowerMonitorState[] mPowerMonitorStates; + private void setPowerMonitorApiEnabled(boolean powerMonitorApiEnabled) { + if (powerMonitorApiEnabled != mPowerMonitorApiEnabled) { + mPowerMonitorApiEnabled = powerMonitorApiEnabled; + mPowerMonitors = null; + mPowerMonitorStates = null; + } + } + private void ensurePowerMonitors() { if (mPowerMonitors != null) { return; @@ -479,6 +522,12 @@ public class PowerStatsService extends SystemService { return; } + if (!mPowerMonitorApiEnabled) { + mPowerMonitors = new PowerMonitor[0]; + mPowerMonitorStates = new PowerMonitorState[0]; + return; + } + List monitors = new ArrayList<>(); List states = new ArrayList<>(); diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java index e1da41b16cf4a..0034597a1d6aa 100644 --- a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java @@ -37,6 +37,8 @@ import android.os.IPowerStatsService; import android.os.Looper; import android.os.PowerMonitor; import android.os.ResultReceiver; +import android.provider.DeviceConfig; +import android.provider.DeviceConfigInterface; import androidx.test.InstrumentationRegistry; @@ -53,6 +55,7 @@ import com.android.server.powerstats.nano.PowerStatsServiceResidencyProto; import com.android.server.powerstats.nano.StateProto; import com.android.server.powerstats.nano.StateResidencyProto; import com.android.server.powerstats.nano.StateResidencyResultProto; +import com.android.server.testutils.FakeDeviceConfigInterface; import org.junit.Before; import org.junit.Test; @@ -105,6 +108,7 @@ public class PowerStatsServiceTest { private BatteryTrigger mBatteryTrigger; private PowerStatsLogger mPowerStatsLogger; private MockClock mMockClock = new MockClock(); + private DeviceConfigInterface mMockDeviceConfig = new FakeDeviceConfigInterface(); private class MockClock extends Clock { public long realtime; @@ -197,6 +201,10 @@ public class PowerStatsServiceTest { false /* trigger enabled */); return mTimerTrigger; } + + DeviceConfigInterface getDeviceConfig() { + return mMockDeviceConfig; + } }; public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper { @@ -1073,6 +1081,7 @@ public class PowerStatsServiceTest { } private static class GetPowerMonitorsResult extends ResultReceiver { + public int resultCode; public long[] energyUws; public long[] timestamps; @@ -1082,8 +1091,11 @@ public class PowerStatsServiceTest { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - energyUws = resultData.getLongArray(IPowerStatsService.KEY_ENERGY); - timestamps = resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS); + this.resultCode = resultCode; + if (resultData != null) { + energyUws = resultData.getLongArray(IPowerStatsService.KEY_ENERGY); + timestamps = resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS); + } } } @@ -1151,4 +1163,30 @@ public class PowerStatsServiceTest { assertThat(result.energyUws).isEqualTo(new long[]{300, 400}); assertThat(result.timestamps).isEqualTo(new long[]{600_301, 600_401}); } + + @Test + public void featureFlag() { + mMockDeviceConfig.setProperty(DeviceConfig.NAMESPACE_BATTERY_STATS, + PowerStatsService.KEY_POWER_MONITOR_API_ENABLED, "false", false); + + mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); + + GetSupportedPowerMonitorsResult supportedPowerMonitorsResult = + new GetSupportedPowerMonitorsResult(); + mService.getSupportedPowerMonitorsImpl(supportedPowerMonitorsResult); + assertThat(supportedPowerMonitorsResult.powerMonitors).isNotNull(); + assertThat(supportedPowerMonitorsResult.powerMonitors).isEmpty(); + + GetPowerMonitorsResult getPowerMonitorsResult = new GetPowerMonitorsResult(); + mService.getPowerMonitorReadingsImpl(new int[]{0}, getPowerMonitorsResult); + assertThat(getPowerMonitorsResult.resultCode).isEqualTo( + IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR); + + mMockDeviceConfig.setProperty(DeviceConfig.NAMESPACE_BATTERY_STATS, + PowerStatsService.KEY_POWER_MONITOR_API_ENABLED, "true", false); + supportedPowerMonitorsResult = new GetSupportedPowerMonitorsResult(); + mService.getSupportedPowerMonitorsImpl(supportedPowerMonitorsResult); + assertThat(Arrays.stream(supportedPowerMonitorsResult.powerMonitors) + .map(pm -> pm.name).toList()).contains("energyconsumer0"); + } } From 1ea5f812f7ef8b45923de61c2a321d6de51a935e Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Tue, 21 Mar 2023 11:15:08 -0700 Subject: [PATCH 3/4] Add random noise generator for Power Monitor API Bug: 273310268 Test: atest FrameworksServicesTests:IntervalRandomNoiseGeneratorTest Change-Id: Icf61ba54096d398e0d3722d05be7e2a196670936 --- services/core/Android.bp | 1 + .../IntervalRandomNoiseGenerator.java | 83 ++++++++++++++ .../IntervalRandomNoiseGeneratorTest.java | 108 ++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 services/core/java/com/android/server/powerstats/IntervalRandomNoiseGenerator.java create mode 100644 services/tests/servicestests/src/com/android/server/powerstats/IntervalRandomNoiseGeneratorTest.java diff --git a/services/core/Android.bp b/services/core/Android.bp index 14247c5d45d67..feb1b7d3147e4 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -185,6 +185,7 @@ java_library_static { "com.android.sysprop.watchdog", "ImmutabilityAnnotation", "securebox", + "apache-commons-math", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/powerstats/IntervalRandomNoiseGenerator.java b/services/core/java/com/android/server/powerstats/IntervalRandomNoiseGenerator.java new file mode 100644 index 0000000000000..444f8855a6a78 --- /dev/null +++ b/services/core/java/com/android/server/powerstats/IntervalRandomNoiseGenerator.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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.powerstats; + +import com.android.internal.annotations.VisibleForTesting; + +import org.apache.commons.math.MathException; +import org.apache.commons.math.distribution.AbstractContinuousDistribution; +import org.apache.commons.math.distribution.BetaDistributionImpl; + +import java.util.Arrays; + +/** + * Adds random noise to provided value, keeping it within the limits of a specified range. + * @hide + */ +public class IntervalRandomNoiseGenerator { + private static final int DISTRIBUTION_SAMPLE_SIZE = 17; + + private final AbstractContinuousDistribution mDistribution; + private final double[] mSamples = new double[DISTRIBUTION_SAMPLE_SIZE]; + + private static final double UNINITIALIZED = -1; + + /** + * Higher alpha makes the distribution more asymmetrical, tightening it + * closer to the high bound. A value of alpha should be > 1 to ensure + * that the samples closer to 1 appear more frequently t those closer + * to 0. + */ + IntervalRandomNoiseGenerator(double alpha) { + if (alpha <= 1) { + throw new IllegalArgumentException("alpha should be > 1"); + } + mDistribution = new BetaDistributionImpl(alpha, 1 /* beta */); + refresh(); + } + + @VisibleForTesting + void reseed(long seed) { + mDistribution.reseedRandomGenerator(seed); + } + + /** + * Returns a random value between the specified bounds, statistically closer to the + * highProbabilityBound. + * + * The same value is returned for a given stickyKey until {@link #refresh()} is called. + */ + long addNoise(long lowProbabilityBound, long highProbabilityBound, int stickyKey) { + double sample = mSamples[stickyKey % DISTRIBUTION_SAMPLE_SIZE]; + if (sample < 0) { // UNINITIALIZED + try { + sample = mDistribution.sample(); + } catch (MathException e) { + throw new IllegalStateException(e); + } + mSamples[stickyKey % DISTRIBUTION_SAMPLE_SIZE] = sample; + } + return lowProbabilityBound + (long) ((highProbabilityBound - lowProbabilityBound) * sample); + } + + /** + * Resets the cache of random samples. + */ + void refresh() { + Arrays.fill(mSamples, UNINITIALIZED); + } +} diff --git a/services/tests/servicestests/src/com/android/server/powerstats/IntervalRandomNoiseGeneratorTest.java b/services/tests/servicestests/src/com/android/server/powerstats/IntervalRandomNoiseGeneratorTest.java new file mode 100644 index 0000000000000..99621460f3608 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/powerstats/IntervalRandomNoiseGeneratorTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 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.powerstats; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.util.function.Supplier; + +public class IntervalRandomNoiseGeneratorTest { + + @Test + public void parameterizedDistribution() { + // Assert closeness to theoretical distribution + assertDistribution(3.0, + 0.0392, + 0.2617, + 0.6990); + + assertDistribution(5.0, + 0.0003, + 0.0098, + 0.0676, + 0.2502, + 0.6720); + + assertDistribution(9.0, + 0.0000, + 0.0002, + 0.0097, + 0.1242, + 0.8658); + } + + private void assertDistribution(double alpha, Double... expectedBuckets) { + IntervalRandomNoiseGenerator generator = new IntervalRandomNoiseGenerator(alpha); + generator.reseed(42); // Make test repeatable + final int sampleCount = 1000; + final int bucketCount = expectedBuckets.length; + int[] histogram = buildHistogram(() -> { + generator.refresh(); + return generator.addNoise(100, 200, 12345); + }, sampleCount, bucketCount, 100, 200); + + for (int i = 0; i < expectedBuckets.length; i++) { + assertWithMessage("Bucket #" + i) + .that((double) histogram[i] / sampleCount) + .isWithin(0.05) + .of(expectedBuckets[i]); + } + } + + @NotNull + private int[] buildHistogram(Supplier generator, int sampleCount, + int bucketCount, int lowBound, int highBound) { + int[] buckets = new int[bucketCount]; + for (int i = 0; i < sampleCount; i++) { + long sample = generator.get(); + assertThat(sample).isAtLeast(lowBound); + assertThat(sample).isAtMost(highBound); + buckets[(int) ((double) (sample - lowBound) / (highBound - lowBound) * bucketCount)]++; + } + return buckets; + } + + @Test + public void stickiness() { + IntervalRandomNoiseGenerator generator = new IntervalRandomNoiseGenerator(9); + generator.reseed(42); // Make test repeatable + + long value1a = generator.addNoise(1000, 5000, 123); + long value1b = generator.addNoise(1000, 5000, 123); + long value1c = generator.addNoise(1000, 5000, 123); + assertThat(value1b).isEqualTo(value1a); + assertThat(value1c).isEqualTo(value1a); + + // Different stickyKey + long value2a = generator.addNoise(1000, 5000, 321); + long value2b = generator.addNoise(1000, 5000, 321); + assertThat(value2a).isNotEqualTo(value1a); + assertThat(value2b).isEqualTo(value2a); + + generator.refresh(); + + // Same stickyKey after a refresh - different value + long value3 = generator.addNoise(1000, 5000, 123); + long value4 = generator.addNoise(1000, 5000, 321); + assertThat(value3).isNotEqualTo(value1a); + assertThat(value4).isNotEqualTo(value2a); + } +} From 293b4b3ad799099773b7a288cf68288f971f0e94 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Wed, 19 Apr 2023 15:45:50 -0700 Subject: [PATCH 4/4] Add random noise to power monitor readings Bug: 273310268 Test: atest FrameworksServicesTests:PowerStatsServiceTest Change-Id: I12127999edcff0609fbccee4db91de86ff338838 --- .../server/powerstats/PowerStatsService.java | 54 ++++++++++++++++--- .../powerstats/PowerStatsServiceTest.java | 19 +++++-- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java index 6d4c5a7891903..9832c490a491b 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsService.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java @@ -26,6 +26,7 @@ import android.hardware.power.stats.EnergyConsumerType; import android.hardware.power.stats.EnergyMeasurement; import android.hardware.power.stats.PowerEntity; import android.hardware.power.stats.StateResidencyResult; +import android.os.Binder; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -41,6 +42,7 @@ import android.os.UserHandle; import android.power.PowerStatsInternal; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; +import android.util.Log; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -58,6 +60,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -82,6 +85,11 @@ public class PowerStatsService extends SystemService { static final String KEY_POWER_MONITOR_API_ENABLED = "power_monitor_api_enabled"; + // The alpha parameter of the Beta distribution used by the random noise generator. + // The higher this value, the smaller the amount of added noise. + private static final double INTERVAL_RANDOM_NOISE_GENERATION_ALPHA = 50; + private static final long MAX_RANDOM_NOISE_UWS = 10_000_000; + private final Injector mInjector; private final Clock mClock; private final DeviceConfigInterface mDeviceConfig; @@ -189,6 +197,10 @@ public class PowerStatsService extends SystemService { DeviceConfigInterface getDeviceConfig() { return DeviceConfigInterface.REAL; } + + IntervalRandomNoiseGenerator createIntervalRandomNoiseGenerator() { + return new IntervalRandomNoiseGenerator(INTERVAL_RANDOM_NOISE_GENERATION_ALPHA); + } } private final IBinder mService = new IPowerStatsService.Stub() { @@ -200,7 +212,9 @@ public class PowerStatsService extends SystemService { @Override public void getPowerMonitorReadings(int[] powerMonitorIds, ResultReceiver resultReceiver) { - getHandler().post(() -> getPowerMonitorReadingsImpl(powerMonitorIds, resultReceiver)); + int callingUid = Binder.getCallingUid(); + getHandler().post(() -> + getPowerMonitorReadingsImpl(powerMonitorIds, resultReceiver, callingUid)); } @Override @@ -493,6 +507,7 @@ public class PowerStatsService extends SystemService { public final int id; public long timestampMs; public long energyUws = PowerMonitorReadings.ENERGY_UNAVAILABLE; + public long prevEnergyUws; private PowerMonitorState(PowerMonitor powerMonitor, int id) { this.powerMonitor = powerMonitor; @@ -502,7 +517,8 @@ public class PowerStatsService extends SystemService { private boolean mPowerMonitorApiEnabled = true; private volatile PowerMonitor[] mPowerMonitors; - private volatile PowerMonitorState[] mPowerMonitorStates; + private PowerMonitorState[] mPowerMonitorStates; + private IntervalRandomNoiseGenerator mIntervalRandomNoiseGenerator; private void setPowerMonitorApiEnabled(boolean powerMonitorApiEnabled) { if (powerMonitorApiEnabled != mPowerMonitorApiEnabled) { @@ -522,6 +538,10 @@ public class PowerStatsService extends SystemService { return; } + if (mIntervalRandomNoiseGenerator == null) { + mIntervalRandomNoiseGenerator = mInjector.createIntervalRandomNoiseGenerator(); + } + if (!mPowerMonitorApiEnabled) { mPowerMonitors = new PowerMonitor[0]; mPowerMonitorStates = new PowerMonitorState[0]; @@ -621,7 +641,7 @@ public class PowerStatsService extends SystemService { */ @VisibleForTesting public void getPowerMonitorReadingsImpl(@NonNull int[] powerMonitorIndices, - ResultReceiver resultReceiver) { + ResultReceiver resultReceiver, int callingUid) { ensurePowerMonitors(); long earliestTimestamp = Long.MAX_VALUE; @@ -644,15 +664,35 @@ public class PowerStatsService extends SystemService { || mClock.elapsedRealtime() - earliestTimestamp > MAX_POWER_MONITOR_AGE_MILLIS) { updateEnergyConsumers(powerMonitorStates); updateEnergyMeasurements(powerMonitorStates); + mIntervalRandomNoiseGenerator.refresh(); } long[] energy = new long[powerMonitorStates.length]; long[] timestamps = new long[powerMonitorStates.length]; for (int i = 0; i < powerMonitorStates.length; i++) { PowerMonitorState state = powerMonitorStates[i]; - - // TODO(273310268): add random noise - energy[i] = state.energyUws; + if (state.energyUws != PowerMonitorReadings.ENERGY_UNAVAILABLE + && state.prevEnergyUws != PowerMonitorReadings.ENERGY_UNAVAILABLE) { + energy[i] = mIntervalRandomNoiseGenerator.addNoise( + Math.max(state.prevEnergyUws, state.energyUws - MAX_RANDOM_NOISE_UWS), + state.energyUws, callingUid); + if (DEBUG) { + Log.d(TAG, String.format(Locale.ENGLISH, + "Monitor=%s timestamp=%d energy=%d" + + " uid=%d noise=%.1f%% returned=%d", + state.powerMonitor.name, + state.timestampMs, + state.energyUws, + callingUid, + state.energyUws != state.prevEnergyUws + ? (state.energyUws - energy[i]) * 100.0 + / (state.energyUws - state.prevEnergyUws) + : 0, + energy[i])); + } + } else { + energy[i] = state.energyUws; + } timestamps[i] = state.timestampMs; } @@ -678,6 +718,7 @@ public class PowerStatsService extends SystemService { == PowerMonitor.POWER_MONITOR_TYPE_CONSUMER) { for (EnergyConsumerResult energyConsumerResult : energyConsumerResults) { if (energyConsumerResult.id == powerMonitorState.id) { + powerMonitorState.prevEnergyUws = powerMonitorState.energyUws; powerMonitorState.energyUws = energyConsumerResult.energyUWs; powerMonitorState.timestampMs = energyConsumerResult.timestampMs; break; @@ -703,6 +744,7 @@ public class PowerStatsService extends SystemService { == PowerMonitor.POWER_MONITOR_TYPE_MEASUREMENT) { for (EnergyMeasurement energyMeasurement : energyMeasurements) { if (energyMeasurement.id == powerMonitorState.id) { + powerMonitorState.prevEnergyUws = powerMonitorState.energyUws; powerMonitorState.energyUws = energyMeasurement.energyUWs; powerMonitorState.timestampMs = energyMeasurement.timestampMs; break; diff --git a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java index 0034597a1d6aa..2ffe4aacda734 100644 --- a/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/powerstats/PowerStatsServiceTest.java @@ -99,6 +99,7 @@ public class PowerStatsServiceTest { private static final int POWER_ENTITY_COUNT = 3; private static final int STATE_INFO_COUNT = 5; private static final int STATE_RESIDENCY_COUNT = 4; + private static final int APP_UID = 10042; private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); private PowerStatsService mService; @@ -109,6 +110,7 @@ public class PowerStatsServiceTest { private PowerStatsLogger mPowerStatsLogger; private MockClock mMockClock = new MockClock(); private DeviceConfigInterface mMockDeviceConfig = new FakeDeviceConfigInterface(); + private IntervalRandomNoiseGenerator mMockNoiseGenerator = new IntervalRandomNoiseGenerator(42); private class MockClock extends Clock { public long realtime; @@ -205,6 +207,11 @@ public class PowerStatsServiceTest { DeviceConfigInterface getDeviceConfig() { return mMockDeviceConfig; } + + @Override + IntervalRandomNoiseGenerator createIntervalRandomNoiseGenerator() { + return mMockNoiseGenerator; + } }; public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper { @@ -1102,6 +1109,7 @@ public class PowerStatsServiceTest { @Test public void getPowerMonitors() { mMockClock.realtime = 10 * 60_000; + mMockNoiseGenerator.reseed(314); mPowerStatsHALWrapper.buildEnergyConsumerResult(); EnergyConsumerResult[] energyConsumerResults = mPowerStatsHALWrapper.energyConsumerResults; @@ -1131,7 +1139,7 @@ public class PowerStatsServiceTest { GetPowerMonitorsResult result = new GetPowerMonitorsResult(); mService.getPowerMonitorReadingsImpl( new int[]{consumer1.index, consumer2.index, measurement1.index, - measurement2.index}, result); + measurement2.index}, result, APP_UID); assertThat(result.energyUws).isEqualTo(new long[]{42, 142, 314, 514}); assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_100, 600_000, 600_200}); @@ -1150,7 +1158,7 @@ public class PowerStatsServiceTest { } mService.getPowerMonitorReadingsImpl(new int[]{consumer1.index, measurement1.index}, - result); + result, APP_UID); assertThat(result.energyUws).isEqualTo(new long[]{42, 314}); assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_000}); @@ -1158,9 +1166,10 @@ public class PowerStatsServiceTest { mMockClock.realtime += 10 * 60000; mService.getPowerMonitorReadingsImpl(new int[]{consumer1.index, measurement1.index}, - result); + result, APP_UID); - assertThat(result.energyUws).isEqualTo(new long[]{300, 400}); + // This time, random noise is added + assertThat(result.energyUws).isEqualTo(new long[]{298, 399}); assertThat(result.timestamps).isEqualTo(new long[]{600_301, 600_401}); } @@ -1178,7 +1187,7 @@ public class PowerStatsServiceTest { assertThat(supportedPowerMonitorsResult.powerMonitors).isEmpty(); GetPowerMonitorsResult getPowerMonitorsResult = new GetPowerMonitorsResult(); - mService.getPowerMonitorReadingsImpl(new int[]{0}, getPowerMonitorsResult); + mService.getPowerMonitorReadingsImpl(new int[]{0}, getPowerMonitorsResult, APP_UID); assertThat(getPowerMonitorsResult.resultCode).isEqualTo( IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR);