// Copyright 2023 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.chrome.browser.metrics;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.browser.base.ColdStartTracker;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.page_load_metrics.PageLoadMetrics;
import org.chromium.chrome.browser.paint_preview.StartupPaintPreviewHelper;
import org.chromium.chrome.browser.paint_preview.StartupPaintPreviewMetrics.PaintPreviewMetricsObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.safe_browsing.SafeBrowsingApiBridge;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;
/**
* Records UMA page load metrics for the first navigation on a cold start.
*
* <p>Uses different cold start heuristics from {@link LegacyTabStartupMetricsTracker}. These
* heuristics aim to replace a few metrics from Startup.Android.Cold.*.
*/
public class StartupMetricsTracker {
private boolean mFirstNavigationCommitted;
private class TabObserver extends TabModelSelectorTabObserver {
private boolean mFirstLoadStarted;
public TabObserver(TabModelSelector selector) {
super(selector);
}
@Override
public void onShown(Tab tab, @TabSelectionType int type) {
if (tab != null && tab.isNativePage()) {
// Avoid recording metrics when the NTP is shown.
destroy();
}
}
@Override
public void onPageLoadStarted(Tab tab, GURL url) {
// Discard startup navigation measurements when the user started another navigation.
if (!mFirstLoadStarted) {
mFirstLoadStarted = true;
} else {
destroy();
}
}
@Override
public void onDidFinishNavigationInPrimaryMainFrame(
Tab tab, @NonNull NavigationHandle navigation) {
if (!mShouldTrack || mFirstNavigationCommitted) return;
boolean shouldTrack =
navigation.hasCommitted()
&& !navigation.isErrorPage()
&& UrlUtilities.isHttpOrHttps(navigation.getUrl())
&& !navigation.isSameDocument();
if (!shouldTrack) {
// When navigation leads to an error page, download or chrome:// URLs, avoid
// recording both commit and FCP.
//
// In rare cases a same-document navigation can commit before all other
// http(s)+non-error navigations (crbug.com/1492721). Filter out such scenarios
// since they are counter-intuitive.
destroy();
} else {
mFirstNavigationCommitted = true;
recordNavigationCommitMetrics(SystemClock.uptimeMillis() - mActivityStartTimeMs);
}
}
}
private class PageObserver implements PageLoadMetrics.Observer {
private static final long NO_NAVIGATION_ID = -1;
private long mNavigationId = NO_NAVIGATION_ID;
@Override
public void onNewNavigation(
WebContents webContents,
long navigationId,
boolean isFirstNavigationInWebContents) {
if (mNavigationId != NO_NAVIGATION_ID) return;
mNavigationId = navigationId;
}
@Override
public void onFirstContentfulPaint(
WebContents webContents,
long navigationId,
long navigationStartMicros,
long firstContentfulPaintMs) {
recordFcpMetricsIfNeeded(navigationId, navigationStartMicros, firstContentfulPaintMs);
destroy();
}
private void recordFcpMetricsIfNeeded(
long navigationId, long navigationStartMicros, long firstContentfulPaintMs) {
if (navigationId != mNavigationId || !mShouldTrack || !mFirstNavigationCommitted) {
return;
}
recordFcpMetrics(
navigationStartMicros / 1000 + firstContentfulPaintMs - mActivityStartTimeMs);
}
}
// The time of the activity onCreate(). All metrics (such as time to first visible content) are
// reported in uptimeMillis relative to this value.
private final long mActivityStartTimeMs;
private boolean mFirstVisibleContent3Recorded;
private TabModelSelectorTabObserver mTabObserver;
private PageObserver mPageObserver;
private boolean mShouldTrack = true;
private @ActivityType int mHistogramSuffix;
// The time it took for SafeBrowsing API to return a Safe Browsing response for the first time.
// The SB request is on the critical path to navigation commit, and the response may be severely
// delayed by GmsCore (see http://crbug.com/1296097). The value is recorded only when the
// navigation commits successfully and the URL of first navigation is checked by SafeBrowsing
// API. Utilizing a volatile long here to ensure the write is immediately visible to other
// threads.
private volatile long mFirstSafeBrowsingResponseTimeMicros;
private boolean mFirstSafeBrowsingResponseTimeRecorded;
public StartupMetricsTracker(ObservableSupplier<TabModelSelector> tabModelSelectorSupplier) {
mActivityStartTimeMs = SystemClock.uptimeMillis();
tabModelSelectorSupplier.addObserver(this::registerObservers);
SafeBrowsingApiBridge.setOneTimeSafeBrowsingApiUrlCheckObserver(
this::updateSafeBrowsingCheckTime);
}
private void updateSafeBrowsingCheckTime(long urlCheckTimeDeltaMicros) {
mFirstSafeBrowsingResponseTimeMicros = urlCheckTimeDeltaMicros;
}
/**
* Choose the UMA histogram to record later. The {@link ActivityType} parameter indicates the
* kind of startup scenario to track.
*
* @param activityType Either TABBED or WEB_APK.
*/
public void setHistogramSuffix(@ActivityType int activityType) {
mHistogramSuffix = activityType;
}
private void registerObservers(TabModelSelector tabModelSelector) {
if (!mShouldTrack) return;
mTabObserver = new TabObserver(tabModelSelector);
mPageObserver = new PageObserver();
PageLoadMetrics.addObserver(mPageObserver, /* supportPrerendering= */ false);
}
/**
* Register an observer to be notified on the first paint of a paint preview if present.
* @param startupPaintPreviewHelper the helper to register the observer to.
*/
public void registerPaintPreviewObserver(StartupPaintPreviewHelper startupPaintPreviewHelper) {
startupPaintPreviewHelper.addMetricsObserver(
new PaintPreviewMetricsObserver() {
@Override
public void onFirstPaint(long durationMs) {
recordTimeToFirstVisibleContent3(durationMs);
}
@Override
public void onUnrecordedFirstPaint() {}
});
}
public void destroy() {
mShouldTrack = false;
if (mTabObserver != null) {
mTabObserver.destroy();
mTabObserver = null;
}
if (mPageObserver != null) {
PageLoadMetrics.removeObserver(mPageObserver);
mPageObserver = null;
}
}
private String activityTypeToSuffix(@ActivityType int type) {
if (type == ActivityType.TABBED) return ".Tabbed";
assert type == ActivityType.WEB_APK;
return ".WebApk";
}
private void recordExperimentalHistogram(String name, long ms) {
RecordHistogram.recordMediumTimesHistogram(
"Startup.Android.Experimental." + name + ".Tabbed.ColdStartTracker", ms);
}
private void recordNavigationCommitMetrics(long firstCommitMs) {
if (!SimpleStartupForegroundSessionDetector.runningCleanForegroundSession()) return;
if (ColdStartTracker.wasColdOnFirstActivityCreationOrNow()) {
RecordHistogram.recordMediumTimesHistogram(
"Startup.Android.Cold.TimeToFirstNavigationCommit3"
+ activityTypeToSuffix(mHistogramSuffix),
firstCommitMs);
if (mHistogramSuffix == ActivityType.TABBED) {
recordExperimentalHistogram("FirstNavigationCommit", firstCommitMs);
recordFirstSafeBrowsingResponseTime();
recordTimeToFirstVisibleContent3(firstCommitMs);
}
}
}
private void recordFcpMetrics(long firstFcpMs) {
if (!SimpleStartupForegroundSessionDetector.runningCleanForegroundSession()) return;
if (ColdStartTracker.wasColdOnFirstActivityCreationOrNow()) {
recordExperimentalHistogram("FirstContentfulPaint", firstFcpMs);
RecordHistogram.recordMediumTimesHistogram(
"Startup.Android.Cold.TimeToFirstContentfulPaint3.Tabbed", firstFcpMs);
}
}
private void recordTimeToFirstVisibleContent3(long durationMs) {
if (mFirstVisibleContent3Recorded) return;
mFirstVisibleContent3Recorded = true;
RecordHistogram.recordMediumTimesHistogram(
"Startup.Android.Cold.TimeToFirstVisibleContent3", durationMs);
}
private void recordFirstSafeBrowsingResponseTime() {
if (mFirstSafeBrowsingResponseTimeRecorded) return;
mFirstSafeBrowsingResponseTimeRecorded = true;
if (mFirstSafeBrowsingResponseTimeMicros != 0) {
RecordHistogram.recordMediumTimesHistogram(
"Startup.Android.Cold.FirstSafeBrowsingApiResponseTime2.Tabbed",
mFirstSafeBrowsingResponseTimeMicros / 1000);
}
}
}