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