/* * Copyright (C) 2020 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.NonNull; import android.annotation.Nullable; import android.os.SystemClock; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.HeavyHitterSketch; import java.util.ArrayList; import java.util.List; /** * A watcher which makes stats on the incoming binder transaction, if the amount of some type of * transactions exceeds the threshold, the listener will be notified. */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public final class BinderCallHeavyHitterWatcher { private static final String TAG = "BinderCallHeavyHitterWatcher"; /** * Whether or not this watcher is enabled. */ @GuardedBy("mLock") private boolean mEnabled; /** * The listener to be notified in case the amount of some type of transactions exceeds the * threshold. */ @GuardedBy("mLock") private BinderCallHeavyHitterListener mListener; /** * The heavy hitter stats. */ @GuardedBy("mLock") private HeavyHitterSketch mHeavyHitterSketch; /** * The candidates that could be the heavy hitters, so we track their hashcode and the actual * containers in this map. */ @GuardedBy("mLock") private final SparseArray mHeavyHitterCandiates = new SparseArray<>(); /** * The cache to receive the list of candidates (consists of the hashcode of heavy hitters). */ @GuardedBy("mLock") private final ArrayList mCachedCandidateList = new ArrayList<>(); /** * The cache to receive the frequencies of each items in {@link #mCachedCandidateList}. */ @GuardedBy("mLock") private final ArrayList mCachedCandidateFrequencies = new ArrayList<>(); /** * The cache set to host the candidates. */ @GuardedBy("mLock") private ArraySet mCachedCandidateSet = new ArraySet<>(); /** * The cache set to host the containers of candidates. */ @GuardedBy("mLock") private HeavyHitterContainer[] mCachedCandidateContainers; /** * The index to the {@link #mCachedCandidateContainers}, denote the first available slot */ @GuardedBy("mLock") private int mCachedCandidateContainersIndex; /** * The input size, should be {@link #mTotalInputSize} - validation size. */ @GuardedBy("mLock") private int mInputSize; /** * The total input size. */ @GuardedBy("mLock") private int mTotalInputSize; /** * The number of inputs so far */ @GuardedBy("mLock") private int mCurrentInputSize; /** * The threshold to be considered as heavy hitters */ @GuardedBy("mLock") private float mThreshold; /** * The timestamp of the start of current tracing. */ @GuardedBy("mLock") private long mBatchStartTimeStamp; /** * The lock object */ private final Object mLock = new Object(); /** * The tolerance within which is approximately equal */ private static final float EPSILON = 0.00001f; /** * Callback interface when the amount of some type of transactions exceeds the threshold. */ public interface BinderCallHeavyHitterListener { /** * @param heavyHitters The list of binder call heavy hitters * @param totalBinderCalls The total binder calls * @param threshold The threshold to be considered as heavy hitters * @param timeSpan The toal time span of all these binder calls */ void onHeavyHit(List heavyHitters, int totalBinderCalls, float threshold, long timeSpan); } /** * Container to hold the potential heavy hitters */ public static final class HeavyHitterContainer { /** * The caller UID */ public int mUid; /** * The class of the Binder object which is being hit heavily */ public Class mClass; /** * The transaction code within the Binder object which is being hit heavily */ public int mCode; /** * The frequency of this being hit (a number between 0...1) */ public float mFrequency; /** * Default constructor */ public HeavyHitterContainer() { } /** * Copy constructor */ public HeavyHitterContainer(@NonNull final HeavyHitterContainer other) { this.mUid = other.mUid; this.mClass = other.mClass; this.mCode = other.mCode; this.mFrequency = other.mFrequency; } @Override public boolean equals(final Object other) { if (other == null || !(other instanceof HeavyHitterContainer)) { return false; } HeavyHitterContainer o = (HeavyHitterContainer) other; return this.mUid == o.mUid && this.mClass == o.mClass && this.mCode == o.mCode && Math.abs(this.mFrequency - o.mFrequency) < EPSILON; } @Override public int hashCode() { return hashCode(mUid, mClass, mCode); } /** * Compute the hashcode with given parameters. */ static int hashCode(int uid, @NonNull Class clazz, int code) { int hash = uid; hash = 31 * hash + clazz.hashCode(); hash = 31 * hash + code; return hash; } } /** * The static lock object */ private static final Object sLock = new Object(); /** * The default instance */ @GuardedBy("sLock") private static BinderCallHeavyHitterWatcher sInstance = null; /** * Return the instance of the watcher */ public static BinderCallHeavyHitterWatcher getInstance() { synchronized (sLock) { if (sInstance == null) { sInstance = new BinderCallHeavyHitterWatcher(); } return sInstance; } } /** * Configure the parameters. * * @param enable Whether or not to enable the watcher * @param batchSize The number of binder transactions it needs to receive before the conclusion * @param threshold The threshold to determine if some type of transactions are too many, it * should be a value between (0.0f, 1.0f] * @param listener The callback interface */ public void setConfig(final boolean enable, final int batchSize, final float threshold, @Nullable final BinderCallHeavyHitterListener listener) { synchronized (mLock) { if (!enable) { if (mEnabled) { resetInternalLocked(null, null, 0, 0, 0.0f, 0); mEnabled = false; } return; } mEnabled = true; // Validate the threshold, which is expected to be within (0.0f, 1.0f] if (threshold < EPSILON || threshold > 1.0f) { return; } if (batchSize == mTotalInputSize && Math.abs(threshold - mThreshold) < EPSILON) { // Shortcut: just update the listener, no need to reset the watcher itself. mListener = listener; return; } final int capacity = (int) (1.0f / threshold); final HeavyHitterSketch sketch = HeavyHitterSketch.newDefault(); final float validationRatio = sketch.getRequiredValidationInputRatio(); int inputSize = batchSize; if (!Float.isNaN(validationRatio)) { inputSize = (int) (batchSize * (1 - validationRatio)); } try { sketch.setConfig(batchSize, capacity); } catch (IllegalArgumentException e) { // invalid parameter, ignore the config. Log.w(TAG, "Invalid parameter to heavy hitter watcher: " + batchSize + ", " + capacity); return; } // Reset the watcher to start over with the new configuration. resetInternalLocked(listener, sketch, inputSize, batchSize, threshold, capacity); } } @GuardedBy("mLock") private void resetInternalLocked(@Nullable final BinderCallHeavyHitterListener listener, @Nullable final HeavyHitterSketch sketch, final int inputSize, final int batchSize, final float threshold, final int capacity) { mListener = listener; mHeavyHitterSketch = sketch; mHeavyHitterCandiates.clear(); mCachedCandidateList.clear(); mCachedCandidateFrequencies.clear(); mCachedCandidateSet.clear(); mInputSize = inputSize; mTotalInputSize = batchSize; mCurrentInputSize = 0; mThreshold = threshold; mBatchStartTimeStamp = SystemClock.elapsedRealtime(); initCachedCandidateContainersLocked(capacity); } @GuardedBy("mLock") private void initCachedCandidateContainersLocked(final int capacity) { if (capacity > 0) { mCachedCandidateContainers = new HeavyHitterContainer[capacity]; for (int i = 0; i < mCachedCandidateContainers.length; i++) { mCachedCandidateContainers[i] = new HeavyHitterContainer(); } } else { mCachedCandidateContainers = null; } mCachedCandidateContainersIndex = 0; } @GuardedBy("mLock") private @NonNull HeavyHitterContainer acquireHeavyHitterContainerLocked() { return mCachedCandidateContainers[mCachedCandidateContainersIndex++]; } @GuardedBy("mLock") private void releaseHeavyHitterContainerLocked(@NonNull HeavyHitterContainer container) { mCachedCandidateContainers[--mCachedCandidateContainersIndex] = container; } /** * Called on incoming binder transaction * * @param callerUid The UID of the binder transaction's caller * @param clazz The class of the Binder object serving the transaction * @param code The binder transaction code */ public void onTransaction(final int callerUid, @NonNull final Class clazz, final int code) { synchronized (mLock) { if (!mEnabled) { return; } final HeavyHitterSketch sketch = mHeavyHitterSketch; if (sketch == null) { return; } // To reduce memory fragmentation, we only feed the hashcode to the sketch, // and keep the mapping from the hashcode to the sketch locally. // However, the mapping will not be built until the validation pass, by then // we will know the potential heavy hitters, so the mapping can focus on // those ones, which will significantly reduce the memory overhead. final int hashCode = HeavyHitterContainer.hashCode(callerUid, clazz, code); sketch.add(hashCode); mCurrentInputSize++; if (mCurrentInputSize == mInputSize) { // Retrieve the candidates sketch.getCandidates(mCachedCandidateList); mCachedCandidateSet.addAll(mCachedCandidateList); mCachedCandidateList.clear(); } else if (mCurrentInputSize > mInputSize && mCurrentInputSize < mTotalInputSize) { // validation pass if (mCachedCandidateSet.contains(hashCode)) { // It's one of the candidates final int index = mHeavyHitterCandiates.indexOfKey(hashCode); if (index < 0) { // We got another hit, now write down its information final HeavyHitterContainer container = acquireHeavyHitterContainerLocked(); container.mUid = callerUid; container.mClass = clazz; container.mCode = code; mHeavyHitterCandiates.put(hashCode, container); } } } else if (mCurrentInputSize == mTotalInputSize) { // Reached the expected number of input, check top ones if (mListener != null) { final List result = sketch.getTopHeavyHitters(0, mCachedCandidateList, mCachedCandidateFrequencies); if (result != null) { final int size = result.size(); if (size > 0) { final ArrayList hitters = new ArrayList<>(); for (int i = 0; i < size; i++) { final HeavyHitterContainer container = mHeavyHitterCandiates.get( result.get(i)); if (container != null) { final HeavyHitterContainer cont = new HeavyHitterContainer(container); cont.mFrequency = mCachedCandidateFrequencies.get(i); hitters.add(cont); } } mListener.onHeavyHit(hitters, mTotalInputSize, mThreshold, SystemClock.elapsedRealtime() - mBatchStartTimeStamp); } } } // reset mHeavyHitterSketch.reset(); mHeavyHitterCandiates.clear(); mCachedCandidateList.clear(); mCachedCandidateFrequencies.clear(); mCachedCandidateSet.clear(); mCachedCandidateContainersIndex = 0; mCurrentInputSize = 0; mBatchStartTimeStamp = SystemClock.elapsedRealtime(); } } } }