// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.metrics;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import androidx.test.filters.MediumTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** Unit tests for {@link CachingUmaRecorderTest}. */
@RunWith(BaseRobolectricTestRunner.class)
@SuppressWarnings("DoNotMock") // Ok to mock UmaRecorder since this is testing metrics.
public final class CachingUmaRecorderTest {
@Mock UmaRecorder mUmaRecorder;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testSetDelegateWithEmptyCache() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testRecordBooleanHistogramGetsFlushed() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.recordBooleanHistogram(
"cachingUmaRecorderTest.recordBooleanHistogram", true);
cachingUmaRecorder.recordBooleanHistogram(
"cachingUmaRecorderTest.recordBooleanHistogram", true);
cachingUmaRecorder.recordBooleanHistogram(
"cachingUmaRecorderTest.recordBooleanHistogram", false);
assertEquals(
3,
cachingUmaRecorder.getHistogramTotalCountForTesting(
"cachingUmaRecorderTest.recordBooleanHistogram"));
assertEquals(
2,
cachingUmaRecorder.getHistogramValueCountForTesting(
"cachingUmaRecorderTest.recordBooleanHistogram", 1));
assertEquals(
1,
cachingUmaRecorder.getHistogramValueCountForTesting(
"cachingUmaRecorderTest.recordBooleanHistogram", 0));
cachingUmaRecorder.setDelegate(mUmaRecorder);
verify(mUmaRecorder, times(2))
.recordBooleanHistogram("cachingUmaRecorderTest.recordBooleanHistogram", true);
verify(mUmaRecorder)
.recordBooleanHistogram("cachingUmaRecorderTest.recordBooleanHistogram", false);
}
@Test
public void testRecordExponentialHistogramGetsFlushed() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.recordExponentialHistogram(
"cachingUmaRecorderTest.recordExponentialHistogram", 72, 1, 1000, 50);
assertEquals(
1,
cachingUmaRecorder.getHistogramTotalCountForTesting(
"cachingUmaRecorderTest.recordExponentialHistogram"));
assertEquals(
1,
cachingUmaRecorder.getHistogramValueCountForTesting(
"cachingUmaRecorderTest.recordExponentialHistogram", 72));
cachingUmaRecorder.setDelegate(mUmaRecorder);
verify(mUmaRecorder)
.recordExponentialHistogram(
"cachingUmaRecorderTest.recordExponentialHistogram", 72, 1, 1000, 50);
}
@Test
public void testRecordLinearHistogramGetsFlushed() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.recordLinearHistogram(
"cachingUmaRecorderTest.recordLinearHistogram", 72, 1, 1000, 50);
assertEquals(
1,
cachingUmaRecorder.getHistogramTotalCountForTesting(
"cachingUmaRecorderTest.recordLinearHistogram"));
assertEquals(
1,
cachingUmaRecorder.getHistogramValueCountForTesting(
"cachingUmaRecorderTest.recordLinearHistogram", 72));
cachingUmaRecorder.setDelegate(mUmaRecorder);
verify(mUmaRecorder)
.recordLinearHistogram(
"cachingUmaRecorderTest.recordLinearHistogram", 72, 1, 1000, 50);
}
@Test
public void testRecordSparseHistogramGetsFlushed() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.recordSparseHistogram(
"cachingUmaRecorderTest.recordSparseHistogram", 72);
assertEquals(
1,
cachingUmaRecorder.getHistogramTotalCountForTesting(
"cachingUmaRecorderTest.recordSparseHistogram"));
assertEquals(
1,
cachingUmaRecorder.getHistogramValueCountForTesting(
"cachingUmaRecorderTest.recordSparseHistogram", 72));
cachingUmaRecorder.setDelegate(mUmaRecorder);
verify(mUmaRecorder)
.recordSparseHistogram("cachingUmaRecorderTest.recordSparseHistogram", 72);
}
@Test
public void testRecordUserActionGetsFlushed() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.recordUserAction("FlushedActionTested", 72);
cachingUmaRecorder.setDelegate(mUmaRecorder);
verify(mUmaRecorder).recordUserAction("FlushedActionTested", 72);
}
@Test
public void testRecordUserActionGetsMultipleFlushed() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.recordUserAction("FlushedActionTested.A", 111);
cachingUmaRecorder.recordUserAction("FlushedActionTested.B", 222);
cachingUmaRecorder.recordUserAction("FlushedActionTested.B", 333);
cachingUmaRecorder.setDelegate(mUmaRecorder);
verify(mUmaRecorder).recordUserAction("FlushedActionTested.A", 111);
verify(mUmaRecorder).recordUserAction("FlushedActionTested.B", 222);
verify(mUmaRecorder).recordUserAction("FlushedActionTested.B", 333);
}
@Test
public void testRecordBooleanHistogramDelegated() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.recordBooleanHistogram(
"CachingUmaRecorderTest.recordBooleanHistogram", true);
verify(mUmaRecorder)
.recordBooleanHistogram("CachingUmaRecorderTest.recordBooleanHistogram", true);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testRecordExponentialHistogramDelegated() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.recordExponentialHistogram(
"CachingUmaRecorderTest.recordExponentialHistogram", 1, 2, 10, 5);
verify(mUmaRecorder)
.recordExponentialHistogram(
"CachingUmaRecorderTest.recordExponentialHistogram", 1, 2, 10, 5);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testRecordLinearHistogramDelegated() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.recordLinearHistogram(
"CachingUmaRecorderTest.recordLinearHistogram", 1, 2, 10, 5);
verify(mUmaRecorder)
.recordLinearHistogram("CachingUmaRecorderTest.recordLinearHistogram", 1, 2, 10, 5);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testRecordSparseHistogramDelegated() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.recordSparseHistogram(
"CachingUmaRecorderTest.recordSparseHistogram", 72);
verify(mUmaRecorder)
.recordSparseHistogram("CachingUmaRecorderTest.recordSparseHistogram", 72);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testRecordUserActionDelegated() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.recordUserAction("ActionTested", 72);
verify(mUmaRecorder).recordUserAction("ActionTested", 72);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testSetDelegateStopsOldDelegation() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.setDelegate(new NoopUmaRecorder());
cachingUmaRecorder.recordSparseHistogram("CachingUmaRecorderTest.stopOldDelegation", 72);
verifyNoMoreInteractions(mUmaRecorder);
}
@Test
public void testSetDelegateStartsNewDelegation() {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
cachingUmaRecorder.setDelegate(new NoopUmaRecorder());
cachingUmaRecorder.setDelegate(mUmaRecorder);
cachingUmaRecorder.recordSparseHistogram("CachingUmaRecorderTest.startNewDelegation", 72);
verify(mUmaRecorder).recordSparseHistogram("CachingUmaRecorderTest.startNewDelegation", 72);
verifyNoMoreInteractions(mUmaRecorder);
}
/**
* {@link UmaRecorder} that has a public lock which can be used to block {@link
* #recordSparseHistogram(String, int)} }.
*/
private static class BlockingUmaRecorder extends NoopUmaRecorder {
public Lock lock = new ReentrantLock();
@SuppressWarnings("LockNotBeforeTry")
@Override
public void recordSparseHistogram(String name, int sample) {
lock.lock();
lock.unlock();
}
}
@Test
public void testSetDelegateBlocksUntilRecordingDone() throws Exception {
CachingUmaRecorder cachingUmaRecorder = new CachingUmaRecorder();
BlockingUmaRecorder blockingUmaRecorder = new BlockingUmaRecorder();
cachingUmaRecorder.setDelegate(blockingUmaRecorder);
Thread swappingThread;
Thread recordingThread;
blockingUmaRecorder.lock.lock();
try {
recordingThread =
new Thread(
() -> {
cachingUmaRecorder.recordSparseHistogram(
"CachingUmaRecorderTest.blockUntilRecordingDone", 16);
});
recordingThread.start();
awaitThreadBlocked(recordingThread, Duration.ofSeconds(1));
swappingThread =
new Thread(
() -> {
cachingUmaRecorder.setDelegate(mUmaRecorder);
});
swappingThread.start();
awaitThreadBlocked(swappingThread, Duration.ofSeconds(1));
} finally {
blockingUmaRecorder.lock.unlock();
}
recordingThread.join();
swappingThread.join();
verifyNoMoreInteractions(mUmaRecorder);
}
@SuppressWarnings("ThreadPriorityCheck")
private static void awaitThreadBlocked(Thread thread, Duration timeLimit) {
final Instant start = Instant.now();
while (true) {
assertThat(timeLimit, greaterThan(Duration.between(start, Instant.now())));
switch (thread.getState()) {
case BLOCKED:
case WAITING:
case TIMED_WAITING:
return;
case NEW:
case RUNNABLE:
{
Thread.yield();
continue;
}
case TERMINATED:
fail("Thread unexpectedly terminated.");
}
}
}
private static class HistogramTestingUmaRecorder implements UmaRecorder {
public final AtomicIntegerArray recordedSamples;
HistogramTestingUmaRecorder(int sampleRange) {
recordedSamples = new AtomicIntegerArray(sampleRange);
}
@Override
public void recordBooleanHistogram(String name, boolean sample) {
throw new UnsupportedOperationException();
}
@Override
public void recordExponentialHistogram(
String name, int sample, int min, int max, int numBuckets) {
throw new UnsupportedOperationException();
}
@Override
public void recordLinearHistogram(
String name, int sample, int min, int max, int numBuckets) {
throw new UnsupportedOperationException();
}
@Override
public void recordSparseHistogram(String name, int sample) {
recordedSamples.addAndGet(sample, 1);
}
@Override
public void recordUserAction(String name, long elapsedRealtimeMillis) {
throw new UnsupportedOperationException();
}
@Override
public int getHistogramValueCountForTesting(String name, int sample) {
throw new UnsupportedOperationException();
}
@Override
public int getHistogramTotalCountForTesting(String name) {
throw new UnsupportedOperationException();
}
@Override
public List<HistogramBucket> getHistogramSamplesForTesting(String name) {
throw new UnsupportedOperationException();
}
@Override
public void addUserActionCallbackForTesting(Callback<String> callback) {
throw new UnsupportedOperationException();
}
@Override
public void removeUserActionCallbackForTesting(Callback<String> callback) {
throw new UnsupportedOperationException();
}
}
@Test
@MediumTest
@SuppressWarnings("ThreadPriorityCheck")
public void testStressParallelHistograms() throws Exception {
final int numThreads = 16;
final int numRecorders = 16;
final int numSamples = CachingUmaRecorder.Histogram.MAX_SAMPLE_COUNT / numThreads;
CachingUmaRecorder cachingRecorder = new CachingUmaRecorder();
HistogramTestingUmaRecorder[] testingRecorders =
new HistogramTestingUmaRecorder[numRecorders];
for (int i = 0; i < numRecorders; i++) {
testingRecorders[i] = new HistogramTestingUmaRecorder(numThreads);
}
Thread[] threads = new Thread[numThreads];
// Each thread records a different sample value.
for (int i = 0; i < numThreads; i++) {
threads[i] = startHistogramRecordingThread(i, numSamples, cachingRecorder);
}
// Change recorders while threads are recording metrics.
for (HistogramTestingUmaRecorder recorder : testingRecorders) {
cachingRecorder.setDelegate(recorder);
Thread.yield();
}
for (Thread thread : threads) {
thread.join();
}
cachingRecorder.setDelegate(mUmaRecorder);
verifyNoMoreInteractions(mUmaRecorder);
for (int i = 0; i < numThreads; i++) {
int actualSamples = 0;
for (HistogramTestingUmaRecorder recorder : testingRecorders) {
actualSamples += recorder.recordedSamples.get(i);
}
assertThat(String.format("thread[%d] total samples", i), actualSamples, is(numSamples));
}
}
@SuppressWarnings("ThreadPriorityCheck")
private static Thread startHistogramRecordingThread(
int sample, int count, UmaRecorder recorder) {
Thread thread =
new Thread(
() -> {
for (int i = 0; i < count; i++) {
recorder.recordSparseHistogram("StressTest", sample);
// Make it more likely this thread will be preempted.
Thread.yield();
}
});
thread.start();
return thread;
}
private static class UserActionTestingUmaRecorder implements UmaRecorder {
public final AtomicIntegerArray recordedSamples;
UserActionTestingUmaRecorder(int sampleRange) {
recordedSamples = new AtomicIntegerArray(sampleRange);
}
@Override
public void recordBooleanHistogram(String name, boolean sample) {
throw new UnsupportedOperationException();
}
@Override
public void recordExponentialHistogram(
String name, int sample, int min, int max, int numBuckets) {
throw new UnsupportedOperationException();
}
@Override
public void recordLinearHistogram(
String name, int sample, int min, int max, int numBuckets) {
throw new UnsupportedOperationException();
}
@Override
public void recordSparseHistogram(String name, int sample) {
throw new UnsupportedOperationException();
}
@Override
public void recordUserAction(String name, long elapsedRealtimeMillis) {
recordedSamples.addAndGet((int) elapsedRealtimeMillis, 1);
}
@Override
public int getHistogramValueCountForTesting(String name, int sample) {
throw new UnsupportedOperationException();
}
@Override
public int getHistogramTotalCountForTesting(String name) {
throw new UnsupportedOperationException();
}
@Override
public List<HistogramBucket> getHistogramSamplesForTesting(String name) {
throw new UnsupportedOperationException();
}
@Override
public void addUserActionCallbackForTesting(Callback<String> callback) {
throw new UnsupportedOperationException();
}
@Override
public void removeUserActionCallbackForTesting(Callback<String> callback) {
throw new UnsupportedOperationException();
}
}
@Test
@MediumTest
@SuppressWarnings("ThreadPriorityCheck")
public void testStressParallelUserActions() throws Exception {
final int numThreads = 16;
final int numRecorders = 16;
final int numSamples = CachingUmaRecorder.MAX_USER_ACTION_COUNT / numThreads;
CachingUmaRecorder cachingRecorder = new CachingUmaRecorder();
UserActionTestingUmaRecorder[] testingRecorders =
new UserActionTestingUmaRecorder[numRecorders];
for (int i = 0; i < numRecorders; i++) {
testingRecorders[i] = new UserActionTestingUmaRecorder(numThreads);
}
Thread[] threads = new Thread[numThreads];
// Each thread records a different timestamp value.
for (int i = 0; i < numThreads; i++) {
threads[i] = startUserActionRecordingThread(i, numSamples, cachingRecorder);
}
// Change recorders while threads are recording metrics.
for (UserActionTestingUmaRecorder recorder : testingRecorders) {
cachingRecorder.setDelegate(recorder);
Thread.yield();
}
for (Thread thread : threads) {
thread.join();
}
cachingRecorder.setDelegate(mUmaRecorder);
verifyNoMoreInteractions(mUmaRecorder);
for (int i = 0; i < numThreads; i++) {
int actualSamples = 0;
for (UserActionTestingUmaRecorder recorder : testingRecorders) {
actualSamples += recorder.recordedSamples.get(i);
}
assertThat(String.format("thread[%d] total samples", i), actualSamples, is(numSamples));
}
}
@SuppressWarnings("ThreadPriorityCheck")
private static Thread startUserActionRecordingThread(
int sample, int count, UmaRecorder recorder) {
Thread thread =
new Thread(
() -> {
for (int i = 0; i < count; i++) {
recorder.recordUserAction("StressTestUserAction." + i, sample);
// Make it more likely this thread will be preempted.
Thread.yield();
}
});
thread.start();
return thread;
}
@Test
public void testUserActionCallbacksTriggeredWithoutDelegate() {
CachingUmaRecorder cachingRecorder = new CachingUmaRecorder();
AtomicInteger callbackCount = new AtomicInteger();
Callback<String> testCallback1 = (result) -> callbackCount.incrementAndGet();
cachingRecorder.addUserActionCallbackForTesting(testCallback1);
// Ensure that the callback is notified if attached to the caching recorder (will not
// be notified once a delegate is responsible).
Assert.assertEquals(0, callbackCount.get());
cachingRecorder.recordUserAction("TEST", 0);
Assert.assertEquals(1, callbackCount.get());
cachingRecorder.recordUserAction("TEST", 0);
Assert.assertEquals(2, callbackCount.get());
// Remove the callback and ensure it is no longer called.
cachingRecorder.removeUserActionCallbackForTesting(testCallback1);
Assert.assertEquals(2, callbackCount.get());
cachingRecorder.recordUserAction("TEST", 0);
Assert.assertEquals(2, callbackCount.get());
}
@Test
public void testUserActionCallbacksSwappedBetweenDelegates() {
CachingUmaRecorder cachingRecorder = new CachingUmaRecorder();
Callback<String> testCallback1 = (result) -> {};
Callback<String> testCallback2 = (result) -> {};
cachingRecorder.addUserActionCallbackForTesting(testCallback1);
cachingRecorder.addUserActionCallbackForTesting(testCallback2);
// Validate that previously added callbacks are passed to the new delegate.
UmaRecorder delegate1 = Mockito.mock(UmaRecorder.class);
cachingRecorder.setDelegate(delegate1);
verify(delegate1).addUserActionCallbackForTesting(testCallback1);
verify(delegate1).addUserActionCallbackForTesting(testCallback2);
// Validate that previously added callbacks are removed from the previous delegate, and
// passed to the new delegate.
UmaRecorder delegate2 = Mockito.mock(UmaRecorder.class);
cachingRecorder.setDelegate(delegate2);
verify(delegate1).removeUserActionCallbackForTesting(testCallback1);
verify(delegate1).removeUserActionCallbackForTesting(testCallback2);
verify(delegate2).addUserActionCallbackForTesting(testCallback1);
verify(delegate2).addUserActionCallbackForTesting(testCallback2);
// Ensure a callback added later is also added correctly.
Callback<String> testCallback3 = (result) -> {};
cachingRecorder.addUserActionCallbackForTesting(testCallback3);
verifyNoMoreInteractions(delegate1);
verify(delegate2).addUserActionCallbackForTesting(testCallback3);
}
}