diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index c34b9f09ecaa3..98082cb40f449 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -6126,6 +6126,17 @@ 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, + Collection callStats) { + synchronized (this) { + getUidStatsLocked(workSourceUid).noteBinderCallStatsLocked(callStats); + } + } + public String[] getWifiIfaces() { synchronized (mWifiNetworkLock) { return mWifiIfaces; @@ -8692,6 +8703,17 @@ public class BatteryStatsImpl extends BatteryStats { } } + /** + * Notes incoming binder call stats associated with this work source UID. + */ + public void noteBinderCallStatsLocked(Collection callStats) { + if (DEBUG) { + Slog.d(TAG, "noteBinderCalls() workSourceUid = [" + mUid + "], callStats = [" + + new ArrayList<>(callStats) + "]"); + } + // TODO(dplotnikov): finish the implementation by actually remembering the stats + } + /** * The statistics associated with a particular wake lock. */ diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java index dbba469dda1a0..4cb5b020915b8 100644 --- a/core/java/com/android/internal/os/BinderCallsStats.java +++ b/core/java/com/android/internal/os/BinderCallsStats.java @@ -19,10 +19,14 @@ package com.android.internal.os; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Binder; +import android.os.Handler; +import android.os.Looper; import android.os.Process; import android.os.SystemClock; +import android.os.UserHandle; import android.text.format.DateFormat; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -95,7 +99,33 @@ public class BinderCallsStats implements BinderInternal.Observer { private CachedDeviceState.Readonly mDeviceState; private CachedDeviceState.TimeInStateStopwatch mBatteryStopwatch; + private static final int CALL_STATS_OBSERVER_DEBOUNCE_MILLIS = 5000; private BinderLatencyObserver mLatencyObserver; + private BinderInternal.CallStatsObserver mCallStatsObserver; + private ArraySet mSendUidsToObserver = new ArraySet<>(32); + private final Handler mCallStatsObserverHandler; + private Runnable mCallStatsObserverRunnable = new Runnable() { + @Override + public void run() { + if (mCallStatsObserver == null) { + return; + } + + noteCallsStatsDelayed(); + + synchronized (mLock) { + int size = mSendUidsToObserver.size(); + for (int i = 0; i < size; i++) { + UidEntry uidEntry = mUidEntries.get(mSendUidsToObserver.valueAt(i)); + if (uidEntry != null) { + mCallStatsObserver.noteCallStats(uidEntry.workSourceUid, + uidEntry.getCallStatsList()); + } + } + mSendUidsToObserver.clear(); + } + } + }; /** Injector for {@link BinderCallsStats}. */ public static class Injector { @@ -103,6 +133,10 @@ public class BinderCallsStats implements BinderInternal.Observer { return new Random(); } + public Handler getHandler() { + return new Handler(Looper.getMainLooper()); + } + public BinderLatencyObserver getLatencyObserver() { return new BinderLatencyObserver(new BinderLatencyObserver.Injector()); } @@ -110,6 +144,7 @@ public class BinderCallsStats implements BinderInternal.Observer { public BinderCallsStats(Injector injector) { this.mRandom = injector.getRandomGenerator(); + this.mCallStatsObserverHandler = injector.getHandler(); this.mLatencyObserver = injector.getLatencyObserver(); } @@ -121,6 +156,24 @@ public class BinderCallsStats implements BinderInternal.Observer { mBatteryStopwatch = deviceState.createTimeOnBatteryStopwatch(); } + /** + * Registers an observer for call stats, which is invoked periodically with accumulated + * binder call stats. + */ + public void setCallStatsObserver( + BinderInternal.CallStatsObserver callStatsObserver) { + mCallStatsObserver = callStatsObserver; + noteCallsStatsDelayed(); + } + + private void noteCallsStatsDelayed() { + mCallStatsObserverHandler.removeCallbacks(mCallStatsObserverRunnable); + if (mCallStatsObserver != null) { + mCallStatsObserverHandler.postDelayed(mCallStatsObserverRunnable, + CALL_STATS_OBSERVER_DEBOUNCE_MILLIS); + } + } + @Override @Nullable public CallSession callStarted(Binder binder, int code, int workSourceUid) { @@ -233,6 +286,9 @@ public class BinderCallsStats implements BinderInternal.Observer { callStat.callCount++; } } + if (mCallStatsObserver != null && !UserHandle.isCore(workSourceUid)) { + mSendUidsToObserver.add(workSourceUid); + } } } @@ -267,7 +323,7 @@ public class BinderCallsStats implements BinderInternal.Observer { } @Nullable - private Method getDefaultTransactionNameMethod(Class binder) { + private static Method getDefaultTransactionNameMethod(Class binder) { try { return binder.getMethod("getDefaultTransactionName", int.class); } catch (NoSuchMethodException e) { @@ -277,16 +333,17 @@ public class BinderCallsStats implements BinderInternal.Observer { } @Nullable - private String resolveTransactionCode(Method getDefaultTransactionName, int transactionCode) { - if (getDefaultTransactionName == null) { - return null; - } - - try { - return (String) getDefaultTransactionName.invoke(null, transactionCode); - } catch (IllegalAccessException | InvocationTargetException | ClassCastException e) { - throw new RuntimeException(e); + 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; } /** @@ -342,11 +399,8 @@ public class BinderCallsStats implements BinderInternal.Observer { || previous.transactionCode != exported.transactionCode; final String methodName; if (isClassDifferent || isCodeDifferent) { - String resolvedCode = resolveTransactionCode( + methodName = resolveTransactionCode( getDefaultTransactionName, exported.transactionCode); - methodName = resolvedCode == null - ? String.valueOf(exported.transactionCode) - : resolvedCode; } else { methodName = previousMethodName; } @@ -657,6 +711,20 @@ public class BinderCallsStats implements BinderInternal.Observer { this.transactionCode = transactionCode; this.screenInteractive = screenInteractive; } + + @Override + public String toString() { + return "CallStat{" + + "callingUid=" + callingUid + + ", transaction=" + binderClass.getSimpleName() + + '.' + resolveTransactionCode( + getDefaultTransactionNameMethod(binderClass), transactionCode) + + ", callCount=" + callCount + + ", recordedCallCount=" + recordedCallCount + + ", cpuTimeMicros=" + cpuTimeMicros + + ", latencyMicros=" + latencyMicros + + '}'; + } } /** Key used to store CallStat object in a Map. */ diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java index 95c36ca8429ef..7253307feb639 100644 --- a/core/java/com/android/internal/os/BinderInternal.java +++ b/core/java/com/android/internal/os/BinderInternal.java @@ -31,6 +31,7 @@ import dalvik.system.VMRuntime; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collection; /** * Private and debugging Binder APIs. @@ -133,6 +134,16 @@ public class BinderInternal { public void callThrewException(CallSession s, Exception exception); } + /** + * Allows to track observe incoming binder call stats. + */ + public interface CallStatsObserver { + /** + * Notes incoming binder call stats associated with this work source UID. + */ + void noteCallStats(int workSourceUid, Collection callStats); + } + /** * Add the calling thread to the IPC thread pool. This function does * not return until the current process is exiting. diff --git a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java index cec62164e57a5..ee6e6c00a4dcf 100644 --- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java @@ -16,10 +16,16 @@ package com.android.internal.os; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.os.Binder; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; @@ -46,11 +52,12 @@ import java.util.Random; @RunWith(AndroidJUnit4.class) @Presubmit public class BinderCallsStatsTest { - private static final int WORKSOURCE_UID = 1; + private static final int WORKSOURCE_UID = Process.FIRST_APPLICATION_UID; private static final int CALLING_UID = 2; private static final int REQUEST_SIZE = 2; private static final int REPLY_SIZE = 3; private final CachedDeviceState mDeviceState = new CachedDeviceState(false, true); + private final TestHandler mHandler = new TestHandler(); @Test public void testDetailedOff() { @@ -753,6 +760,51 @@ public class BinderCallsStatsTest { assertEquals(1, callStats.recordedCallCount); } + @Test + public void testCallStatsObserver() { + TestBinderCallsStats bcs = new TestBinderCallsStats(); + bcs.setSamplingInterval(1); + bcs.setTrackScreenInteractive(false); + + final ArrayList callStatsList = new ArrayList<>(); + bcs.setCallStatsObserver((workSourceUid, callStats) -> callStatsList.addAll(callStats)); + + Binder binder = new Binder(); + + CallSession callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID); + bcs.time += 10; + bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID); + + callSession = bcs.callStarted(binder, 1, WORKSOURCE_UID); + bcs.time += 20; + bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID); + + callSession = bcs.callStarted(binder, 2, WORKSOURCE_UID); + bcs.time += 30; + bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID); + + for (Runnable runnable: mHandler.mRunnables) { + // Execute all pending runnables. Ignore the delay. + runnable.run(); + } + + assertThat(callStatsList).hasSize(2); + for (int i = 0; i < 2; i++) { + BinderCallsStats.CallStat callStats = callStatsList.get(i); + if (callStats.transactionCode == 1) { + assertEquals(2, callStats.callCount); + assertEquals(2, callStats.recordedCallCount); + assertEquals(30, callStats.cpuTimeMicros); + assertEquals(20, callStats.maxCpuTimeMicros); + } else { + assertEquals(1, callStats.callCount); + assertEquals(1, callStats.recordedCallCount); + assertEquals(30, callStats.cpuTimeMicros); + assertEquals(30, callStats.maxCpuTimeMicros); + } + } + } + @Test public void testLatencyCollectionEnabled() { TestBinderCallsStats bcs = new TestBinderCallsStats(); @@ -781,6 +833,20 @@ public class BinderCallsStatsTest { assertEquals(0, bcs.getLatencyObserver().getLatencyHistograms().size()); } + private static class TestHandler extends Handler { + ArrayList mRunnables = new ArrayList<>(); + + TestHandler() { + super(Looper.getMainLooper()); + } + + @Override + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + mRunnables.add(msg.getCallback()); + return true; + } + } + class TestBinderCallsStats extends BinderCallsStats { public int callingUid = CALLING_UID; public long time = 1234; @@ -803,6 +869,10 @@ public class BinderCallsStatsTest { }; } + public Handler getHandler() { + return mHandler; + } + public BinderLatencyObserver getLatencyObserver() { return new BinderLatencyObserverTest.TestBinderLatencyObserver(); } diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java index 679f18e1e8609..5c5edb2bcd499 100644 --- a/services/core/java/android/os/BatteryStatsInternal.java +++ b/services/core/java/android/os/BatteryStatsInternal.java @@ -16,6 +16,10 @@ package android.os; +import com.android.internal.os.BinderCallsStats; + +import java.util.Collection; + /** * Battery stats local system service interface. This is used to pass internal data out of * BatteryStatsImpl, as well as make unchecked calls into BatteryStatsImpl. @@ -41,4 +45,10 @@ public abstract class BatteryStatsInternal { * @param sinceLast how long in millis has it been since a job was run */ public abstract void noteJobsDeferred(int uid, int numDeferred, long sinceLast); + + /** + * Informs battery stats of binder stats for the given work source UID. + */ + public abstract void noteBinderCallStats(int workSourceUid, + Collection callStats); } diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java index f4a8f372bc36f..339ca84c93cf4 100644 --- a/services/core/java/com/android/server/BinderCallsStatsService.java +++ b/services/core/java/com/android/server/BinderCallsStatsService.java @@ -26,6 +26,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.database.ContentObserver; import android.net.Uri; +import android.os.BatteryStatsInternal; import android.os.Binder; import android.os.Process; import android.os.SystemProperties; @@ -173,10 +174,10 @@ public class BinderCallsStatsService extends Binder { } try { - mParser.setString(Settings.Global.getString(mContext.getContentResolver(), - Settings.Global.BINDER_CALLS_STATS)); + mParser.setString(Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.BINDER_CALLS_STATS)); } catch (IllegalArgumentException e) { - Slog.e(TAG, "Bad binder call stats settings", e); + Slog.e(TAG, "Bad binder call stats settings", e); } mBinderCallsStats.setDetailedTracking(mParser.getBoolean( SETTINGS_DETAILED_TRACKING_KEY, BinderCallsStats.DETAILED_TRACKING_DEFAULT)); @@ -298,6 +299,11 @@ public class BinderCallsStatsService extends Binder { CachedDeviceState.Readonly deviceState = getLocalService( CachedDeviceState.Readonly.class); mBinderCallsStats.setDeviceState(deviceState); + + BatteryStatsInternal batteryStatsInternal = getLocalService( + BatteryStatsInternal.class); + mBinderCallsStats.setCallStatsObserver(batteryStatsInternal::noteBinderCallStats); + // It needs to be called before mService.systemReady to make sure the observer is // initialized before installing it. mWorkSourceProvider.systemReady(getContext()); diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 226802c74f252..0260bebbbeac1 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -66,6 +66,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IBatteryStats; import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.BatteryStatsImpl; +import com.android.internal.os.BinderCallsStats; import com.android.internal.os.PowerProfile; import com.android.internal.os.RailStats; import com.android.internal.os.RpmStats; @@ -87,6 +88,7 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -304,6 +306,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub if (DBG) Slog.d(TAG, "Jobs deferred " + uid + ": " + numDeferred + " " + sinceLast); BatteryStatsService.this.noteJobsDeferred(uid, numDeferred, sinceLast); } + + @Override + public void noteBinderCallStats(int workSourceUid, + Collection callStats) { + mStats.noteBinderCallStats(workSourceUid, callStats); + } } private static void awaitUninterruptibly(Future future) { @@ -1771,5 +1779,4 @@ public final class BatteryStatsService extends IBatteryStats.Stub Binder.restoreCallingIdentity(ident); } } - }