// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/page_load_metrics/observers/android_page_load_metrics_observer.h"
#include <string>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/android/scoped_java_ref.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "components/page_load_metrics/browser/observers/core/largest_contentful_paint_handler.h"
#include "components/page_load_metrics/browser/page_load_metrics_util.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "services/network/public/cpp/network_quality_tracker.h"
#include "url/gurl.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/browser/page_load_metrics/jni_headers/PageLoadMetrics_jni.h"
AndroidPageLoadMetricsObserver::AndroidPageLoadMetricsObserver() {
network_quality_tracker_ = g_browser_process->network_quality_tracker();
DCHECK(network_quality_tracker_);
}
AndroidPageLoadMetricsObserver::ObservePolicy
AndroidPageLoadMetricsObserver::OnStart(
content::NavigationHandle* navigation_handle,
const GURL& currently_committed_url,
bool started_in_foreground) {
ReportNewNavigation(navigation_handle->GetNavigationId());
return CONTINUE_OBSERVING;
}
page_load_metrics::PageLoadMetricsObserver::ObservePolicy
AndroidPageLoadMetricsObserver::OnFencedFramesStart(
content::NavigationHandle* navigation_handle,
const GURL& currently_committed_url) {
// This class uses the event OnLoadedResource, but only uses the one with
// network::mojom::RequestDestination::kDocument, which never occur in
// FencedFrames' navigation. So, we can use STOP_OBSERVING.
return STOP_OBSERVING;
}
page_load_metrics::PageLoadMetricsObserver::ObservePolicy
AndroidPageLoadMetricsObserver::OnPrerenderStart(
content::NavigationHandle* navigation_handle,
const GURL& currently_committed_url) {
ReportNewNavigation(navigation_handle->GetNavigationId());
return CONTINUE_OBSERVING;
}
void AndroidPageLoadMetricsObserver::DidActivatePrerenderedPage(
content::NavigationHandle* navigation_handle) {
// NavigationHandle for the activation contains the source of
// `activation_start` tick as NavigationStart.
ReportActivation(navigation_handle->GetNavigationId(),
navigation_handle->NavigationStart());
}
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::OnComplete(
const page_load_metrics::mojom::PageLoadTiming& timing) {
// TODO(http://crbug.com/1363952): Java side WebContents insntace obtained
// via GetJavaWebContents() was already released here, and newly created wrong
// instance will be obtained in the following calls.
ReportBufferedMetrics(timing);
}
void AndroidPageLoadMetricsObserver::OnFirstContentfulPaintInPage(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ReportFirstContentfulPaint(GetDelegate().GetNavigationStart(),
*timing.paint_timing->first_contentful_paint);
}
void AndroidPageLoadMetricsObserver::OnFirstInputInPage(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ReportFirstInputDelay(*timing.interactive_timing->first_input_delay);
}
void AndroidPageLoadMetricsObserver::OnFirstMeaningfulPaintInMainFrameDocument(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ReportFirstMeaningfulPaint(GetDelegate().GetNavigationStart(),
*timing.paint_timing->first_meaningful_paint);
}
void AndroidPageLoadMetricsObserver::OnLoadEventStart(
const page_load_metrics::mojom::PageLoadTiming& timing) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ReportLoadEventStart(GetDelegate().GetNavigationStart(),
*timing.document_timing->load_event_start);
}
void AndroidPageLoadMetricsObserver::OnLoadedResource(
const page_load_metrics::ExtraRequestCompleteInfo&
extra_request_complete_info) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (extra_request_complete_info.request_destination ==
network::mojom::RequestDestination::kDocument) {
DCHECK(!did_dispatch_on_main_resource_);
if (did_dispatch_on_main_resource_) {
// We are defensive for the case of something strange happening and return
// in order not to post multiple times.
return;
}
did_dispatch_on_main_resource_ = true;
const net::LoadTimingInfo& timing =
*extra_request_complete_info.load_timing_info;
int64_t domain_lookup_start =
timing.connect_timing.domain_lookup_start.since_origin()
.InMilliseconds();
int64_t domain_lookup_end =
timing.connect_timing.domain_lookup_end.since_origin().InMilliseconds();
int64_t connect_start =
timing.connect_timing.connect_start.since_origin().InMilliseconds();
int64_t connect_end =
timing.connect_timing.connect_end.since_origin().InMilliseconds();
int64_t request_start =
timing.request_start.since_origin().InMilliseconds();
int64_t send_start = timing.send_start.since_origin().InMilliseconds();
int64_t send_end = timing.send_end.since_origin().InMilliseconds();
ReportLoadedMainResource(domain_lookup_start, domain_lookup_end,
connect_start, connect_end, request_start,
send_start, send_end);
}
}
void AndroidPageLoadMetricsObserver::ReportNewNavigation(
int64_t navigation_id) {
DCHECK_GE(navigation_id, 0);
navigation_id_ = navigation_id;
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onNewNavigation(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jboolean>(GetDelegate().IsFirstNavigationInWebContents()),
static_cast<jboolean>(IsPrerendering()));
int64_t http_rtt = network_quality_tracker_->GetHttpRTT().InMilliseconds();
int64_t transport_rtt =
network_quality_tracker_->GetTransportRTT().InMilliseconds();
ReportNetworkQualityEstimate(
network_quality_tracker_->GetEffectiveConnectionType(), http_rtt,
transport_rtt);
}
void AndroidPageLoadMetricsObserver::ReportActivation(
int64_t activating_navigation_id,
base::TimeTicks activation_start_tick) {
DCHECK_GE(activating_navigation_id, 0);
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onActivation(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jlong>(activating_navigation_id),
activation_start_tick.ToUptimeMicros());
navigation_id_ = activating_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().ToUptimeMicros();
const page_load_metrics::ContentfulPaintTimingInfo& largest_contentful_paint =
GetDelegate()
.GetLargestContentfulPaintHandler()
.MergeMainFrameAndSubframes();
if (largest_contentful_paint.ContainsValidTime()) {
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),
static_cast<jboolean>(IsPrerendering()));
}
void AndroidPageLoadMetricsObserver::ReportNetworkQualityEstimate(
net::EffectiveConnectionType connection_type,
int64_t http_rtt_ms,
int64_t transport_rtt_ms) {
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onNetworkQualityEstimate(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jint>(connection_type), static_cast<jlong>(http_rtt_ms),
static_cast<jlong>(transport_rtt_ms),
static_cast<jboolean>(IsPrerendering()));
}
void AndroidPageLoadMetricsObserver::ReportFirstContentfulPaint(
base::TimeTicks navigation_start_tick,
base::TimeDelta first_contentful_paint) {
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onFirstContentfulPaint(
env, java_web_contents, static_cast<jlong>(navigation_id_),
navigation_start_tick.ToUptimeMicros(),
static_cast<jlong>(first_contentful_paint.InMilliseconds()));
}
void AndroidPageLoadMetricsObserver::ReportFirstMeaningfulPaint(
base::TimeTicks navigation_start_tick,
base::TimeDelta first_meaningful_paint) {
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onFirstMeaningfulPaint(
env, java_web_contents, static_cast<jlong>(navigation_id_),
navigation_start_tick.ToUptimeMicros(),
static_cast<jlong>(first_meaningful_paint.InMilliseconds()));
}
void AndroidPageLoadMetricsObserver::ReportLoadEventStart(
base::TimeTicks navigation_start_tick,
base::TimeDelta load_event_start) {
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onLoadEventStart(
env, java_web_contents, static_cast<jlong>(navigation_id_),
navigation_start_tick.ToUptimeMicros(),
static_cast<jlong>(load_event_start.InMilliseconds()),
static_cast<jboolean>(IsPrerendering()));
}
void AndroidPageLoadMetricsObserver::ReportLoadedMainResource(
int64_t dns_start_ms,
int64_t dns_end_ms,
int64_t connect_start_ms,
int64_t connect_end_ms,
int64_t request_start_ms,
int64_t send_start_ms,
int64_t send_end_ms) {
base::android::ScopedJavaLocalRef<jobject> java_web_contents =
GetDelegate().GetWebContents()->GetJavaWebContents();
JNIEnv* env = base::android::AttachCurrentThread();
Java_PageLoadMetrics_onLoadedMainResource(
env, java_web_contents, static_cast<jlong>(navigation_id_),
static_cast<jlong>(dns_start_ms), static_cast<jlong>(dns_end_ms),
static_cast<jlong>(connect_start_ms), static_cast<jlong>(connect_end_ms),
static_cast<jlong>(request_start_ms), static_cast<jlong>(send_start_ms),
static_cast<jlong>(send_end_ms), static_cast<jboolean>(IsPrerendering()));
}
void AndroidPageLoadMetricsObserver::ReportFirstInputDelay(
base::TimeDelta first_input_delay) {
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.InMilliseconds()));
}
bool AndroidPageLoadMetricsObserver::IsPrerendering() {
// So that the isPrerendering argument works to realize STOP_OBSERVING on
// OnPrerenderStart().
return GetDelegate().GetPrerenderingState() !=
page_load_metrics::PrerenderingState::kNoPrerendering;
}