// Copyright 2018 The Chromium Authors
// 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.os.Handler;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.build.BuildConfig;
import org.chromium.content_public.browser.ChildProcessImportance;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
/** Ranking of ChildProcessConnections for a particular ChildConnectionAllocator. */
public class ChildProcessRanking implements Iterable<ChildProcessConnection> {
private static final boolean ENABLE_CHECKS = BuildConfig.ENABLE_ASSERTS;
private static final int NO_GROUP = 0;
private static final int LOW_RANK_GROUP = 1;
// If there is a gap in importance that's larger than 2 * FROM_RIGHT, insert connection
// with importance right - FROM_RIGHT rather than in the middle. Use 15 out of 31 bits
// so should support 2^16 connections, which should be way more than enough.
private static final int FROM_RIGHT = 32768;
// Delay after group is rebound so that higher ranked processes are more recent.
// Note the post and delay is to avoid extra rebinds when rank for multiple connections
// change together, eg if visibility changes for a tab with a number of out-of-process
// iframes.
private static final int REBIND_DELAY_MS = 1000;
private static class ConnectionWithRank {
public final ChildProcessConnection connection;
// Info for ranking a connection.
public boolean visible;
public long frameDepth;
public boolean intersectsViewport;
@ChildProcessImportance public int importance;
public ConnectionWithRank(
ChildProcessConnection connection,
boolean visible,
long frameDepth,
boolean intersectsViewport,
@ChildProcessImportance int importance) {
this.connection = connection;
this.visible = visible;
this.frameDepth = frameDepth;
this.intersectsViewport = intersectsViewport;
this.importance = importance;
}
// Returns true for low ranked connection that it should be in the low rank group.
// Note this must be kept up-to-date with RankComparator so that all shouldBeInLowRankGroup
// connections are sorted to the end of the list.
// Note being in the low rank group does not necessarily imply the connection is not
// important or that it only has waived binding.
public boolean shouldBeInLowRankGroup() {
boolean inViewport = visible && (frameDepth == 0 || intersectsViewport);
return importance == ChildProcessImportance.NORMAL && !inViewport;
}
}
private static class RankComparator implements Comparator<ConnectionWithRank> {
private static int compareByIntersectsViewportAndDepth(
ConnectionWithRank o1, ConnectionWithRank o2) {
if (o1.intersectsViewport && !o2.intersectsViewport) {
return -1;
} else if (!o1.intersectsViewport && o2.intersectsViewport) {
return 1;
}
return Long.signum(o1.frameDepth - o2.frameDepth);
}
@Override
public int compare(ConnectionWithRank o1, ConnectionWithRank o2) {
assert o1 != null;
assert o2 != null;
// Ranking order:
// * (visible and main frame) or ChildProcessImportance.IMPORTANT
// * (visible and subframe and intersect viewport) or ChildProcessImportance.MODERATE
// ---- cutoff for shouldBeInLowRankGroup ----
// * visible subframe and not intersect viewport
// * invisible main and sub frames (not ranked by frame depth)
// Within each group, ties are broken by intersect viewport and then frame depth where
// applicable. Note boostForPendingViews is not used for ranking.
boolean o1IsVisibleMainOrImportant =
(o1.visible && o1.frameDepth == 0)
|| o1.importance == ChildProcessImportance.IMPORTANT;
boolean o2IsVisibleMainOrImportant =
(o2.visible && o2.frameDepth == 0)
|| o2.importance == ChildProcessImportance.IMPORTANT;
if (o1IsVisibleMainOrImportant && o2IsVisibleMainOrImportant) {
return compareByIntersectsViewportAndDepth(o1, o2);
} else if (o1IsVisibleMainOrImportant && !o2IsVisibleMainOrImportant) {
return -1;
} else if (!o1IsVisibleMainOrImportant && o2IsVisibleMainOrImportant) {
return 1;
}
boolean o1VisibleIntersectSubframeOrModerate =
(o1.visible && o1.frameDepth > 0 && o1.intersectsViewport)
|| o1.importance == ChildProcessImportance.MODERATE;
boolean o2VisibleIntersectSubframeOrModerate =
(o2.visible && o2.frameDepth > 0 && o2.intersectsViewport)
|| o2.importance == ChildProcessImportance.MODERATE;
if (o1VisibleIntersectSubframeOrModerate && o2VisibleIntersectSubframeOrModerate) {
return compareByIntersectsViewportAndDepth(o1, o2);
} else if (o1VisibleIntersectSubframeOrModerate
&& !o2VisibleIntersectSubframeOrModerate) {
return -1;
} else if (!o1VisibleIntersectSubframeOrModerate
&& o2VisibleIntersectSubframeOrModerate) {
return 1;
}
if (o1.visible && o2.visible) {
return compareByIntersectsViewportAndDepth(o1, o2);
} else if (o1.visible && !o2.visible) {
return -1;
} else if (!o1.visible && o2.visible) {
return 1;
}
// Invisible are in one group and are purposefully not ranked by frame depth.
// This is because a crashed sub frame will cause the whole tab to be reloaded
// when it becomes visible, so there is no need to specifically protect the
// main frame or lower depth frames.
return 0;
}
}
private class ReverseRankIterator implements Iterator<ChildProcessConnection> {
private final int mSizeOnConstruction;
private int mNextIndex;
public ReverseRankIterator() {
mSizeOnConstruction = ChildProcessRanking.this.mRankings.size();
mNextIndex = mSizeOnConstruction - 1;
}
@Override
public boolean hasNext() {
modificationCheck();
return mNextIndex >= 0;
}
@Override
public ChildProcessConnection next() {
modificationCheck();
return ChildProcessRanking.this.mRankings.get(mNextIndex--).connection;
}
private void modificationCheck() {
assert mSizeOnConstruction == ChildProcessRanking.this.mRankings.size();
}
}
private static final RankComparator COMPARATOR = new RankComparator();
private final Handler mHandler = new Handler();
// |mMaxSize| can be -1 to indicate there can be arbitrary number of connections.
private final int mMaxSize;
// ArrayList is not the most theoretically efficient data structure, but is good enough
// for sizes in production and more memory efficient than linked data structures.
private final List<ConnectionWithRank> mRankings = new ArrayList<>();
private final Runnable mRebindRunnable = this::rebindHighRankConnections;
private boolean mEnableServiceGroupImportance;
private boolean mRebindRunnablePending;
public ChildProcessRanking() {
mMaxSize = -1;
}
/** Create with a maxSize. Trying to insert more will throw exceptions. */
public ChildProcessRanking(int maxSize) {
assert maxSize > 0;
mMaxSize = maxSize;
}
public void enableServiceGroupImportance() {
assert !mEnableServiceGroupImportance;
mEnableServiceGroupImportance = true;
reshuffleGroupImportance();
postRebindHighRankConnectionsIfNeeded();
if (ENABLE_CHECKS) checkGroupImportance();
}
/**
* Iterate from lowest to highest rank. Ranking should not be modified during iteration,
* including using Iterator.delete.
*/
@Override
public Iterator<ChildProcessConnection> iterator() {
return new ReverseRankIterator();
}
public void addConnection(
ChildProcessConnection connection,
boolean visible,
long frameDepth,
boolean intersectsViewport,
@ChildProcessImportance int importance) {
assert connection != null;
assert indexOf(connection) == -1;
if (mMaxSize != -1 && mRankings.size() >= mMaxSize) {
throw new RuntimeException(
"mRankings.size:" + mRankings.size() + " mMaxSize:" + mMaxSize);
}
mRankings.add(
new ConnectionWithRank(
connection, visible, frameDepth, intersectsViewport, importance));
reposition(mRankings.size() - 1);
}
public void removeConnection(ChildProcessConnection connection) {
assert connection != null;
assert mRankings.size() > 0;
int i = indexOf(connection);
assert i != -1;
// Null is sorted to the end.
mRankings.remove(i);
if (ENABLE_CHECKS) checkOrder();
}
public void updateConnection(
ChildProcessConnection connection,
boolean visible,
long frameDepth,
boolean intersectsViewport,
@ChildProcessImportance int importance) {
assert connection != null;
assert mRankings.size() > 0;
int i = indexOf(connection);
assert i != -1;
ConnectionWithRank rank = mRankings.get(i);
rank.visible = visible;
rank.frameDepth = frameDepth;
rank.intersectsViewport = intersectsViewport;
rank.importance = importance;
reposition(i);
}
public ChildProcessConnection getLowestRankedConnection() {
if (mRankings.isEmpty()) return null;
return mRankings.get(mRankings.size() - 1).connection;
}
private int indexOf(ChildProcessConnection connection) {
for (int i = 0; i < mRankings.size(); ++i) {
if (mRankings.get(i).connection == connection) return i;
}
return -1;
}
private void reposition(final int originalIndex) {
ConnectionWithRank connection = mRankings.remove(originalIndex);
int newIndex = 0;
while (newIndex < mRankings.size()
&& COMPARATOR.compare(mRankings.get(newIndex), connection) < 0) {
++newIndex;
}
mRankings.add(newIndex, connection);
if (ENABLE_CHECKS) checkOrder();
if (!mEnableServiceGroupImportance) return;
if (!connection.shouldBeInLowRankGroup()) {
if (connection.connection.getGroup() != NO_GROUP) {
connection.connection.updateGroupImportance(NO_GROUP, 0);
}
return;
}
final boolean atStart = newIndex == 0;
final boolean atEnd = newIndex == mRankings.size() - 1;
final int left =
atStart ? 0 : mRankings.get(newIndex - 1).connection.getImportanceInGroup();
assert atEnd || mRankings.get(newIndex + 1).connection.getGroup() > NO_GROUP;
final int right =
atEnd
? Integer.MAX_VALUE
: mRankings.get(newIndex + 1).connection.getImportanceInGroup();
if (connection.connection.getImportanceInGroup() > left
&& connection.connection.getImportanceInGroup() < right) {
return;
}
assert right >= left;
final int gap = right - left;
// If there is a large enough gap, place connection close to the end. This is a heuristic
// since updating a connection to be the highest ranked (lowest index) occurs very
// frequently, eg when switching between tabs.
// If gap is small, use average.
// If there is no room left, reshuffle everything.
if (gap > 2 * FROM_RIGHT) {
connection.connection.updateGroupImportance(LOW_RANK_GROUP, right - FROM_RIGHT);
} else if (gap > 2) {
connection.connection.updateGroupImportance(LOW_RANK_GROUP, left + gap / 2);
} else {
reshuffleGroupImportance();
}
postRebindHighRankConnectionsIfNeeded();
if (ENABLE_CHECKS) checkGroupImportance();
}
private void reshuffleGroupImportance() {
int importance = Integer.MAX_VALUE - FROM_RIGHT;
for (int i = mRankings.size() - 1; i >= 0; --i) {
ConnectionWithRank connection = mRankings.get(i);
if (!connection.shouldBeInLowRankGroup()) break;
connection.connection.updateGroupImportance(LOW_RANK_GROUP, importance);
importance -= FROM_RIGHT;
}
}
private void postRebindHighRankConnectionsIfNeeded() {
if (mRebindRunnablePending) return;
mHandler.postDelayed(mRebindRunnable, REBIND_DELAY_MS);
mRebindRunnablePending = true;
}
private void rebindHighRankConnections() {
mRebindRunnablePending = false;
for (int i = mRankings.size() - 1; i >= 0; --i) {
ConnectionWithRank connection = mRankings.get(i);
if (connection.shouldBeInLowRankGroup()) continue;
connection.connection.rebind();
}
}
private void checkOrder() {
boolean crossedLowRankGroupCutoff = false;
for (int i = 0; i < mRankings.size(); ++i) {
ConnectionWithRank connection = mRankings.get(i);
if (i > 0 && COMPARATOR.compare(mRankings.get(i - 1), connection) > 0) {
throw new RuntimeException("Not sorted " + mRankings.get(i - 1) + " " + connection);
}
boolean inLowGroup = connection.shouldBeInLowRankGroup();
if (crossedLowRankGroupCutoff && !inLowGroup) {
throw new RuntimeException("Not in low rank " + connection);
}
crossedLowRankGroupCutoff = inLowGroup;
}
}
private void checkGroupImportance() {
int importance = -1;
for (int i = 0; i < mRankings.size(); ++i) {
ConnectionWithRank connection = mRankings.get(i);
if (connection.shouldBeInLowRankGroup()) {
if (connection.connection.getGroup() != LOW_RANK_GROUP) {
throw new RuntimeException("Not in low rank group " + connection);
}
if (connection.connection.getImportanceInGroup() <= importance) {
throw new RuntimeException(
"Wrong group importance order "
+ connection
+ " "
+ connection.connection.getImportanceInGroup()
+ " "
+ importance);
}
importance = connection.connection.getImportanceInGroup();
} else {
if (connection.connection.getGroup() != NO_GROUP) {
throw new RuntimeException("Should not be in group " + connection);
}
}
}
}
}