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

Commit d2e374b6 authored by Dmitri Plotnikov's avatar Dmitri Plotnikov
Browse files

Include binder stats in battery stats

Bug: 158232997

Test: atest FrameworksCoreTests:com.android.internal.os.BatteryStatsBinderCallStatsTest
Change-Id: I2234776d710adf73c363bdfa6a334f3a6f36d986
parent 9a52c288
Loading
Loading
Loading
Loading
+151 −5
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBatteryPropertiesRegistrar;
@@ -70,6 +71,7 @@ import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.IntArray;
import android.util.KeyValueListParser;
@@ -120,6 +122,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -6127,10 +6130,11 @@ public class BatteryStatsImpl extends BatteryStats {
     * Records timing data related to an incoming Binder call in order to attribute
     * the power consumption to the calling app.
     */
    public void noteBinderCallStats(int workSourceUid,
    public void noteBinderCallStats(int workSourceUid, long incrementalCallCount,
            Collection<BinderCallsStats.CallStat> callStats) {
        synchronized (this) {
            getUidStatsLocked(workSourceUid).noteBinderCallStatsLocked(callStats);
            getUidStatsLocked(workSourceUid).noteBinderCallStatsLocked(incrementalCallCount,
                    callStats);
        }
    }
@@ -6576,6 +6580,65 @@ public class BatteryStatsImpl extends BatteryStats {
        }
    }
    /**
     * Accumulates stats for a specific binder transaction.
     */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    protected static class BinderCallStats {
        static final Comparator<BinderCallStats> COMPARATOR =
                Comparator.comparing(BinderCallStats::getClassName)
                        .thenComparing(BinderCallStats::getMethodName);
        public Class<? extends Binder> binderClass;
        public int transactionCode;
        public String methodName;
        public long callCount;
        public long recordedCallCount;
        public long recordedCpuTimeMicros;
        @Override
        public int hashCode() {
            return binderClass.hashCode() * 31 + transactionCode;
        }
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof BinderCallStats)) {
                return false;
            }
            BinderCallStats bcsk = (BinderCallStats) obj;
            return binderClass.equals(bcsk.binderClass) && transactionCode == bcsk.transactionCode;
        }
        public String getClassName() {
            return binderClass.getName();
        }
        public String getMethodName() {
            return methodName;
        }
        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
        public void ensureMethodName(BinderTransactionNameResolver resolver) {
            if (methodName == null) {
                methodName = resolver.getMethodName(binderClass, transactionCode);
            }
        }
        @Override
        public String toString() {
            return "BinderCallStats{"
                    + binderClass
                    + " transaction=" + transactionCode
                    + " callCount=" + callCount
                    + " recordedCallCount=" + recordedCallCount
                    + " recorderCpuTimeMicros=" + recordedCpuTimeMicros
                    + "}";
        }
    }
    /**
     * The statistics associated with a particular uid.
     */
@@ -6749,6 +6812,16 @@ public class BatteryStatsImpl extends BatteryStats {
         */
        final SparseArray<Pid> mPids = new SparseArray<>();
        /**
         * Grand total of system server binder calls made by this uid.
         */
        private long mBinderCallCount;
        /**
         * Detailed information about system server binder calls made by this uid.
         */
        private final ArraySet<BinderCallStats> mBinderCallStats = new ArraySet<>();
        public Uid(BatteryStatsImpl bsi, int uid) {
            mBsi = bsi;
            mUid = uid;
@@ -6857,6 +6930,14 @@ public class BatteryStatsImpl extends BatteryStats {
            return nullIfAllZeros(mProcStateScreenOffTimeMs[procState], which);
        }
        public long getBinderCallCount() {
            return mBinderCallCount;
        }
        public ArraySet<BinderCallStats> getBinderCallStats() {
            return mBinderCallStats;
        }
        public void addIsolatedUid(int isolatedUid) {
            if (mChildUids == null) {
                mChildUids = new IntArray();
@@ -7945,6 +8026,9 @@ public class BatteryStatsImpl extends BatteryStats {
            }
            mPackageStats.clear();
            mBinderCallCount = 0;
            mBinderCallStats.clear();
            mLastStepUserTime = mLastStepSystemTime = 0;
            mCurStepUserTime = mCurStepSystemTime = 0;
@@ -8700,15 +8784,38 @@ public class BatteryStatsImpl extends BatteryStats {
            }
        }
        // Reusable object used as a key to lookup values in mBinderCallStats
        private static BinderCallStats sTempBinderCallStats = new BinderCallStats();
        /**
         * Notes incoming binder call stats associated with this work source UID.
         */
        public void noteBinderCallStatsLocked(Collection<BinderCallsStats.CallStat> callStats) {
        public void noteBinderCallStatsLocked(long incrementalCallCount,
                Collection<BinderCallsStats.CallStat> callStats) {
            if (DEBUG) {
                Slog.d(TAG, "noteBinderCalls() workSourceUid = [" + mUid + "], callStats = ["
                Slog.d(TAG, "noteBinderCalls() workSourceUid = [" + mUid + "], "
                        + " incrementalCallCount: " + incrementalCallCount + " callStats = ["
                        + new ArrayList<>(callStats) + "]");
            }
            // TODO(dplotnikov): finish the implementation by actually remembering the stats
            mBinderCallCount += incrementalCallCount;
            for (BinderCallsStats.CallStat stat : callStats) {
                BinderCallStats bcs;
                sTempBinderCallStats.binderClass = stat.binderClass;
                sTempBinderCallStats.transactionCode = stat.transactionCode;
                int index = mBinderCallStats.indexOf(sTempBinderCallStats);
                if (index >= 0) {
                    bcs = mBinderCallStats.valueAt(index);
                } else {
                    bcs = new BinderCallStats();
                    bcs.binderClass = stat.binderClass;
                    bcs.transactionCode = stat.transactionCode;
                    mBinderCallStats.add(bcs);
                }
                bcs.callCount += stat.incrementalCallCount;
                bcs.recordedCallCount = stat.recordedCallCount;
                bcs.recordedCpuTimeMicros = stat.cpuTimeMicros;
            }
        }
        /**
@@ -13220,6 +13327,45 @@ public class BatteryStatsImpl extends BatteryStats {
            pw.print(uid.getUserCpuTimeUs(STATS_SINCE_CHARGED) / 1000); pw.print(" ");
            pw.println(uid.getSystemCpuTimeUs(STATS_SINCE_CHARGED) / 1000);
        }
        pw.println("Per UID system service calls:");
        BinderTransactionNameResolver nameResolver = new BinderTransactionNameResolver();
        for (int i = 0; i < size; i++) {
            int u = mUidStats.keyAt(i);
            Uid uid = mUidStats.get(u);
            long binderCallCount = uid.getBinderCallCount();
            if (binderCallCount != 0) {
                pw.print(" ");
                pw.print(u);
                pw.print(" system service calls: ");
                pw.print(binderCallCount);
                ArraySet<BinderCallStats> binderCallStats = uid.getBinderCallStats();
                if (!binderCallStats.isEmpty()) {
                    pw.println(", including");
                    BinderCallStats[] bcss = new BinderCallStats[binderCallStats.size()];
                    binderCallStats.toArray(bcss);
                    for (BinderCallStats bcs : bcss) {
                        bcs.ensureMethodName(nameResolver);
                    }
                    Arrays.sort(bcss, BinderCallStats.COMPARATOR);
                    for (BinderCallStats callStats : bcss) {
                        pw.print("    ");
                        pw.print(callStats.getClassName());
                        pw.print('#');
                        pw.print(callStats.getMethodName());
                        pw.print(" calls: ");
                        pw.print(callStats.callCount);
                        if (callStats.recordedCallCount != 0) {
                            pw.print(" time: ");
                            pw.print(callStats.callCount * callStats.recordedCpuTimeMicros
                                    / callStats.recordedCallCount / 1000);
                        }
                        pw.println();
                    }
                } else {
                    pw.println();
                }
            }
        }
        pw.println("Per UID CPU active time in ms:");
        for (int i = 0; i < size; i++) {
            int u = mUidStats.keyAt(i);
+23 −41
Original line number Diff line number Diff line
@@ -36,13 +36,10 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BinderInternal.CallSession;

import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -115,8 +112,13 @@ public class BinderCallsStats implements BinderInternal.Observer {
                for (int i = 0; i < size; i++) {
                    UidEntry uidEntry = mUidEntries.get(mSendUidsToObserver.valueAt(i));
                    if (uidEntry != null) {
                        ArrayMap<CallStatKey, CallStat> callStats = uidEntry.mCallStats;
                        mCallStatsObserver.noteCallStats(uidEntry.workSourceUid,
                                uidEntry.getCallStatsList());
                                uidEntry.incrementalCallCount, callStats.values());
                        uidEntry.incrementalCallCount = 0;
                        for (int j = callStats.size() - 1; j >= 0; j--) {
                            callStats.valueAt(j).incrementalCallCount = 0;
                        }
                    }
                }
                mSendUidsToObserver.clear();
@@ -233,6 +235,7 @@ public class BinderCallsStats implements BinderInternal.Observer {

            final UidEntry uidEntry = getUidEntry(workSourceUid);
            uidEntry.callCount++;
            uidEntry.incrementalCallCount++;

            if (recordCall) {
                uidEntry.cpuTimeMicros += duration;
@@ -248,6 +251,7 @@ public class BinderCallsStats implements BinderInternal.Observer {
                }

                callStat.callCount++;
                callStat.incrementalCallCount++;
                callStat.recordedCallCount++;
                callStat.cpuTimeMicros += duration;
                callStat.maxCpuTimeMicros = Math.max(callStat.maxCpuTimeMicros, duration);
@@ -269,6 +273,7 @@ public class BinderCallsStats implements BinderInternal.Observer {
                        screenInteractive);
                if (callStat != null) {
                    callStat.callCount++;
                    callStat.incrementalCallCount++;
                }
            }
            if (mCallStatsObserver != null && !UserHandle.isCore(workSourceUid)) {
@@ -307,30 +312,6 @@ public class BinderCallsStats implements BinderInternal.Observer {
        }
    }

    @Nullable
    private static Method getDefaultTransactionNameMethod(Class<? extends Binder> binder) {
        try {
            return binder.getMethod("getDefaultTransactionName", int.class);
        } catch (NoSuchMethodException e) {
            // The method might not be present for stubs not generated with AIDL.
            return null;
        }
    }

    @Nullable
    private static String resolveTransactionCode(Method getDefaultTransactionName,
            int transactionCode) {
        String resolvedCode = null;
        if (getDefaultTransactionName != null) {
            try {
                resolvedCode = (String) getDefaultTransactionName.invoke(null, transactionCode);
            } catch (IllegalAccessException | InvocationTargetException | ClassCastException e) {
                throw new RuntimeException(e);
            }
        }
        return resolvedCode == null ? String.valueOf(transactionCode) : resolvedCode;
    }

    /**
     * This method is expensive to call.
     */
@@ -369,28 +350,23 @@ public class BinderCallsStats implements BinderInternal.Observer {

        // Resolve codes outside of the lock since it can be slow.
        ExportedCallStat previous = null;
        // Cache the previous method/transaction code.
        Method getDefaultTransactionName = null;
        String previousMethodName = null;
        resultCallStats.sort(BinderCallsStats::compareByBinderClassAndCode);
        BinderTransactionNameResolver resolver = new BinderTransactionNameResolver();
        for (ExportedCallStat exported : resultCallStats) {
            final boolean isClassDifferent = previous == null
                    || !previous.className.equals(exported.className);
            if (isClassDifferent) {
                getDefaultTransactionName = getDefaultTransactionNameMethod(exported.binderClass);
            }

            final boolean isCodeDifferent = previous == null
                    || previous.transactionCode != exported.transactionCode;
            final String methodName;
            if (isClassDifferent || isCodeDifferent) {
                methodName = resolveTransactionCode(
                        getDefaultTransactionName, exported.transactionCode);
                methodName = resolver.getMethodName(exported.binderClass, exported.transactionCode);
            } else {
                methodName = previousMethodName;
            }
            previousMethodName = methodName;
            exported.methodName = methodName;
            previous = exported;
        }

        // Debug entries added to help validate the data.
@@ -675,8 +651,10 @@ public class BinderCallsStats implements BinderInternal.Observer {
        public long maxRequestSizeBytes;
        public long maxReplySizeBytes;
        public long exceptionCount;
        // Call count since reset
        public long incrementalCallCount;

        CallStat(int callingUid, Class<? extends Binder> binderClass, int transactionCode,
        public CallStat(int callingUid, Class<? extends Binder> binderClass, int transactionCode,
                boolean screenInteractive) {
            this.callingUid = callingUid;
            this.binderClass = binderClass;
@@ -686,12 +664,14 @@ public class BinderCallsStats implements BinderInternal.Observer {

        @Override
        public String toString() {
            // This is expensive, but CallStat.toString() is only used for debugging.
            String methodName = new BinderTransactionNameResolver().getMethodName(binderClass,
                    transactionCode);
            return "CallStat{"
                    + "callingUid=" + callingUid
                    + ", transaction=" + binderClass.getSimpleName()
                    + '.' + resolveTransactionCode(
                    getDefaultTransactionNameMethod(binderClass), transactionCode)
                    + ", transaction=" + binderClass.getSimpleName() + '.' + methodName
                    + ", callCount=" + callCount
                    + ", incrementalCallCount=" + incrementalCallCount
                    + ", recordedCallCount=" + recordedCallCount
                    + ", cpuTimeMicros=" + cpuTimeMicros
                    + ", latencyMicros=" + latencyMicros
@@ -744,13 +724,15 @@ public class BinderCallsStats implements BinderInternal.Observer {
        // Approximate total CPU usage can be computed by
        // cpuTimeMicros * callCount / recordedCallCount
        public long cpuTimeMicros;
        // Call count that gets reset after delivery to BatteryStats
        public long incrementalCallCount;

        UidEntry(int uid) {
            this.workSourceUid = uid;
        }

        // Aggregate time spent per each call name: call_desc -> cpu_time_micros
        private Map<CallStatKey, CallStat> mCallStats = new ArrayMap<>();
        private ArrayMap<CallStatKey, CallStat> mCallStats = new ArrayMap<>();
        private CallStatKey mTempKey = new CallStatKey();

        @Nullable
+2 −1
Original line number Diff line number Diff line
@@ -141,7 +141,8 @@ public class BinderInternal {
        /**
         * Notes incoming binder call stats associated with this work source UID.
         */
        void noteCallStats(int workSourceUid, Collection<BinderCallsStats.CallStat> callStats);
        void noteCallStats(int workSourceUid, long incrementalCallCount,
                Collection<BinderCallsStats.CallStat> callStats);
    }

    /**
+89 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

import android.os.Binder;

import com.android.internal.annotations.VisibleForTesting;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;

/**
 * Maps a binder class and transaction code to the default transaction name.  Since this
 * resolution is class-based as opposed to instance-based, any custom implementation of
 * {@link Binder#getTransactionName} will be ignored.
 *
 * The class is NOT thread safe
 *
 * @hide
 */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class BinderTransactionNameResolver {
    private static final Method NO_GET_DEFAULT_TRANSACTION_NAME_METHOD;

    /**
     * Generates the default transaction method name, which is just the transaction code.
     * Used when the binder does not define a static "getDefaultTransactionName" method.
     *
     * @hide
     */
    public static String noDefaultTransactionName(int transactionCode) {
        return String.valueOf(transactionCode);
    }

    static {
        try {
            NO_GET_DEFAULT_TRANSACTION_NAME_METHOD = BinderTransactionNameResolver.class.getMethod(
                    "noDefaultTransactionName", int.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    private final HashMap<Class<? extends Binder>, Method>
            mGetDefaultTransactionNameMethods = new HashMap<>();

    /**
     * Given a binder class name and transaction code, returns the corresponding method name.
     *
     * @hide
     */
    public String getMethodName(Class<? extends Binder> binderClass, int transactionCode) {
        Method method = mGetDefaultTransactionNameMethods.get(binderClass);
        if (method == null) {
            try {
                method = binderClass.getMethod("getDefaultTransactionName", int.class);
            } catch (NoSuchMethodException e) {
                method = NO_GET_DEFAULT_TRANSACTION_NAME_METHOD;
            }
            if (method.getReturnType() != String.class
                    || !Modifier.isStatic(method.getModifiers())) {
                method = NO_GET_DEFAULT_TRANSACTION_NAME_METHOD;
            }
            mGetDefaultTransactionNameMethods.put(binderClass, method);
        }

        try {
            return (String) method.invoke(null, transactionCode);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
}
+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.os;

import android.os.Binder;
import android.os.Process;
import android.util.ArraySet;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import junit.framework.TestCase;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Test cases for android.os.BatteryStats, system server Binder call stats.
 */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BatteryStatsBinderCallStatsTest extends TestCase {

    private static final int TRANSACTION_CODE = 100;

    /**
     * Test BatteryStatsImpl.Uid.noteBinderCallStats.
     */
    @Test
    public void testNoteBinderCallStats() throws Exception {
        final MockClocks clocks = new MockClocks(); // holds realtime and uptime in ms
        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);

        int callingUid = Process.FIRST_APPLICATION_UID + 1;
        int workSourceUid = Process.FIRST_APPLICATION_UID + 1;

        Collection<BinderCallsStats.CallStat> callStats = new ArrayList<>();
        BinderCallsStats.CallStat stat1 = new BinderCallsStats.CallStat(callingUid,
                MockBinder.class, TRANSACTION_CODE, true /*screenInteractive */);
        stat1.incrementalCallCount = 21;
        stat1.recordedCallCount = 5;
        stat1.cpuTimeMicros = 1000;
        callStats.add(stat1);

        bi.noteBinderCallStats(workSourceUid, 42, callStats);

        callStats.clear();
        BinderCallsStats.CallStat stat2 = new BinderCallsStats.CallStat(callingUid,
                MockBinder.class, TRANSACTION_CODE, true /*screenInteractive */);
        stat2.incrementalCallCount = 9;
        stat2.recordedCallCount = 8;
        stat2.cpuTimeMicros = 500;
        callStats.add(stat2);

        bi.noteBinderCallStats(workSourceUid, 8, callStats);

        BatteryStatsImpl.Uid uid = bi.getUidStatsLocked(workSourceUid);
        assertEquals(42 + 8, uid.getBinderCallCount());

        BinderTransactionNameResolver resolver = new BinderTransactionNameResolver();
        ArraySet<BatteryStatsImpl.BinderCallStats> stats = uid.getBinderCallStats();
        assertEquals(1, stats.size());
        BatteryStatsImpl.BinderCallStats value = stats.valueAt(0);
        value.ensureMethodName(resolver);
        assertEquals("testMethod", value.getMethodName());
        assertEquals(21 + 9, value.callCount);
        assertEquals(8, value.recordedCallCount);
        assertEquals(500, value.recordedCpuTimeMicros);
    }

    private static class MockBinder extends Binder {
        public static String getDefaultTransactionName(int txCode) {
            return txCode == TRANSACTION_CODE ? "testMethod" : "unknown";
        }
    }
}
Loading