blob: 84ca0c395a876a6e61119b3f6bfda6375f3b4339 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
import android.util.Pair;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.process_launcher.ChildProcessCreationParams;
import org.chromium.base.test.util.Feature;
import org.chromium.testing.local.LocalRobolectricTestRunner;
import java.util.ArrayList;
/**
* Unit tests for BindingManagerImpl and ChildProcessConnection.
*
* Default property of being low-end device is overriden, so that both low-end and high-end policies
* are tested.
*/
@RunWith(LocalRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class BindingManagerImplTest {
private static class MockChildServiceConnection
implements ChildProcessConnection.ChildServiceConnection {
private boolean mBound;
@Override
public boolean bind() {
mBound = true;
return true;
}
@Override
public void unbind() {
mBound = false;
}
@Override
public boolean isBound() {
return mBound;
}
}
private static class TestChildProcessConnection extends ChildProcessConnection {
private final int mPid;
private boolean mConnected;
/**
* Creates a mock binding corresponding to real ManagedChildProcessConnection after the
* connection is established: with initial binding bound and no strong binding.
*/
private TestChildProcessConnection(int pid) {
super(null /* context */, new ComponentName("org.chromium.test", "TestService"),
false /* isExternalService */, null /* childProcessCommonParameters */,
new ChildProcessCreationParams("org.chromium.test",
false /* isExternalService */, 0 /* libraryProcessType */,
false /* bindToCallerCheck */));
mPid = pid;
}
@Override
public int getPid() {
return mPid;
}
@Override
protected ChildServiceConnection createServiceConnection(int bindFlags) {
return new MockChildServiceConnection();
}
// We don't have a real service so we have to mock the connection status.
@Override
public void start(boolean useStrongBinding, ServiceCallback serviceCallback) {
super.start(useStrongBinding, serviceCallback);
mConnected = true;
}
@Override
public void stop() {
super.stop();
mConnected = false;
}
@Override
public boolean isConnected() {
return mConnected;
}
}
// Creates a mocked ChildProcessConnection that is optionally added to a BindingManager.
private static ChildProcessConnection createTestChildProcessConnection(
int pid, BindingManager manager) {
ChildProcessConnection connection = new TestChildProcessConnection(pid);
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
if (manager != null) {
manager.addNewConnection(pid, connection);
}
return connection;
}
/**
* Helper class that stores a manager along with its text label. This is used for tests that
* iterate over all managers to indicate which manager was being tested when an assertion
* failed.
*/
private static class ManagerEntry {
BindingManagerImpl mManager;
String mLabel; // Name of the manager.
ManagerEntry(BindingManagerImpl manager, String label) {
mManager = manager;
mLabel = label;
}
String getErrorMessage() {
return "Failed for the " + mLabel + " manager.";
}
}
Activity mActivity;
// The managers are created in setUp() for convenience.
BindingManagerImpl mLowEndManager;
BindingManagerImpl mHighEndManager;
BindingManagerImpl mModerateBindingManager;
ManagerEntry[] mAllManagers;
@Before
public void setUp() {
// The tests run on only one thread. Pretend that is the launcher thread so LauncherThread
// asserts are not triggered.
LauncherThread.setCurrentThreadAsLauncherThread();
mActivity = Robolectric.buildActivity(Activity.class).setup().get();
mLowEndManager =
BindingManagerImpl.createBindingManagerForTesting(true /* isLowEndDevice */);
mHighEndManager =
BindingManagerImpl.createBindingManagerForTesting(false /* isLowEndDevice */);
mModerateBindingManager =
BindingManagerImpl.createBindingManagerForTesting(false /* isLowEndDevice */);
mModerateBindingManager.startModerateBindingManagement(mActivity, 4 /* maxSize */);
mAllManagers = new ManagerEntry[] {
new ManagerEntry(mLowEndManager, "low-end"),
new ManagerEntry(mHighEndManager, "high-end"),
new ManagerEntry(mModerateBindingManager, "moderate-binding")};
}
@After
public void tearDown() {
LauncherThread.setLauncherThreadAsLauncherThread();
}
/**
* Verifies that when running on low-end, the binding manager drops the oom bindings for the
* previously bound connection when a new connection is used in foreground.
*/
@Test
@Feature({"ProcessManagement"})
public void testNewConnectionDropsPreviousOnLowEnd() {
// This test applies only to the low-end manager.
BindingManagerImpl manager = mLowEndManager;
// Add a connection to the manager.
ChildProcessConnection firstConnection =
createTestChildProcessConnection(1 /* pid */, manager);
// Bind a strong binding on the connection.
manager.setPriority(firstConnection.getPid(), true /* foreground */, false /* boost */);
Assert.assertTrue(firstConnection.isStrongBindingBound());
// Add a new connection.
ChildProcessConnection secondConnection =
createTestChildProcessConnection(2 /* pid */, manager);
// Verify that the strong binding for the first connection wasn't dropped.
Assert.assertTrue(firstConnection.isStrongBindingBound());
// Verify that the strong binding for the first connection was dropped when a new connection
// got used in foreground.
manager.setPriority(secondConnection.getPid(), true /* foreground */, false /* boost */);
Assert.assertFalse(firstConnection.isStrongBindingBound());
Assert.assertTrue(secondConnection.isStrongBindingBound());
}
/**
* Verifies the strong binding removal policies for low end devices:
* - removal of a strong binding should be executed synchronously
*/
@Test
@Feature({"ProcessManagement"})
public void testStrongBindingRemovalOnLowEnd() throws Throwable {
// This test applies only to the low-end manager.
BindingManagerImpl manager = mLowEndManager;
// Add a connection to the manager.
ChildProcessConnection connection = createTestChildProcessConnection(1 /* pid */, manager);
Assert.assertTrue(connection.isInitialBindingBound());
Assert.assertFalse(connection.isStrongBindingBound());
// Add a strong binding.
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
Assert.assertTrue(connection.isStrongBindingBound());
// Remove the strong binding, verify that the strong binding is removed immediately.
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
Assert.assertFalse(connection.isStrongBindingBound());
}
/**
* Verifies the strong binding removal policies for high end devices, where the removal should
* be delayed.
*/
@Test
@Feature({"ProcessManagement"})
public void testStrongBindingRemovalOnHighEnd() throws Throwable {
// This test applies only to the high-end manager.
BindingManagerImpl manager = mHighEndManager;
// Add a connection to the manager.
ChildProcessConnection connection = createTestChildProcessConnection(1 /* pid */, manager);
Assert.assertTrue(connection.isInitialBindingBound());
Assert.assertFalse(connection.isStrongBindingBound());
// Add a strong binding, verify that the initial binding is not removed.
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
Assert.assertTrue(connection.isStrongBindingBound());
// Remove the strong binding, verify that the strong binding is not removed
// immediately.
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
Assert.assertTrue(connection.isStrongBindingBound());
// Wait until the posted unbinding tasks get executed and verify that the strong binding was
// removed while the initial binding is not affected.
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertFalse(connection.isStrongBindingBound());
}
/**
* Verifies the strong binding removal policies with moderate binding management, where the
* moderate binding should be bound.
*/
@Test
@Feature({"ProcessManagement"})
public void testStrongBindingRemovalWithModerateBinding() throws Throwable {
// This test applies only to the moderate-binding manager.
BindingManagerImpl manager = mModerateBindingManager;
// Add a connection to the manager and start it.
ChildProcessConnection connection = createTestChildProcessConnection(1 /* pid */, manager);
Assert.assertTrue(connection.isInitialBindingBound());
Assert.assertFalse(connection.isStrongBindingBound());
Assert.assertFalse(connection.isModerateBindingBound());
// Add a strong binding, verify that the initial binding is not removed.
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
Assert.assertTrue(connection.isStrongBindingBound());
Assert.assertFalse(connection.isModerateBindingBound());
// Remove the strong binding, verify that the strong binding is not removed
// immediately.
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
Assert.assertTrue(connection.isStrongBindingBound());
Assert.assertFalse(connection.isModerateBindingBound());
// Wait until the posted unbinding tasks get executed and verify that the strong binding was
// removed while the initial binding is not affected, and the moderate binding is bound.
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertFalse(connection.isStrongBindingBound());
Assert.assertTrue(connection.isModerateBindingBound());
}
/**
* This test corresponds to a process crash scenario: after a process dies and its connection is
* cleared, isWaivedBoundOnlyOrWasWhenDied() may be called on the connection to decide if it was
* a crash or out-of-memory kill.
*/
@Test
@Feature({"ProcessManagement"})
public void testIsWaivedBoundOnly() {
// This test applies to low-end, high-end and moderate-binding policies.
for (ManagerEntry managerEntry : mAllManagers) {
BindingManagerImpl manager = managerEntry.mManager;
String message = managerEntry.getErrorMessage();
// Add a connection to the manager.
ChildProcessConnection connection =
createTestChildProcessConnection(1 /* pid */, manager);
// Initial binding is a moderate binding.
Assert.assertFalse(message, connection.isWaivedBoundOnlyOrWasWhenDied());
// After initial binding is removed, the connection is no longer waived bound only.
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
if (managerEntry.mManager == mModerateBindingManager) {
// The moderate binding manager adds a moderate binding.
Assert.assertFalse(message, connection.isWaivedBoundOnlyOrWasWhenDied());
} else {
Assert.assertTrue(message, connection.isWaivedBoundOnlyOrWasWhenDied());
}
// Add a strong binding.
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
Assert.assertFalse(message, connection.isWaivedBoundOnlyOrWasWhenDied());
// Simulate a process crash - clear a connection in binding manager and remove the
// bindings.
Assert.assertFalse(manager.isConnectionCleared(connection.getPid()));
manager.removeConnection(connection.getPid());
Assert.assertTrue(manager.isConnectionCleared(connection.getPid()));
connection.stop();
// Verify that manager reports the the connection was waived bound.
Assert.assertFalse(message, connection.isInitialBindingBound());
Assert.assertFalse(message, connection.isModerateBindingBound());
Assert.assertFalse(message, connection.isStrongBindingBound());
Assert.assertFalse(message, connection.isWaivedBoundOnlyOrWasWhenDied());
}
}
/**
* Verifies that onSentToBackground() / onBroughtToForeground() correctly attach and remove
* additional strong binding kept on the most recently bound renderer for the background
* period.
*
* The renderer that will be bound for the background period should be the one that was most
* recendly bound using .setPriority(), even if there is one that was added using
* .addNewConnection() after that. Otherwise we would bound a background renderer when user
* loads a new tab in background and leaves the browser.
*/
@Test
@Feature({"ProcessManagement"})
public void testBackgroundPeriodBinding() {
// This test applies to low-end, high-end and moderate-binding policies.
for (ManagerEntry managerEntry : mAllManagers) {
BindingManagerImpl manager = managerEntry.mManager;
String message = managerEntry.getErrorMessage();
// Add two connections, bind and release each.
ChildProcessConnection firstConnection =
createTestChildProcessConnection(1 /* pid */, manager);
manager.setPriority(firstConnection.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(
firstConnection.getPid(), false /* foreground */, false /* boost */);
ChildProcessConnection secondConnection =
createTestChildProcessConnection(2 /* pid */, manager);
manager.setPriority(
secondConnection.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(
secondConnection.getPid(), false /* foreground */, false /* boost */);
// Add third connection, do not bind it.
ChildProcessConnection thirdConnection =
createTestChildProcessConnection(3 /* pid */, manager);
manager.setPriority(
thirdConnection.getPid(), false /* foreground */, false /* boost */);
// Sanity check: verify that no connection has a strong binding.
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertFalse(message, firstConnection.isStrongBindingBound());
Assert.assertFalse(message, secondConnection.isStrongBindingBound());
Assert.assertFalse(message, thirdConnection.isStrongBindingBound());
// Call onSentToBackground() and verify that a strong binding was added for the second
// connection:
// - not the first one, because it was bound earlier than the second
// - not the thirs one, because it was never bound at all
manager.onSentToBackground();
Assert.assertFalse(message, firstConnection.isStrongBindingBound());
Assert.assertTrue(message, secondConnection.isStrongBindingBound());
Assert.assertFalse(message, thirdConnection.isStrongBindingBound());
// Call onBroughtToForeground() and verify that the strong binding was removed.
manager.onBroughtToForeground();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertFalse(message, firstConnection.isStrongBindingBound());
Assert.assertFalse(message, secondConnection.isStrongBindingBound());
Assert.assertFalse(message, thirdConnection.isStrongBindingBound());
}
}
/**
* Verifies that onSentToBackground() drops all the moderate bindings after some delay, and
* onBroughtToForeground() doesn't recover them.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingDropOnBackground() {
// This test applies only to the moderate-binding manager.
final BindingManagerImpl manager = mModerateBindingManager;
ChildProcessConnection[] connections = new ChildProcessConnection[3];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(i + 1 /* pid */, manager);
}
// Verify that each connection has a moderate binding after binding and releasing a strong
// binding.
for (ChildProcessConnection connection : connections) {
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertTrue(connection.isModerateBindingBound());
}
// Exclude lastInForeground because it will be kept in foreground when onSentToBackground()
// is called as |mLastInForeground|.
ChildProcessConnection lastInForeground =
createTestChildProcessConnection(0 /* pid */, manager);
manager.setPriority(lastInForeground.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(lastInForeground.getPid(), false /* foreground */, false /* boost */);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
// Verify that leaving the application for a short time doesn't clear the moderate bindings.
manager.onSentToBackground();
for (ChildProcessConnection connection : connections) {
Assert.assertTrue(connection.isModerateBindingBound());
}
Assert.assertTrue(lastInForeground.isStrongBindingBound());
Assert.assertFalse(lastInForeground.isModerateBindingBound());
manager.onBroughtToForeground();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
for (ChildProcessConnection connection : connections) {
Assert.assertTrue(connection.isModerateBindingBound());
}
// Call onSentToBackground() and verify that all the moderate bindings drop after some
// delay.
manager.onSentToBackground();
for (ChildProcessConnection connection : connections) {
Assert.assertTrue(connection.isModerateBindingBound());
}
Assert.assertTrue(lastInForeground.isStrongBindingBound());
Assert.assertFalse(lastInForeground.isModerateBindingBound());
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
for (ChildProcessConnection connection : connections) {
Assert.assertFalse(connection.isModerateBindingBound());
}
// Call onBroughtToForeground() and verify that the previous moderate bindings aren't
// recovered.
manager.onBroughtToForeground();
for (ChildProcessConnection connection : connections) {
Assert.assertFalse(connection.isModerateBindingBound());
}
}
/**
* Verifies that onLowMemory() drops all the moderate bindings.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingDropOnLowMemory() {
final Application app = mActivity.getApplication();
final BindingManagerImpl manager = mModerateBindingManager;
ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(i + 1 /* pid */, manager);
}
// Verify that each connection has a moderate binding after binding and releasing a strong
// binding.
for (ChildProcessConnection connection : connections) {
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertTrue(connection.isModerateBindingBound());
}
// Call onLowMemory() and verify that all the moderate bindings drop.
app.onLowMemory();
for (ChildProcessConnection connection : connections) {
Assert.assertFalse(connection.isModerateBindingBound());
}
}
/**
* Verifies that onTrimMemory() drops moderate bindings properly.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingDropOnTrimMemory() {
final Application app = mActivity.getApplication();
// This test applies only to the moderate-binding manager.
final BindingManagerImpl manager = mModerateBindingManager;
ArrayList<Pair<Integer, Integer>> levelAndExpectedVictimCountList = new ArrayList<>();
levelAndExpectedVictimCountList.add(
new Pair<Integer, Integer>(TRIM_MEMORY_RUNNING_MODERATE, 1));
levelAndExpectedVictimCountList.add(new Pair<Integer, Integer>(TRIM_MEMORY_RUNNING_LOW, 2));
levelAndExpectedVictimCountList.add(
new Pair<Integer, Integer>(TRIM_MEMORY_RUNNING_CRITICAL, 4));
ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(i + 1 /* pid */, manager);
}
for (Pair<Integer, Integer> pair : levelAndExpectedVictimCountList) {
String message = "Failed for the level=" + pair.first;
// Verify that each connection has a moderate binding after binding and releasing a
// strong binding.
for (ChildProcessConnection connection : connections) {
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertTrue(message, connection.isModerateBindingBound());
}
app.onTrimMemory(pair.first);
// Verify that some of the moderate bindings have been dropped.
for (int i = 0; i < connections.length; i++) {
Assert.assertEquals(
message, i >= pair.second, connections[i].isModerateBindingBound());
}
}
}
/**
* Verifies that BindingManager.releaseAllModerateBindings() drops all the moderate bindings.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingDropOnReleaseAllModerateBindings() {
// This test applies only to the moderate-binding manager.
final BindingManagerImpl manager = mModerateBindingManager;
ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(i + 1 /* pid */, manager);
}
// Verify that each connection has a moderate binding after binding and releasing a strong
// binding.
for (ChildProcessConnection connection : connections) {
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertTrue(connection.isModerateBindingBound());
}
// Call BindingManager.releaseAllModerateBindings() and verify that all the moderate
// bindings drop.
manager.releaseAllModerateBindings();
for (ChildProcessConnection connection : connections) {
Assert.assertFalse(connection.isModerateBindingBound());
}
}
/*
* Test that a moderate binding is added to background renderer processes when the initial
* binding is removed.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingTillBackgroundedBackgroundProcess() {
BindingManagerImpl manager = BindingManagerImpl.createBindingManagerForTesting(false);
manager.startModerateBindingManagement(mActivity, 4);
ChildProcessConnection connection = createTestChildProcessConnection(0 /* pid */, manager);
Assert.assertTrue(connection.isInitialBindingBound());
Assert.assertFalse(connection.isModerateBindingBound());
manager.setPriority(connection.getPid(), false, false /* boost */);
Assert.assertFalse(connection.isInitialBindingBound());
Assert.assertTrue(connection.isModerateBindingBound());
}
/*
* Test that a moderate binding is not added to foreground renderer processes when the initial
* binding is removed.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingTillBackgroundedForegroundProcess() {
BindingManagerImpl manager = BindingManagerImpl.createBindingManagerForTesting(false);
manager.startModerateBindingManagement(mActivity, 4);
ChildProcessConnection connection = createTestChildProcessConnection(0 /* pid */, manager);
Assert.assertTrue(connection.isInitialBindingBound());
Assert.assertFalse(connection.isStrongBindingBound());
Assert.assertFalse(connection.isModerateBindingBound());
manager.setPriority(connection.getPid(), true /* foreground */, false /* boost */);
Assert.assertFalse(connection.isInitialBindingBound());
Assert.assertTrue(connection.isStrongBindingBound());
Assert.assertFalse(connection.isModerateBindingBound());
}
/*
* Test that Chrome is sent to the background, that the initially added moderate bindings are
* removed and are not re-added when Chrome is brought back to the foreground.
*/
@Test
@Feature({"ProcessManagement"})
public void testModerateBindingTillBackgroundedSentToBackground() {
BindingManagerImpl manager = BindingManagerImpl.createBindingManagerForTesting(false);
manager.startModerateBindingManagement(mActivity, 4);
ChildProcessConnection connection = createTestChildProcessConnection(0, manager);
manager.setPriority(connection.getPid(), false /* foreground */, false /* boost */);
Assert.assertTrue(connection.isModerateBindingBound());
manager.onSentToBackground();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Assert.assertFalse(connection.isModerateBindingBound());
// Bringing Chrome to the foreground should not re-add the moderate bindings.
manager.onBroughtToForeground();
Assert.assertFalse(connection.isModerateBindingBound());
}
}