Binder calls stats - random sampling.

Change the sampling method to be random. The previous mechanism recorded
one call for each key (uid/API name) and every X calls for each key:
- This is biased and will make it to interpret the data from
westworld.  It was especially unfair for apps using many different APIs
since the first call to each API was always recorded.
- It uses more memory since we will keep track of all the long tail

Simplify/unify the way we keep track of sampled calls.

Do not estimate the CPU usage of non-recorded calls
    long samplesCount = cs.callCount / mPeriodicSamplingInterval + 1;
    duration = cs.cpuTimeMicros / samplesCount;
It biases the results, let's use an example with 3 calls: 1ms, 3ms, 5ms
with an sampling interval of 2. With the previous algorithm we would get an
average per call of (1+1+3+2+5)/5=2.4ms. With the new one (1+3+5)/3=3ms.

Test: unit tests
Change-Id: I1dd7eb3c6c631b86a53485bccbfa397882cccc92
diff --git a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
index e126fb8..2b8b8f2 100644
--- a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
@@ -23,6 +23,8 @@
 
 import com.android.internal.os.BinderCallsStats;
 
+import java.util.Random;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -45,7 +47,7 @@
 
     @Before
     public void setUp() {
-        mBinderCallsStats = new BinderCallsStats();
+        mBinderCallsStats = new BinderCallsStats(new Random());
     }
 
     @After
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index f87c081..63c7dd0 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -38,6 +38,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
+import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.function.ToDoubleFunction;
@@ -57,7 +58,7 @@
     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 static final BinderCallsStats sInstance = new BinderCallsStats(new Random());
 
     private volatile boolean mEnabled = ENABLED_DEFAULT;
     private volatile boolean mDetailedTracking = DETAILED_TRACKING_DEFAULT;
@@ -68,12 +69,12 @@
     private final ArrayMap<String, Integer> mExceptionCounts = new ArrayMap<>();
     private final Queue<CallSession> mCallSessionsPool = new ConcurrentLinkedQueue<>();
     private final Object mLock = new Object();
+    private final Random mRandom;
     private long mStartTime = System.currentTimeMillis();
-    @GuardedBy("mLock")
-    private UidEntry mSampledEntries = new UidEntry(-1);
 
     @VisibleForTesting  // Use getInstance() instead.
-    public BinderCallsStats() {
+    public BinderCallsStats(Random random) {
+        this.mRandom = random;
     }
 
     public CallSession callStarted(Binder binder, int code) {
@@ -82,7 +83,7 @@
 
     private CallSession callStarted(String className, int code, @Nullable String methodName) {
         if (!mEnabled) {
-          return NOT_ENABLED;
+            return NOT_ENABLED;
         }
 
         CallSession s = mCallSessionsPool.poll();
@@ -98,16 +99,12 @@
         s.timeStarted = -1;
 
         synchronized (mLock) {
-            if (mDetailedTracking) {
-                s.cpuTimeStarted = getThreadTimeMicro();
-                s.timeStarted = getElapsedRealtimeMicro();
-            } else {
-                s.sampledCallStat = mSampledEntries.getOrCreate(s.callStat);
-                if (s.sampledCallStat.callCount % mPeriodicSamplingInterval == 0) {
-                    s.cpuTimeStarted = getThreadTimeMicro();
-                    s.timeStarted = getElapsedRealtimeMicro();
-                }
+            if (!mDetailedTracking && !shouldTrackCall()) {
+                return s;
             }
+
+            s.cpuTimeStarted = getThreadTimeMicro();
+            s.timeStarted = getElapsedRealtimeMicro();
         }
         return s;
     }
@@ -115,7 +112,7 @@
     public void callEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
         Preconditions.checkNotNull(s);
         if (s == NOT_ENABLED) {
-          return;
+            return;
         }
 
         processCallEnded(s, parcelRequestSize, parcelReplySize);
@@ -128,60 +125,42 @@
     private void processCallEnded(CallSession s, int parcelRequestSize, int parcelReplySize) {
         synchronized (mLock) {
             if (!mEnabled) {
-              return;
+                return;
             }
 
-            long duration;
-            long latencyDuration;
-            if (mDetailedTracking) {
-                duration = getThreadTimeMicro() - s.cpuTimeStarted;
-                latencyDuration = getElapsedRealtimeMicro() - s.timeStarted;
-            } else {
-                CallStat cs = s.sampledCallStat;
-                // Non-negative time signals beginning of the new sampling interval
-                if (s.cpuTimeStarted >= 0) {
-                    duration = getThreadTimeMicro() - s.cpuTimeStarted;
-                    latencyDuration = getElapsedRealtimeMicro() - s.timeStarted;
-                } else {
-                    // callCount is always incremented, but time only once per sampling interval
-                    long samplesCount = cs.callCount / mPeriodicSamplingInterval + 1;
-                    duration = cs.cpuTimeMicros / samplesCount;
-                    latencyDuration = cs.latencyMicros / samplesCount;
-                }
-            }
-
-            int callingUid = getCallingUid();
-
+            final int callingUid = getCallingUid();
             UidEntry uidEntry = mUidEntries.get(callingUid);
             if (uidEntry == null) {
                 uidEntry = new UidEntry(callingUid);
                 mUidEntries.put(callingUid, uidEntry);
             }
-
-            CallStat callStat;
-            if (mDetailedTracking) {
-                // Find CallStat entry and update its total time
-                callStat = uidEntry.getOrCreate(s.callStat);
-                callStat.exceptionCount += s.exceptionThrown ? 1 : 0;
-                callStat.maxRequestSizeBytes =
-                        Math.max(callStat.maxRequestSizeBytes, parcelRequestSize);
-                callStat.maxReplySizeBytes =
-                        Math.max(callStat.maxReplySizeBytes, parcelReplySize);
-            } else {
-                // update sampled timings in the beginning of each interval
-                callStat = s.sampledCallStat;
-            }
+            uidEntry.callCount++;
+            CallStat callStat = uidEntry.getOrCreate(s.callStat);
             callStat.callCount++;
-            callStat.methodName = s.callStat.methodName;
-            if (s.cpuTimeStarted >= 0) {
+
+            // Non-negative time signals we need to record data for this call.
+            final boolean recordCall = s.cpuTimeStarted >= 0;
+            if (recordCall) {
+                final long duration = getThreadTimeMicro() - s.cpuTimeStarted;
+                final long latencyDuration = getElapsedRealtimeMicro() - s.timeStarted;
+                uidEntry.cpuTimeMicros += duration;
+                uidEntry.recordedCallCount++;
+
+                callStat.recordedCallCount++;
+                callStat.methodName = s.callStat.methodName;
                 callStat.cpuTimeMicros += duration;
                 callStat.maxCpuTimeMicros = Math.max(callStat.maxCpuTimeMicros, duration);
                 callStat.latencyMicros += latencyDuration;
-                callStat.maxLatencyMicros = Math.max(callStat.maxLatencyMicros, latencyDuration);
+                callStat.maxLatencyMicros =
+                        Math.max(callStat.maxLatencyMicros, latencyDuration);
+                if (mDetailedTracking) {
+                    callStat.exceptionCount += s.exceptionThrown ? 1 : 0;
+                    callStat.maxRequestSizeBytes =
+                            Math.max(callStat.maxRequestSizeBytes, parcelRequestSize);
+                    callStat.maxReplySizeBytes =
+                            Math.max(callStat.maxReplySizeBytes, parcelReplySize);
+                }
             }
-
-            uidEntry.cpuTimeMicros += duration;
-            uidEntry.callCount++;
         }
     }
 
@@ -195,28 +174,28 @@
     public void callThrewException(CallSession s, Exception exception) {
         Preconditions.checkNotNull(s);
         if (!mEnabled) {
-          return;
+            return;
         }
         s.exceptionThrown = true;
         try {
             String className = exception.getClass().getName();
             synchronized (mLock) {
                 if (mExceptionCounts.size() >= MAX_EXCEPTION_COUNT_SIZE) {
-                  className = EXCEPTION_COUNT_OVERFLOW_NAME;
+                    className = EXCEPTION_COUNT_OVERFLOW_NAME;
                 }
                 Integer count = mExceptionCounts.get(className);
                 mExceptionCounts.put(className, count == null ? 1 : count + 1);
             }
         } catch (RuntimeException e) {
-          // Do not propagate the exception. We do not want to swallow original exception.
-          Log.wtf(TAG, "Unexpected exception while updating mExceptionCounts", e);
+            // Do not propagate the exception. We do not want to swallow original exception.
+            Log.wtf(TAG, "Unexpected exception while updating mExceptionCounts", e);
         }
     }
 
     public ArrayList<ExportedCallStat> getExportedCallStats() {
         // We do not collect all the data if detailed tracking is off.
         if (!mDetailedTracking) {
-          return new ArrayList<ExportedCallStat>();
+            return new ArrayList<ExportedCallStat>();
         }
 
         ArrayList<ExportedCallStat> resultCallStats = new ArrayList<>();
@@ -229,11 +208,12 @@
                     exported.uid = entry.uid;
                     exported.className = stat.className;
                     exported.methodName = stat.methodName == null
-                        ? String.valueOf(stat.msg) : stat.methodName;
+                            ? String.valueOf(stat.msg) : stat.methodName;
                     exported.cpuTimeMicros = stat.cpuTimeMicros;
                     exported.maxCpuTimeMicros = stat.maxCpuTimeMicros;
                     exported.latencyMicros = stat.latencyMicros;
                     exported.maxLatencyMicros = stat.maxLatencyMicros;
+                    exported.recordedCallCount = stat.recordedCallCount;
                     exported.callCount = stat.callCount;
                     exported.maxRequestSizeBytes = stat.maxRequestSizeBytes;
                     exported.maxReplySizeBytes = stat.maxReplySizeBytes;
@@ -254,14 +234,16 @@
 
     private void dumpLocked(PrintWriter pw, Map<Integer,String> appIdToPkgNameMap, boolean verbose) {
         if (!mEnabled) {
-          pw.println("Binder calls stats disabled.");
-          return;
+            pw.println("Binder calls stats disabled.");
+            return;
         }
 
         long totalCallsCount = 0;
+        long totalRecordedCallsCount = 0;
         long totalCpuTime = 0;
         pw.print("Start time: ");
         pw.println(DateFormat.format("yyyy-MM-dd HH:mm:ss", mStartTime));
+        pw.println("Sampling interval period: " + mPeriodicSamplingInterval);
         List<UidEntry> entries = new ArrayList<>();
 
         int uidEntriesSize = mUidEntries.size();
@@ -269,70 +251,53 @@
             UidEntry e = mUidEntries.valueAt(i);
             entries.add(e);
             totalCpuTime += e.cpuTimeMicros;
+            totalRecordedCallsCount += e.recordedCallCount;
             totalCallsCount += e.callCount;
         }
 
         entries.sort(Comparator.<UidEntry>comparingDouble(value -> value.cpuTimeMicros).reversed());
         String datasetSizeDesc = verbose ? "" : "(top 90% by cpu time) ";
         StringBuilder sb = new StringBuilder();
-        if (mDetailedTracking) {
-            pw.println("Per-UID raw data " + datasetSizeDesc
-                    + "(package/uid, call_desc, cpu_time_micros, max_cpu_time_micros, "
-                    + "latency_time_micros, max_latency_time_micros, exception_count, "
-                    + "max_request_size_bytes, max_reply_size_bytes, call_count):");
-            List<UidEntry> topEntries = verbose ? entries
-                    : getHighestValues(entries, value -> value.cpuTimeMicros, 0.9);
-            for (UidEntry uidEntry : topEntries) {
-                for (CallStat e : uidEntry.getCallStatsList()) {
-                    sb.setLength(0);
-                    sb.append("    ")
-                            .append(uidToString(uidEntry.uid, appIdToPkgNameMap))
-                            .append(",").append(e)
-                            .append(',').append(e.cpuTimeMicros)
-                            .append(',').append(e.maxCpuTimeMicros)
-                            .append(',').append(e.latencyMicros)
-                            .append(',').append(e.maxLatencyMicros)
-                            .append(',').append(e.exceptionCount)
-                            .append(',').append(e.maxRequestSizeBytes)
-                            .append(',').append(e.maxReplySizeBytes)
-                            .append(',').append(e.callCount);
-                    pw.println(sb);
-                }
-            }
-            pw.println();
-        } else {
-            pw.println("Sampled stats " + datasetSizeDesc
-                    + "(call_desc, cpu_time, call_count, exception_count):");
-            List<CallStat> sampledStatsList = mSampledEntries.getCallStatsList();
-            // Show all if verbose, otherwise 90th percentile
-            if (!verbose) {
-                sampledStatsList = getHighestValues(sampledStatsList,
-                        value -> value.cpuTimeMicros, 0.9);
-            }
-            for (CallStat e : sampledStatsList) {
+        List<UidEntry> topEntries = verbose ? entries
+                : getHighestValues(entries, value -> value.cpuTimeMicros, 0.9);
+        pw.println("Per-UID raw data " + datasetSizeDesc
+                + "(package/uid, call_desc, cpu_time_micros, max_cpu_time_micros, "
+                + "latency_time_micros, max_latency_time_micros, exception_count, "
+                + "max_request_size_bytes, max_reply_size_bytes, recorded_call_count, "
+                + "call_count):");
+        for (UidEntry uidEntry : topEntries) {
+            for (CallStat e : uidEntry.getCallStatsList()) {
                 sb.setLength(0);
-                sb.append("    ").append(e)
-                        .append(',').append(e.cpuTimeMicros * mPeriodicSamplingInterval)
-                        .append(',').append(e.callCount)
-                        .append(',').append(e.exceptionCount);
+                sb.append("    ")
+                        .append(uidToString(uidEntry.uid, appIdToPkgNameMap))
+                        .append(',').append(e)
+                        .append(',').append(e.cpuTimeMicros)
+                        .append(',').append(e.maxCpuTimeMicros)
+                        .append(',').append(e.latencyMicros)
+                        .append(',').append(e.maxLatencyMicros)
+                        .append(',').append(mDetailedTracking ? e.exceptionCount : '_')
+                        .append(',').append(mDetailedTracking ? e.maxRequestSizeBytes : '_')
+                        .append(',').append(mDetailedTracking ? e.maxReplySizeBytes : '_')
+                        .append(',').append(e.recordedCallCount)
+                        .append(',').append(e.callCount);
                 pw.println(sb);
             }
-            pw.println();
         }
+        pw.println();
         pw.println("Per-UID Summary " + datasetSizeDesc
-                + "(cpu_time, % of total cpu_time, call_count, exception_count, package/uid):");
+                + "(cpu_time, % of total cpu_time, recorded_call_count, call_count, package/uid):");
         List<UidEntry> summaryEntries = verbose ? entries
                 : getHighestValues(entries, value -> value.cpuTimeMicros, 0.9);
         for (UidEntry entry : summaryEntries) {
             String uidStr = uidToString(entry.uid, appIdToPkgNameMap);
-            pw.println(String.format("  %10d %3.0f%% %8d %3d %s",
-                    entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime, entry.callCount,
-                    entry.exceptionCount, uidStr));
+            pw.println(String.format("  %10d %3.0f%% %8d %8d %s",
+                        entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime,
+                        entry.recordedCallCount, entry.callCount, uidStr));
         }
         pw.println();
         pw.println(String.format("  Summary: total_cpu_time=%d, "
-                        + "calls_count=%d, avg_call_cpu_time=%.0f",
-                totalCpuTime, totalCallsCount, (double)totalCpuTime / totalCallsCount));
+                    + "calls_count=%d, avg_call_cpu_time=%.0f",
+                    totalCpuTime, totalCallsCount, (double)totalCpuTime / totalRecordedCallsCount));
         pw.println();
 
         pw.println("Exceptions thrown (exception_count, class_name):");
@@ -340,10 +305,15 @@
         // We cannot use new ArrayList(Collection) constructor because MapCollections does not
         // implement toArray method.
         mExceptionCounts.entrySet().iterator().forEachRemaining(
-            (e) -> exceptionEntries.add(Pair.create(e.getKey(), e.getValue())));
+                (e) -> exceptionEntries.add(Pair.create(e.getKey(), e.getValue())));
         exceptionEntries.sort((e1, e2) -> Integer.compare(e2.second, e1.second));
         for (Pair<String, Integer> entry : exceptionEntries) {
-          pw.println(String.format("  %6d %s", entry.second, entry.first));
+            pw.println(String.format("  %6d %s", entry.second, entry.first));
+        }
+
+        if (!mDetailedTracking && mPeriodicSamplingInterval != 1) {
+            pw.println("");
+            pw.println("/!\\ Displayed data is sampled. See sampling interval at the top.");
         }
     }
 
@@ -366,16 +336,20 @@
         return SystemClock.elapsedRealtimeNanos() / 1000;
     }
 
+    private boolean shouldTrackCall() {
+        return mRandom.nextInt() % mPeriodicSamplingInterval == 0;
+    }
+
     public static BinderCallsStats getInstance() {
         return sInstance;
     }
 
     public void setDetailedTracking(boolean enabled) {
         synchronized (mLock) {
-          if (enabled != mDetailedTracking) {
-              mDetailedTracking = enabled;
-              reset();
-          }
+            if (enabled != mDetailedTracking) {
+                mDetailedTracking = enabled;
+                reset();
+            }
         }
     }
 
@@ -401,7 +375,6 @@
         synchronized (mLock) {
             mUidEntries.clear();
             mExceptionCounts.clear();
-            mSampledEntries.mCallStats.clear();
             mStartTime = System.currentTimeMillis();
         }
     }
@@ -418,6 +391,7 @@
         public long latencyMicros;
         public long maxLatencyMicros;
         public long callCount;
+        public long recordedCallCount;
         public long maxRequestSizeBytes;
         public long maxReplySizeBytes;
         public long exceptionCount;
@@ -430,11 +404,21 @@
         // Method name might be null when we cannot resolve the transaction code. For instance, if
         // the binder was not generated by AIDL.
         public @Nullable String methodName;
+        // Number of calls for which we collected data for. We do not record data for all the calls
+        // when sampling is on.
+        public long recordedCallCount;
+        // Real number of total calls.
+        public long callCount;
+        // Total CPU of all for all the recorded calls.
+        // Approximate total CPU usage can be computed by
+        // cpuTimeMicros * callCount / recordedCallCount
         public long cpuTimeMicros;
         public long maxCpuTimeMicros;
+        // Total latency of all for all the recorded calls.
+        // Approximate average latency can be computed by
+        // latencyMicros * callCount / recordedCallCount
         public long latencyMicros;
         public long maxLatencyMicros;
-        public long callCount;
         // The following fields are only computed if mDetailedTracking is set.
         public long maxRequestSizeBytes;
         public long maxReplySizeBytes;
@@ -455,7 +439,6 @@
             }
 
             CallStat callStat = (CallStat) o;
-
             return msg == callStat.msg && (className.equals(callStat.className));
         }
 
@@ -477,15 +460,20 @@
         long timeStarted;
         boolean exceptionThrown;
         final CallStat callStat = new CallStat();
-        CallStat sampledCallStat;
     }
 
     @VisibleForTesting
     public static class UidEntry {
         int uid;
-        public long cpuTimeMicros;
+        // Number of calls for which we collected data for. We do not record data for all the calls
+        // when sampling is on.
+        public long recordedCallCount;
+        // Real number of total calls.
         public long callCount;
-        public int exceptionCount;
+        // Total CPU of all for all the recorded calls.
+        // Approximate total CPU usage can be computed by
+        // cpuTimeMicros * callCount / recordedCallCount
+        public long cpuTimeMicros;
 
         UidEntry(int uid) {
             this.uid = uid;
@@ -551,11 +539,6 @@
     }
 
     @VisibleForTesting
-    public UidEntry getSampledEntries() {
-        return mSampledEntries;
-    }
-
-    @VisibleForTesting
     public ArrayMap<String, Integer> getExceptionCounts() {
         return mExceptionCounts;
     }
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 d46c154..07e2af8 100644
--- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
@@ -33,6 +33,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Random;
 
 import static org.junit.Assert.assertEquals;
 
@@ -48,6 +49,7 @@
     public void testDetailedOff() {
         TestBinderCallsStats bcs = new TestBinderCallsStats();
         bcs.setDetailedTracking(false);
+        bcs.setSamplingInterval(5);
 
         Binder binder = new Binder();
         BinderCallsStats.CallSession callSession = bcs.callStarted(binder, 1);
@@ -58,44 +60,31 @@
         assertEquals(1, uidEntries.size());
         BinderCallsStats.UidEntry uidEntry = uidEntries.get(TEST_UID);
         Assert.assertNotNull(uidEntry);
+        List<BinderCallsStats.CallStat> callStatsList = uidEntry.getCallStatsList();
         assertEquals(1, uidEntry.callCount);
+        assertEquals(1, uidEntry.recordedCallCount);
         assertEquals(10, uidEntry.cpuTimeMicros);
-        assertEquals("Detailed tracking off - no entries should be returned",
-                0, uidEntry.getCallStatsList().size());
+        assertEquals(binder.getClass().getName(), callStatsList.get(0).className);
+        assertEquals(1, callStatsList.get(0).msg);
 
-        BinderCallsStats.UidEntry sampledEntries = bcs.getSampledEntries();
-        List<BinderCallsStats.CallStat> sampledCallStatsList = sampledEntries.getCallStatsList();
-        assertEquals(1, sampledCallStatsList.size());
-
-
-        assertEquals(1, sampledCallStatsList.get(0).callCount);
-        assertEquals(10, sampledCallStatsList.get(0).cpuTimeMicros);
-        assertEquals(binder.getClass().getName(), sampledCallStatsList.get(0).className);
-        assertEquals(1, sampledCallStatsList.get(0).msg);
-
+        // CPU usage is sampled, should not be tracked here.
         callSession = bcs.callStarted(binder, 1);
         bcs.time += 20;
         bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
-
-        uidEntry = bcs.getUidEntries().get(TEST_UID);
         assertEquals(2, uidEntry.callCount);
-        // When sampling is enabled, cpu time is only measured for the first transaction in the
-        // sampling interval, for others an average duration of previous transactions is used as
-        // approximation
-        assertEquals(20, uidEntry.cpuTimeMicros);
-        sampledCallStatsList = sampledEntries.getCallStatsList();
-        assertEquals(1, sampledCallStatsList.size());
+        assertEquals(1, uidEntry.recordedCallCount);
+        assertEquals(10, uidEntry.cpuTimeMicros);
+        assertEquals(1, callStatsList.size());
 
         callSession = bcs.callStarted(binder, 2);
         bcs.time += 50;
         bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
         uidEntry = bcs.getUidEntries().get(TEST_UID);
         assertEquals(3, uidEntry.callCount);
-
-        // This is the first transaction of a new type, so the real CPU time will be measured
-        assertEquals(70, uidEntry.cpuTimeMicros);
-        sampledCallStatsList = sampledEntries.getCallStatsList();
-        assertEquals(2, sampledCallStatsList.size());
+        assertEquals(1, uidEntry.recordedCallCount);
+        // Still sampled even for another API.
+        callStatsList = uidEntry.getCallStatsList();
+        assertEquals(2, callStatsList.size());
     }
 
     @Test
@@ -116,10 +105,6 @@
         assertEquals(10, uidEntry.cpuTimeMicros);
         assertEquals(1, uidEntry.getCallStatsList().size());
 
-        BinderCallsStats.UidEntry sampledEntries = bcs.getSampledEntries();
-        assertEquals("Sampling is not used when detailed tracking on",
-                0, bcs.getSampledEntries().getCallStatsList().size());
-
         List<BinderCallsStats.CallStat> callStatsList = uidEntry.getCallStatsList();
         assertEquals(1, callStatsList.get(0).callCount);
         assertEquals(10, callStatsList.get(0).cpuTimeMicros);
@@ -216,13 +201,57 @@
         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());
+        assertEquals(60 /* 10 + 50 */, uidEntry.cpuTimeMicros);
 
-        BinderCallsStats.UidEntry sampledEntries = bcs.getSampledEntries();
-        List<BinderCallsStats.CallStat> sampledCallStatsList = sampledEntries.getCallStatsList();
-        assertEquals(1, sampledCallStatsList.size());
+        List<BinderCallsStats.CallStat> callStatsList = uidEntry.getCallStatsList();
+        assertEquals(1, callStatsList.size());
+        BinderCallsStats.CallStat callStats = callStatsList.get(0);
+        assertEquals(3, callStats.callCount);
+        assertEquals(2, callStats.recordedCallCount);
+        assertEquals(60, callStats.cpuTimeMicros);
+        assertEquals(50, callStats.maxCpuTimeMicros);
+        assertEquals(0, callStats.maxRequestSizeBytes);
+        assertEquals(0, callStats.maxReplySizeBytes);
+    }
+
+    @Test
+    public void testSamplingWithDifferentApis() {
+        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, 2 /* another method */);
+        bcs.time += 1000;  // shoud be ignored.
+        bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
+
+
+        SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
+        assertEquals(1, uidEntries.size());
+        BinderCallsStats.UidEntry uidEntry = uidEntries.get(TEST_UID);
+        assertEquals(2, uidEntry.callCount);
+        assertEquals(1, uidEntry.recordedCallCount);
+        assertEquals(10, uidEntry.cpuTimeMicros);
+
+        List<BinderCallsStats.CallStat> callStatsList = uidEntry.getCallStatsList();
+        assertEquals(2, callStatsList.size());
+
+        BinderCallsStats.CallStat callStats = callStatsList.get(0);
+        assertEquals(1, callStats.callCount);
+        assertEquals(1, callStats.recordedCallCount);
+        assertEquals(10, callStats.cpuTimeMicros);
+        assertEquals(10, callStats.maxCpuTimeMicros);
+
+        // Only call count should is tracked, rest is sampled.
+        callStats = callStatsList.get(1);
+        assertEquals(1, callStats.callCount);
+        assertEquals(0, callStats.recordedCallCount);
+        assertEquals(0, callStats.cpuTimeMicros);
+        assertEquals(0, callStats.maxCpuTimeMicros);
     }
 
     @Test
@@ -374,6 +403,7 @@
         assertEquals(20, stat.latencyMicros);
         assertEquals(20, stat.maxLatencyMicros);
         assertEquals(1, stat.callCount);
+        assertEquals(1, stat.recordedCallCount);
         assertEquals(REQUEST_SIZE, stat.maxRequestSizeBytes);
         assertEquals(REPLY_SIZE, stat.maxReplySizeBytes);
         assertEquals(0, stat.exceptionCount);
@@ -385,6 +415,14 @@
         long elapsedTime = 0;
 
         TestBinderCallsStats() {
+          // Make random generator not random.
+          super(new Random() {
+            int mCallCount = 0;
+
+            public int nextInt() {
+              return mCallCount++;
+            }
+          });
         }
 
         @Override