New MessageRouter interface to replace Handler.

Proposal introduced in http://go/sysui-message-router

This introduces a testable, controllerable MessageRouter interface
intended to replace Handler#sendMessage. Listeners subscribe to
specific messages rather than subclassing Handler.

Also, metadata can be passed in any form, rather than the generic
android.os.Message.

To use, developers simply inject either an @Main or a @Background
MessageRouter. This provides them with a new instance of a
MessageRouter, tied to the corresponding Executor.

As an example, the internal Handler subclass in GarbageMonitor has
been replaced. There is an overall reduction in the number of lines
of code, and the corresponding test has been refactored to be more
deterministic and useful.

Bug: 191077376
Test: atest SystemUITests

Change-Id: Ibd1e05b1b5b7421c2085d8811b8f5cd61cf0ffde
This commit is contained in:
Dave Mankoff
2021-06-15 10:11:35 -04:00
parent ed048c6a2a
commit 4dda2323e9
6 changed files with 831 additions and 101 deletions

View File

@@ -0,0 +1,198 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.concurrency;
/**
* Allows triggering methods based on a passed in id or message, generally on another thread.
*
* Messages sent on to this router must be processed in order. That is to say, if three
* messages are sent with no delay, they must be processed in the order they were sent. Moreover,
* if messages are sent with various delays, they must be processed in order of their delay.
*
* Messages can be passed by either a simple integer or an instance of a class. Unique integers are
* considered unique messages. Unique message classes (not instances) are considered unique
* messages. You can use message classes to pass extra data for processing to subscribers.
*
* <pre>
* // Three messages with three unique integer messages.
* // They can be subscribed to independently.
* router.sendMessage(0);
* router.sendMessage(1);
* router.sendMessage(2);
*
* // Three messages with two unique message classes.
* // The first and third messages will be delivered to the same subscribers.
* router.sendMessage(new Foo(0));
* router.sendMessage(new Bar(1));
* router.sendMessage(new Foo(2));
* </pre>
*
* The number of unique ids and message types used should be relatively constrained. Construct
* a custom message-class and put unique, per-message data inside of it.
*/
public interface MessageRouter {
/**
* Alerts any listeners subscribed to the passed in id.
*
* The number of unique ids used should be relatively constrained - used to identify the type
* of message being sent. If unique information needs to be passed with each call, use
* {@link #sendMessage(Object)}.
*
* @param id An identifier for the message
*/
default void sendMessage(int id) {
sendMessageDelayed(id, 0);
}
/**
* Alerts any listeners subscribed to the passed in message.
*
* The number of message types used should be relatively constrained. If no unique information
* needs to be passed in, you can simply use {@link #sendMessage(int)}} which takes an integer
* instead of a unique class type.
*
* The class of the passed in object will be used to router the message.
*
* @param data A message containing extra data for processing.
*/
default void sendMessage(Object data) {
sendMessageDelayed(data, 0);
}
/**
* Alerts any listeners subscribed to the passed in id in the future.
*
* The number of unique ids used should be relatively constrained - used to identify the type
* of message being sent. If unique information needs to be passed with each call, use
* {@link #sendMessageDelayed(Object, long)}.
*
* @param id An identifier for the message
* @param delayMs Number of milliseconds to wait before alerting.
*/
void sendMessageDelayed(int id, long delayMs);
/**
* Alerts any listeners subscribed to the passed in message in the future.
*
* The number of message types used should be relatively constrained. If no unique information
* needs to be passed in, you can simply use {@link #sendMessageDelayed(int, long)} which takes
* an integer instead of a unique class type.
*
* @param data A message containing extra data for processing.
* @param delayMs Number of milliseconds to wait before alerting.
*/
void sendMessageDelayed(Object data, long delayMs);
/**
* Cancel all unprocessed messages for a given id.
*
* If a message has multiple listeners and one of those listeners has been alerted, the other
* listeners that follow it may also be alerted. This is only guaranteed to cancel messages
* that are still queued.
*
* @param id The message id to cancel.
*/
void cancelMessages(int id);
/**
* Cancel all unprocessed messages for a given message type.
*
* If a message has multiple listeners and one of those listeners has been alerted, the other
* listeners that follow it may also be alerted. This is only guaranteed to cancel messages
* that are still queued.
*
* @param messageType The class of the message to cancel
*/
<T> void cancelMessages(Class<T> messageType);
/**
* Add a listener for a message that does not handle any extra data.
*
* See also {@link #subscribeTo(Class, DataMessageListener)}.
*
* @param id The message id to listener for.
* @param listener
*/
void subscribeTo(int id, SimpleMessageListener listener);
/**
* Add a listener for a message of a specific type.
*
* See also {@link #subscribeTo(Class, DataMessageListener)}.
*
* @param messageType The class of message to listen for.
* @param listener
*/
<T> void subscribeTo(Class<T> messageType, DataMessageListener<T> listener);
/**
* Remove a listener for a specific message.
*
* See also {@link #unsubscribeFrom(Class, DataMessageListener)}
*
* @param id The message id to stop listening for.
* @param listener The listener to remove.
*/
void unsubscribeFrom(int id, SimpleMessageListener listener);
/**
* Remove a listener for a specific message.
*
* See also {@link #unsubscribeFrom(int, SimpleMessageListener)}.
*
* @param messageType The class of message to stop listening for.
* @param listener The listener to remove.
*/
<T> void unsubscribeFrom(Class<T> messageType, DataMessageListener<T> listener);
/**
* Remove a listener for all messages that it is subscribed to.
*
* See also {@link #unsubscribeFrom(DataMessageListener)}.
*
* @param listener The listener to remove.
*/
void unsubscribeFrom(SimpleMessageListener listener);
/**
* Remove a listener for all messages that it is subscribed to.
*
* See also {@link #unsubscribeFrom(SimpleMessageListener)}.
*
* @param listener The listener to remove.
*/
<T> void unsubscribeFrom(DataMessageListener<T> listener);
/**
* A Listener interface for when no extra data is expected or desired.
*/
interface SimpleMessageListener {
/** */
void onMessage(int id);
}
/**
* A Listener interface for when extra data is expected or desired.
*
* @param <T>
*/
interface DataMessageListener<T> {
/** */
void onMessage(T data);
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.concurrency;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implementation of {@link MessageRouter}.
*/
public class MessageRouterImpl implements MessageRouter {
private final DelayableExecutor mDelayableExecutor;
private final Map<Integer, List<Runnable>> mIdMessageCancelers = new HashMap<>();
private final Map<Class<Object>, List<Runnable>> mDataMessageCancelers = new HashMap<>();
private final Map<Integer, List<SimpleMessageListener>> mSimpleMessageListenerMap =
new HashMap<>();
private final Map<Class<?>, List<DataMessageListener<Object>>> mDataMessageListenerMap =
new HashMap<>();
public MessageRouterImpl(DelayableExecutor delayableExecutor) {
mDelayableExecutor = delayableExecutor;
}
@Override
public void sendMessageDelayed(int id, long delayMs) {
addCanceler(id, mDelayableExecutor.executeDelayed(() -> onMessage(id), delayMs));
}
@Override
public void sendMessageDelayed(Object data, long delayMs) {
addCanceler((Class<Object>) data.getClass(), mDelayableExecutor.executeDelayed(
() -> onMessage(data), delayMs));
}
@Override
public void cancelMessages(int id) {
synchronized (mIdMessageCancelers) {
if (mIdMessageCancelers.containsKey(id)) {
for (Runnable canceler : mIdMessageCancelers.get(id)) {
canceler.run();
}
// Remove, don't clear, otherwise this could look like a memory leak as
// more and more unique message ids are passed in.
mIdMessageCancelers.remove(id);
}
}
}
@Override
public <T> void cancelMessages(Class<T> messageType) {
synchronized (mDataMessageCancelers) {
if (mDataMessageCancelers.containsKey(messageType)) {
for (Runnable canceler : mDataMessageCancelers.get(messageType)) {
canceler.run();
}
// Remove, don't clear, otherwise this could look like a memory leak as
// more and more unique message types are passed in.
mDataMessageCancelers.remove(messageType);
}
}
}
@Override
public void subscribeTo(int id, SimpleMessageListener listener) {
synchronized (mSimpleMessageListenerMap) {
mSimpleMessageListenerMap.putIfAbsent(id, new ArrayList<>());
mSimpleMessageListenerMap.get(id).add(listener);
}
}
@Override
public <T> void subscribeTo(Class<T> messageType, DataMessageListener<T> listener) {
synchronized (mDataMessageListenerMap) {
mDataMessageListenerMap.putIfAbsent(messageType, new ArrayList<>());
mDataMessageListenerMap.get(messageType).add((DataMessageListener<Object>) listener);
}
}
@Override
public void unsubscribeFrom(int id, SimpleMessageListener listener) {
synchronized (mSimpleMessageListenerMap) {
if (mSimpleMessageListenerMap.containsKey(id)) {
mSimpleMessageListenerMap.get(id).remove(listener);
}
}
}
@Override
public <T> void unsubscribeFrom(Class<T> messageType, DataMessageListener<T> listener) {
synchronized (mDataMessageListenerMap) {
if (mDataMessageListenerMap.containsKey(messageType)) {
mDataMessageListenerMap.get(messageType).remove(listener);
}
}
}
@Override
public void unsubscribeFrom(SimpleMessageListener listener) {
synchronized (mSimpleMessageListenerMap) {
for (Integer id : mSimpleMessageListenerMap.keySet()) {
mSimpleMessageListenerMap.get(id).remove(listener);
}
}
}
@Override
public <T> void unsubscribeFrom(DataMessageListener<T> listener) {
synchronized (mDataMessageListenerMap) {
for (Class<?> messageType : mDataMessageListenerMap.keySet()) {
mDataMessageListenerMap.get(messageType).remove(listener);
}
}
}
private void addCanceler(int id, Runnable canceler) {
synchronized (mIdMessageCancelers) {
mIdMessageCancelers.putIfAbsent(id, new ArrayList<>());
mIdMessageCancelers.get(id).add(canceler);
}
}
private void addCanceler(Class<Object> data, Runnable canceler) {
synchronized (mDataMessageCancelers) {
mDataMessageCancelers.putIfAbsent(data, new ArrayList<>());
mDataMessageCancelers.get(data).add(canceler);
}
}
private void onMessage(int id) {
synchronized (mSimpleMessageListenerMap) {
if (mSimpleMessageListenerMap.containsKey(id)) {
for (SimpleMessageListener listener : mSimpleMessageListenerMap.get(id)) {
listener.onMessage(id);
}
}
}
synchronized (mIdMessageCancelers) {
if (mIdMessageCancelers.containsKey(id) && !mIdMessageCancelers.get(id).isEmpty()) {
mIdMessageCancelers.get(id).remove(0);
if (mIdMessageCancelers.get(id).isEmpty()) {
mIdMessageCancelers.remove(id);
}
}
}
}
private void onMessage(Object data) {
synchronized (mDataMessageListenerMap) {
if (mDataMessageListenerMap.containsKey(data.getClass())) {
for (DataMessageListener<Object> listener : mDataMessageListenerMap.get(
data.getClass())) {
listener.onMessage(data);
}
}
}
synchronized (mDataMessageCancelers) {
if (mDataMessageCancelers.containsKey(data.getClass())
&& !mDataMessageCancelers.get(data.getClass()).isEmpty()) {
mDataMessageCancelers.get(data.getClass()).remove(0);
if (mDataMessageCancelers.get(data.getClass()).isEmpty()) {
mDataMessageCancelers.remove(data.getClass());
}
}
}
}
}

View File

@@ -170,4 +170,20 @@ public abstract class SysUIConcurrencyModule {
public static Executor provideUiBackgroundExecutor() {
return Executors.newSingleThreadExecutor();
}
/** */
@Provides
@Main
public static MessageRouter providesMainMessageRouter(
@Main DelayableExecutor executor) {
return new MessageRouterImpl(executor);
}
/** */
@Provides
@Background
public static MessageRouter providesBackgroundMessageRouter(
@Background DelayableExecutor executor) {
return new MessageRouterImpl(executor);
}
}

View File

@@ -36,7 +36,6 @@ import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemProperties;
import android.provider.Settings;
@@ -60,6 +59,8 @@ import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.tileimpl.QSIconViewImpl;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.MessageRouter;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -110,18 +111,18 @@ public class GarbageMonitor implements Dumpable {
private static final int DO_GARBAGE_INSPECTION = 1000;
private static final int DO_HEAP_TRACK = 3000;
private static final int GARBAGE_ALLOWANCE = 5;
static final int GARBAGE_ALLOWANCE = 5;
private static final String TAG = "GarbageMonitor";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final Handler mHandler;
private final MessageRouter mMessageRouter;
private final TrackedGarbage mTrackedGarbage;
private final LeakReporter mLeakReporter;
private final Context mContext;
private final ActivityManager mAm;
private final DelayableExecutor mDelayableExecutor;
private MemoryTile mQSTile;
private DumpTruck mDumpTruck;
private final DumpTruck mDumpTruck;
private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
private final ArrayList<Long> mPids = new ArrayList<>();
@@ -133,13 +134,16 @@ public class GarbageMonitor implements Dumpable {
@Inject
public GarbageMonitor(
Context context,
@Background Looper bgLooper,
@Background DelayableExecutor delayableExecutor,
@Background MessageRouter messageRouter,
LeakDetector leakDetector,
LeakReporter leakReporter) {
mContext = context.getApplicationContext();
mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
mHandler = new BackgroundHeapCheckHandler(bgLooper);
mDelayableExecutor = delayableExecutor;
mMessageRouter = messageRouter;
mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection);
mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack);
mTrackedGarbage = leakDetector.getTrackedGarbage();
mLeakReporter = leakReporter;
@@ -158,13 +162,13 @@ public class GarbageMonitor implements Dumpable {
return;
}
mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION);
}
public void startHeapTracking() {
startTrackingProcess(
android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
mHandler.sendEmptyMessage(DO_HEAP_TRACK);
mMessageRouter.sendMessage(DO_HEAP_TRACK);
}
private boolean gcAndCheckGarbage() {
@@ -586,33 +590,18 @@ public class GarbageMonitor implements Dumpable {
}
}
private class BackgroundHeapCheckHandler extends Handler {
BackgroundHeapCheckHandler(Looper onLooper) {
super(onLooper);
if (Looper.getMainLooper().equals(onLooper)) {
throw new RuntimeException(
"BackgroundHeapCheckHandler may not run on the ui thread");
}
private void doGarbageInspection(int id) {
if (gcAndCheckGarbage()) {
mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100);
}
@Override
public void handleMessage(Message m) {
switch (m.what) {
case DO_GARBAGE_INSPECTION:
if (gcAndCheckGarbage()) {
postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100);
}
mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION);
mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
}
removeMessages(DO_GARBAGE_INSPECTION);
sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
break;
case DO_HEAP_TRACK:
update();
removeMessages(DO_HEAP_TRACK);
sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
break;
}
}
private void doHeapTrack(int id) {
update();
mMessageRouter.cancelMessages(DO_HEAP_TRACK);
mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
}
}

View File

@@ -0,0 +1,371 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.concurrency;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class MessageRouterImplTest extends SysuiTestCase {
private static final int MESSAGE_A = 0;
private static final int MESSAGE_B = 1;
private static final int MESSAGE_C = 2;
private static final String METADATA_A = "A";
private static final String METADATA_B = "B";
private static final String METADATA_C = "C";
private static final Foobar METADATA_FOO = new Foobar();
FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
MessageRouter.SimpleMessageListener mNoMdListener;
@Mock
MessageRouter.DataMessageListener<String> mStringListener;
@Mock
MessageRouter.DataMessageListener<Foobar> mFoobarListener;
private MessageRouterImpl mMR;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mMR = new MessageRouterImpl(mFakeExecutor);
}
@Test
public void testSingleMessage_NoMetaData() {
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mMR.sendMessage(MESSAGE_A);
verify(mNoMdListener, never()).onMessage(anyInt());
mFakeExecutor.runAllReady();
verify(mNoMdListener).onMessage(MESSAGE_A);
}
@Test
public void testSingleMessage_WithMetaData() {
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessage(METADATA_A);
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.runAllReady();
verify(mStringListener).onMessage(METADATA_A);
}
@Test
public void testMessages_WithMixedMetaData() {
mMR.subscribeTo(String.class, mStringListener);
mMR.subscribeTo(Foobar.class, mFoobarListener);
mMR.sendMessage(METADATA_A);
verify(mStringListener, never()).onMessage(anyString());
verify(mFoobarListener, never()).onMessage(any(Foobar.class));
mFakeExecutor.runAllReady();
verify(mStringListener).onMessage(METADATA_A);
verify(mFoobarListener, never()).onMessage(any(Foobar.class));
reset(mStringListener);
reset(mFoobarListener);
mMR.sendMessage(METADATA_FOO);
verify(mStringListener, never()).onMessage(anyString());
verify(mFoobarListener, never()).onMessage(any(Foobar.class));
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(anyString());
verify(mFoobarListener).onMessage(METADATA_FOO);
}
@Test
public void testMessages_WithAndWithoutMetaData() {
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessage(MESSAGE_A);
verify(mNoMdListener, never()).onMessage(anyInt());
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.runAllReady();
verify(mNoMdListener).onMessage(MESSAGE_A);
verify(mStringListener, never()).onMessage(anyString());
reset(mNoMdListener);
reset(mStringListener);
mMR.sendMessage(METADATA_A);
verify(mNoMdListener, never()).onMessage(anyInt());
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.runAllReady();
verify(mNoMdListener, never()).onMessage(anyInt());
verify(mStringListener).onMessage(METADATA_A);
}
@Test
public void testRepeatedMessage() {
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessage(METADATA_A);
mMR.sendMessage(METADATA_A);
mMR.sendMessage(METADATA_B);
verify(mStringListener, never()).onMessage(anyString());
InOrder ordered = inOrder(mStringListener);
mFakeExecutor.runNextReady();
ordered.verify(mStringListener).onMessage(METADATA_A);
mFakeExecutor.runNextReady();
ordered.verify(mStringListener).onMessage(METADATA_A);
mFakeExecutor.runNextReady();
ordered.verify(mStringListener).onMessage(METADATA_B);
}
@Test
public void testCancelMessage() {
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mMR.subscribeTo(MESSAGE_B, mNoMdListener);
mMR.subscribeTo(MESSAGE_C, mNoMdListener);
mMR.sendMessage(MESSAGE_A);
mMR.sendMessage(MESSAGE_B);
mMR.sendMessage(MESSAGE_B);
mMR.sendMessage(MESSAGE_B);
mMR.sendMessage(MESSAGE_B);
mMR.sendMessage(MESSAGE_C);
verify(mNoMdListener, never()).onMessage(anyInt());
mMR.cancelMessages(MESSAGE_B);
mFakeExecutor.runAllReady();
InOrder ordered = inOrder(mNoMdListener);
ordered.verify(mNoMdListener).onMessage(MESSAGE_A);
ordered.verify(mNoMdListener).onMessage(MESSAGE_C);
}
@Test
public void testSendMessage_NoSubscriber() {
mMR.sendMessage(MESSAGE_A);
verify(mNoMdListener, never()).onMessage(anyInt());
mFakeExecutor.runAllReady();
verify(mNoMdListener, never()).onMessage(anyInt());
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mFakeExecutor.runAllReady();
verify(mNoMdListener, never()).onMessage(anyInt());
mMR.sendMessage(MESSAGE_A);
verify(mNoMdListener, never()).onMessage(anyInt());
mFakeExecutor.runAllReady();
verify(mNoMdListener).onMessage(MESSAGE_A);
}
@Test
public void testUnsubscribe_SpecificMessage_NoMetadata() {
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mMR.subscribeTo(MESSAGE_B, mNoMdListener);
mMR.sendMessage(MESSAGE_A);
mMR.sendMessage(MESSAGE_B);
mFakeExecutor.runAllReady();
InOrder ordered = inOrder(mNoMdListener);
ordered.verify(mNoMdListener).onMessage(MESSAGE_A);
ordered.verify(mNoMdListener).onMessage(MESSAGE_B);
reset(mNoMdListener);
mMR.unsubscribeFrom(MESSAGE_A, mNoMdListener);
mMR.sendMessage(MESSAGE_A);
mMR.sendMessage(MESSAGE_B);
mFakeExecutor.runAllReady();
verify(mNoMdListener, never()).onMessage(MESSAGE_A);
verify(mNoMdListener).onMessage(MESSAGE_B);
}
@Test
public void testUnsubscribe_SpecificMessage_WithMetadata() {
mMR.subscribeTo(String.class, mStringListener);
mMR.subscribeTo(Foobar.class, mFoobarListener);
mMR.sendMessage(METADATA_A);
mMR.sendMessage(METADATA_FOO);
mFakeExecutor.runNextReady();
verify(mStringListener).onMessage(METADATA_A);
mFakeExecutor.runNextReady();
verify(mFoobarListener).onMessage(METADATA_FOO);
reset(mStringListener);
reset(mFoobarListener);
mMR.unsubscribeFrom(String.class, mStringListener);
mMR.sendMessage(METADATA_A);
mMR.sendMessage(METADATA_FOO);
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(METADATA_A);
verify(mFoobarListener).onMessage(METADATA_FOO);
}
@Test
public void testUnsubscribe_AllMessages_NoMetadata() {
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mMR.subscribeTo(MESSAGE_B, mNoMdListener);
mMR.subscribeTo(MESSAGE_C, mNoMdListener);
mMR.sendMessage(MESSAGE_A);
mMR.sendMessage(MESSAGE_B);
mMR.sendMessage(MESSAGE_C);
mFakeExecutor.runAllReady();
verify(mNoMdListener).onMessage(MESSAGE_A);
verify(mNoMdListener).onMessage(MESSAGE_B);
verify(mNoMdListener).onMessage(MESSAGE_C);
reset(mNoMdListener);
mMR.unsubscribeFrom(mNoMdListener);
mMR.sendMessage(MESSAGE_A);
mMR.sendMessage(MESSAGE_B);
mMR.sendMessage(MESSAGE_C);
mFakeExecutor.runAllReady();
verify(mNoMdListener, never()).onMessage(MESSAGE_A);
verify(mNoMdListener, never()).onMessage(MESSAGE_B);
verify(mNoMdListener, never()).onMessage(MESSAGE_C);
}
@Test
public void testUnsubscribe_AllMessages_WithMetadata() {
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessage(METADATA_A);
mMR.sendMessage(METADATA_B);
mMR.sendMessage(METADATA_C);
mFakeExecutor.runAllReady();
verify(mStringListener).onMessage(METADATA_A);
verify(mStringListener).onMessage(METADATA_B);
verify(mStringListener).onMessage(METADATA_C);
reset(mStringListener);
mMR.unsubscribeFrom(mStringListener);
mMR.sendMessage(METADATA_A);
mMR.sendMessage(METADATA_B);
mMR.sendMessage(METADATA_C);
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(METADATA_A);
verify(mStringListener, never()).onMessage(METADATA_B);
verify(mStringListener, never()).onMessage(METADATA_C);
}
@Test
public void testSingleDelayedMessage_NoMetaData() {
mMR.subscribeTo(MESSAGE_A, mNoMdListener);
mMR.sendMessageDelayed(MESSAGE_A, 100);
verify(mNoMdListener, never()).onMessage(anyInt());
mFakeExecutor.runAllReady();
verify(mNoMdListener, never()).onMessage(anyInt());
mFakeExecutor.advanceClockToNext();
mFakeExecutor.runAllReady();
verify(mNoMdListener).onMessage(MESSAGE_A);
}
@Test
public void testSingleDelayedMessage_WithMetaData() {
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessageDelayed(METADATA_C, 1000);
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.advanceClockToNext();
mFakeExecutor.runAllReady();
verify(mStringListener).onMessage(METADATA_C);
}
@Test
public void testMultipleDelayedMessages() {
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessageDelayed(METADATA_A, 100);
mMR.sendMessageDelayed(METADATA_B, 1000);
mMR.sendMessageDelayed(METADATA_C, 500);
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.advanceClockToLast();
mFakeExecutor.runAllReady();
InOrder ordered = inOrder(mStringListener);
ordered.verify(mStringListener).onMessage(METADATA_A);
ordered.verify(mStringListener).onMessage(METADATA_C);
ordered.verify(mStringListener).onMessage(METADATA_B);
}
@Test
public void testCancelDelayedMessages() {
mMR.subscribeTo(String.class, mStringListener);
mMR.sendMessageDelayed(METADATA_A, 100);
mMR.sendMessageDelayed(METADATA_B, 1000);
mMR.sendMessageDelayed(METADATA_C, 500);
verify(mStringListener, never()).onMessage(anyString());
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(anyString());
mMR.cancelMessages(String.class);
mFakeExecutor.advanceClockToLast();
mFakeExecutor.runAllReady();
verify(mStringListener, never()).onMessage(anyString());
}
private static class Foobar {}
}

View File

@@ -17,124 +17,94 @@
package com.android.systemui.util.leak;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.concurrency.MessageRouterImpl;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
public class GarbageMonitorTest extends SysuiTestCase {
private LeakReporter mLeakReporter;
private TrackedGarbage mTrackedGarbage;
private TestableGarbageMonitor mGarbageMonitor;
@Mock private LeakReporter mLeakReporter;
@Mock private TrackedGarbage mTrackedGarbage;
private GarbageMonitor mGarbageMonitor;
private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Before
public void setup() {
mTrackedGarbage = mock(TrackedGarbage.class);
mLeakReporter = mock(LeakReporter.class);
MockitoAnnotations.initMocks(this);
mGarbageMonitor =
new TestableGarbageMonitor(
new GarbageMonitor(
mContext,
TestableLooper.get(this).getLooper(),
mFakeExecutor,
new MessageRouterImpl(mFakeExecutor),
new LeakDetector(null, mTrackedGarbage, null),
mLeakReporter);
}
@Test
public void testCallbacks_getScheduled() {
mGarbageMonitor.startLeakMonitor();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.runCallbacksOnce();
}
@Test
public void testNoGarbage_doesntDump() {
when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
mGarbageMonitor.startLeakMonitor();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.runCallbacksOnce();
verify(mLeakReporter, never()).dumpLeak(anyInt());
}
@Test
public void testALittleGarbage_doesntDump() {
when(mTrackedGarbage.countOldGarbage()).thenReturn(4);
when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE);
mGarbageMonitor.startLeakMonitor();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.reinspectGarbageAfterGc();
verify(mLeakReporter, never()).dumpLeak(anyInt());
}
@Test
public void testTransientGarbage_doesntDump() {
when(mTrackedGarbage.countOldGarbage()).thenReturn(100);
when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
// Start the leak monitor. Nothing gets reported immediately.
mGarbageMonitor.startLeakMonitor();
mGarbageMonitor.runInspectCallback();
mFakeExecutor.runAllReady();
verify(mLeakReporter, never()).dumpLeak(anyInt());
// Garbage gets reset to 0 before the leak reporte actually gets called.
when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
mFakeExecutor.advanceClockToLast();
mFakeExecutor.runAllReady();
mGarbageMonitor.runReinspectCallback();
// Therefore nothing gets dumped.
verify(mLeakReporter, never()).dumpLeak(anyInt());
}
@Test
public void testLotsOfPersistentGarbage_dumps() {
when(mTrackedGarbage.countOldGarbage()).thenReturn(100);
when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
mGarbageMonitor.startLeakMonitor();
mGarbageMonitor.runCallbacksOnce();
mGarbageMonitor.reinspectGarbageAfterGc();
verify(mLeakReporter).dumpLeak(anyInt());
verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
}
private static class TestableGarbageMonitor extends GarbageMonitor {
public TestableGarbageMonitor(
Context context,
Looper looper,
LeakDetector leakDetector,
LeakReporter leakReporter) {
super(context, looper, leakDetector, leakReporter);
}
@Test
public void testLotsOfPersistentGarbage_dumpsAfterAtime() {
when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
void runInspectCallback() {
startLeakMonitor();
}
// Start the leak monitor. Nothing gets reported immediately.
mGarbageMonitor.startLeakMonitor();
mFakeExecutor.runAllReady();
verify(mLeakReporter, never()).dumpLeak(anyInt());
void runReinspectCallback() {
reinspectGarbageAfterGc();
}
mFakeExecutor.advanceClockToLast();
mFakeExecutor.runAllReady();
void runCallbacksOnce() {
// Note that TestableLooper doesn't currently support delayed messages so we need to run
// callbacks explicitly.
runInspectCallback();
runReinspectCallback();
}
verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
}
}