From 795bfbaabfac07be7063b12789a5286ac1ebf579 Mon Sep 17 00:00:00 2001 From: Joe Onorato Date: Fri, 16 Nov 2018 17:41:09 -0800 Subject: [PATCH] Power model calculation based on batterystats data. Similar to the ActivityReport, the PowerReport contains the power usage for a device. To do the calculations, each of the ComponentActivity objects are called, giving them the whole activity info (in case they need to apportion blame) and the PowerProfile. From that, they compute the per-component power usage, which is then summed up into the AppPower and PowerReport objects. Test: atest frameworks/base/tools/powermodel --host Change-Id: Ibc9ada6f7f4a667152fc4af388f04766125ca74c --- .../src/com/android/powermodel/AppPower.java | 86 ++++++++++++ .../android/powermodel/ComponentActivity.java | 14 +- .../android/powermodel/ComponentPower.java | 41 ++++++ .../com/android/powermodel/PowerReport.java | 101 ++++++++++++++ .../component/ModemAppActivity.java | 39 ++++++ .../powermodel/component/ModemAppPower.java | 24 ++++ .../component/ModemGlobalActivity.java | 2 + .../component/ModemRemainderActivity.java | 38 +++++- .../component/ModemRemainderPower.java | 30 ++++ .../android/powermodel/util/Conversion.java | 4 + .../android/powermodel/PowerReportTest.java | 128 ++++++++++++++++++ 11 files changed, 504 insertions(+), 3 deletions(-) create mode 100644 tools/powermodel/src/com/android/powermodel/AppPower.java create mode 100644 tools/powermodel/src/com/android/powermodel/ComponentPower.java create mode 100644 tools/powermodel/src/com/android/powermodel/PowerReport.java create mode 100644 tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java create mode 100644 tools/powermodel/src/com/android/powermodel/component/ModemRemainderPower.java create mode 100644 tools/powermodel/test/com/android/powermodel/PowerReportTest.java diff --git a/tools/powermodel/src/com/android/powermodel/AppPower.java b/tools/powermodel/src/com/android/powermodel/AppPower.java new file mode 100644 index 0000000000000..283982b8eda6b --- /dev/null +++ b/tools/powermodel/src/com/android/powermodel/AppPower.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.powermodel; + +import java.util.HashMap; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; + +public class AppPower extends AppInfo { + private ImmutableMap mComponents; + + private double mAppPowerMah; + + + private AppPower() { + } + + /** + * Returns the {@link ComponentPower} for the {@link Component} provided, + * or null if this AppPower does not have that component. + * @more + * If the component was in the power profile for this device, there + * will be a component for it, even if there was no power used + * by that component. In that case, the + * {@link ComponentPower.getUsage() ComponentPower.getUsage()} + * method will return 0. + */ + public ComponentPower getComponentPower(Component component) { + return mComponents.get(component); + } + + public Set getComponents() { + return mComponents.keySet(); + } + + /** + * Return the total power used by this app. + */ + public double getAppPowerMah() { + return mAppPowerMah; + } + + /** + * Builder class for {@link AppPower} + */ + public static class Builder extends AppInfo.Builder { + private HashMap mComponents = new HashMap(); + + public Builder() { + } + + public AppPower build() { + final AppPower result = new AppPower(); + init(result); + result.mComponents = ImmutableMap.copyOf(mComponents); + + // Add up the components + double appPowerMah = 0; + for (final ComponentPower componentPower: mComponents.values()) { + appPowerMah += componentPower.powerMah; + } + result.mAppPowerMah = appPowerMah; + + return result; + } + + public void addComponentPower(Component component, ComponentPower componentPower) { + mComponents.put(component, componentPower); + } + } +} diff --git a/tools/powermodel/src/com/android/powermodel/ComponentActivity.java b/tools/powermodel/src/com/android/powermodel/ComponentActivity.java index e619a96687ab6..c1e2662b7b5f4 100644 --- a/tools/powermodel/src/com/android/powermodel/ComponentActivity.java +++ b/tools/powermodel/src/com/android/powermodel/ComponentActivity.java @@ -23,8 +23,20 @@ package com.android.powermodel; public class ComponentActivity { public AttributionKey attribution; - public ComponentActivity(AttributionKey attribution) { + protected ComponentActivity(AttributionKey attribution) { this.attribution = attribution; } + + // TODO: Can we refactor what goes into the activities so this function + // doesn't need the global state? + /** + * Apply the power profile for this component. Subclasses should implement this + * to do the per-component calculatinos. The default implementation returns null. + * If this method returns null, then there will be no power associated for this + * component, which, for example is true with some of the GLOBAL activities. + */ + public ComponentPower applyProfile(ActivityReport activityReport, PowerProfile profile) { + return null; + } } diff --git a/tools/powermodel/src/com/android/powermodel/ComponentPower.java b/tools/powermodel/src/com/android/powermodel/ComponentPower.java new file mode 100644 index 0000000000000..b22ff8731d6fe --- /dev/null +++ b/tools/powermodel/src/com/android/powermodel/ComponentPower.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.powermodel; + +/** + * The hardware component that uses power on a device. + *

+ * This base class contains the total power used by each Component in an app. + * Subclasses may add more detail, which is a drill-down, but is not to be + * added to {@link #powerMah}. + */ +public abstract class ComponentPower { + /** + * The app associated with this ComponentPower. + */ + public AttributionKey attribution; + + /** + * The app activity that resulted in the power usage for this component. + */ + public ACTIVITY activity; + + /** + * The total power used by this component in this app. + */ + public double powerMah; +} diff --git a/tools/powermodel/src/com/android/powermodel/PowerReport.java b/tools/powermodel/src/com/android/powermodel/PowerReport.java new file mode 100644 index 0000000000000..76ba67235b0a3 --- /dev/null +++ b/tools/powermodel/src/com/android/powermodel/PowerReport.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.powermodel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; + +/** + * PowerReport contains the summary of all power used on a device + * as reported by batterystats or statsd, based on the power profile. + */ +public class PowerReport { + private AppList mApps; + private double mTotalPowerMah; + + private PowerReport() { + } + + /** + * The total power used by this device for this PowerReport. + */ + public double getTotalPowerMah() { + return mTotalPowerMah; + } + + public List getAllApps() { + return mApps.getAllApps(); + } + + public List getRegularApps() { + return mApps.getRegularApps(); + } + + public List findApp(String pkg) { + return mApps.findApp(pkg); + } + + public AppPower findApp(SpecialApp specialApp) { + return mApps.findApp(specialApp); + } + + public static PowerReport createReport(PowerProfile profile, ActivityReport activityReport) { + final PowerReport.Builder powerReport = new PowerReport.Builder(); + for (final AppActivity appActivity: activityReport.getAllApps()) { + final AppPower.Builder appPower = new AppPower.Builder(); + appPower.setAttribution(appActivity.getAttribution()); + + for (final ImmutableMap.Entry entry: + appActivity.getComponentActivities().entrySet()) { + final ComponentPower componentPower = entry.getValue() + .applyProfile(activityReport, profile); + if (componentPower != null) { + appPower.addComponentPower(entry.getKey(), componentPower); + } + } + + powerReport.add(appPower); + } + return powerReport.build(); + } + + private static class Builder { + private AppList.Builder mApps = new AppList.Builder(); + + public Builder() { + } + + public PowerReport build() { + final PowerReport report = new PowerReport(); + + report.mApps = mApps.build(); + + for (AppPower app: report.mApps.getAllApps()) { + report.mTotalPowerMah += app.getAppPowerMah(); + } + + return report; + } + + public void add(AppPower.Builder app) { + mApps.put(app.getAttribution(), app); + } + } +} diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java b/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java index 53aa3c00aa40a..cb70051f1ae6a 100644 --- a/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java +++ b/tools/powermodel/src/com/android/powermodel/component/ModemAppActivity.java @@ -21,6 +21,7 @@ import com.android.powermodel.AttributionKey; import com.android.powermodel.Component; import com.android.powermodel.ComponentActivity; import com.android.powermodel.PowerProfile; +import com.android.powermodel.util.Conversion; /** * Encapsulates the work done by the celluar modem on behalf of an app. @@ -42,5 +43,43 @@ public class ModemAppActivity extends ComponentActivity { * The number of packets sent by the app. */ public long txPacketCount; + + @Override + public ModemAppPower applyProfile(ActivityReport activityReport, PowerProfile profile) { + // Profile + final ModemProfile modemProfile = (ModemProfile)profile.getComponent(Component.MODEM); + if (modemProfile == null) { + // TODO: This is kind of a big problem... Should this throw instead? + return null; + } + + // Activity + final ModemGlobalActivity global + = (ModemGlobalActivity)activityReport.findGlobalComponent(Component.MODEM); + if (global == null) { + return null; + } + + final double averageModemPowerMa = getAverageModemPowerMa(modemProfile); + final long totalPacketCount = global.rxPacketCount + global.txPacketCount; + final long appPacketCount = this.rxPacketCount + this.txPacketCount; + + final ModemAppPower result = new ModemAppPower(); + result.attribution = this.attribution; + result.activity = this; + result.powerMah = Conversion.msToHr( + (totalPacketCount > 0 ? (appPacketCount / (double)totalPacketCount) : 0) + * global.totalActiveTimeMs + * averageModemPowerMa); + return result; + } + + static final double getAverageModemPowerMa(ModemProfile profile) { + double sumMa = profile.getRxMa(); + for (float powerAtTxLevelMa: profile.getTxMa()) { + sumMa += powerAtTxLevelMa; + } + return sumMa / (profile.getTxMa().length + 1); + } } diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java b/tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java new file mode 100644 index 0000000000000..f5531272d0b95 --- /dev/null +++ b/tools/powermodel/src/com/android/powermodel/component/ModemAppPower.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.powermodel.component; + +import com.android.powermodel.Component; +import com.android.powermodel.ComponentPower; + +public class ModemAppPower extends ComponentPower { +} + diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java b/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java index b9b39672f0141..a53b53eede2bc 100644 --- a/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java +++ b/tools/powermodel/src/com/android/powermodel/component/ModemGlobalActivity.java @@ -19,6 +19,8 @@ package com.android.powermodel.component; import com.android.powermodel.ActivityReport; import com.android.powermodel.AttributionKey; import com.android.powermodel.ComponentActivity; +import com.android.powermodel.ComponentPower; +import com.android.powermodel.PowerProfile; /** * Encapsulates total work done by the modem for the device. diff --git a/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java b/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java index 5694de301d2d0..0e268c21d01df 100644 --- a/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java +++ b/tools/powermodel/src/com/android/powermodel/component/ModemRemainderActivity.java @@ -20,13 +20,13 @@ import com.android.powermodel.ActivityReport; import com.android.powermodel.AttributionKey; import com.android.powermodel.Component; import com.android.powermodel.ComponentActivity; +import com.android.powermodel.PowerProfile; +import com.android.powermodel.util.Conversion; /** * Encapsulates the work done by the remaining */ public class ModemRemainderActivity extends ComponentActivity { - private static final double MS_PER_HR = 3600000.0; - /** * Construct a new ModemRemainderActivity. */ @@ -49,5 +49,39 @@ public class ModemRemainderActivity extends ComponentActivity { * than an app transmitting and receiving data. */ public long activeTimeMs; + + @Override + public ModemRemainderPower applyProfile(ActivityReport activityReport, PowerProfile profile) { + // Profile + final ModemProfile modemProfile = (ModemProfile)profile.getComponent(Component.MODEM); + if (modemProfile == null) { + return null; + } + + // Activity + final ModemRemainderPower result = new ModemRemainderPower(); + result.attribution = this.attribution; + result.activity = this; + + // strengthMah + // TODO: If the array lengths don't match... then? + result.strengthMah = new double[this.strengthTimeMs.length]; + for (int i=0; i { + + public double[] strengthMah; + + public double scanningMah; + + public double activeMah; +} + diff --git a/tools/powermodel/src/com/android/powermodel/util/Conversion.java b/tools/powermodel/src/com/android/powermodel/util/Conversion.java index 9a79a2d48a59a..e556c251a1c92 100644 --- a/tools/powermodel/src/com/android/powermodel/util/Conversion.java +++ b/tools/powermodel/src/com/android/powermodel/util/Conversion.java @@ -35,6 +35,10 @@ public class Conversion { return result; } + public static double msToHr(double ms) { + return ms / 3600.0 / 1000.0; + } + /** * No public constructor. */ diff --git a/tools/powermodel/test/com/android/powermodel/PowerReportTest.java b/tools/powermodel/test/com/android/powermodel/PowerReportTest.java new file mode 100644 index 0000000000000..1a61737a4b2f0 --- /dev/null +++ b/tools/powermodel/test/com/android/powermodel/PowerReportTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.powermodel; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.Test; +import org.junit.Assert; + +import com.android.powermodel.component.ModemAppPower; +import com.android.powermodel.component.ModemRemainderPower; + +/** + * Tests {@link PowerReport}. + */ +public class PowerReportTest { + private static final double EPSILON = 0.001; + private static final double MS_PER_HR = 3600000.0; + + private static final double AVERAGE_MODEM_POWER = ((11+16+19+22+73+132) / 6.0); + private static final double GMAIL_MODEM_MAH = ((9925+5577) / (double)(97840+72941)) + * 5113727 * AVERAGE_MODEM_POWER * (1.0 / 3600 / 1000); + private static final double GMAIL_MAH + = GMAIL_MODEM_MAH; + + private static final double REMAINDER_MODEM_MAH + = (1.0 / 3600 / 1000) + * ((3066958 * 16) + (0 * 19) + (34678 * 22) + (1643364 * 73) + (7045084 * 132) + + (2443805 * 12) + + (4923676 * AVERAGE_MODEM_POWER)); + private static final double REMAINDER_MAH + = REMAINDER_MODEM_MAH; + + private static final double TOTAL_MAH + = GMAIL_MAH + + REMAINDER_MAH; + + private static InputStream loadPowerProfileStream() { + return PowerProfileTest.class.getResourceAsStream("/power_profile.xml"); + } + + private static InputStream loadCsvStream() { + return BatteryStatsReaderTest.class.getResourceAsStream("/bs.csv"); + } + + private static PowerReport loadPowerReport() throws Exception { + final PowerProfile profile = PowerProfile.parse(loadPowerProfileStream()); + final ActivityReport activity = BatteryStatsReader.parse(loadCsvStream()); + return PowerReport.createReport(profile, activity); + } + + @Test public void testModemApp() throws Exception { + final PowerReport report = loadPowerReport(); + + final List gmailList = report.findApp("com.google.android.gm"); + Assert.assertEquals(1, gmailList.size()); + final AppPower gmail = gmailList.get(0); + + final ModemAppPower modem = (ModemAppPower)gmail.getComponentPower(Component.MODEM); + Assert.assertNotNull(modem); + Assert.assertEquals(GMAIL_MODEM_MAH, modem.powerMah, EPSILON); + } + + @Test public void testModemRemainder() throws Exception { + final PowerReport report = loadPowerReport(); + + final AppPower remainder = report.findApp(SpecialApp.REMAINDER); + Assert.assertNotNull(remainder); + + final ModemRemainderPower modem + = (ModemRemainderPower)remainder.getComponentPower(Component.MODEM); + Assert.assertNotNull(modem); + + Assert.assertArrayEquals(new double[] { + 3066958 * 16.0 / MS_PER_HR, + 0 * 19.0 / MS_PER_HR, + 34678 * 22.0 / MS_PER_HR, + 1643364 * 73.0 / MS_PER_HR, + 7045084 * 132.0 / MS_PER_HR }, + modem.strengthMah, EPSILON); + Assert.assertEquals(2443805 * 12 / MS_PER_HR, modem.scanningMah, EPSILON); + Assert.assertEquals(4923676 * AVERAGE_MODEM_POWER / MS_PER_HR, modem.activeMah, EPSILON); + + Assert.assertEquals(REMAINDER_MODEM_MAH, modem.powerMah, EPSILON); + } + + @Test public void testAppTotal() throws Exception { + final PowerReport report = loadPowerReport(); + + final List gmailList = report.findApp("com.google.android.gm"); + Assert.assertEquals(1, gmailList.size()); + final AppPower gmail = gmailList.get(0); + + Assert.assertEquals(GMAIL_MAH, gmail.getAppPowerMah(), EPSILON); + } + + @Test public void testRemainderTotal() throws Exception { + final PowerReport report = loadPowerReport(); + + final AppPower remainder = report.findApp(SpecialApp.REMAINDER); + Assert.assertNotNull(remainder); + + Assert.assertEquals(REMAINDER_MAH, remainder.getAppPowerMah(), EPSILON); + } + + @Test public void testTotal() throws Exception { + final PowerReport report = loadPowerReport(); + + Assert.assertEquals(TOTAL_MAH, report.getTotalPowerMah(), EPSILON); + } +} +