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

Commit 00f18654 authored by Siim Sammul's avatar Siim Sammul
Browse files

Store the collected binder latency samples in buckets to form a

histogram. Add the ability to generate histograms with varying bucket
counts and sizes.

Bug: 179999191
Test: unit tests

Change-Id: I58d3cbdd9ae43c8da121ec41d994b077e29bb371
Merged-In: I58d3cbdd9ae43c8da121ec41d994b077e29bb371
(cherry picked from commit dcd3dc69)
parent 76277193
Loading
Loading
Loading
Loading
+91 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+45 −12
Original line number Diff line number Diff line
@@ -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;
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -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 {
+69 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}
+63 −13
Original line number Diff line number Diff line
@@ -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;
        }
    }
}
Loading