1 // Copyright 2020 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.metrics; 6 7 import android.annotation.SuppressLint; 8 9 import androidx.annotation.IntDef; 10 import androidx.annotation.Nullable; 11 import androidx.annotation.VisibleForTesting; 12 13 import org.chromium.base.Callback; 14 import org.chromium.base.Log; 15 import org.chromium.build.BuildConfig; 16 17 import java.lang.annotation.Retention; 18 import java.lang.annotation.RetentionPolicy; 19 import java.util.ArrayList; 20 import java.util.Arrays; 21 import java.util.Collections; 22 import java.util.HashMap; 23 import java.util.List; 24 import java.util.Map; 25 import java.util.concurrent.atomic.AtomicInteger; 26 import java.util.concurrent.locks.ReentrantReadWriteLock; 27 28 import javax.annotation.concurrent.GuardedBy; 29 30 /** 31 * Stores metrics until given an {@link UmaRecorder} to forward the samples to. After flushing, no 32 * longer stores metrics, instead immediately forwards them to the given {@link UmaRecorder}. 33 */ 34 /* package */ final class CachingUmaRecorder implements UmaRecorder { 35 private static final String TAG = "CachingUmaRecorder"; 36 37 /** 38 * Maximum number of histograms cached at the same time. It is better to drop some samples 39 * rather than have a bug cause the cache to grow without limit. 40 * <p> 41 * Each sample uses 4 bytes, each histogram uses approx. 12 references (at least 4 bytes each). 42 * With {@code MAX_HISTOGRAM_COUNT = 256} and {@code MAX_SAMPLE_COUNT = 256} this limits cache 43 * size to 270KiB. Changing either value by one, adds or removes approx. 1KiB. 44 */ 45 private static final int MAX_HISTOGRAM_COUNT = 256; 46 47 /** 48 * Maximum number of user actions cached at the same time. It is better to drop some samples 49 * rather than have a bug cause the cache to grow without limit. 50 */ 51 @VisibleForTesting static final int MAX_USER_ACTION_COUNT = 256; 52 53 /** Stores the definition and samples of a single cached histogram. */ 54 @VisibleForTesting 55 static class Histogram { 56 /** 57 * Maximum number of cached samples in a single histogram. it is better to drop some samples 58 * rather than have a bug cause the cache to grow without limit 59 */ 60 @VisibleForTesting static final int MAX_SAMPLE_COUNT = 256; 61 62 /** Identifies the type of the histogram. */ 63 @IntDef({ 64 Type.BOOLEAN, 65 Type.EXPONENTIAL, 66 Type.LINEAR, 67 Type.SPARSE, 68 }) 69 @Retention(RetentionPolicy.SOURCE) 70 @interface Type { 71 /** Used by histograms recorded with {@link UmaRecorder#recordBooleanHistogram}. */ 72 int BOOLEAN = 1; 73 74 /** Used by histograms recorded with {@link UmaRecorder#recordExponentialHistogram}. */ 75 int EXPONENTIAL = 2; 76 77 /** Used by histograms recorded with {@link UmaRecorder#recordLinearHistogram}. */ 78 int LINEAR = 3; 79 80 /** Used by histograms recorded with {@link UmaRecorder#recordSparseHistogram}. */ 81 int SPARSE = 4; 82 } 83 84 @Type private final int mType; 85 private final String mName; 86 87 private final int mMin; 88 private final int mMax; 89 private final int mNumBuckets; 90 91 @GuardedBy("this") 92 private final List<Integer> mSamples; 93 94 /** 95 * Constructs a {@code Histogram} with the specified definition and no samples. 96 * 97 * @param type histogram type. 98 * @param name histogram name. 99 * @param min histogram min value. Must be {@code 0} for boolean or sparse histograms. 100 * @param max histogram max value. Must be {@code 0} for boolean or sparse histograms. 101 * @param numBuckets number of histogram buckets. Must be {@code 0} for boolean or sparse 102 * histograms. 103 */ Histogram(@ype int type, String name, int min, int max, int numBuckets)104 Histogram(@Type int type, String name, int min, int max, int numBuckets) { 105 assert type == Type.EXPONENTIAL 106 || type == Type.LINEAR 107 || (min == 0 && max == 0 && numBuckets == 0) 108 : "Histogram type " + type + " must have no min/max/buckets set"; 109 mType = type; 110 mName = name; 111 mMin = min; 112 mMax = max; 113 mNumBuckets = numBuckets; 114 115 mSamples = new ArrayList<>(/* initialCapacity= */ 1); 116 } 117 118 /** 119 * Appends a sample to values cached in this histogram. Verifies that histogram definition 120 * matches the definition used to create this object: attempts to fail with an assertion, 121 * otherwise records failure statistics. 122 * 123 * @param type histogram type. 124 * @param name histogram name. 125 * @param sample sample value to cache. 126 * @param min histogram min value. Must be {@code 0} for boolean or sparse histograms. 127 * @param max histogram max value. Must be {@code 0} for boolean or sparse histograms. 128 * @param numBuckets number of histogram buckets. Must be {@code 0} for boolean or sparse 129 * histograms. 130 * @return true if the sample was recorded. 131 */ addSample( @ype int type, String name, int sample, int min, int max, int numBuckets)132 synchronized boolean addSample( 133 @Type int type, String name, int sample, int min, int max, int numBuckets) { 134 assert mType == type; 135 assert mName.equals(name); 136 assert mMin == min; 137 assert mMax == max; 138 assert mNumBuckets == numBuckets; 139 if (mSamples.size() >= MAX_SAMPLE_COUNT) { 140 // A cache filling up is most likely an indication of a bug. 141 assert false : "Histogram exceeded sample cache size limit"; 142 return false; 143 } 144 mSamples.add(sample); 145 return true; 146 } 147 148 /** 149 * Writes all histogram samples to {@code recorder}, clears the cache. 150 * 151 * @param recorder destination {@link UmaRecorder}. 152 * @return number of flushed histogram samples. 153 */ flushTo(UmaRecorder recorder)154 synchronized int flushTo(UmaRecorder recorder) { 155 switch (mType) { 156 case Type.BOOLEAN: 157 for (int i = 0; i < mSamples.size(); i++) { 158 final int sample = mSamples.get(i); 159 recorder.recordBooleanHistogram(mName, sample != 0); 160 } 161 break; 162 case Type.EXPONENTIAL: 163 for (int i = 0; i < mSamples.size(); i++) { 164 final int sample = mSamples.get(i); 165 recorder.recordExponentialHistogram(mName, sample, mMin, mMax, mNumBuckets); 166 } 167 break; 168 case Type.LINEAR: 169 for (int i = 0; i < mSamples.size(); i++) { 170 final int sample = mSamples.get(i); 171 recorder.recordLinearHistogram(mName, sample, mMin, mMax, mNumBuckets); 172 } 173 break; 174 case Type.SPARSE: 175 for (int i = 0; i < mSamples.size(); i++) { 176 final int sample = mSamples.get(i); 177 recorder.recordSparseHistogram(mName, sample); 178 } 179 break; 180 default: 181 assert false : "Unknown histogram type " + mType; 182 } 183 int count = mSamples.size(); 184 mSamples.clear(); 185 return count; 186 } 187 } 188 189 /** Stores a single cached user action. */ 190 private static class UserAction { 191 private final String mName; 192 private final long mElapsedRealtimeMillis; 193 UserAction(String name, long elapsedRealtimeMillis)194 UserAction(String name, long elapsedRealtimeMillis) { 195 mName = name; 196 mElapsedRealtimeMillis = elapsedRealtimeMillis; 197 } 198 199 /** Writes this user action to a {@link UmaRecorder}. */ flushTo(UmaRecorder recorder)200 void flushTo(UmaRecorder recorder) { 201 recorder.recordUserAction(mName, mElapsedRealtimeMillis); 202 } 203 } 204 205 /** 206 * The lock doesn't need to be fair - in the worst case a writing record*Histogram call will be 207 * starved until reading calls reach cache size limits. 208 * 209 * <p>A read-write lock is used rather than {@code synchronized} blocks to the limit 210 * opportunities for stutter on the UI thread when waiting for this shared resource. 211 */ 212 private final ReentrantReadWriteLock mRwLock = new ReentrantReadWriteLock(/* fair= */ false); 213 214 /** Cached histograms keyed by histogram name. */ 215 @GuardedBy("mRwLock") 216 private Map<String, Histogram> mHistogramByName = new HashMap<>(); 217 218 /** 219 * Number of histogram samples that couldn't be cached, because some limit of cache size been 220 * reached. 221 * <p> 222 * Using {@link AtomicInteger} because the value may need to be updated with a read lock held. 223 */ 224 private AtomicInteger mDroppedHistogramSampleCount = new AtomicInteger(); 225 226 /** Cache of user actions. */ 227 @GuardedBy("mRwLock") 228 private List<UserAction> mUserActions = new ArrayList<>(); 229 230 /** 231 * Number of user actions that couldn't be cached, because the number of user actions in cache 232 * has reached its limit. 233 */ 234 @GuardedBy("mRwLock") 235 private int mDroppedUserActionCount; 236 237 /** 238 * If not {@code null}, all metrics are forwarded to this {@link UmaRecorder}. 239 * <p> 240 * The read lock must be held while invoking methods on {@code mDelegate}. 241 */ 242 @GuardedBy("mRwLock") 243 @Nullable 244 private UmaRecorder mDelegate; 245 246 @GuardedBy("mRwLock") 247 @Nullable 248 private List<Callback<String>> mUserActionCallbacksForTesting; 249 250 /** 251 * Sets the current delegate to {@code recorder}. Forwards and clears all cached metrics if 252 * {@code recorder} is not {@code null}. 253 * 254 * @param recorder new delegate. 255 * @return the previous delegate. 256 */ setDelegate(@ullable final UmaRecorder recorder)257 public UmaRecorder setDelegate(@Nullable final UmaRecorder recorder) { 258 UmaRecorder previous; 259 Map<String, Histogram> histogramCache = null; 260 int droppedHistogramSampleCount = 0; 261 List<UserAction> userActionCache = null; 262 int droppedUserActionCount = 0; 263 264 mRwLock.writeLock().lock(); 265 try { 266 previous = mDelegate; 267 mDelegate = recorder; 268 if (BuildConfig.IS_FOR_TEST) { 269 swapUserActionCallbacksForTesting(previous, recorder); 270 } 271 if (recorder == null) { 272 return previous; 273 } 274 if (!mHistogramByName.isEmpty()) { 275 histogramCache = mHistogramByName; 276 mHistogramByName = new HashMap<>(); 277 droppedHistogramSampleCount = mDroppedHistogramSampleCount.getAndSet(0); 278 } 279 if (!mUserActions.isEmpty()) { 280 userActionCache = mUserActions; 281 mUserActions = new ArrayList<>(); 282 droppedUserActionCount = mDroppedUserActionCount; 283 mDroppedUserActionCount = 0; 284 } 285 // Downgrade by acquiring read lock before releasing write lock 286 mRwLock.readLock().lock(); 287 } finally { 288 mRwLock.writeLock().unlock(); 289 } 290 // Cache is flushed only after downgrading from a write lock to a read lock. 291 try { 292 if (histogramCache != null) { 293 flushHistogramsAlreadyLocked(histogramCache, droppedHistogramSampleCount); 294 } 295 if (userActionCache != null) { 296 flushUserActionsAlreadyLocked(userActionCache, droppedUserActionCount); 297 } 298 } finally { 299 mRwLock.readLock().unlock(); 300 } 301 return previous; 302 } 303 304 /** 305 * Writes histogram samples from {@code cache} to the delegate. Assumes that a read lock is held 306 * by the current thread. 307 * 308 * @param cache the cache to be flushed. 309 * @param droppedHistogramSampleCount number of histogram samples that were not recorded due to 310 * cache size limits. 311 */ 312 @GuardedBy("mRwLock") flushHistogramsAlreadyLocked( Map<String, Histogram> cache, int droppedHistogramSampleCount)313 private void flushHistogramsAlreadyLocked( 314 Map<String, Histogram> cache, int droppedHistogramSampleCount) { 315 assert mDelegate != null : "Unexpected: cache is flushed, but delegate is null"; 316 assert mRwLock.getReadHoldCount() > 0; 317 int flushedHistogramSampleCount = 0; 318 final int flushedHistogramCount = cache.size(); 319 for (Histogram histogram : cache.values()) { 320 flushedHistogramSampleCount += histogram.flushTo(mDelegate); 321 } 322 Log.i( 323 TAG, 324 "Flushed %d samples from %d histograms, %d samples were dropped.", 325 flushedHistogramSampleCount, 326 flushedHistogramCount, 327 droppedHistogramSampleCount); 328 } 329 330 /** 331 * Writes user actions from {@code cache} to the delegate. Assumes that a read lock is held by 332 * the current thread. 333 * 334 * @param cache the cache to be flushed. 335 * @param droppedUserActionCount number of user actions that were not recorded in {@code cache} 336 * to stay within {@link MAX_USER_ACTION_COUNT}. 337 */ flushUserActionsAlreadyLocked(List<UserAction> cache, int droppedUserActionCount)338 private void flushUserActionsAlreadyLocked(List<UserAction> cache, int droppedUserActionCount) { 339 assert mDelegate != null : "Unexpected: cache is flushed, but delegate is null"; 340 assert mRwLock.getReadHoldCount() > 0; 341 for (UserAction userAction : cache) { 342 userAction.flushTo(mDelegate); 343 } 344 Log.i( 345 TAG, 346 "Flushed %d user action samples, %d samples were dropped.", 347 cache.size(), 348 droppedUserActionCount); 349 } 350 351 /** 352 * Forwards or stores a histogram sample. Stores samples iff there is no delegate {@link 353 * UmaRecorder} set. 354 * 355 * @param type histogram type. 356 * @param name histogram name. 357 * @param sample sample value. 358 * @param min histogram min value. 359 * @param max histogram max value. 360 * @param numBuckets number of histogram buckets. 361 */ cacheOrRecordHistogramSample( @istogram.Type int type, String name, int sample, int min, int max, int numBuckets)362 private void cacheOrRecordHistogramSample( 363 @Histogram.Type int type, String name, int sample, int min, int max, int numBuckets) { 364 // Optimistic attempt without creating a Histogram. 365 if (tryAppendOrRecordSample(type, name, sample, min, max, numBuckets)) { 366 return; 367 } 368 369 mRwLock.writeLock().lock(); 370 try { 371 if (mDelegate == null) { 372 cacheHistogramSampleAlreadyWriteLocked(type, name, sample, min, max, numBuckets); 373 return; // Skip the lock downgrade. 374 } 375 // Downgrade by acquiring read lock before releasing write lock 376 mRwLock.readLock().lock(); 377 } finally { 378 mRwLock.writeLock().unlock(); 379 } 380 381 // Downgraded to read lock. 382 // See base/android/java/src/org/chromium/base/metrics/forwarding_synchronization.md 383 try { 384 assert mDelegate != null; 385 recordHistogramSampleAlreadyLocked(type, name, sample, min, max, numBuckets); 386 } finally { 387 mRwLock.readLock().unlock(); 388 } 389 } 390 391 /** 392 * Tries to cache or record a histogram sample without creating a new {@link Histogram}. 393 * 394 * @param type histogram type. 395 * @param name histogram name. 396 * @param sample sample value. 397 * @param min histogram min value. 398 * @param max histogram max value. 399 * @param numBuckets number of histogram buckets. 400 * @return {@code false} if the sample needs to be recorded with a write lock. 401 */ tryAppendOrRecordSample( @istogram.Type int type, String name, int sample, int min, int max, int numBuckets)402 private boolean tryAppendOrRecordSample( 403 @Histogram.Type int type, String name, int sample, int min, int max, int numBuckets) { 404 mRwLock.readLock().lock(); 405 try { 406 if (mDelegate != null) { 407 recordHistogramSampleAlreadyLocked(type, name, sample, min, max, numBuckets); 408 return true; 409 } 410 Histogram histogram = mHistogramByName.get(name); 411 if (histogram == null) { 412 return false; 413 } 414 if (!histogram.addSample(type, name, sample, min, max, numBuckets)) { 415 mDroppedHistogramSampleCount.incrementAndGet(); 416 } 417 return true; 418 } finally { 419 mRwLock.readLock().unlock(); 420 } 421 } 422 423 /** 424 * Appends a histogram {@code sample} to a cached {@link Histogram}. Creates the {@code 425 * Histogram} if needed. Assumes that the <b>write lock</b> is held by the current thread. 426 * 427 * @param type histogram type. 428 * @param name histogram name. 429 * @param sample sample value. 430 * @param min histogram min value. 431 * @param max histogram max value. 432 * @param numBuckets number of histogram buckets. 433 */ 434 @GuardedBy("mRwLock") cacheHistogramSampleAlreadyWriteLocked( @istogram.Type int type, String name, int sample, int min, int max, int numBuckets)435 private void cacheHistogramSampleAlreadyWriteLocked( 436 @Histogram.Type int type, String name, int sample, int min, int max, int numBuckets) { 437 assert mRwLock.isWriteLockedByCurrentThread(); 438 Histogram histogram = mHistogramByName.get(name); 439 if (histogram == null) { 440 if (mHistogramByName.size() >= MAX_HISTOGRAM_COUNT) { 441 // A cache filling up is most likely an indication of a bug. 442 assert false : "Too many histograms in cache"; 443 mDroppedHistogramSampleCount.incrementAndGet(); 444 return; 445 } 446 histogram = new Histogram(type, name, min, max, numBuckets); 447 mHistogramByName.put(name, histogram); 448 } 449 if (!histogram.addSample(type, name, sample, min, max, numBuckets)) { 450 mDroppedHistogramSampleCount.incrementAndGet(); 451 } 452 } 453 454 /** 455 * Forwards a histogram sample to the delegate. Assumes that a read lock is held by the current 456 * thread. Shouldn't be called with a write lock held. 457 * 458 * @param type histogram type. 459 * @param name histogram name. 460 * @param sample sample value. 461 * @param min histogram min value. 462 * @param max histogram max value. 463 * @param numBuckets number of histogram buckets. 464 */ 465 @GuardedBy("mRwLock") recordHistogramSampleAlreadyLocked( @istogram.Type int type, String name, int sample, int min, int max, int numBuckets)466 private void recordHistogramSampleAlreadyLocked( 467 @Histogram.Type int type, String name, int sample, int min, int max, int numBuckets) { 468 assert mRwLock.getReadHoldCount() > 0; 469 assert !mRwLock.isWriteLockedByCurrentThread(); 470 assert mDelegate != null : "recordSampleAlreadyLocked called with no delegate to record to"; 471 switch (type) { 472 case Histogram.Type.BOOLEAN: 473 mDelegate.recordBooleanHistogram(name, sample != 0); 474 break; 475 case Histogram.Type.EXPONENTIAL: 476 mDelegate.recordExponentialHistogram(name, sample, min, max, numBuckets); 477 break; 478 case Histogram.Type.LINEAR: 479 mDelegate.recordLinearHistogram(name, sample, min, max, numBuckets); 480 break; 481 case Histogram.Type.SPARSE: 482 mDelegate.recordSparseHistogram(name, sample); 483 break; 484 default: 485 throw new UnsupportedOperationException("Unknown histogram type " + type); 486 } 487 } 488 489 @Override recordBooleanHistogram(String name, boolean boolSample)490 public void recordBooleanHistogram(String name, boolean boolSample) { 491 final int sample = boolSample ? 1 : 0; 492 final int min = 0; 493 final int max = 0; 494 final int numBuckets = 0; 495 cacheOrRecordHistogramSample(Histogram.Type.BOOLEAN, name, sample, min, max, numBuckets); 496 } 497 498 @Override recordExponentialHistogram( String name, int sample, int min, int max, int numBuckets)499 public void recordExponentialHistogram( 500 String name, int sample, int min, int max, int numBuckets) { 501 cacheOrRecordHistogramSample( 502 Histogram.Type.EXPONENTIAL, name, sample, min, max, numBuckets); 503 } 504 505 @Override recordLinearHistogram(String name, int sample, int min, int max, int numBuckets)506 public void recordLinearHistogram(String name, int sample, int min, int max, int numBuckets) { 507 cacheOrRecordHistogramSample(Histogram.Type.LINEAR, name, sample, min, max, numBuckets); 508 } 509 510 @Override recordSparseHistogram(String name, int sample)511 public void recordSparseHistogram(String name, int sample) { 512 final int min = 0; 513 final int max = 0; 514 final int numBuckets = 0; 515 cacheOrRecordHistogramSample(Histogram.Type.SPARSE, name, sample, min, max, numBuckets); 516 } 517 518 @Override recordUserAction(String name, long elapsedRealtimeMillis)519 public void recordUserAction(String name, long elapsedRealtimeMillis) { 520 mRwLock.readLock().lock(); 521 try { 522 if (mDelegate != null) { 523 mDelegate.recordUserAction(name, elapsedRealtimeMillis); 524 return; 525 } 526 } finally { 527 mRwLock.readLock().unlock(); 528 } 529 530 mRwLock.writeLock().lock(); 531 try { 532 if (mDelegate == null) { 533 if (mUserActions.size() < MAX_USER_ACTION_COUNT) { 534 mUserActions.add(new UserAction(name, elapsedRealtimeMillis)); 535 } else { 536 assert false : "Too many user actions in cache"; 537 mDroppedUserActionCount++; 538 } 539 if (mUserActionCallbacksForTesting != null) { 540 for (int i = 0; i < mUserActionCallbacksForTesting.size(); i++) { 541 mUserActionCallbacksForTesting.get(i).onResult(name); 542 } 543 } 544 return; // Skip the lock downgrade. 545 } 546 // Downgrade by acquiring read lock before releasing write lock 547 mRwLock.readLock().lock(); 548 } finally { 549 mRwLock.writeLock().unlock(); 550 } 551 552 // Downgraded to read lock. 553 // See base/android/java/src/org/chromium/base/metrics/forwarding_synchronization.md 554 try { 555 assert mDelegate != null; 556 mDelegate.recordUserAction(name, elapsedRealtimeMillis); 557 } finally { 558 mRwLock.readLock().unlock(); 559 } 560 } 561 562 @VisibleForTesting 563 @Override getHistogramValueCountForTesting(String name, int sample)564 public int getHistogramValueCountForTesting(String name, int sample) { 565 mRwLock.readLock().lock(); 566 try { 567 if (mDelegate != null) return mDelegate.getHistogramValueCountForTesting(name, sample); 568 569 Histogram histogram = mHistogramByName.get(name); 570 if (histogram == null) return 0; 571 int sampleCount = 0; 572 synchronized (histogram) { 573 for (int i = 0; i < histogram.mSamples.size(); i++) { 574 if (histogram.mSamples.get(i) == sample) sampleCount++; 575 } 576 } 577 return sampleCount; 578 } finally { 579 mRwLock.readLock().unlock(); 580 } 581 } 582 583 @VisibleForTesting 584 @Override getHistogramTotalCountForTesting(String name)585 public int getHistogramTotalCountForTesting(String name) { 586 mRwLock.readLock().lock(); 587 try { 588 if (mDelegate != null) return mDelegate.getHistogramTotalCountForTesting(name); 589 590 Histogram histogram = mHistogramByName.get(name); 591 if (histogram == null) return 0; 592 synchronized (histogram) { 593 return histogram.mSamples.size(); 594 } 595 } finally { 596 mRwLock.readLock().unlock(); 597 } 598 } 599 600 @VisibleForTesting 601 @Override getHistogramSamplesForTesting(String name)602 public List<HistogramBucket> getHistogramSamplesForTesting(String name) { 603 mRwLock.readLock().lock(); 604 try { 605 if (mDelegate != null) return mDelegate.getHistogramSamplesForTesting(name); 606 607 Histogram histogram = mHistogramByName.get(name); 608 if (histogram == null) return Collections.emptyList(); 609 Integer[] samplesCopy; 610 synchronized (histogram) { 611 samplesCopy = histogram.mSamples.toArray(new Integer[0]); 612 } 613 Arrays.sort(samplesCopy); 614 List<HistogramBucket> buckets = new ArrayList<>(); 615 for (int i = 0; i < samplesCopy.length; ) { 616 int value = samplesCopy[i]; 617 int countInBucket = 0; 618 do { 619 countInBucket++; 620 i++; 621 } while (i < samplesCopy.length && samplesCopy[i] == value); 622 623 buckets.add(new HistogramBucket(value, value + 1, countInBucket)); 624 } 625 return buckets; 626 } finally { 627 mRwLock.readLock().unlock(); 628 } 629 } 630 631 @VisibleForTesting 632 @Override addUserActionCallbackForTesting(Callback<String> callback)633 public void addUserActionCallbackForTesting(Callback<String> callback) { 634 mRwLock.writeLock().lock(); 635 try { 636 if (mUserActionCallbacksForTesting == null) { 637 mUserActionCallbacksForTesting = new ArrayList<>(); 638 } 639 mUserActionCallbacksForTesting.add(callback); 640 if (mDelegate != null) mDelegate.addUserActionCallbackForTesting(callback); 641 } finally { 642 mRwLock.writeLock().unlock(); 643 } 644 } 645 646 @VisibleForTesting 647 @Override removeUserActionCallbackForTesting(Callback<String> callback)648 public void removeUserActionCallbackForTesting(Callback<String> callback) { 649 mRwLock.writeLock().lock(); 650 try { 651 if (mUserActionCallbacksForTesting == null) { 652 assert false 653 : "Attempting to remove a user action callback without previously " 654 + "registering any."; 655 return; 656 } 657 mUserActionCallbacksForTesting.remove(callback); 658 if (mDelegate != null) mDelegate.removeUserActionCallbackForTesting(callback); 659 } finally { 660 mRwLock.writeLock().unlock(); 661 } 662 } 663 664 @SuppressLint("VisibleForTests") 665 @GuardedBy("mRwLock") swapUserActionCallbacksForTesting( @ullable UmaRecorder previousRecorder, @Nullable UmaRecorder newRecorder)666 private void swapUserActionCallbacksForTesting( 667 @Nullable UmaRecorder previousRecorder, @Nullable UmaRecorder newRecorder) { 668 if (mUserActionCallbacksForTesting == null) return; 669 670 for (int i = 0; i < mUserActionCallbacksForTesting.size(); i++) { 671 if (previousRecorder != null) { 672 previousRecorder.removeUserActionCallbackForTesting( 673 mUserActionCallbacksForTesting.get(i)); 674 } 675 if (newRecorder != null) { 676 newRecorder.addUserActionCallbackForTesting(mUserActionCallbacksForTesting.get(i)); 677 } 678 } 679 } 680 } 681