1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.os; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.os.SystemClock; 22 import android.util.ArraySet; 23 import android.util.Log; 24 import android.util.SparseArray; 25 26 import com.android.internal.annotations.GuardedBy; 27 import com.android.internal.util.HeavyHitterSketch; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 32 /** 33 * A watcher which makes stats on the incoming binder transaction, if the amount of some type of 34 * transactions exceeds the threshold, the listener will be notified. 35 */ 36 @android.ravenwood.annotation.RavenwoodKeepWholeClass 37 public final class BinderCallHeavyHitterWatcher { 38 private static final String TAG = "BinderCallHeavyHitterWatcher"; 39 40 /** 41 * Whether or not this watcher is enabled. 42 */ 43 @GuardedBy("mLock") 44 private boolean mEnabled; 45 46 /** 47 * The listener to be notified in case the amount of some type of transactions exceeds the 48 * threshold. 49 */ 50 @GuardedBy("mLock") 51 private BinderCallHeavyHitterListener mListener; 52 53 /** 54 * The heavy hitter stats. 55 */ 56 @GuardedBy("mLock") 57 private HeavyHitterSketch<Integer> mHeavyHitterSketch; 58 59 /** 60 * The candidates that could be the heavy hitters, so we track their hashcode and the actual 61 * containers in this map. 62 */ 63 @GuardedBy("mLock") 64 private final SparseArray<HeavyHitterContainer> mHeavyHitterCandiates = new SparseArray<>(); 65 66 /** 67 * The cache to receive the list of candidates (consists of the hashcode of heavy hitters). 68 */ 69 @GuardedBy("mLock") 70 private final ArrayList<Integer> mCachedCandidateList = new ArrayList<>(); 71 72 /** 73 * The cache to receive the frequencies of each items in {@link #mCachedCandidateList}. 74 */ 75 @GuardedBy("mLock") 76 private final ArrayList<Float> mCachedCandidateFrequencies = new ArrayList<>(); 77 78 /** 79 * The cache set to host the candidates. 80 */ 81 @GuardedBy("mLock") 82 private ArraySet<Integer> mCachedCandidateSet = new ArraySet<>(); 83 84 /** 85 * The cache set to host the containers of candidates. 86 */ 87 @GuardedBy("mLock") 88 private HeavyHitterContainer[] mCachedCandidateContainers; 89 90 /** 91 * The index to the {@link #mCachedCandidateContainers}, denote the first available slot 92 */ 93 @GuardedBy("mLock") 94 private int mCachedCandidateContainersIndex; 95 96 /** 97 * The input size, should be {@link #mTotalInputSize} - validation size. 98 */ 99 @GuardedBy("mLock") 100 private int mInputSize; 101 102 /** 103 * The total input size. 104 */ 105 @GuardedBy("mLock") 106 private int mTotalInputSize; 107 108 /** 109 * The number of inputs so far 110 */ 111 @GuardedBy("mLock") 112 private int mCurrentInputSize; 113 114 /** 115 * The threshold to be considered as heavy hitters 116 */ 117 @GuardedBy("mLock") 118 private float mThreshold; 119 120 /** 121 * The timestamp of the start of current tracing. 122 */ 123 @GuardedBy("mLock") 124 private long mBatchStartTimeStamp; 125 126 /** 127 * The lock object 128 */ 129 private final Object mLock = new Object(); 130 131 /** 132 * The tolerance within which is approximately equal 133 */ 134 private static final float EPSILON = 0.00001f; 135 136 /** 137 * Callback interface when the amount of some type of transactions exceeds the threshold. 138 */ 139 public interface BinderCallHeavyHitterListener { 140 /** 141 * @param heavyHitters The list of binder call heavy hitters 142 * @param totalBinderCalls The total binder calls 143 * @param threshold The threshold to be considered as heavy hitters 144 * @param timeSpan The toal time span of all these binder calls 145 */ onHeavyHit(List<HeavyHitterContainer> heavyHitters, int totalBinderCalls, float threshold, long timeSpan)146 void onHeavyHit(List<HeavyHitterContainer> heavyHitters, 147 int totalBinderCalls, float threshold, long timeSpan); 148 } 149 150 /** 151 * Container to hold the potential heavy hitters 152 */ 153 public static final class HeavyHitterContainer { 154 /** 155 * The caller UID 156 */ 157 public int mUid; 158 159 /** 160 * The class of the Binder object which is being hit heavily 161 */ 162 public Class mClass; 163 164 /** 165 * The transaction code within the Binder object which is being hit heavily 166 */ 167 public int mCode; 168 169 /** 170 * The frequency of this being hit (a number between 0...1) 171 */ 172 public float mFrequency; 173 174 /** 175 * Default constructor 176 */ HeavyHitterContainer()177 public HeavyHitterContainer() { 178 } 179 180 /** 181 * Copy constructor 182 */ HeavyHitterContainer(@onNull final HeavyHitterContainer other)183 public HeavyHitterContainer(@NonNull final HeavyHitterContainer other) { 184 this.mUid = other.mUid; 185 this.mClass = other.mClass; 186 this.mCode = other.mCode; 187 this.mFrequency = other.mFrequency; 188 } 189 190 @Override equals(final Object other)191 public boolean equals(final Object other) { 192 if (other == null || !(other instanceof HeavyHitterContainer)) { 193 return false; 194 } 195 HeavyHitterContainer o = (HeavyHitterContainer) other; 196 return this.mUid == o.mUid && this.mClass == o.mClass && this.mCode == o.mCode 197 && Math.abs(this.mFrequency - o.mFrequency) < EPSILON; 198 } 199 200 @Override hashCode()201 public int hashCode() { 202 return hashCode(mUid, mClass, mCode); 203 } 204 205 /** 206 * Compute the hashcode with given parameters. 207 */ hashCode(int uid, @NonNull Class clazz, int code)208 static int hashCode(int uid, @NonNull Class clazz, int code) { 209 int hash = uid; 210 hash = 31 * hash + clazz.hashCode(); 211 hash = 31 * hash + code; 212 return hash; 213 } 214 } 215 216 /** 217 * The static lock object 218 */ 219 private static final Object sLock = new Object(); 220 221 /** 222 * The default instance 223 */ 224 @GuardedBy("sLock") 225 private static BinderCallHeavyHitterWatcher sInstance = null; 226 227 /** 228 * Return the instance of the watcher 229 */ getInstance()230 public static BinderCallHeavyHitterWatcher getInstance() { 231 synchronized (sLock) { 232 if (sInstance == null) { 233 sInstance = new BinderCallHeavyHitterWatcher(); 234 } 235 return sInstance; 236 } 237 } 238 239 /** 240 * Configure the parameters. 241 * 242 * @param enable Whether or not to enable the watcher 243 * @param batchSize The number of binder transactions it needs to receive before the conclusion 244 * @param threshold The threshold to determine if some type of transactions are too many, it 245 * should be a value between (0.0f, 1.0f] 246 * @param listener The callback interface 247 */ setConfig(final boolean enable, final int batchSize, final float threshold, @Nullable final BinderCallHeavyHitterListener listener)248 public void setConfig(final boolean enable, final int batchSize, final float threshold, 249 @Nullable final BinderCallHeavyHitterListener listener) { 250 synchronized (mLock) { 251 if (!enable) { 252 if (mEnabled) { 253 resetInternalLocked(null, null, 0, 0, 0.0f, 0); 254 mEnabled = false; 255 } 256 return; 257 } 258 mEnabled = true; 259 // Validate the threshold, which is expected to be within (0.0f, 1.0f] 260 if (threshold < EPSILON || threshold > 1.0f) { 261 return; 262 } 263 264 if (batchSize == mTotalInputSize && Math.abs(threshold - mThreshold) < EPSILON) { 265 // Shortcut: just update the listener, no need to reset the watcher itself. 266 mListener = listener; 267 return; 268 } 269 270 final int capacity = (int) (1.0f / threshold); 271 final HeavyHitterSketch<Integer> sketch = HeavyHitterSketch.<Integer>newDefault(); 272 final float validationRatio = sketch.getRequiredValidationInputRatio(); 273 int inputSize = batchSize; 274 if (!Float.isNaN(validationRatio)) { 275 inputSize = (int) (batchSize * (1 - validationRatio)); 276 } 277 try { 278 sketch.setConfig(batchSize, capacity); 279 } catch (IllegalArgumentException e) { 280 // invalid parameter, ignore the config. 281 Log.w(TAG, "Invalid parameter to heavy hitter watcher: " 282 + batchSize + ", " + capacity); 283 return; 284 } 285 // Reset the watcher to start over with the new configuration. 286 resetInternalLocked(listener, sketch, inputSize, batchSize, threshold, capacity); 287 } 288 } 289 290 @GuardedBy("mLock") resetInternalLocked(@ullable final BinderCallHeavyHitterListener listener, @Nullable final HeavyHitterSketch<Integer> sketch, final int inputSize, final int batchSize, final float threshold, final int capacity)291 private void resetInternalLocked(@Nullable final BinderCallHeavyHitterListener listener, 292 @Nullable final HeavyHitterSketch<Integer> sketch, final int inputSize, 293 final int batchSize, final float threshold, final int capacity) { 294 mListener = listener; 295 mHeavyHitterSketch = sketch; 296 mHeavyHitterCandiates.clear(); 297 mCachedCandidateList.clear(); 298 mCachedCandidateFrequencies.clear(); 299 mCachedCandidateSet.clear(); 300 mInputSize = inputSize; 301 mTotalInputSize = batchSize; 302 mCurrentInputSize = 0; 303 mThreshold = threshold; 304 mBatchStartTimeStamp = SystemClock.elapsedRealtime(); 305 initCachedCandidateContainersLocked(capacity); 306 } 307 308 @GuardedBy("mLock") initCachedCandidateContainersLocked(final int capacity)309 private void initCachedCandidateContainersLocked(final int capacity) { 310 if (capacity > 0) { 311 mCachedCandidateContainers = new HeavyHitterContainer[capacity]; 312 for (int i = 0; i < mCachedCandidateContainers.length; i++) { 313 mCachedCandidateContainers[i] = new HeavyHitterContainer(); 314 } 315 } else { 316 mCachedCandidateContainers = null; 317 } 318 mCachedCandidateContainersIndex = 0; 319 } 320 321 @GuardedBy("mLock") acquireHeavyHitterContainerLocked()322 private @NonNull HeavyHitterContainer acquireHeavyHitterContainerLocked() { 323 return mCachedCandidateContainers[mCachedCandidateContainersIndex++]; 324 } 325 326 @GuardedBy("mLock") releaseHeavyHitterContainerLocked(@onNull HeavyHitterContainer container)327 private void releaseHeavyHitterContainerLocked(@NonNull HeavyHitterContainer container) { 328 mCachedCandidateContainers[--mCachedCandidateContainersIndex] = container; 329 } 330 331 /** 332 * Called on incoming binder transaction 333 * 334 * @param callerUid The UID of the binder transaction's caller 335 * @param clazz The class of the Binder object serving the transaction 336 * @param code The binder transaction code 337 */ onTransaction(final int callerUid, @NonNull final Class clazz, final int code)338 public void onTransaction(final int callerUid, @NonNull final Class clazz, 339 final int code) { 340 synchronized (mLock) { 341 if (!mEnabled) { 342 return; 343 } 344 345 final HeavyHitterSketch<Integer> sketch = mHeavyHitterSketch; 346 if (sketch == null) { 347 return; 348 } 349 350 // To reduce memory fragmentation, we only feed the hashcode to the sketch, 351 // and keep the mapping from the hashcode to the sketch locally. 352 // However, the mapping will not be built until the validation pass, by then 353 // we will know the potential heavy hitters, so the mapping can focus on 354 // those ones, which will significantly reduce the memory overhead. 355 final int hashCode = HeavyHitterContainer.hashCode(callerUid, clazz, code); 356 357 sketch.add(hashCode); 358 mCurrentInputSize++; 359 if (mCurrentInputSize == mInputSize) { 360 // Retrieve the candidates 361 sketch.getCandidates(mCachedCandidateList); 362 mCachedCandidateSet.addAll(mCachedCandidateList); 363 mCachedCandidateList.clear(); 364 } else if (mCurrentInputSize > mInputSize && mCurrentInputSize < mTotalInputSize) { 365 // validation pass 366 if (mCachedCandidateSet.contains(hashCode)) { 367 // It's one of the candidates 368 final int index = mHeavyHitterCandiates.indexOfKey(hashCode); 369 if (index < 0) { 370 // We got another hit, now write down its information 371 final HeavyHitterContainer container = 372 acquireHeavyHitterContainerLocked(); 373 container.mUid = callerUid; 374 container.mClass = clazz; 375 container.mCode = code; 376 mHeavyHitterCandiates.put(hashCode, container); 377 } 378 } 379 } else if (mCurrentInputSize == mTotalInputSize) { 380 // Reached the expected number of input, check top ones 381 if (mListener != null) { 382 final List<Integer> result = sketch.getTopHeavyHitters(0, 383 mCachedCandidateList, mCachedCandidateFrequencies); 384 if (result != null) { 385 final int size = result.size(); 386 if (size > 0) { 387 final ArrayList<HeavyHitterContainer> hitters = new ArrayList<>(); 388 for (int i = 0; i < size; i++) { 389 final HeavyHitterContainer container = mHeavyHitterCandiates.get( 390 result.get(i)); 391 if (container != null) { 392 final HeavyHitterContainer cont = 393 new HeavyHitterContainer(container); 394 cont.mFrequency = mCachedCandidateFrequencies.get(i); 395 hitters.add(cont); 396 } 397 } 398 mListener.onHeavyHit(hitters, mTotalInputSize, mThreshold, 399 SystemClock.elapsedRealtime() - mBatchStartTimeStamp); 400 } 401 } 402 } 403 // reset 404 mHeavyHitterSketch.reset(); 405 mHeavyHitterCandiates.clear(); 406 mCachedCandidateList.clear(); 407 mCachedCandidateFrequencies.clear(); 408 mCachedCandidateSet.clear(); 409 mCachedCandidateContainersIndex = 0; 410 mCurrentInputSize = 0; 411 mBatchStartTimeStamp = SystemClock.elapsedRealtime(); 412 } 413 } 414 } 415 } 416