• 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 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