1 // Copyright 2014 The Chromium Authors. All rights reserved.
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.content.browser;
6 
7 import android.content.ComponentCallbacks2;
8 import android.content.Context;
9 import android.content.res.Configuration;
10 
11 import androidx.collection.ArraySet;
12 
13 import org.chromium.base.Log;
14 import org.chromium.base.library_loader.LibraryLoader;
15 import org.chromium.base.metrics.RecordHistogram;
16 import org.chromium.base.process_launcher.ChildProcessConnection;
17 
18 import java.util.Iterator;
19 import java.util.Set;
20 
21 /**
22  * Manages oom bindings used to bound child services.
23  * This object must only be accessed from the launcher thread.
24  */
25 class BindingManager implements ComponentCallbacks2 {
26     private static final String TAG = "BindingManager";
27 
28     // Low reduce ratio of moderate binding.
29     private static final float MODERATE_BINDING_LOW_REDUCE_RATIO = 0.25f;
30     // High reduce ratio of moderate binding.
31     private static final float MODERATE_BINDING_HIGH_REDUCE_RATIO = 0.5f;
32 
33     // Delays used when clearing moderate binding pool when onSentToBackground happens.
34     private static final long MODERATE_BINDING_POOL_CLEARER_DELAY_MILLIS = 10 * 1000;
35 
36     private final Set<ChildProcessConnection> mConnections = new ArraySet<ChildProcessConnection>();
37     // Can be -1 to mean no max size.
38     private final int mMaxSize;
39     private final Iterable<ChildProcessConnection> mRanking;
40     private final Runnable mDelayedClearer;
41 
42     // If not null, this is a connection in |mConnections| that does not have a moderate binding
43     // added by BindingManager.
44     private ChildProcessConnection mWaivedConnection;
45 
46     @Override
onTrimMemory(final int level)47     public void onTrimMemory(final int level) {
48         LauncherThread.post(new Runnable() {
49             @Override
50             public void run() {
51                 Log.i(TAG, "onTrimMemory: level=%d, size=%d", level, mConnections.size());
52                 if (mConnections.isEmpty()) {
53                     return;
54                 }
55                 if (level <= TRIM_MEMORY_RUNNING_MODERATE) {
56                     reduce(MODERATE_BINDING_LOW_REDUCE_RATIO);
57                 } else if (level <= TRIM_MEMORY_RUNNING_LOW) {
58                     reduce(MODERATE_BINDING_HIGH_REDUCE_RATIO);
59                 } else if (level == TRIM_MEMORY_UI_HIDDEN) {
60                     // This will be handled by |mDelayedClearer|.
61                     return;
62                 } else {
63                     removeAllConnections();
64                 }
65             }
66         });
67     }
68 
69     @Override
onLowMemory()70     public void onLowMemory() {
71         LauncherThread.post(new Runnable() {
72             @Override
73             public void run() {
74                 Log.i(TAG, "onLowMemory: evict %d bindings", mConnections.size());
75                 removeAllConnections();
76             }
77         });
78     }
79 
80     @Override
onConfigurationChanged(Configuration configuration)81     public void onConfigurationChanged(Configuration configuration) {}
82 
reduce(float reduceRatio)83     private void reduce(float reduceRatio) {
84         int oldSize = mConnections.size();
85         int newSize = (int) (oldSize * (1f - reduceRatio));
86         Log.i(TAG, "Reduce connections from %d to %d", oldSize, newSize);
87         removeOldConnections(oldSize - newSize);
88         assert mConnections.size() == newSize;
89         ensureLowestRankIsWaived();
90     }
91 
removeAllConnections()92     private void removeAllConnections() {
93         removeOldConnections(mConnections.size());
94     }
95 
removeOldConnections(int numberOfConnections)96     private void removeOldConnections(int numberOfConnections) {
97         assert numberOfConnections <= mConnections.size();
98         int numRemoved = 0;
99         for (ChildProcessConnection connection : mRanking) {
100             if (mConnections.contains(connection)) {
101                 removeModerateBindingIfNeeded(connection);
102                 mConnections.remove(connection);
103                 if (++numRemoved == numberOfConnections) break;
104             }
105         }
106     }
107 
removeModerateBindingIfNeeded(ChildProcessConnection connection)108     private void removeModerateBindingIfNeeded(ChildProcessConnection connection) {
109         if (connection == mWaivedConnection) {
110             mWaivedConnection = null;
111         } else {
112             connection.removeModerateBinding();
113         }
114     }
115 
ensureLowestRankIsWaived()116     private void ensureLowestRankIsWaived() {
117         Iterator<ChildProcessConnection> itr = mRanking.iterator();
118         if (!itr.hasNext()) return;
119         ChildProcessConnection lowestRanked = itr.next();
120 
121         if (lowestRanked == mWaivedConnection) return;
122         if (mWaivedConnection != null) {
123             assert mConnections.contains(mWaivedConnection);
124             mWaivedConnection.addModerateBinding();
125             mWaivedConnection = null;
126         }
127         if (!mConnections.contains(lowestRanked)) return;
128         lowestRanked.removeModerateBinding();
129         mWaivedConnection = lowestRanked;
130     }
131 
132     /**
133      * Called when the embedding application is sent to background.
134      * The embedder needs to ensure that:
135      *  - every onBroughtToForeground() is followed by onSentToBackground()
136      *  - pairs of consecutive onBroughtToForeground() / onSentToBackground() calls do not overlap
137      */
onSentToBackground()138     void onSentToBackground() {
139         assert LauncherThread.runningOnLauncherThread();
140         if (mConnections.isEmpty()) return;
141         LauncherThread.postDelayed(mDelayedClearer, MODERATE_BINDING_POOL_CLEARER_DELAY_MILLIS);
142     }
143 
144     /** Called when the embedding application is brought to foreground. */
onBroughtToForeground()145     void onBroughtToForeground() {
146         assert LauncherThread.runningOnLauncherThread();
147         LauncherThread.removeCallbacks(mDelayedClearer);
148     }
149 
150     /**
151      * Construct instance without maxsize and can support arbitrary number of connections.
152      */
BindingManager(Context context, Iterable<ChildProcessConnection> ranking)153     BindingManager(Context context, Iterable<ChildProcessConnection> ranking) {
154         this(-1, ranking, context);
155     }
156 
157     /**
158      * Construct instance with maxSize.
159      */
BindingManager(Context context, int maxSize, Iterable<ChildProcessConnection> ranking)160     BindingManager(Context context, int maxSize, Iterable<ChildProcessConnection> ranking) {
161         this(maxSize, ranking, context);
162         assert maxSize > 0;
163     }
164 
BindingManager(int maxSize, Iterable<ChildProcessConnection> ranking, Context context)165     private BindingManager(int maxSize, Iterable<ChildProcessConnection> ranking, Context context) {
166         assert LauncherThread.runningOnLauncherThread();
167         Log.i(TAG, "Moderate binding enabled: maxSize=%d", maxSize);
168 
169         mMaxSize = maxSize;
170         mRanking = ranking;
171         assert mMaxSize > 0 || mMaxSize == -1;
172 
173         mDelayedClearer = new Runnable() {
174             @Override
175             public void run() {
176                 Log.i(TAG, "Release moderate connections: %d", mConnections.size());
177                 // Tests may not load the native library which is required for
178                 // recording histograms.
179                 if (LibraryLoader.getInstance().isInitialized()) {
180                     RecordHistogram.recordCountHistogram(
181                             "Android.ModerateBindingCount", mConnections.size());
182                 }
183                 removeAllConnections();
184             }
185         };
186 
187         // Note that it is safe to call Context.registerComponentCallbacks from a background
188         // thread.
189         context.registerComponentCallbacks(this);
190     }
191 
addConnection(ChildProcessConnection connection)192     public void addConnection(ChildProcessConnection connection) {
193         assert LauncherThread.runningOnLauncherThread();
194 
195         // Note that the size of connections is currently fairly small (40).
196         // If it became bigger we should consider using an alternate data structure.
197         boolean alreadyInQueue = !mConnections.add(connection);
198         if (!alreadyInQueue) connection.addModerateBinding();
199         assert mMaxSize == -1 || mConnections.size() <= mMaxSize;
200     }
201 
removeConnection(ChildProcessConnection connection)202     public void removeConnection(ChildProcessConnection connection) {
203         assert LauncherThread.runningOnLauncherThread();
204         boolean alreadyInQueue = mConnections.remove(connection);
205         if (alreadyInQueue) removeModerateBindingIfNeeded(connection);
206         assert !mConnections.contains(connection);
207     }
208 
209     // Separate from other public methods so it allows client to update ranking after
210     // adding and removing connection.
rankingChanged()211     public void rankingChanged() {
212         ensureLowestRankIsWaived();
213     }
214 }
215