• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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