1 // Copyright 2015 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.chrome.browser; 6 7 import android.annotation.SuppressLint; 8 import android.content.Context; 9 import android.net.Uri; 10 import android.os.SystemClock; 11 import android.view.ContextThemeWrapper; 12 import android.view.InflateException; 13 import android.view.View; 14 import android.view.ViewGroup; 15 import android.view.ViewStub; 16 import android.widget.FrameLayout; 17 18 import androidx.annotation.IntDef; 19 import androidx.annotation.VisibleForTesting; 20 21 import org.chromium.base.Log; 22 import org.chromium.base.ThreadUtils; 23 import org.chromium.base.TraceEvent; 24 import org.chromium.base.annotations.NativeMethods; 25 import org.chromium.base.library_loader.LibraryLoader; 26 import org.chromium.base.metrics.RecordHistogram; 27 import org.chromium.base.task.AsyncTask; 28 import org.chromium.chrome.R; 29 import org.chromium.chrome.browser.app.ChromeActivity; 30 import org.chromium.chrome.browser.profiles.Profile; 31 import org.chromium.chrome.browser.toolbar.ControlContainer; 32 import org.chromium.components.embedder_support.util.UrlConstants; 33 import org.chromium.content_public.browser.WebContents; 34 import org.chromium.content_public.browser.WebContentsObserver; 35 import org.chromium.ui.LayoutInflaterUtils; 36 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.net.InetAddress; 40 import java.net.MalformedURLException; 41 import java.net.URL; 42 import java.net.UnknownHostException; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.Map; 46 import java.util.Set; 47 48 /** 49 * This class is a singleton that holds utilities for warming up Chrome and prerendering urls 50 * without creating the Activity. 51 * 52 * This class is not thread-safe and must only be used on the UI thread. 53 */ 54 public class WarmupManager { 55 private static final String TAG = "WarmupManager"; 56 57 @VisibleForTesting 58 static final String WEBCONTENTS_STATUS_HISTOGRAM = "CustomTabs.SpareWebContents.Status2"; 59 60 public static final boolean FOR_CCT = true; 61 62 // See CustomTabs.SpareWebContentsStatus histogram. Append-only. 63 @IntDef({WebContentsStatus.CREATED, WebContentsStatus.USED, WebContentsStatus.KILLED, 64 WebContentsStatus.DESTROYED, WebContentsStatus.STOLEN}) 65 @Retention(RetentionPolicy.SOURCE) 66 @interface WebContentsStatus { 67 @VisibleForTesting 68 int CREATED = 0; 69 @VisibleForTesting 70 int USED = 1; 71 @VisibleForTesting 72 int KILLED = 2; 73 @VisibleForTesting 74 int DESTROYED = 3; 75 @VisibleForTesting 76 int STOLEN = 4; 77 int NUM_ENTRIES = 5; 78 } 79 80 /** 81 * Observes spare WebContents deaths. In case of death, records stats, and cleanup the objects. 82 */ 83 private class RenderProcessGoneObserver extends WebContentsObserver { 84 @Override renderProcessGone(boolean wasOomProtected)85 public void renderProcessGone(boolean wasOomProtected) { 86 long elapsed = SystemClock.elapsedRealtime() - mWebContentsCreationTimeMs; 87 RecordHistogram.recordLongTimesHistogram( 88 "CustomTabs.SpareWebContents.TimeBeforeDeath", elapsed); 89 recordWebContentsStatus(WebContentsStatus.KILLED); 90 destroySpareWebContentsInternal(); 91 } 92 } 93 94 @SuppressLint("StaticFieldLeak") 95 private static WarmupManager sWarmupManager; 96 97 private final Set<String> mDnsRequestsInFlight; 98 private final Map<String, Profile> mPendingPreconnectWithProfile; 99 100 private int mToolbarContainerId; 101 private ViewGroup mMainView; 102 @VisibleForTesting 103 WebContents mSpareWebContents; 104 private long mWebContentsCreationTimeMs; 105 private RenderProcessGoneObserver mObserver; 106 private boolean mWebContentsCreatedForCCT; 107 108 /** 109 * @return The singleton instance for the WarmupManager, creating one if necessary. 110 */ getInstance()111 public static WarmupManager getInstance() { 112 ThreadUtils.assertOnUiThread(); 113 if (sWarmupManager == null) sWarmupManager = new WarmupManager(); 114 return sWarmupManager; 115 } 116 WarmupManager()117 private WarmupManager() { 118 mDnsRequestsInFlight = new HashSet<>(); 119 mPendingPreconnectWithProfile = new HashMap<>(); 120 } 121 122 /** 123 * Inflates and constructs the view hierarchy that the app will use. 124 * @param baseContext The base context to use for creating the ContextWrapper. 125 * @param toolbarContainerId Id of the toolbar container. 126 * @param toolbarId The toolbar's layout ID. 127 */ initializeViewHierarchy(Context baseContext, int toolbarContainerId, int toolbarId)128 public void initializeViewHierarchy(Context baseContext, int toolbarContainerId, 129 int toolbarId) { 130 ThreadUtils.assertOnUiThread(); 131 if (mMainView != null && mToolbarContainerId == toolbarContainerId) return; 132 mMainView = inflateViewHierarchy(baseContext, toolbarContainerId, toolbarId); 133 mToolbarContainerId = toolbarContainerId; 134 } 135 136 /** 137 * Inflates and constructs the view hierarchy that the app will use. 138 * Calls to this are not restricted to the UI thread. 139 * @param baseContext The base context to use for creating the ContextWrapper. 140 * @param toolbarContainerId Id of the toolbar container. 141 * @param toolbarId The toolbar's layout ID. 142 */ inflateViewHierarchy( Context baseContext, int toolbarContainerId, int toolbarId)143 public static ViewGroup inflateViewHierarchy( 144 Context baseContext, int toolbarContainerId, int toolbarId) { 145 try (TraceEvent e = TraceEvent.scoped("WarmupManager.inflateViewHierarchy")) { 146 ContextThemeWrapper context = 147 new ContextThemeWrapper(baseContext, ChromeActivity.getThemeId()); 148 FrameLayout contentHolder = new FrameLayout(context); 149 ViewGroup mainView = 150 (ViewGroup) LayoutInflaterUtils.inflate(context, R.layout.main, contentHolder); 151 if (toolbarContainerId != ChromeActivity.NO_CONTROL_CONTAINER) { 152 ViewStub stub = (ViewStub) mainView.findViewById(R.id.control_container_stub); 153 stub.setLayoutResource(toolbarContainerId); 154 stub.inflate(); 155 } 156 // It cannot be assumed that the result of toolbarContainerStub.inflate() will be 157 // the control container since it may be wrapped in another view. 158 ControlContainer controlContainer = 159 (ControlContainer) mainView.findViewById(R.id.control_container); 160 161 if (toolbarId != ChromeActivity.NO_TOOLBAR_LAYOUT && controlContainer != null) { 162 controlContainer.initWithToolbar(toolbarId); 163 } 164 return mainView; 165 } catch (InflateException e) { 166 // See https://crbug.com/606715. 167 Log.e(TAG, "Inflation exception.", e); 168 return null; 169 } 170 } 171 172 /** 173 * Transfers all the children in the local view hierarchy {@link #mMainView} to the given 174 * ViewGroup {@param contentView} as child. 175 * @param contentView The parent ViewGroup to use for the transfer. 176 */ transferViewHierarchyTo(ViewGroup contentView)177 public void transferViewHierarchyTo(ViewGroup contentView) { 178 ThreadUtils.assertOnUiThread(); 179 ViewGroup viewHierarchy = mMainView; 180 mMainView = null; 181 if (viewHierarchy == null) return; 182 transferViewHeirarchy(viewHierarchy, contentView); 183 } 184 185 /** 186 * Transfers all the children in one view hierarchy {@param from} to another {@param to}. 187 * @param from The parent ViewGroup to transfer children from. 188 * @param to The parent ViewGroup to transfer children to. 189 */ transferViewHeirarchy(ViewGroup from, ViewGroup to)190 public static void transferViewHeirarchy(ViewGroup from, ViewGroup to) { 191 while (from.getChildCount() > 0) { 192 View currentChild = from.getChildAt(0); 193 from.removeView(currentChild); 194 to.addView(currentChild); 195 } 196 } 197 198 /** 199 * @return Whether a pre-built view hierarchy exists for the given toolbarContainerId. 200 */ hasViewHierarchyWithToolbar(int toolbarContainerId)201 public boolean hasViewHierarchyWithToolbar(int toolbarContainerId) { 202 ThreadUtils.assertOnUiThread(); 203 return mMainView != null && mToolbarContainerId == toolbarContainerId; 204 } 205 206 /** 207 * Clears the inflated view hierarchy. 208 */ clearViewHierarchy()209 public void clearViewHierarchy() { 210 ThreadUtils.assertOnUiThread(); 211 mMainView = null; 212 } 213 214 /** 215 * Launches a background DNS query for a given URL. 216 * 217 * @param url URL from which the domain to query is extracted. 218 */ prefetchDnsForUrlInBackground(final String url)219 private void prefetchDnsForUrlInBackground(final String url) { 220 mDnsRequestsInFlight.add(url); 221 new AsyncTask<Void>() { 222 @Override 223 protected Void doInBackground() { 224 try (TraceEvent e = 225 TraceEvent.scoped("WarmupManager.prefetchDnsForUrlInBackground")) { 226 InetAddress.getByName(new URL(url).getHost()); 227 } catch (MalformedURLException e) { 228 // We don't do anything with the result of the request, it 229 // is only here to warm up the cache, thus ignoring the 230 // exception is fine. 231 } catch (UnknownHostException e) { 232 // As above. 233 } 234 return null; 235 } 236 237 @Override 238 protected void onPostExecute(Void result) { 239 mDnsRequestsInFlight.remove(url); 240 if (mPendingPreconnectWithProfile.containsKey(url)) { 241 Profile profile = mPendingPreconnectWithProfile.get(url); 242 mPendingPreconnectWithProfile.remove(url); 243 maybePreconnectUrlAndSubResources(profile, url); 244 } 245 } 246 } 247 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 248 } 249 250 /** Launches a background DNS query for a given URL. 251 * 252 * @param context The Application context. 253 * @param url URL from which the domain to query is extracted. 254 */ maybePrefetchDnsForUrlInBackground(Context context, String url)255 public void maybePrefetchDnsForUrlInBackground(Context context, String url) { 256 ThreadUtils.assertOnUiThread(); 257 prefetchDnsForUrlInBackground(url); 258 } 259 260 /** 261 * Starts asynchronous initialization of the preconnect predictor. 262 * 263 * Without this call, |maybePreconnectUrlAndSubresources()| will not use a database of origins 264 * to connect to, unless the predictor has already been initialized in another way. 265 * 266 * @param profile The profile to use for the predictor. 267 */ startPreconnectPredictorInitialization(Profile profile)268 public static void startPreconnectPredictorInitialization(Profile profile) { 269 ThreadUtils.assertOnUiThread(); 270 WarmupManagerJni.get().startPreconnectPredictorInitialization(profile); 271 } 272 273 /** 274 * Reports to WarmupManager on the next set of URLs that the user is expected to navigate to 275 * next. The set of URLs are reported by an external Android app. 276 * 277 * @param profile The profile to use. 278 * @param packagesName Possible names of the external Android apps that may have reported the 279 * set of URLs. 280 * @param urls Ordered list of URLs that the user is expected to navigate to next. The URLs are 281 * ordered in non-increasing probability of navigation. 282 */ reportNextLikelyNavigationsOnUiThread( Profile profile, String[] packagesName, String[] urls)283 public static void reportNextLikelyNavigationsOnUiThread( 284 Profile profile, String[] packagesName, String[] urls) { 285 ThreadUtils.assertOnUiThread(); 286 WarmupManagerJni.get().reportNextLikelyNavigations(profile, packagesName, urls); 287 } 288 289 /** Asynchronously preconnects to a given URL if the data reduction proxy is not in use. 290 * 291 * @param profile The profile to use for the preconnection. 292 * @param url The URL we want to preconnect to. 293 */ maybePreconnectUrlAndSubResources(Profile profile, String url)294 public void maybePreconnectUrlAndSubResources(Profile profile, String url) { 295 ThreadUtils.assertOnUiThread(); 296 297 Uri uri = Uri.parse(url); 298 if (uri == null) return; 299 String scheme = uri.normalizeScheme().getScheme(); 300 if (!UrlConstants.HTTP_SCHEME.equals(scheme) && !UrlConstants.HTTPS_SCHEME.equals(scheme)) { 301 return; 302 } 303 304 // If there is already a DNS request in flight for this URL, then the preconnection will 305 // start by issuing a DNS request for the same domain, as the result is not cached. However, 306 // such a DNS request has already been sent from this class, so it is better to wait for the 307 // answer to come back before preconnecting. Otherwise, the preconnection logic will wait 308 // for the result of the second DNS request, which should arrive after the result of the 309 // first one. Note that we however need to wait for the main thread to be available in this 310 // case, since the preconnection will be sent from AsyncTask.onPostExecute(), which may 311 // delay it. 312 if (mDnsRequestsInFlight.contains(url)) { 313 // Note that if two requests come for the same URL with two different profiles, the last 314 // one will win. 315 mPendingPreconnectWithProfile.put(url, profile); 316 } else { 317 WarmupManagerJni.get().preconnectUrlAndSubresources(profile, url); 318 } 319 } 320 321 /** 322 * Warms up a spare, empty RenderProcessHost that may be used for subsequent navigations. 323 * 324 * The spare RenderProcessHost will be used automatically in subsequent navigations. 325 * There is nothing further the WarmupManager needs to do to enable that use. 326 * 327 * This uses a different mechanism than createSpareWebContents, below, and is subject 328 * to fewer restrictions. 329 * 330 * This must be called from the UI thread. 331 */ createSpareRenderProcessHost(Profile profile)332 public void createSpareRenderProcessHost(Profile profile) { 333 ThreadUtils.assertOnUiThread(); 334 if (!LibraryLoader.getInstance().isInitialized()) return; 335 336 destroySpareWebContents(); 337 WarmupManagerJni.get().warmupSpareRenderer(profile); 338 } 339 340 /** 341 * Creates and initializes a spare WebContents, to be used in a subsequent navigation. 342 * 343 * This creates a renderer that is suitable for any navigation. It can be picked up by any tab. 344 * Can be called multiple times, and must be called from the UI thread. 345 * 346 * @param forCCT Whether this WebContents is being created for CCT. 347 */ createSpareWebContents(boolean forCCT)348 public void createSpareWebContents(boolean forCCT) { 349 ThreadUtils.assertOnUiThread(); 350 if (!LibraryLoader.getInstance().isInitialized() || mSpareWebContents != null) return; 351 352 mWebContentsCreatedForCCT = forCCT; 353 mSpareWebContents = new WebContentsFactory().createWebContentsWithWarmRenderer( 354 Profile.getLastUsedRegularProfile(), true /* initiallyHidden */); 355 mObserver = new RenderProcessGoneObserver(); 356 mSpareWebContents.addObserver(mObserver); 357 mWebContentsCreationTimeMs = SystemClock.elapsedRealtime(); 358 recordWebContentsStatus(WebContentsStatus.CREATED); 359 } 360 361 /** 362 * Destroys the spare WebContents if there is one. 363 */ destroySpareWebContents()364 public void destroySpareWebContents() { 365 ThreadUtils.assertOnUiThread(); 366 if (mSpareWebContents == null) return; 367 recordWebContentsStatus(WebContentsStatus.DESTROYED); 368 destroySpareWebContentsInternal(); 369 } 370 371 /** 372 * Returns a spare WebContents or null, depending on the availability of one. 373 * 374 * The parameters are the same as for {@link WebContentsFactory#createWebContents()}. 375 * @param forCCT Whether this WebContents is being taken by CCT. 376 * 377 * @return a WebContents, or null. 378 */ takeSpareWebContents( boolean incognito, boolean initiallyHidden, boolean forCCT)379 public WebContents takeSpareWebContents( 380 boolean incognito, boolean initiallyHidden, boolean forCCT) { 381 ThreadUtils.assertOnUiThread(); 382 if (incognito) return null; 383 WebContents result = mSpareWebContents; 384 if (result == null) return null; 385 mSpareWebContents = null; 386 result.removeObserver(mObserver); 387 mObserver = null; 388 if (!initiallyHidden) result.onShow(); 389 recordWebContentsStatus(mWebContentsCreatedForCCT == forCCT ? WebContentsStatus.USED 390 : WebContentsStatus.STOLEN); 391 return result; 392 } 393 394 /** 395 * @return Whether a spare renderer is available. 396 */ hasSpareWebContents()397 public boolean hasSpareWebContents() { 398 return mSpareWebContents != null; 399 } 400 destroySpareWebContentsInternal()401 private void destroySpareWebContentsInternal() { 402 mSpareWebContents.removeObserver(mObserver); 403 mSpareWebContents.destroy(); 404 mSpareWebContents = null; 405 mObserver = null; 406 } 407 recordWebContentsStatus(@ebContentsStatus int status)408 private void recordWebContentsStatus(@WebContentsStatus int status) { 409 if (!mWebContentsCreatedForCCT) return; 410 RecordHistogram.recordEnumeratedHistogram( 411 WEBCONTENTS_STATUS_HISTOGRAM, status, WebContentsStatus.NUM_ENTRIES); 412 } 413 414 @NativeMethods 415 interface Natives { startPreconnectPredictorInitialization(Profile profile)416 void startPreconnectPredictorInitialization(Profile profile); preconnectUrlAndSubresources(Profile profile, String url)417 void preconnectUrlAndSubresources(Profile profile, String url); warmupSpareRenderer(Profile profile)418 void warmupSpareRenderer(Profile profile); reportNextLikelyNavigations(Profile profile, String[] packagesName, String[] urls)419 void reportNextLikelyNavigations(Profile profile, String[] packagesName, String[] urls); 420 } 421 } 422