// Copyright 2013 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.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. "Oom binding" is a binding that raises the * process oom priority so that it shouldn't be killed by the OS out-of-memory killer under * normal conditions (it can still be killed under drastic memory pressure). ChildProcessConnections * have two oom bindings: initial binding and strong binding. * * This class serves as a proxy between external calls that manipulate the bindings and the * connections, allowing to enforce policies: * - delayed removal of the oom bindings * - dropping the current oom bindings when a new connection is started on a low-memory device * * Thread-safety: most of the methods will be called only on the main thread, exceptions are * explicitly noted. */ class 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 static class ManagedConnection { private ChildProcessConnection mConnection; // When mConnection is cleared, oom binding status is stashed here. private boolean mWasOomProtected; ManagedConnection(ChildProcessConnection connection) { mConnection = connection; } ChildProcessConnection getConnection() { return mConnection; } 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; } } // This can be manipulated on different threads, synchronize access on mManagedConnections. private final SparseArray<ManagedConnection> mManagedConnections = new SparseArray<ManagedConnection>(); /** * A helper method that returns the bare ChildProcessConnection for the given pid, returning * null with a log message if there is no entry for the pid. * * This is visible for testing, so that we can verify that we are not holding onto the reference * when we shouldn't. */ @VisibleForTesting ChildProcessConnection getConnectionForPid(int pid) { ManagedConnection managedConnection; synchronized (mManagedConnections) { managedConnection = mManagedConnections.get(pid); } if (managedConnection == null) { ChildProcessLauncher.logPidWarning(pid, "BindingManager never saw a connection for the pid: " + Integer.toString(pid)); return null; } return managedConnection.getConnection(); } // Pid of the renderer that was most recently bound using bindAsHighPriority(). This is used on // low-memory devices to drop oom bindings of a process when another one acquires them, making // sure that only one renderer process at a time is oom bound. private int mLastOomPid = -1; // Pid of the renderer that is bound with a strong binding for the background period. Equals // -1 when there is no such renderer. private int mBoundForBackgroundPeriodPid = -1; // Synchronizes operations that access mLastOomPid: addNewConnection() and bindAsHighPriority(). private final Object mLastOomPidLock = new Object(); /** * The constructor is private to hide parameters exposed for testing from the regular consumer. * Use factory methods to create an instance. */ private BindingManager(boolean isLowMemoryDevice, long removeInitialBindingDelay, long removeStrongBindingDelay) { mIsLowMemoryDevice = isLowMemoryDevice; mRemoveInitialBindingDelay = removeInitialBindingDelay; mRemoveStrongBindingDelay = removeStrongBindingDelay; } public static BindingManager createBindingManager() { return new BindingManager(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 BindingManager createBindingManagerForTesting(boolean isLowEndDevice) { return new BindingManager(isLowEndDevice, 0, 0); } /** * Registers a freshly started child process. On low-memory devices this will also drop the * oom bindings of the last process that was oom-bound. We can do that, because every time a * connection is created on the low-end, it is used in foreground (no prerendering, no * loading of tabs opened in background). This can be called on any thread. * @param pid handle of the process. */ void addNewConnection(int pid, ChildProcessConnection connection) { synchronized (mLastOomPidLock) { if (mIsLowMemoryDevice && mLastOomPid >= 0) { ChildProcessConnection lastOomConnection = getConnectionForPid(mLastOomPid); if (lastOomConnection != null) lastOomConnection.dropOomBindings(); } } // 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)); } } /** * Remove the initial binding of the child process. Child processes are bound with initial * binding to protect them from getting killed before they are put to use. This method * allows to remove the binding once it is no longer needed. The binding is removed after a * fixed delay period so that the renderer will not be killed immediately after the call. */ void removeInitialBinding(final int pid) { final ChildProcessConnection connection = getConnectionForPid(pid); if (connection == null || !connection.isInitialBindingBound()) return; ThreadUtils.postOnUiThreadDelayed(new Runnable() { @Override public void run() { if (connection.isInitialBindingBound()) { connection.removeInitialBinding(); } } }, mRemoveInitialBindingDelay); } /** * Binds a child process as a high priority process so that it has the same priority as the main * process. This can be used for the foreground renderer process to distinguish it from the * background renderer process. */ void bindAsHighPriority(final int pid) { ChildProcessConnection connection = getConnectionForPid(pid); if (connection == null) return; synchronized (mLastOomPidLock) { connection.attachAsActive(); mLastOomPid = pid; } } /** * Unbinds a high priority process which was previous bound with bindAsHighPriority. */ void unbindAsHighPriority(final int pid) { final ChildProcessConnection connection = getConnectionForPid(pid); 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.detachAsActive(); } } }; if (mIsLowMemoryDevice) { doUnbind.run(); } else { ThreadUtils.postOnUiThreadDelayed(doUnbind, mRemoveStrongBindingDelay); } } /** * Called when the embedding application is sent to background. We want to maintain a strong * binding on the most recently used renderer while the embedder is in background, to indicate * the relative importance of the renderer to system oom killer. * * The embedder needs to ensure that: * - every onBroughtToForeground() is followed by onSentToBackground() * - pairs of consecutive onBroughtToForeground() / onSentToBackground() calls do not overlap */ void onSentToBackground() { assert mBoundForBackgroundPeriodPid == -1; synchronized (mLastOomPidLock) { // mLastOomPid can be -1 at this point as the embedding application could be used in // foreground without spawning any renderers. if (mLastOomPid >= 0) { bindAsHighPriority(mLastOomPid); mBoundForBackgroundPeriodPid = mLastOomPid; } } } /** * Called when the embedding application is brought to foreground. This will drop the strong * binding kept on the main renderer during the background period, so the embedder should make * sure that this is called after the regular strong binding is attached for the foreground * session. */ void onBroughtToForeground() { if (mBoundForBackgroundPeriodPid >= 0) { unbindAsHighPriority(mBoundForBackgroundPeriodPid); mBoundForBackgroundPeriodPid = -1; } } /** * @return True iff the given service process is protected from the out-of-memory killing, or it * was protected when it died unexpectedly. This can be used to decide if a disconnection of a * renderer was a crash or a probable out-of-memory kill. This can be called on any thread. */ 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; } /** * Should be called when the connection to the child process goes away (either after a clean * exit or an unexpected crash). At this point we let go of the reference to the * ChildProcessConnection. This can be called on any thread. */ void clearConnection(int pid) { ManagedConnection managedConnection; synchronized (mManagedConnections) { managedConnection = mManagedConnections.get(pid); } if (managedConnection != null) managedConnection.clearConnection(); } }