Add experiment flag to control binder call stats.

For instance, to enabled detailed tracking locally.
adb shell settings put global binder_calls_stats detailed_tracking=true

Also adds the ability to turn off data collection completely and
changing the sampling interval. Uploading data through westworld can
re-use the same flag once implemented.

Test: Unit tested

Change-Id: I808c9902b8124ab643d9b197703d537da040ae3e
diff --git a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
index e4a8503..e126fb8 100644
--- a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
@@ -45,7 +45,7 @@
 
     @Before
     public void setUp() {
-        mBinderCallsStats = new BinderCallsStats(true);
+        mBinderCallsStats = new BinderCallsStats();
     }
 
     @After
@@ -54,6 +54,7 @@
 
     @Test
     public void timeCallSession() {
+        mBinderCallsStats.setDetailedTracking(true);
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         Binder b = new Binder();
         int i = 0;
@@ -66,9 +67,9 @@
 
     @Test
     public void timeCallSessionTrackingDisabled() {
+        mBinderCallsStats.setDetailedTracking(false);
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         Binder b = new Binder();
-        mBinderCallsStats = new BinderCallsStats(false);
         while (state.keepRunning()) {
             BinderCallsStats.CallSession s = mBinderCallsStats.callStarted(b, 0);
             mBinderCallsStats.callEnded(s, 0, 0);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 12f4ca85..bd428ce 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -13008,6 +13008,21 @@
          */
         public static final String GNSS_HAL_LOCATION_REQUEST_DURATION_MILLIS =
                 "gnss_hal_location_request_duration_millis";
+
+        /**
+         * Binder call stats settings.
+         *
+         * The following strings are supported as keys:
+         * <pre>
+         *     enabled              (boolean)
+         *     detailed_tracking    (boolean)
+         *     upload_data          (boolean)
+         *     sampling_interval    (int)
+         * </pre>
+         *
+         * @hide
+         */
+        public static final String BINDER_CALLS_STATS = "binder_calls_stats";
     }
 
     /**
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index 96702a0..fb0a18e 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -45,14 +45,21 @@
  * per thread, uid or call description.
  */
 public class BinderCallsStats {
+    public static final boolean ENABLED_DEFAULT = true;
+    public static final boolean DETAILED_TRACKING_DEFAULT = true;
+    public static final int PERIODIC_SAMPLING_INTERVAL_DEFAULT = 10;
+
     private static final String TAG = "BinderCallsStats";
     private static final int CALL_SESSIONS_POOL_SIZE = 100;
     private static final int PERIODIC_SAMPLING_INTERVAL = 10;
     private static final int MAX_EXCEPTION_COUNT_SIZE = 50;
     private static final String EXCEPTION_COUNT_OVERFLOW_NAME = "overflow";
+    private static final CallSession NOT_ENABLED = new CallSession();
     private static final BinderCallsStats sInstance = new BinderCallsStats();
 
-    private volatile boolean mDetailedTracking = false;
+    private volatile boolean mEnabled = ENABLED_DEFAULT;
+    private volatile boolean mDetailedTracking = DETAILED_TRACKING_DEFAULT;
+    private volatile int mPeriodicSamplingInterval = PERIODIC_SAMPLING_INTERVAL_DEFAULT;
     @GuardedBy("mLock")
     private final SparseArray<UidEntry> mUidEntries = new SparseArray<>();
     @GuardedBy("mLock")
@@ -63,12 +70,8 @@
     @GuardedBy("mLock")
     private UidEntry mSampledEntries = new UidEntry(-1);
 
-    private BinderCallsStats() {
-    }
-
-    @VisibleForTesting
-    public BinderCallsStats(boolean detailedTracking) {
-        mDetailedTracking = detailedTracking;
+    @VisibleForTesting  // Use getInstance() instead.
+    public BinderCallsStats() {
     }
 
     public CallSession callStarted(Binder binder, int code) {
@@ -76,10 +79,15 @@
     }
 
     private CallSession callStarted(String className, int code) {
+        if (!mEnabled) {
+          return NOT_ENABLED;
+        }
+
         CallSession s = mCallSessionsPool.poll();
         if (s == null) {
             s = new CallSession();
         }
+
         s.callStat.className = className;
         s.callStat.msg = code;
         s.exceptionThrown = false;
@@ -92,7 +100,7 @@
                 s.timeStarted = getElapsedRealtimeMicro();
             } else {
                 s.sampledCallStat = mSampledEntries.getOrCreate(s.callStat);
-                if (s.sampledCallStat.callCount % PERIODIC_SAMPLING_INTERVAL == 0) {
+                if (s.sampledCallStat.callCount % mPeriodicSamplingInterval == 0) {
                     s.cpuTimeStarted = getThreadTimeMicro();
                     s.timeStarted = getElapsedRealtimeMicro();
                 }
@@ -103,7 +111,23 @@
 
     public void callEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
         Preconditions.checkNotNull(s);
+        if (s == NOT_ENABLED) {
+          return;
+        }
+
+        processCallEnded(s, parcelRequestSize, parcelReplySize);
+
+        if (mCallSessionsPool.size() < CALL_SESSIONS_POOL_SIZE) {
+            mCallSessionsPool.add(s);
+        }
+    }
+
+    private void processCallEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
         synchronized (mLock) {
+            if (!mEnabled) {
+              return;
+            }
+
             long duration;
             long latencyDuration;
             if (mDetailedTracking) {
@@ -117,7 +141,7 @@
                     latencyDuration = getElapsedRealtimeMicro() - s.timeStarted;
                 } else {
                     // callCount is always incremented, but time only once per sampling interval
-                    long samplesCount = cs.callCount / PERIODIC_SAMPLING_INTERVAL + 1;
+                    long samplesCount = cs.callCount / mPeriodicSamplingInterval + 1;
                     duration = cs.cpuTimeMicros / samplesCount;
                     latencyDuration = cs.latencyMicros / samplesCount;
                 }
@@ -155,9 +179,6 @@
             uidEntry.cpuTimeMicros += duration;
             uidEntry.callCount++;
         }
-        if (mCallSessionsPool.size() < CALL_SESSIONS_POOL_SIZE) {
-            mCallSessionsPool.add(s);
-        }
     }
 
     /**
@@ -169,6 +190,9 @@
      */
     public void callThrewException(CallSession s, Exception exception) {
         Preconditions.checkNotNull(s);
+        if (!mEnabled) {
+          return;
+        }
         s.exceptionThrown = true;
         try {
             String className = exception.getClass().getName();
@@ -192,6 +216,11 @@
     }
 
     private void dumpLocked(PrintWriter pw, Map<Integer,String> appIdToPkgNameMap, boolean verbose) {
+        if (!mEnabled) {
+          pw.println("Binder calls stats disabled.");
+          return;
+        }
+
         long totalCallsCount = 0;
         long totalCpuTime = 0;
         pw.print("Start time: ");
@@ -245,7 +274,7 @@
             for (CallStat e : sampledStatsList) {
                 sb.setLength(0);
                 sb.append("    ").append(e)
-                        .append(',').append(e.cpuTimeMicros * PERIODIC_SAMPLING_INTERVAL)
+                        .append(',').append(e.cpuTimeMicros * mPeriodicSamplingInterval)
                         .append(',').append(e.callCount)
                         .append(',').append(e.exceptionCount);
                 pw.println(sb);
@@ -304,9 +333,29 @@
     }
 
     public void setDetailedTracking(boolean enabled) {
-        if (enabled != mDetailedTracking) {
-            reset();
-            mDetailedTracking = enabled;
+        synchronized (mLock) {
+          if (enabled != mDetailedTracking) {
+              mDetailedTracking = enabled;
+              reset();
+          }
+        }
+    }
+
+    public void setEnabled(boolean enabled) {
+        synchronized (mLock) {
+            if (enabled != mEnabled) {
+                mEnabled = enabled;
+                reset();
+            }
+        }
+    }
+
+    public void setSamplingInterval(int samplingInterval) {
+        synchronized (mLock) {
+            if (samplingInterval != mPeriodicSamplingInterval) {
+                mPeriodicSamplingInterval = samplingInterval;
+                reset();
+            }
         }
     }
 
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 98e3589..dafaebc 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -124,6 +124,7 @@
                     Settings.Global.BATTERY_DISCHARGE_THRESHOLD,
                     Settings.Global.BATTERY_SAVER_DEVICE_SPECIFIC_CONSTANTS,
                     Settings.Global.BATTERY_STATS_CONSTANTS,
+                    Settings.Global.BINDER_CALLS_STATS,
                     Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE,
                     Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS,
                     Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS,
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 2f9f758..914fb74 100644
--- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
@@ -46,7 +46,9 @@
 
     @Test
     public void testDetailedOff() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(false);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(false);
+
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.time += 10;
@@ -98,7 +100,9 @@
 
     @Test
     public void testDetailedOn() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(true);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(true);
+
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.time += 10;
@@ -145,8 +149,86 @@
     }
 
     @Test
+    public void testDisabled() {
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setEnabled(false);
+
+        Binder binder = new Binder();
+        BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
+        bcs.time += 10;
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+        SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
+        assertEquals(0, uidEntries.size());
+    }
+
+    @Test
+    public void testDisableInBetweenCall() {
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setEnabled(true);
+
+        Binder binder = new Binder();
+        BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
+        bcs.time += 10;
+        bcs.setEnabled(false);
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+        SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
+        assertEquals(0, uidEntries.size());
+    }
+
+    @Test
+    public void testEnableInBetweenCall() {
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setEnabled(false);
+
+        Binder binder = new Binder();
+        BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
+        bcs.time += 10;
+        bcs.setEnabled(true);
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+        SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
+        assertEquals(0, uidEntries.size());
+    }
+
+    @Test
+    public void testSampling() {
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(false);
+        bcs.setSamplingInterval(2);
+
+        Binder binder = new Binder();
+        BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
+        bcs.time += 10;
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+        callSession = bcs.callStarted(binder, 1);
+        bcs.time += 1000;  // shoud be ignored.
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+        callSession = bcs.callStarted(binder, 1);
+        bcs.time += 50;
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+        SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
+        assertEquals(1, uidEntries.size());
+        BinderCallsStats.UidEntry uidEntry = uidEntries.get(TEST_UID);
+        Assert.assertNotNull(uidEntry);
+        assertEquals(3, uidEntry.callCount);
+        assertEquals(70, uidEntry.cpuTimeMicros);
+        assertEquals("Detailed tracking off - no entries should be returned",
+                0, uidEntry.getCallStatsList().size());
+
+        BinderCallsStats.UidEntry sampledEntries = bcs.getSampledEntries();
+        List<BinderCallsStats.CallStat> sampledCallStatsList = sampledEntries.getCallStatsList();
+        assertEquals(1, sampledCallStatsList.size());
+    }
+
+    @Test
     public void testParcelSize() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(true);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(true);
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.time += 10;
@@ -161,7 +243,8 @@
 
     @Test
     public void testMaxCpu() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(true);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(true);
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.time += 50;
@@ -179,7 +262,8 @@
 
     @Test
     public void testMaxLatency() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(true);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(true);
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.elapsedTime += 5;
@@ -205,7 +289,8 @@
 
     @Test
     public void testExceptionCount() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(true);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(true);
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.callThrewException(callSession, new IllegalStateException());
@@ -227,7 +312,8 @@
 
     @Test
     public void testDumpDoesNotThrowException() {
-        TestBinderCallsStats bcs = new TestBinderCallsStats(true);
+        TestBinderCallsStats bcs = new TestBinderCallsStats();
+        bcs.setDetailedTracking(true);
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
         bcs.callThrewException(callSession, new IllegalStateException());
@@ -242,8 +328,7 @@
         long time = 1234;
         long elapsedTime = 0;
 
-        TestBinderCallsStats(boolean detailedTracking) {
-            super(detailedTracking);
+        TestBinderCallsStats() {
         }
 
         @Override
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index 490fcc1..3d779d8 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -17,15 +17,20 @@
 package com.android.server;
 
 import android.app.AppGlobals;
+import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
 import android.util.Slog;
 
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.BinderCallsStats;
 
 import java.io.FileDescriptor;
@@ -41,18 +46,90 @@
     private static final String PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING
             = "persist.sys.binder_calls_detailed_tracking";
 
-    public static void start() {
-        BinderCallsStatsService service = new BinderCallsStatsService();
-        ServiceManager.addService("binder_calls_stats", service);
-        boolean detailedTrackingEnabled = SystemProperties.getBoolean(
-                PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING, false);
+    /** Listens for flag changes. */
+    private static class SettingsObserver extends ContentObserver {
+        private static final String SETTINGS_ENABLED_KEY = "enabled";
+        private static final String SETTINGS_DETAILED_TRACKING_KEY = "detailed_tracking";
+        private static final String SETTINGS_UPLOAD_DATA_KEY = "upload_data";
+        private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
 
-        if (detailedTrackingEnabled) {
-            Slog.i(TAG, "Enabled CPU usage tracking for binder calls. Controlled by "
-                    + PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING
-                    + " or via dumpsys binder_calls_stats --enable-detailed-tracking");
-            BinderCallsStats.getInstance().setDetailedTracking(true);
+        private final Uri mUri = Settings.Global.getUriFor(Settings.Global.BINDER_CALLS_STATS);
+        private final Context mContext;
+        private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+        public SettingsObserver(Context context) {
+            super(BackgroundThread.getHandler());
+            mContext = context;
+            context.getContentResolver().registerContentObserver(mUri, false, this,
+                    UserHandle.USER_SYSTEM);
+            // Always kick once to ensure that we match current state
+            onChange();
         }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri, int userId) {
+            if (mUri.equals(uri)) {
+                onChange();
+            }
+        }
+
+        public void onChange() {
+            // Do not overwrite the default set manually.
+            if (!SystemProperties.get(PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING).isEmpty()) {
+              return;
+            }
+
+            BinderCallsStats stats = BinderCallsStats.getInstance();
+            try {
+                    mParser.setString(Settings.Global.getString(mContext.getContentResolver(),
+                            Settings.Global.BINDER_CALLS_STATS));
+            } catch (IllegalArgumentException e) {
+                    Slog.e(TAG, "Bad binder call stats settings", e);
+            }
+            stats.setEnabled(
+                    mParser.getBoolean(SETTINGS_ENABLED_KEY, BinderCallsStats.ENABLED_DEFAULT));
+            stats.setDetailedTracking(mParser.getBoolean(
+                    SETTINGS_DETAILED_TRACKING_KEY, BinderCallsStats.DETAILED_TRACKING_DEFAULT));
+            stats.setSamplingInterval(mParser.getInt(
+                    SETTINGS_SAMPLING_INTERVAL_KEY,
+                    BinderCallsStats.PERIODIC_SAMPLING_INTERVAL_DEFAULT));
+        }
+    }
+
+    public static class LifeCycle extends SystemService {
+        private BinderCallsStatsService mService;
+
+        public LifeCycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            mService = new BinderCallsStatsService();
+            publishBinderService("binder_calls_stats", mService);
+            boolean detailedTrackingEnabled = SystemProperties.getBoolean(
+                    PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING, false);
+
+            if (detailedTrackingEnabled) {
+                Slog.i(TAG, "Enabled CPU usage tracking for binder calls. Controlled by "
+                        + PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING
+                        + " or via dumpsys binder_calls_stats --enable-detailed-tracking");
+                BinderCallsStats.getInstance().setDetailedTracking(true);
+            }
+        }
+
+        @Override
+        public void onBootPhase(int phase) {
+            if (SystemService.PHASE_SYSTEM_SERVICES_READY == phase) {
+                mService.systemReady(getContext());
+            }
+        }
+    }
+
+    private SettingsObserver mSettingsObserver;
+
+    public void systemReady(Context context) {
+        mSettingsObserver = new SettingsObserver(context);
     }
 
     public static void reset() {
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 1f1b3f8..252a1fd 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -722,7 +722,7 @@
 
         // Tracks cpu time spent in binder calls
         traceBeginAndSlog("StartBinderCallsStatsService");
-        BinderCallsStatsService.start();
+        mSystemServiceManager.startService(BinderCallsStatsService.LifeCycle.class);
         traceEnd();
     }