Commit fffe1325 authored by Bryan McQuade's avatar Bryan McQuade Committed by Commit Bot

Report additional page load metrics on Android.

This adds support for reporting 3 new page load metrics
(largest contentful paint, first input delay, and
cumulative layout shift) to embedders on Android.

Change-Id: I3e49c4ca8cddbc6471d215cb8fc17a6a2ebcc5a5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1785882Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Reviewed-by: default avatarBenoit L <lizeb@chromium.org>
Commit-Queue: Bryan McQuade <bmcquade@chromium.org>
Cr-Commit-Position: refs/heads/master@{#695751}
parent aa2ad75d
......@@ -904,8 +904,9 @@ public class CustomTabsConnection {
if (referrer == null) return ParallelRequestStatus.FAILURE_INVALID_REFERRER;
if (policy < 0 || policy > ReferrerPolicy.LAST) policy = ReferrerPolicy.DEFAULT;
if (url.toString().equals("") || !isValid(url))
if (url.toString().equals("") || !isValid(url)) {
return ParallelRequestStatus.FAILURE_INVALID_URL;
}
if (!canDoParallelRequest(session, referrer)) {
return ParallelRequestStatus.FAILURE_INVALID_REFERRER_FOR_SESSION;
}
......@@ -1141,20 +1142,16 @@ public class CustomTabsConnection {
}
/**
* Notifies the application of a page load metric for a single metric.
*
* TODD(lizeb): Move this to a proper method in {@link CustomTabsCallback} once one is
* available.
* Creates a Bundle with a value for navigation start and the specified page load metric.
*
* @param session Session identifier.
* @param metricName Name of the page load metric.
* @param navigationStartTick Absolute navigation start time, as TimeTicks taken from native.
* @param offsetMs Offset in ms from navigationStart for the page load metric.
*
* @return Whether the metric has been dispatched to the client.
* @return A Bundle containing navigation start and the page load metric.
*/
boolean notifySinglePageLoadMetric(CustomTabsSessionToken session, String metricName,
long navigationStartTick, long offsetMs) {
Bundle createBundleWithNavigationStartAndPageLoadMetric(
String metricName, long navigationStartTick, long offsetMs) {
if (!mNativeTickOffsetUsComputed) {
// Compute offset from time ticks to uptimeMillis.
mNativeTickOffsetUsComputed = true;
......@@ -1170,8 +1167,24 @@ public class CustomTabsConnection {
// SystemClock.uptimeMillis() value.
args.putLong(PageLoadMetrics.NAVIGATION_START,
(navigationStartTick - mNativeTickOffsetUs) / 1000);
return args;
}
return notifyPageLoadMetrics(session, args);
/**
* Notifies the application of a page load metric for a single metric.
*
* @param session Session identifier.
* @param metricName Name of the page load metric.
* @param navigationStartTick Absolute navigation start time, as TimeTicks taken from native.
* @param offsetMs Offset in ms from navigationStart for the page load metric.
*
* @return Whether the metric has been dispatched to the client.
*/
boolean notifySinglePageLoadMetric(CustomTabsSessionToken session, String metricName,
long navigationStartTick, long offsetMs) {
return notifyPageLoadMetrics(session,
createBundleWithNavigationStartAndPageLoadMetric(
metricName, navigationStartTick, offsetMs));
}
/**
......
......@@ -52,6 +52,19 @@ public class PageLoadMetricsObserver implements PageLoadMetrics.Observer {
navigationStartTick, firstContentfulPaintMs);
}
@Override
public void onLargestContentfulPaint(WebContents webContents, long navigationId,
long navigationStartTick, long largestContentfulPaintMs,
long largestContentfulPaintSize) {
if (webContents != mTab.getWebContents()) return;
Bundle args = mConnection.createBundleWithNavigationStartAndPageLoadMetric(
PageLoadMetrics.LARGEST_CONTENTFUL_PAINT, navigationStartTick,
largestContentfulPaintMs);
args.putLong(PageLoadMetrics.LARGEST_CONTENTFUL_PAINT_SIZE, largestContentfulPaintSize);
mConnection.notifyPageLoadMetrics(mSession, args);
}
@Override
public void onLoadEventStart(WebContents webContents, long navigationId,
long navigationStartTick, long loadEventStartMs) {
......@@ -77,4 +90,26 @@ public class PageLoadMetricsObserver implements PageLoadMetrics.Observer {
args.putLong(PageLoadMetrics.SEND_END, sendEndMs);
mConnection.notifyPageLoadMetrics(mSession, args);
}
@Override
public void onFirstInputDelay(
WebContents webContents, long navigationId, long firstInputDelayMs) {
if (webContents != mTab.getWebContents()) return;
Bundle args = new Bundle();
args.putLong(PageLoadMetrics.FIRST_INPUT_DELAY, firstInputDelayMs);
mConnection.notifyPageLoadMetrics(mSession, args);
}
@Override
public void onLayoutShiftScore(WebContents webContents, long navigationId,
float layoutShiftScoreBeforeInputOrScroll, float layoutShiftScoreOverall) {
if (webContents != mTab.getWebContents()) return;
Bundle args = new Bundle();
args.putFloat(PageLoadMetrics.LAYOUT_SHIFT_SCORE, layoutShiftScoreOverall);
args.putFloat(PageLoadMetrics.LAYOUT_SHIFT_SCORE_BEFORE_INPUT_OR_SCROLL,
layoutShiftScoreBeforeInputOrScroll);
mConnection.notifyPageLoadMetrics(mSession, args);
}
}
......@@ -17,8 +17,14 @@ import org.chromium.content_public.browser.WebContents;
*/
public class PageLoadMetrics {
public static final String FIRST_CONTENTFUL_PAINT = "firstContentfulPaint";
public static final String LARGEST_CONTENTFUL_PAINT = "largestContentfulPaint";
public static final String LARGEST_CONTENTFUL_PAINT_SIZE = "largestContentfulPaintSize";
public static final String NAVIGATION_START = "navigationStart";
public static final String LOAD_EVENT_START = "loadEventStart";
public static final String FIRST_INPUT_DELAY = "firstInputDelay";
public static final String LAYOUT_SHIFT_SCORE = "layoutShiftScore";
public static final String LAYOUT_SHIFT_SCORE_BEFORE_INPUT_OR_SCROLL =
"layoutShiftScoreBeforeInputOrScroll";
public static final String DOMAIN_LOOKUP_START = "domainLookupStart";
public static final String DOMAIN_LOOKUP_END = "domainLookupEnd";
public static final String CONNECT_START = "connectStart";
......@@ -70,6 +76,19 @@ public class PageLoadMetrics {
default void onFirstContentfulPaint(WebContents webContents, long navigationId,
long navigationStartTick, long firstContentfulPaintMs) {}
/**
* Called when the largest contentful paint page load metric is available.
*
* @param webContents the WebContents this metrics is related to.
* @param navigationId the unique id of a navigation this metrics is related to.
* @param navigationStartTick Absolute navigation start time, as TimeTicks.
* @param largestContentfulPaintMs Time to largest contentful paint from navigation start.
* @param largestContentfulPaintSize Size of largest contentful paint, in CSS pixels.
*/
default void onLargestContentfulPaint(WebContents webContents, long navigationId,
long navigationStartTick, long largestContentfulPaintMs,
long largestContentfulPaintSize) {}
/**
* Called when the first meaningful paint page load metric is available. See
* FirstMeaningfulPaintDetector.cpp
......@@ -82,6 +101,16 @@ public class PageLoadMetrics {
default void onFirstMeaningfulPaint(WebContents webContents, long navigationId,
long navigationStartTick, long firstMeaningfulPaintMs) {}
/**
* Called when the first input delay page load metric is available.
*
* @param webContents the WebContents this metrics is related to.
* @param navigationId the unique id of a navigation this metrics is related to.
* @param firstInputDelayMs First input delay.
*/
default void onFirstInputDelay(
WebContents webContents, long navigationId, long firstInputDelayMs) {}
/**
* Called when the load event start metric is available.
*
......@@ -105,6 +134,19 @@ public class PageLoadMetrics {
default void onLoadedMainResource(WebContents webContents, long navigationId,
long dnsStartMs, long dnsEndMs, long connectStartMs, long connectEndMs,
long requestStartMs, long sendStartMs, long sendEndMs) {}
/**
* Called when the layout shift score is available.
*
* @param webContents the WebContents this metrics is related to.
* @param navigationId the unique id of a navigation this metrics is related to.
* @param layoutShiftScoreBeforeInputOrScroll the cumulative layout shift score, before user
* input or scroll.
* @param layoutShiftScoreOverall the cumulative layout shift score over the lifetime of the
* web page.
*/
default void onLayoutShiftScore(WebContents webContents, long navigationId,
float layoutShiftScoreBeforeInputOrScroll, float layoutShiftScoreOverall) {}
}
private static ObserverList<Observer> sObservers;
......@@ -154,6 +196,18 @@ public class PageLoadMetrics {
}
}
@CalledByNative
static void onLargestContentfulPaint(WebContents webContents, long navigationId,
long navigationStartTick, long largestContentfulPaintMs,
long largestContentfulPaintSize) {
ThreadUtils.assertOnUiThread();
if (sObservers == null) return;
for (Observer observer : sObservers) {
observer.onLargestContentfulPaint(webContents, navigationId, navigationStartTick,
largestContentfulPaintMs, largestContentfulPaintSize);
}
}
@CalledByNative
static void onFirstMeaningfulPaint(WebContents webContents, long navigationId,
long navigationStartTick, long firstMeaningfulPaintMs) {
......@@ -165,6 +219,16 @@ public class PageLoadMetrics {
}
}
@CalledByNative
static void onFirstInputDelay(
WebContents webContents, long navigationId, long firstInputDelayMs) {
ThreadUtils.assertOnUiThread();
if (sObservers == null) return;
for (Observer observer : sObservers) {
observer.onFirstInputDelay(webContents, navigationId, firstInputDelayMs);
}
}
@CalledByNative
static void onLoadEventStart(WebContents webContents, long navigationId,
long navigationStartTick, long loadEventStartMs) {
......@@ -188,5 +252,16 @@ public class PageLoadMetrics {
}
}
@CalledByNative
static void onLayoutShiftScore(WebContents webContents, long navigationId,
float layoutShiftScoreBeforeInputOrScroll, float layoutShiftScoreOverall) {
ThreadUtils.assertOnUiThread();
if (sObservers == null) return;
for (Observer observer : sObservers) {
observer.onLayoutShiftScore(webContents, navigationId,
layoutShiftScoreBeforeInputOrScroll, layoutShiftScoreOverall);
}
}
private PageLoadMetrics() {}
}
......@@ -2703,8 +2703,10 @@ public class CustomTabActivityTest {
private void checkPageLoadMetrics(boolean allowMetrics)
throws InterruptedException, TimeoutException {
final AtomicReference<Long> firstContentfulPaintMs = new AtomicReference<>(-1L);
final AtomicReference<Long> largestContentfulPaintMs = new AtomicReference<>(-1L);
final AtomicReference<Long> activityStartTimeMs = new AtomicReference<>(-1L);
final AtomicReference<Long> loadEventStartMs = new AtomicReference<>(-1L);
final AtomicReference<Float> layoutShiftScore = new AtomicReference<>(-1f);
final AtomicReference<Boolean> sawNetworkQualityEstimates = new AtomicReference<>(false);
CustomTabsCallback cb = new CustomTabsCallback() {
......@@ -2721,6 +2723,12 @@ public class CustomTabActivityTest {
sawNetworkQualityEstimates.set(true);
}
float layoutShiftScoreValue =
args.getFloat(PageLoadMetrics.LAYOUT_SHIFT_SCORE, -1f);
if (layoutShiftScoreValue >= 0f) {
layoutShiftScore.set(layoutShiftScoreValue);
}
long navigationStart = args.getLong(PageLoadMetrics.NAVIGATION_START, -1);
if (navigationStart == -1) {
// Untested metric callback.
......@@ -2737,6 +2745,13 @@ public class CustomTabActivityTest {
firstContentfulPaintMs.set(firstContentfulPaint);
}
long largestContentfulPaint =
args.getLong(PageLoadMetrics.LARGEST_CONTENTFUL_PAINT, -1);
if (largestContentfulPaint > 0) {
Assert.assertTrue(largestContentfulPaint <= (current - navigationStart));
largestContentfulPaintMs.set(largestContentfulPaint);
}
long loadEventStart = args.getLong(PageLoadMetrics.LOAD_EVENT_START, -1);
if (loadEventStart > 0) {
Assert.assertTrue(loadEventStart <= (current - navigationStart));
......@@ -2772,6 +2787,25 @@ public class CustomTabActivityTest {
// Expected.
}
assertEquals(-1L, (long) firstContentfulPaintMs.get());
try {
CriteriaHelper.pollInstrumentationThread(() -> largestContentfulPaintMs.get() > 0);
} catch (AssertionError e) {
// Expected.
}
assertEquals(-1L, (long) largestContentfulPaintMs.get());
}
// Navigate to a new page, as metrics like LCP are only reported at the end of the page load
// lifetime.
TestThreadUtils.runOnUiThreadBlocking(() -> {
final CustomTabActivity activity = mCustomTabActivityTestRule.getActivity();
activity.getComponent().resolveNavigationController().navigate("about:blank");
});
if (allowMetrics) {
CriteriaHelper.pollInstrumentationThread(() -> largestContentfulPaintMs.get() > 0);
CriteriaHelper.pollInstrumentationThread(() -> layoutShiftScore.get() != -1f);
}
}
......
......@@ -40,6 +40,39 @@ AndroidPageLoadMetricsObserver::OnStart(
return CONTINUE_OBSERVING;
}
page_load_metrics::PageLoadMetricsObserver::ObservePolicy
AndroidPageLoadMetricsObserver::FlushMetricsOnAppEnterBackground(
const page_load_metrics::mojom::PageLoadTiming& timing) {
// FlushMetricsOnAppEnterBackground is invoked on Android in cases where the
// app is about to be backgrounded, as part of the Activity.onPause()
// flow. After this method is invoked, Chrome may be killed without further
// notification, so we record final metrics collected up to this point.
ReportBufferedMetrics(timing);
// We continue observing after being backgrounded, in case we are foregrounded
// again without being killed. In those cases we may still report non-buffered
// metrics such as FCP after being re-foregrounded.
return CONTINUE_OBSERVING;
}
AndroidPageLoadMetricsObserver::ObservePolicy
AndroidPageLoadMetricsObserver::OnHidden(
const page_load_metrics::mojom::PageLoadTiming& timing) {
ReportBufferedMetrics(timing);
return CONTINUE_OBSERVING;
}
void AndroidPageLoadMetricsObserver::OnDidFinishSubFrameNavigation(
content::NavigationHandle* navigation_handle) {
largest_contentful_paint_handler_.OnDidFinishSubFrameNavigation(
navigation_handle, GetDelegate());
}
void AndroidPageLoadMetricsObserver::OnComplete(
const page_load_metrics::mojom::PageLoadTiming& timing) {
ReportBufferedMetrics(timing);
}
void AndroidPageLoadMetricsObserver::OnFirstContentfulPaintInPage(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
......@@ -50,6 +83,14 @@ void AndroidPageLoadMetricsObserver::OnFirstContentfulPaintInPage(
first_contentful_paint_ms);
}
void AndroidPageLoadMetricsObserver::OnFirstInputInPage(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
int64_t first_input_delay_ms =
timing.interactive_timing->first_input_delay->InMilliseconds();
ReportFirstInputDelay(first_input_delay_ms);
}
void AndroidPageLoadMetricsObserver::OnFirstMeaningfulPaintInMainFrameDocument(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
......@@ -103,6 +144,13 @@ void AndroidPageLoadMetricsObserver::OnLoadedResource(
}
}
void AndroidPageLoadMetricsObserver::OnTimingUpdate(
content::RenderFrameHost* subframe_rfh,
const page_load_metrics::mojom::PageLoadTiming& timing) {
largest_contentful_paint_handler_.RecordTiming(timing.paint_timing,
subframe_rfh);
}
void AndroidPageLoadMetricsObserver::ReportNewNavigation() {
DCHECK_GE(navigation_id_, 0);
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
......@@ -112,6 +160,41 @@ void AndroidPageLoadMetricsObserver::ReportNewNavigation() {
static_cast<jlong>(navigation_id_));
}
void AndroidPageLoadMetricsObserver::ReportBufferedMetrics(
const page_load_metrics::mojom::PageLoadTiming& timing) {
// This method may be invoked multiple times. Make sure that if we already
// reported, we do not report again.
if (reported_buffered_metrics_)
return;
reported_buffered_metrics_ = true;
// Buffered metrics aren't available until after the navigation commits.
if (!GetDelegate().DidCommit())
return;
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
int64_t navigation_start_tick =
(GetDelegate().GetNavigationStart() - base::TimeTicks()).InMicroseconds();
const page_load_metrics::ContentfulPaintTimingInfo& largest_contentful_paint =
largest_contentful_paint_handler_.MergeMainFrameAndSubframes();
if (!largest_contentful_paint.IsEmpty()) {
Java_PageLoadMetrics_onLargestContentfulPaint(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jlong>(navigation_start_tick),
static_cast<jlong>(largest_contentful_paint.Time()->InMilliseconds()),
static_cast<jlong>(largest_contentful_paint.Size()));
}
Java_PageLoadMetrics_onLayoutShiftScore(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jfloat>(GetDelegate()
.GetMainFrameRenderData()
.layout_shift_score_before_input_or_scroll),
static_cast<jfloat>(
GetDelegate().GetPageRenderData().layout_shift_score));
}
void AndroidPageLoadMetricsObserver::ReportNetworkQualityEstimate(
net::EffectiveConnectionType connection_type,
int64_t http_rtt_ms,
......@@ -179,3 +262,13 @@ void AndroidPageLoadMetricsObserver::ReportLoadedMainResource(
static_cast<jlong>(request_start_ms), static_cast<jlong>(send_start_ms),
static_cast<jlong>(send_end_ms));
}
void AndroidPageLoadMetricsObserver::ReportFirstInputDelay(
int64_t first_input_delay_ms) {
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onFirstInputDelay(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jlong>(first_input_delay_ms));
}
......@@ -8,6 +8,7 @@
#include <jni.h>
#include "base/macros.h"
#include "chrome/browser/page_load_metrics/observers/largest_contentful_paint_handler.h"
#include "chrome/browser/page_load_metrics/page_load_metrics_observer.h"
namespace network {
......@@ -23,17 +24,33 @@ class AndroidPageLoadMetricsObserver
AndroidPageLoadMetricsObserver();
// page_load_metrics::PageLoadMetricsObserver:
// PageLoadMetricsObserver lifecycle callbacks
ObservePolicy OnStart(content::NavigationHandle* navigation_handle,
const GURL& currently_committed_url,
bool started_in_foreground) override;
ObservePolicy FlushMetricsOnAppEnterBackground(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
ObservePolicy OnHidden(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void OnDidFinishSubFrameNavigation(
content::NavigationHandle* navigation_handle) override;
void OnComplete(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
// PageLoadMetricsObserver event callbacks
void OnFirstContentfulPaintInPage(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void OnFirstInputInPage(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void OnFirstMeaningfulPaintInMainFrameDocument(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void OnLoadEventStart(
const page_load_metrics::mojom::PageLoadTiming& timing) override;
void OnLoadedResource(const page_load_metrics::ExtraRequestCompleteInfo&
extra_request_complete_info) override;
void OnTimingUpdate(
content::RenderFrameHost* subframe_rfh,
const page_load_metrics::mojom::PageLoadTiming& timing) override;
protected:
AndroidPageLoadMetricsObserver(
......@@ -42,6 +59,9 @@ class AndroidPageLoadMetricsObserver
virtual void ReportNewNavigation();
virtual void ReportBufferedMetrics(
const page_load_metrics::mojom::PageLoadTiming& timing);
virtual void ReportNetworkQualityEstimate(
net::EffectiveConnectionType connection_type,
int64_t http_rtt_ms,
......@@ -64,12 +84,18 @@ class AndroidPageLoadMetricsObserver
int64_t send_start_ms,
int64_t send_end_ms);
virtual void ReportFirstInputDelay(int64_t first_input_delay_ms);
private:
bool did_dispatch_on_main_resource_ = false;
bool reported_buffered_metrics_ = false;
int64_t navigation_id_ = -1;
network::NetworkQualityTracker* network_quality_tracker_ = nullptr;
page_load_metrics::LargestContentfulPaintHandler
largest_contentful_paint_handler_;
DISALLOW_COPY_AND_ASSIGN(AndroidPageLoadMetricsObserver);
};
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment