blob: 20cf102953e40dc5720c9c51cd968c10215d61ca [file] [log] [blame]
/*
* 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.annotation.Nullable;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BinderInternal.CallSession;
import com.android.internal.os.BinderLatencyProto.ApiStats;
import com.android.internal.os.BinderLatencyProto.Dims;
import com.android.internal.os.BinderLatencyProto.RepeatedApiStats;
import com.android.internal.util.FrameworkStatsLog;
import java.util.Random;
/** Collects statistics about Binder call latency per calling API and method. */
public class BinderLatencyObserver {
private static final String TAG = "BinderLatencyObserver";
private static final int MAX_ATOM_SIZE_BYTES = 4064;
// Be conservative and leave 1K space for the last histogram so we don't go over the size limit.
private static final int LAST_HISTOGRAM_BUFFER_SIZE_BYTES = 1000;
// Latency observer parameters.
public static final int PERIODIC_SAMPLING_INTERVAL_DEFAULT = 10;
public static final int SHARDING_MODULO_DEFAULT = 1;
public static final int STATSD_PUSH_INTERVAL_MINUTES_DEFAULT = 360;
// 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, 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;
// Controls how many APIs will be collected per device. 1 means all APIs, 10 means every 10th
// API will be collected.
private int mShardingModulo = SHARDING_MODULO_DEFAULT;
// Controls which shards will be collected on this device.
private int mShardingOffset;
private int mBucketCount = BUCKET_COUNT_DEFAULT;
private int mFirstBucketSize = FIRST_BUCKET_SIZE_DEFAULT;
private float mBucketScaleFactor = BUCKET_SCALE_FACTOR_DEFAULT;
private int mStatsdPushIntervalMinutes = STATSD_PUSH_INTERVAL_MINUTES_DEFAULT;
private final Random mRandom;
private final Handler mLatencyObserverHandler;
private final int mProcessSource;
private BinderLatencyBuckets mLatencyBuckets;
private Runnable mLatencyObserverRunnable = new Runnable() {
@Override
public void run() {
// Schedule the next push.
noteLatencyDelayed();
ArrayMap<LatencyDims, int[]> histogramMap;
synchronized (mLock) {
// Copy the histograms map so we don't use the lock for longer than needed.
histogramMap = new ArrayMap<>(mLatencyHistograms);
mLatencyHistograms.clear();
}
BinderTransactionNameResolver resolver = new BinderTransactionNameResolver();
ProtoOutputStream proto = new ProtoOutputStream();
int histogramsWritten = 0;
for (LatencyDims dims : histogramMap.keySet()) {
// Start a new atom if the next histogram risks going over the atom size limit.
if (proto.getRawSize() + LAST_HISTOGRAM_BUFFER_SIZE_BYTES > getMaxAtomSizeBytes()) {
if (histogramsWritten > 0) {
writeAtomToStatsd(proto);
}
proto = new ProtoOutputStream();
histogramsWritten = 0;
}
String transactionName = resolver.getMethodName(
dims.getBinderClass(), dims.getTransactionCode());
fillApiStatsProto(proto, dims, transactionName, histogramMap.get(dims));
histogramsWritten++;
}
// Push the final atom.
if (histogramsWritten > 0) {
writeAtomToStatsd(proto);
}
}
};
private void fillApiStatsProto(
ProtoOutputStream proto, LatencyDims dims, String transactionName, int[] histogram) {
// Find the part of the histogram to write.
int firstNonEmptyBucket = 0;
for (int i = 0; i < mBucketCount; i++) {
if (histogram[i] != 0) {
firstNonEmptyBucket = i;
break;
}
}
int lastNonEmptyBucket = mBucketCount - 1;
for (int i = mBucketCount - 1; i >= 0; i--) {
if (histogram[i] != 0) {
lastNonEmptyBucket = i;
break;
}
}
// Start a new ApiStats proto.
long apiStatsToken = proto.start(RepeatedApiStats.API_STATS);
// Write the dims.
long dimsToken = proto.start(ApiStats.DIMS);
proto.write(Dims.PROCESS_SOURCE, mProcessSource);
proto.write(Dims.SERVICE_CLASS_NAME, dims.getBinderClass().getName());
proto.write(Dims.SERVICE_METHOD_NAME, transactionName);
proto.end(dimsToken);
// Write the histogram.
proto.write(ApiStats.FIRST_BUCKET_INDEX, firstNonEmptyBucket);
for (int i = firstNonEmptyBucket; i <= lastNonEmptyBucket; i++) {
proto.write(ApiStats.BUCKETS, histogram[i]);
}
proto.end(apiStatsToken);
}
protected int getMaxAtomSizeBytes() {
return MAX_ATOM_SIZE_BYTES;
}
protected void writeAtomToStatsd(ProtoOutputStream atom) {
FrameworkStatsLog.write(
FrameworkStatsLog.BINDER_LATENCY_REPORTED,
atom.getBytes(),
mPeriodicSamplingInterval,
mShardingModulo,
mBucketCount,
mFirstBucketSize,
mBucketScaleFactor);
}
private void noteLatencyDelayed() {
mLatencyObserverHandler.removeCallbacks(mLatencyObserverRunnable);
mLatencyObserverHandler.postDelayed(mLatencyObserverRunnable,
mStatsdPushIntervalMinutes * 60 * 1000);
}
/** Injector for {@link BinderLatencyObserver}. */
public static class Injector {
public Random getRandomGenerator() {
return new Random();
}
public Handler getHandler() {
return new Handler(Looper.getMainLooper());
}
}
public BinderLatencyObserver(Injector injector, int processSource) {
mRandom = injector.getRandomGenerator();
mLatencyObserverHandler = injector.getHandler();
mLatencyBuckets = new BinderLatencyBuckets(
mBucketCount, mFirstBucketSize, mBucketScaleFactor);
mProcessSource = processSource;
mShardingOffset = mRandom.nextInt(mShardingModulo);
noteLatencyDelayed();
}
/** Should be called when a Binder call completes, will store latency data. */
public void callEnded(@Nullable CallSession s) {
if (s == null || s.exceptionThrown || !shouldKeepSample()) {
return;
}
LatencyDims dims = LatencyDims.create(s.binderClass, s.transactionCode);
if (!shouldCollect(dims)) {
return;
}
long elapsedTimeMicro = getElapsedRealtimeMicro();
long callDuration = elapsedTimeMicro - 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) {
int[] buckets = mLatencyHistograms.get(dims);
if (buckets == null) {
buckets = new int[mBucketCount];
mLatencyHistograms.put(dims, buckets);
}
// Increment the correct bucket.
if (buckets[bucketIdx] < Integer.MAX_VALUE) {
buckets[bucketIdx] += 1;
}
}
}
protected long getElapsedRealtimeMicro() {
return SystemClock.elapsedRealtimeNanos() / 1000;
}
protected boolean shouldCollect(LatencyDims dims) {
return (dims.hashCode() + mShardingOffset) % mShardingModulo == 0;
}
protected boolean shouldKeepSample() {
return mRandom.nextInt() % mPeriodicSamplingInterval == 0;
}
/** Updates the sampling interval. */
public void setSamplingInterval(int samplingInterval) {
if (samplingInterval <= 0) {
Slog.w(TAG, "Ignored invalid sampling interval (value must be positive): "
+ samplingInterval);
return;
}
synchronized (mLock) {
if (samplingInterval != mPeriodicSamplingInterval) {
mPeriodicSamplingInterval = samplingInterval;
reset();
}
}
}
/** Updates the sharding modulo. */
public void setShardingModulo(int shardingModulo) {
if (shardingModulo <= 0) {
Slog.w(TAG, "Ignored invalid sharding modulo (value must be positive): "
+ shardingModulo);
return;
}
synchronized (mLock) {
if (shardingModulo != mShardingModulo) {
mShardingModulo = shardingModulo;
mShardingOffset = mRandom.nextInt(shardingModulo);
reset();
}
}
}
/** Updates the statsd push interval. */
public void setPushInterval(int pushIntervalMinutes) {
if (pushIntervalMinutes <= 0) {
Slog.w(TAG, "Ignored invalid push interval (value must be positive): "
+ pushIntervalMinutes);
return;
}
synchronized (mLock) {
if (pushIntervalMinutes != mStatsdPushIntervalMinutes) {
mStatsdPushIntervalMinutes = pushIntervalMinutes;
reset();
}
}
}
/** 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) {
mLatencyHistograms.clear();
}
noteLatencyDelayed();
}
/** Container for binder latency information. */
public static class LatencyDims {
// Binder interface descriptor.
private Class<? extends Binder> mBinderClass;
// Binder transaction code.
private int mTransactionCode;
// Cached hash code, 0 if not set yet.
private int mHashCode = 0;
/** Creates a new instance of LatencyDims. */
public static LatencyDims create(Class<? extends Binder> binderClass, int transactionCode) {
return new LatencyDims(binderClass, transactionCode);
}
private LatencyDims(Class<? extends Binder> binderClass, int transactionCode) {
this.mBinderClass = binderClass;
this.mTransactionCode = transactionCode;
}
public Class<? extends Binder> getBinderClass() {
return mBinderClass;
}
public int getTransactionCode() {
return mTransactionCode;
}
@Override
public boolean equals(final Object other) {
if (other == null || !(other instanceof LatencyDims)) {
return false;
}
LatencyDims o = (LatencyDims) other;
return mTransactionCode == o.getTransactionCode() && mBinderClass == o.getBinderClass();
}
@Override
public int hashCode() {
if (mHashCode != 0) {
return mHashCode;
}
int hash = mTransactionCode;
hash = 31 * hash + mBinderClass.getName().hashCode();
mHashCode = hash;
return hash;
}
}
@VisibleForTesting
public ArrayMap<LatencyDims, int[]> getLatencyHistograms() {
return mLatencyHistograms;
}
@VisibleForTesting
public Runnable getStatsdPushRunnable() {
return mLatencyObserverRunnable;
}
@VisibleForTesting
public int getProcessSource() {
return mProcessSource;
}
}