1 // Copyright 2022 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.memory; 6 7 import android.os.Looper; 8 9 import androidx.test.filters.SmallTest; 10 11 import org.junit.Assert; 12 import org.junit.Before; 13 import org.junit.Rule; 14 import org.junit.Test; 15 import org.junit.runner.RunWith; 16 import org.robolectric.annotation.Config; 17 import org.robolectric.shadows.ShadowLooper; 18 19 import org.chromium.base.ApplicationState; 20 import org.chromium.base.BaseFeatures; 21 import org.chromium.base.FakeTimeTestRule; 22 import org.chromium.base.FeatureList; 23 import org.chromium.base.ThreadUtils; 24 import org.chromium.base.metrics.RecordHistogram; 25 import org.chromium.base.test.BaseRobolectricTestRunner; 26 27 import java.util.Map; 28 import java.util.concurrent.Callable; 29 import java.util.concurrent.TimeUnit; 30 31 /** Tests for MemoryPurgeManager. */ 32 @RunWith(BaseRobolectricTestRunner.class) 33 @Config(manifest = Config.NONE) 34 public class MemoryPurgeManagerTest { 35 @Rule public FakeTimeTestRule mFakeTimeTestRule = new FakeTimeTestRule(); 36 private Callable<Integer> mGetCount = 37 () -> { 38 return RecordHistogram.getHistogramTotalCountForTesting( 39 MemoryPurgeManager.BACKGROUND_DURATION_HISTOGRAM_NAME); 40 }; 41 42 private class MemoryPurgeManagerForTest extends MemoryPurgeManager { MemoryPurgeManagerForTest(int initialState)43 public MemoryPurgeManagerForTest(int initialState) { 44 super(); 45 mApplicationState = initialState; 46 } 47 48 @Override onApplicationStateChange(int state)49 public void onApplicationStateChange(int state) { 50 mApplicationState = state; 51 super.onApplicationStateChange(state); 52 } 53 54 @Override notifyMemoryPressure()55 protected void notifyMemoryPressure() { 56 mMemoryPressureNotifiedCount += 1; 57 } 58 59 @Override getApplicationState()60 protected int getApplicationState() { 61 return mApplicationState; 62 } 63 64 public int mMemoryPressureNotifiedCount; 65 public int mApplicationState = ApplicationState.UNKNOWN; 66 } 67 68 @Before setUp()69 public void setUp() { 70 // Explicitly set main thread as UiThread. Other places rely on that. 71 ThreadUtils.setUiThread(Looper.getMainLooper()); 72 73 // Pause main thread to get control over when tasks are run (see runUiThreadFor()). 74 ShadowLooper.pauseMainLooper(); 75 } 76 77 @Test 78 @SmallTest testSimple()79 public void testSimple() throws Exception { 80 FeatureList.setTestFeatures(Map.of(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE, true)); 81 82 int count = mGetCount.call(); 83 var manager = new MemoryPurgeManagerForTest(ApplicationState.HAS_RUNNING_ACTIVITIES); 84 manager.start(); 85 86 // No notification when initial state has activities. 87 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 88 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 89 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 90 Assert.assertEquals(count, (int) mGetCount.call()); 91 92 // Notify after a delay. 93 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 94 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); // Should be delayed. 95 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 96 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 97 98 // Only one notification. 99 manager.onApplicationStateChange(ApplicationState.HAS_DESTROYED_ACTIVITIES); 100 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 101 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 102 103 // Started in foreground, went to background once, and came back. 104 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 105 Assert.assertEquals(count + 1, (int) mGetCount.call()); 106 } 107 108 @Test 109 @SmallTest testInitializedOnceInBackground()110 public void testInitializedOnceInBackground() throws Exception { 111 FeatureList.setTestFeatures(Map.of(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE, true)); 112 113 int count = mGetCount.call(); 114 var manager = new MemoryPurgeManagerForTest(ApplicationState.HAS_STOPPED_ACTIVITIES); 115 manager.start(); 116 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 117 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 118 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 119 120 // Started in background, no histogram recording when coming to foreground. 121 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 122 Assert.assertEquals(count, (int) mGetCount.call()); 123 } 124 125 @Test 126 @SmallTest testDontTriggerForProcessesWithNoActivities()127 public void testDontTriggerForProcessesWithNoActivities() { 128 FeatureList.setTestFeatures(Map.of(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE, true)); 129 130 var manager = new MemoryPurgeManagerForTest(ApplicationState.HAS_DESTROYED_ACTIVITIES); 131 manager.start(); 132 133 // Don't purge if the process never hosted any activity. 134 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 135 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 136 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 137 138 // Starts when we cycle through foreground and background. 139 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 140 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 141 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 142 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 143 } 144 145 @Test 146 @SmallTest testMultiple()147 public void testMultiple() throws Exception { 148 FeatureList.setTestFeatures(Map.of(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE, true)); 149 150 int count = mGetCount.call(); 151 var manager = new MemoryPurgeManagerForTest(ApplicationState.HAS_RUNNING_ACTIVITIES); 152 manager.start(); 153 154 // Notify after a delay. 155 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 156 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 157 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 158 159 // Back to foreground, no notification. 160 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 161 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 162 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 163 Assert.assertEquals(count + 1, (int) mGetCount.call()); 164 165 // Background again, notify 166 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 167 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 168 Assert.assertEquals(2, manager.mMemoryPressureNotifiedCount); 169 Assert.assertEquals(count + 1, (int) mGetCount.call()); 170 171 // Foreground again, record the histogram. 172 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 173 Assert.assertEquals(count + 2, (int) mGetCount.call()); 174 } 175 176 @Test 177 @SmallTest testNoEnoughTimeInBackground()178 public void testNoEnoughTimeInBackground() { 179 FeatureList.setTestFeatures(Map.of(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE, true)); 180 181 var manager = new MemoryPurgeManagerForTest(ApplicationState.HAS_RUNNING_ACTIVITIES); 182 manager.start(); 183 184 // Background, then foregound inside the delay period. 185 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 186 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS / 2); 187 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 188 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS); 189 // Went back to foreground, do nothing. 190 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 191 192 // Starts the new task 193 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 194 // After some time, foregroung/background cycle. 195 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS / 2); 196 manager.onApplicationStateChange(ApplicationState.HAS_RUNNING_ACTIVITIES); 197 manager.onApplicationStateChange(ApplicationState.HAS_STOPPED_ACTIVITIES); 198 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS / 2); 199 // Not enough time in background. 200 Assert.assertEquals(0, manager.mMemoryPressureNotifiedCount); 201 // But the task got rescheduled. 202 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS / 2); 203 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 204 205 // No new notification. 206 runUiThreadFor(MemoryPurgeManager.PURGE_DELAY_MS * 2); 207 Assert.assertEquals(1, manager.mMemoryPressureNotifiedCount); 208 } 209 runUiThreadFor(long delayMs)210 private void runUiThreadFor(long delayMs) { 211 mFakeTimeTestRule.advanceMillis(delayMs); 212 ShadowLooper.idleMainLooper(delayMs, TimeUnit.MILLISECONDS); 213 } 214 } 215