// 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 android.util.Log; import android.util.SparseArray; import com.google.common.annotations.VisibleForTesting; import org.chromium.base.SysUtils; import org.chromium.base.ThreadUtils; /** * Manages oom bindings used to bound child services. */ class BindingManagerImpl implements BindingManager { private static final String TAG = "BindingManager"; // Delay of 1 second used when removing the initial oom binding of a process. private static final long REMOVE_INITIAL_BINDING_DELAY_MILLIS = 1 * 1000; // Delay of 1 second used when removing temporary strong binding of a process (only on // non-low-memory devices). private static final long DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS = 1 * 1000; // These fields allow to override the parameters for testing - see // createBindingManagerForTesting(). private final long mRemoveInitialBindingDelay; private final long mRemoveStrongBindingDelay; private final boolean mIsLowMemoryDevice; /** * Wraps ChildProcessConnection keeping track of additional information needed to manage the * bindings of the connection. The reference to ChildProcessConnection is cleared when the * connection goes away, but ManagedConnection itself is kept (until overwritten by a new entry * for the same pid). */ private class ManagedConnection { // Set in constructor, cleared in clearConnection(). private ChildProcessConnection mConnection; // True iff there is a strong binding kept on the service because it is working in // foreground. private boolean mInForeground; // True iff there is a strong binding kept on the service because it was bound for the // application background period. private boolean mBoundForBackgroundPeriod; // When mConnection is cleared, oom binding status is stashed here. private boolean mWasOomProtected; /** Removes the initial service binding. */ private void removeInitialBinding() { final ChildProcessConnection connection = mConnection; if (connection == null || !connection.isInitialBindingBound()) return; ThreadUtils.postOnUiThreadDelayed(new Runnable() { @Override public void run() { if (connection.isInitialBindingBound()) { connection.removeInitialBinding(); } } }, mRemoveInitialBindingDelay); } /** Adds a strong service binding. */ private void addStrongBinding() { ChildProcessConnection connection = mConnection; if (connection == null) return; connection.addStrongBinding(); } /** Removes a strong service binding. */ private void removeStrongBinding() { final ChildProcessConnection connection = mConnection; // We have to fail gracefully if the strong binding is not present, as on low-end the // binding could have been removed by dropOomBindings() when a new service was started. if (connection == null || !connection.isStrongBindingBound()) return; // This runnable performs the actual unbinding. It will be executed synchronously when // on low-end devices and posted with a delay otherwise. Runnable doUnbind = new Runnable() { @Override public void run() { if (connection.isStrongBindingBound()) { connection.removeStrongBinding(); } } }; if (mIsLowMemoryDevice) { doUnbind.run(); } else { ThreadUtils.postOnUiThreadDelayed(doUnbind, mRemoveStrongBindingDelay); } } /** * Drops the service bindings. This is used on low-end to drop bindings of the current * service when a new one is created. */ private void dropBindings() { assert mIsLowMemoryDevice; ChildProcessConnection connection = mConnection; if (connection == null) return; connection.dropOomBindings(); } ManagedConnection(ChildProcessConnection connection) { mConnection = connection; } /** * Sets the visibility of the service, adding or removing the strong binding as needed. This * also removes the initial binding, as the service visibility is now known. */ void setInForeground(boolean nextInForeground) { if (!mInForeground && nextInForeground) { addStrongBinding(); } else if (mInForeground && !nextInForeground) { removeStrongBinding(); } removeInitialBinding(); mInForeground = nextInForeground; } /** * Sets or removes additional binding when the service is main service during the embedder * background period. */ void setBoundForBackgroundPeriod(boolean nextBound) { if (!mBoundForBackgroundPeriod && nextBound) { addStrongBinding(); } else if (mBoundForBackgroundPeriod && !nextBound) { removeStrongBinding(); } mBoundForBackgroundPeriod = nextBound; } boolean isOomProtected() { // When a process crashes, we can be queried about its oom status before or after the // connection is cleared. For the latter case, the oom status is stashed in // mWasOomProtected. return mConnection != null ? mConnection.isOomProtectedOrWasWhenDied() : mWasOomProtected; } void clearConnection() { mWasOomProtected = mConnection.isOomProtectedOrWasWhenDied(); mConnection = null; } /** @return true iff the reference to the connection is no longer held */ @VisibleForTesting boolean isConnectionCleared() { return mConnection == null; } } // This can be manipulated on different threads, synchronize access on mManagedConnections. private final SparseArray<ManagedConnection> mManagedConnections = new SparseArray<ManagedConnection>(); // The connection that was most recently set as foreground (using setInForeground()). This is // used to add additional binding on it when the embedder goes to background. On low-end, this // is also used to drop process bidnings when a new one is created, making sure that only one // renderer process at a time is protected from oom killing. private ManagedConnection mLastInForeground; // Synchronizes operations that access mLastInForeground: setInForeground() and // addNewConnection(). private final Object mLastInForegroundLock = new Object(); // The connection bound with additional binding in onSentToBackground(). private ManagedConnection mBoundForBackgroundPeriod; /** * The constructor is private to hide parameters exposed for testing from the regular consumer. * Use factory methods to create an instance. */ private BindingManagerImpl(boolean isLowMemoryDevice, long removeInitialBindingDelay, long removeStrongBindingDelay) { mIsLowMemoryDevice = isLowMemoryDevice; mRemoveInitialBindingDelay = removeInitialBindingDelay; mRemoveStrongBindingDelay = removeStrongBindingDelay; } public static BindingManagerImpl createBindingManager() { return new BindingManagerImpl(SysUtils.isLowEndDevice(), REMOVE_INITIAL_BINDING_DELAY_MILLIS, DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS); } /** * Creates a testing instance of BindingManager. Testing instance will have the unbinding delays * set to 0, so that the tests don't need to deal with actual waiting. * @param isLowEndDevice true iff the created instance should apply low-end binding policies */ public static BindingManagerImpl createBindingManagerForTesting(boolean isLowEndDevice) { return new BindingManagerImpl(isLowEndDevice, 0, 0); } @Override public void addNewConnection(int pid, ChildProcessConnection connection) { synchronized (mLastInForegroundLock) { if (mIsLowMemoryDevice && mLastInForeground != null) mLastInForeground.dropBindings(); } // This will reset the previous entry for the pid in the unlikely event of the OS // reusing renderer pids. synchronized (mManagedConnections) { mManagedConnections.put(pid, new ManagedConnection(connection)); } } @Override public void setInForeground(int pid, boolean inForeground) { ManagedConnection managedConnection; synchronized (mManagedConnections) { managedConnection = mManagedConnections.get(pid); } if (managedConnection == null) { Log.w(TAG, "Cannot setInForeground() - never saw a connection for the pid: " + Integer.toString(pid)); return; } synchronized (mLastInForegroundLock) { managedConnection.setInForeground(inForeground); if (inForeground) mLastInForeground = managedConnection; } } @Override public void onSentToBackground() { assert mBoundForBackgroundPeriod == null; synchronized (mLastInForegroundLock) { // mLastInForeground can be null at this point as the embedding application could be // used in foreground without spawning any renderers. if (mLastInForeground != null) { mLastInForeground.setBoundForBackgroundPeriod(true); mBoundForBackgroundPeriod = mLastInForeground; } } } @Override public void onBroughtToForeground() { if (mBoundForBackgroundPeriod != null) { mBoundForBackgroundPeriod.setBoundForBackgroundPeriod(false); mBoundForBackgroundPeriod = null; } } @Override public boolean isOomProtected(int pid) { // In the unlikely event of the OS reusing renderer pid, the call will refer to the most // recent renderer of the given pid. The binding state for a pid is being reset in // addNewConnection(). ManagedConnection managedConnection; synchronized (mManagedConnections) { managedConnection = mManagedConnections.get(pid); } return managedConnection != null ? managedConnection.isOomProtected() : false; } @Override public void clearConnection(int pid) { ManagedConnection managedConnection; synchronized (mManagedConnections) { managedConnection = mManagedConnections.get(pid); } if (managedConnection != null) managedConnection.clearConnection(); } /** @return true iff the connection reference is no longer held */ @VisibleForTesting public boolean isConnectionCleared(int pid) { synchronized (mManagedConnections) { return mManagedConnections.get(pid).isConnectionCleared(); } } }