diff options
6 files changed, 288 insertions, 28 deletions
diff --git a/core/java/com/android/internal/os/BinderLatencyBuckets.java b/core/java/com/android/internal/os/BinderLatencyBuckets.java new file mode 100644 index 000000000000..bdee4ca8a6b8 --- /dev/null +++ b/core/java/com/android/internal/os/BinderLatencyBuckets.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 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.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Generates the bucket thresholds (with a custom logarithmic scale) for a histogram to store + * latency samples in. + */ +public class BinderLatencyBuckets { + private static final String TAG = "BinderLatencyBuckets"; + private ArrayList<Integer> mBuckets; + + /** + * @param bucketCount the number of buckets the histogram should have + * @param firstBucketSize the size of the first bucket (used to avoid excessive small buckets) + * @param scaleFactor the rate in which each consecutive bucket increases (before rounding) + */ + public BinderLatencyBuckets(int bucketCount, int firstBucketSize, float scaleFactor) { + mBuckets = new ArrayList<>(bucketCount - 1); + mBuckets.add(firstBucketSize); + + // Last value and the target are disjoint as we never want to create buckets smaller than 1. + double lastTarget = firstBucketSize; + int lastValue = firstBucketSize; + + // First bucket is already created and the last bucket is anything greater than the final + // bucket in the list, so create 'bucketCount' - 2 buckets. + for (int i = 1; i < bucketCount - 1; i++) { + // Increase the target bucket limit value by the scale factor. + double nextTarget = lastTarget * scaleFactor; + + if (nextTarget > Integer.MAX_VALUE || lastValue == Integer.MAX_VALUE) { + // Do not throw an exception here as this should not affect binder calls. + Slog.w(TAG, "Attempted to create a bucket larger than maxint"); + return; + } + + if ((int) nextTarget > lastValue) { + // Convert the target bucket limit value to an integer. + mBuckets.add((int) nextTarget); + lastValue = (int) nextTarget; + } else { + // Avoid creating redundant buckets, so bucket size should be 1 at a minimum. + mBuckets.add(lastValue + 1); + lastValue = lastValue + 1; + } + lastTarget = nextTarget; + } + } + + /** Gets the bucket index to insert the provided sample in. */ + public int sampleToBucket(int sample) { + if (sample > mBuckets.get(mBuckets.size() - 1)) { + return mBuckets.size(); + } + + // Binary search returns the element index if it is contained in the list - in this case the + // correct bucket is the index after as we use [minValue, maxValue) for bucket boundaries. + // Otherwise, it returns (-(insertion point) - 1), where insertion point is the point where + // to insert the element so that the array remains sorted - in this case the bucket index + // is the insertion point. + int searchResult = Collections.binarySearch(mBuckets, sample); + return searchResult < 0 ? -(1 + searchResult) : searchResult + 1; + } + + @VisibleForTesting + public ArrayList<Integer> getBuckets() { + return mBuckets; + } +} diff --git a/core/java/com/android/internal/os/BinderLatencyObserver.java b/core/java/com/android/internal/os/BinderLatencyObserver.java index 92b495284de2..59cc66d79bce 100644 --- a/core/java/com/android/internal/os/BinderLatencyObserver.java +++ b/core/java/com/android/internal/os/BinderLatencyObserver.java @@ -26,7 +26,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BinderInternal.CallSession; -import java.util.ArrayList; import java.util.Random; /** Collects statistics about Binder call latency per calling API and method. */ @@ -34,18 +33,25 @@ public class BinderLatencyObserver { private static final String TAG = "BinderLatencyObserver"; public static final int PERIODIC_SAMPLING_INTERVAL_DEFAULT = 10; - // This is not the final data structure - we will eventually store latency histograms instead of - // raw samples as it is much more memory / disk space efficient. - // TODO(b/179999191): change this to store the histogram. - // TODO(b/179999191): pre allocate the array size so we would not have to resize this. + // Histogram buckets parameters. + public static final int BUCKET_COUNT_DEFAULT = 100; + public static final int FIRST_BUCKET_SIZE_DEFAULT = 5; + public static final float BUCKET_SCALE_FACTOR_DEFAULT = 1.125f; + @GuardedBy("mLock") - private final ArrayMap<LatencyDims, ArrayList<Long>> mLatencySamples = new ArrayMap<>(); + private final ArrayMap<LatencyDims, int[]> mLatencyHistograms = new ArrayMap<>(); private final Object mLock = new Object(); // Sampling period to control how often to track CPU usage. 1 means all calls, 100 means ~1 out // of 100 requests. private int mPeriodicSamplingInterval = PERIODIC_SAMPLING_INTERVAL_DEFAULT; + + private int mBucketCount = BUCKET_COUNT_DEFAULT; + private int mFirstBucketSize = FIRST_BUCKET_SIZE_DEFAULT; + private float mBucketScaleFactor = BUCKET_SCALE_FACTOR_DEFAULT; + private final Random mRandom; + private BinderLatencyBuckets mLatencyBuckets; /** Injector for {@link BinderLatencyObserver}. */ public static class Injector { @@ -56,6 +62,8 @@ public class BinderLatencyObserver { public BinderLatencyObserver(Injector injector) { mRandom = injector.getRandomGenerator(); + mLatencyBuckets = new BinderLatencyBuckets( + mBucketCount, mFirstBucketSize, mBucketScaleFactor); } /** Should be called when a Binder call completes, will store latency data. */ @@ -67,12 +75,21 @@ public class BinderLatencyObserver { LatencyDims dims = new LatencyDims(s.binderClass, s.transactionCode); long callDuration = getElapsedRealtimeMicro() - s.timeStarted; + // Find the bucket this sample should go to. + int bucketIdx = mLatencyBuckets.sampleToBucket( + callDuration > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) callDuration); + synchronized (mLock) { - if (!mLatencySamples.containsKey(dims)) { - mLatencySamples.put(dims, new ArrayList<Long>()); + int[] buckets = mLatencyHistograms.get(dims); + if (buckets == null) { + buckets = new int[mBucketCount]; + mLatencyHistograms.put(dims, buckets); } - mLatencySamples.get(dims).add(callDuration); + // Increment the correct bucket. + if (buckets[bucketIdx] < Integer.MAX_VALUE) { + buckets[bucketIdx] += 1; + } } } @@ -100,10 +117,26 @@ public class BinderLatencyObserver { } } + /** Updates the histogram buckets parameters. */ + public void setHistogramBucketsParams( + int bucketCount, int firstBucketSize, float bucketScaleFactor) { + synchronized (mLock) { + if (bucketCount != mBucketCount || firstBucketSize != mFirstBucketSize + || bucketScaleFactor != mBucketScaleFactor) { + mBucketCount = bucketCount; + mFirstBucketSize = firstBucketSize; + mBucketScaleFactor = bucketScaleFactor; + mLatencyBuckets = new BinderLatencyBuckets( + mBucketCount, mFirstBucketSize, mBucketScaleFactor); + reset(); + } + } + } + /** Resets the sample collection. */ public void reset() { synchronized (mLock) { - mLatencySamples.clear(); + mLatencyHistograms.clear(); } } @@ -151,7 +184,7 @@ public class BinderLatencyObserver { } @VisibleForTesting - public ArrayMap<LatencyDims, ArrayList<Long>> getLatencySamples() { - return mLatencySamples; + public ArrayMap<LatencyDims, int[]> getLatencyHistograms() { + return mLatencyHistograms; } } 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 5334a455ce9a..78901683eca1 100644 --- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java @@ -934,7 +934,7 @@ public class BinderCallsStatsTest { bcs.elapsedTime += 20; bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID); - assertEquals(1, bcs.getLatencyObserver().getLatencySamples().size()); + assertEquals(1, bcs.getLatencyObserver().getLatencyHistograms().size()); } @Test @@ -948,7 +948,7 @@ public class BinderCallsStatsTest { bcs.elapsedTime += 20; bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE, WORKSOURCE_UID); - assertEquals(0, bcs.getLatencyObserver().getLatencySamples().size()); + assertEquals(0, bcs.getLatencyObserver().getLatencyHistograms().size()); } private static class TestHandler extends Handler { diff --git a/core/tests/coretests/src/com/android/internal/os/BinderLatencyBucketsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderLatencyBucketsTest.java new file mode 100644 index 000000000000..00443a967c79 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/os/BinderLatencyBucketsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.os; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@Presubmit +public class BinderLatencyBucketsTest { + @Test + public void testBucketThresholds() { + BinderLatencyBuckets latencyBuckets = new BinderLatencyBuckets(10, 2, 1.45f); + assertThat(latencyBuckets.getBuckets()) + .containsExactly(2, 3, 4, 6, 8, 12, 18, 26, 39) + .inOrder(); + } + + @Test + public void testSampleAssignment() { + BinderLatencyBuckets latencyBuckets = new BinderLatencyBuckets(10, 2, 1.45f); + assertEquals(0, latencyBuckets.sampleToBucket(0)); + assertEquals(0, latencyBuckets.sampleToBucket(1)); + assertEquals(1, latencyBuckets.sampleToBucket(2)); + assertEquals(2, latencyBuckets.sampleToBucket(3)); + assertEquals(3, latencyBuckets.sampleToBucket(4)); + assertEquals(5, latencyBuckets.sampleToBucket(9)); + assertEquals(6, latencyBuckets.sampleToBucket(13)); + assertEquals(7, latencyBuckets.sampleToBucket(25)); + assertEquals(9, latencyBuckets.sampleToBucket(100)); + } + + @Test + public void testMaxIntBuckets() { + BinderLatencyBuckets latencyBuckets = new BinderLatencyBuckets(5, Integer.MAX_VALUE / 2, 2); + assertThat(latencyBuckets.getBuckets()) + .containsExactly(Integer.MAX_VALUE / 2, Integer.MAX_VALUE - 1) + .inOrder(); + + assertEquals(0, latencyBuckets.sampleToBucket(0)); + assertEquals(0, latencyBuckets.sampleToBucket(Integer.MAX_VALUE / 2 - 1)); + assertEquals(1, latencyBuckets.sampleToBucket(Integer.MAX_VALUE - 2)); + assertEquals(2, latencyBuckets.sampleToBucket(Integer.MAX_VALUE)); + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java b/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java index 36915a205ee0..f65fb9583d6c 100644 --- a/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BinderLatencyObserverTest.java @@ -33,7 +33,6 @@ import com.android.internal.os.BinderLatencyObserver.LatencyDims; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.ArrayList; import java.util.Arrays; import java.util.Random; @@ -44,6 +43,7 @@ public class BinderLatencyObserverTest { @Test public void testLatencyCollectionWithMultipleClasses() { TestBinderLatencyObserver blo = new TestBinderLatencyObserver(); + blo.setHistogramBucketsParams(5, 5, 1.125f); Binder binder = new Binder(); CallSession callSession = new CallSession(); @@ -51,20 +51,24 @@ public class BinderLatencyObserverTest { callSession.transactionCode = 1; blo.callEnded(callSession); blo.callEnded(callSession); + blo.callEnded(callSession); callSession.transactionCode = 2; blo.callEnded(callSession); + blo.callEnded(callSession); - ArrayMap<LatencyDims, ArrayList<Long>> latencySamples = blo.getLatencySamples(); - assertEquals(2, latencySamples.keySet().size()); - assertThat(latencySamples.get(new LatencyDims(binder.getClass(), 1))) - .containsExactlyElementsIn(Arrays.asList(1L, 2L)); - assertThat(latencySamples.get(new LatencyDims(binder.getClass(), 2))).containsExactly(3L); + ArrayMap<LatencyDims, int[]> latencyHistograms = blo.getLatencyHistograms(); + assertEquals(2, latencyHistograms.keySet().size()); + assertThat(latencyHistograms.get(new LatencyDims(binder.getClass(), 1))) + .asList().containsExactly(2, 0, 1, 0, 0).inOrder(); + assertThat(latencyHistograms.get(new LatencyDims(binder.getClass(), 2))) + .asList().containsExactly(0, 0, 0, 0, 2).inOrder(); } @Test public void testSampling() { TestBinderLatencyObserver blo = new TestBinderLatencyObserver(); blo.setSamplingInterval(2); + blo.setHistogramBucketsParams(5, 5, 1.125f); Binder binder = new Binder(); CallSession callSession = new CallSession(); @@ -74,17 +78,58 @@ public class BinderLatencyObserverTest { callSession.transactionCode = 2; blo.callEnded(callSession); - ArrayMap<LatencyDims, ArrayList<Long>> latencySamples = blo.getLatencySamples(); - assertEquals(1, latencySamples.size()); - LatencyDims dims = latencySamples.keySet().iterator().next(); + ArrayMap<LatencyDims, int[]> latencyHistograms = blo.getLatencyHistograms(); + assertEquals(1, latencyHistograms.size()); + LatencyDims dims = latencyHistograms.keySet().iterator().next(); assertEquals(binder.getClass(), dims.getBinderClass()); assertEquals(1, dims.getTransactionCode()); - ArrayList<Long> values = latencySamples.get(dims); - assertThat(values).containsExactly(1L); + assertThat(latencyHistograms.get(dims)).asList().containsExactly(1, 0, 0, 0, 0).inOrder(); + } + + @Test + public void testTooCallLengthOverflow() { + TestBinderLatencyObserver blo = new TestBinderLatencyObserver(); + blo.setElapsedTime(2L + (long) Integer.MAX_VALUE); + blo.setHistogramBucketsParams(5, 5, 1.125f); + + Binder binder = new Binder(); + CallSession callSession = new CallSession(); + callSession.binderClass = binder.getClass(); + callSession.transactionCode = 1; + blo.callEnded(callSession); + + // The long call should be capped to maxint (to not overflow) and placed in the last bucket. + assertThat(blo.getLatencyHistograms() + .get(new LatencyDims(binder.getClass(), 1))) + .asList().containsExactly(0, 0, 0, 0, 1) + .inOrder(); + } + + @Test + public void testHistogramBucketOverflow() { + TestBinderLatencyObserver blo = new TestBinderLatencyObserver(); + blo.setHistogramBucketsParams(3, 5, 1.125f); + + Binder binder = new Binder(); + CallSession callSession = new CallSession(); + callSession.binderClass = binder.getClass(); + callSession.transactionCode = 1; + blo.callEnded(callSession); + + LatencyDims dims = new LatencyDims(binder.getClass(), 1); + // Fill the buckets with maxint. + Arrays.fill(blo.getLatencyHistograms().get(dims), Integer.MAX_VALUE); + assertThat(blo.getLatencyHistograms().get(dims)) + .asList().containsExactly(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + // Try to add another sample. + blo.callEnded(callSession); + // Make sure the buckets don't overflow. + assertThat(blo.getLatencyHistograms().get(dims)) + .asList().containsExactly(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); } public static class TestBinderLatencyObserver extends BinderLatencyObserver { - private long mElapsedTimeCallCount = 0; + private long mElapsedTime = 0; TestBinderLatencyObserver() { // Make random generator not random. @@ -104,7 +149,12 @@ public class BinderLatencyObserverTest { @Override protected long getElapsedRealtimeMicro() { - return ++mElapsedTimeCallCount; + mElapsedTime += 2; + return mElapsedTime; + } + + public void setElapsedTime(long time) { + mElapsedTime = time; } } } diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java index 9e126d74b172..c49b8e8e77c6 100644 --- a/services/core/java/com/android/server/BinderCallsStatsService.java +++ b/services/core/java/com/android/server/BinderCallsStatsService.java @@ -138,6 +138,12 @@ public class BinderCallsStatsService extends Binder { private static final String SETTINGS_COLLECT_LATENCY_DATA_KEY = "collect_Latency_data"; private static final String SETTINGS_LATENCY_OBSERVER_SAMPLING_INTERVAL_KEY = "latency_observer_sampling_interval"; + private static final String SETTINGS_LATENCY_HISTOGRAM_BUCKET_COUNT_KEY = + "latency_histogram_bucket_count"; + private static final String SETTINGS_LATENCY_HISTOGRAM_FIRST_BUCKET_SIZE_KEY = + "latency_histogram_first_bucket_size"; + private static final String SETTINGS_LATENCY_HISTOGRAM_BUCKET_SCALE_FACTOR_KEY = + "latency_histogram_bucket_scale_factor"; private boolean mEnabled; private final Uri mUri = Settings.Global.getUriFor(Settings.Global.BINDER_CALLS_STATS); @@ -198,9 +204,20 @@ public class BinderCallsStatsService extends Binder { mParser.getBoolean(SETTINGS_COLLECT_LATENCY_DATA_KEY, BinderCallsStats.DEFAULT_COLLECT_LATENCY_DATA)); // Binder latency observer settings. - mBinderCallsStats.getLatencyObserver().setSamplingInterval(mParser.getInt( + BinderLatencyObserver binderLatencyObserver = mBinderCallsStats.getLatencyObserver(); + binderLatencyObserver.setSamplingInterval(mParser.getInt( SETTINGS_LATENCY_OBSERVER_SAMPLING_INTERVAL_KEY, BinderLatencyObserver.PERIODIC_SAMPLING_INTERVAL_DEFAULT)); + binderLatencyObserver.setHistogramBucketsParams( + mParser.getInt( + SETTINGS_LATENCY_HISTOGRAM_BUCKET_COUNT_KEY, + BinderLatencyObserver.BUCKET_COUNT_DEFAULT), + mParser.getInt( + SETTINGS_LATENCY_HISTOGRAM_FIRST_BUCKET_SIZE_KEY, + BinderLatencyObserver.FIRST_BUCKET_SIZE_DEFAULT), + mParser.getFloat( + SETTINGS_LATENCY_HISTOGRAM_BUCKET_SCALE_FACTOR_KEY, + BinderLatencyObserver.BUCKET_SCALE_FACTOR_DEFAULT)); final boolean enabled = |