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