// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/web/model/web_performance_metrics/web_performance_metrics_java_script_feature.h"
#import "base/ios/ios_util.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/no_destructor.h"
#import "base/strings/strcat.h"
#import "base/values.h"
#import "ios/chrome/browser/web/model/web_performance_metrics/web_performance_metrics_java_script_feature_util.h"
#import "ios/chrome/browser/web/model/web_performance_metrics/web_performance_metrics_tab_helper.h"
#import "ios/web/public/js_messaging/java_script_feature_util.h"
#import "ios/web/public/js_messaging/script_message.h"
namespace {
const char kPerformanceMetricsScript[] = "web_performance_metrics";
const char kWebPerformanceMetricsScriptName[] = "WebPerformanceMetricsHandler";
// The time range's expected min and max values for FirstContentfulPaint
// histograms.
constexpr base::TimeDelta kTimeRangePaintHistogramMin = base::Milliseconds(10);
constexpr base::TimeDelta kTimeRangePaintHistogramMax = base::Minutes(10);
// Number of buckets for the FirstContentfulPaint histograms.
constexpr int kTimeRangePaintHistogramBucketCount = 100;
// The time range's expected min and max values for FirstInputDelay
// histograms.
constexpr base::TimeDelta kTimeRangeInputDelayHistogramMin =
base::Milliseconds(1);
constexpr base::TimeDelta kTimeRangeInputDelayHistogramMax = base::Seconds(60);
// Number of buckets for the FirstInputDelay histograms.
constexpr int kTimeRangeInputDelayHistogramBucketCount = 50;
} // namespace
WebPerformanceMetricsJavaScriptFeature::WebPerformanceMetricsJavaScriptFeature()
: JavaScriptFeature(web::ContentWorld::kIsolatedWorld,
{FeatureScript::CreateWithFilename(
kPerformanceMetricsScript,
FeatureScript::InjectionTime::kDocumentStart,
FeatureScript::TargetFrames::kAllFrames)}) {}
WebPerformanceMetricsJavaScriptFeature::
~WebPerformanceMetricsJavaScriptFeature() = default;
WebPerformanceMetricsJavaScriptFeature*
WebPerformanceMetricsJavaScriptFeature::GetInstance() {
static base::NoDestructor<WebPerformanceMetricsJavaScriptFeature> instance;
return instance.get();
}
std::optional<std::string>
WebPerformanceMetricsJavaScriptFeature::GetScriptMessageHandlerName() const {
return kWebPerformanceMetricsScriptName;
}
void WebPerformanceMetricsJavaScriptFeature::ScriptMessageReceived(
web::WebState* web_state,
const web::ScriptMessage& message) {
DCHECK(web_state);
// Verify that the message is well-formed before using it
if (!message.body()->is_dict()) {
return;
}
base::Value::Dict& body_dict = message.body()->GetDict();
std::string* metric = body_dict.FindString("metric");
if (!metric || metric->empty()) {
return;
}
std::optional<double> value = body_dict.FindDouble("value");
if (!value) {
return;
}
if (*metric == "FirstContentfulPaint") {
std::optional<double> frame_navigation_start_time =
body_dict.FindDouble("frameNavigationStartTime");
if (!frame_navigation_start_time) {
return;
}
LogRelativeFirstContentfulPaint(value.value(), message.is_main_frame());
LogAggregateFirstContentfulPaint(web_state,
frame_navigation_start_time.value(),
value.value(), message.is_main_frame());
} else if (*metric == "FirstInputDelay") {
std::optional<bool> loaded_from_cache = body_dict.FindBool("cached");
if (!loaded_from_cache.has_value()) {
return;
}
LogRelativeFirstInputDelay(value.value(), message.is_main_frame(),
loaded_from_cache.value());
LogAggregateFirstInputDelay(web_state, value.value(),
loaded_from_cache.value());
}
}
void WebPerformanceMetricsJavaScriptFeature::LogRelativeFirstContentfulPaint(
double value,
bool is_main_frame) {
if (is_main_frame) {
UmaHistogramCustomTimes(
"IOS.Frame.FirstContentfulPaint.MainFrame", base::Milliseconds(value),
kTimeRangePaintHistogramMin, kTimeRangePaintHistogramMax,
kTimeRangePaintHistogramBucketCount);
} else {
UmaHistogramCustomTimes(
"IOS.Frame.FirstContentfulPaint.SubFrame", base::Milliseconds(value),
kTimeRangePaintHistogramMin, kTimeRangePaintHistogramMax,
kTimeRangePaintHistogramBucketCount);
}
}
void WebPerformanceMetricsJavaScriptFeature::LogAggregateFirstContentfulPaint(
web::WebState* web_state,
double frame_navigation_start_time,
double relative_first_contentful_paint,
bool is_main_frame) {
WebPerformanceMetricsTabHelper* tab_helper =
WebPerformanceMetricsTabHelper::FromWebState(web_state);
if (!tab_helper || tab_helper->HasBeenHiddenSinceNavigationStarted()) {
return;
}
const double aggregate =
tab_helper->GetAggregateAbsoluteFirstContentfulPaint();
if (is_main_frame) {
// Finds the earliest First Contentful Paint time across
// main and subframes and logs that time to UMA.
web_performance_metrics::FirstContentfulPaint frame = {
frame_navigation_start_time, relative_first_contentful_paint,
web_performance_metrics::CalculateAbsoluteFirstContentfulPaint(
frame_navigation_start_time, relative_first_contentful_paint)};
base::TimeDelta aggregate_first_contentful_paint =
web_performance_metrics::CalculateAggregateFirstContentfulPaint(
aggregate, frame);
UmaHistogramCustomTimes(
"PageLoad.PaintTiming.NavigationToFirstContentfulPaint",
aggregate_first_contentful_paint, kTimeRangePaintHistogramMin,
kTimeRangePaintHistogramMax, kTimeRangePaintHistogramBucketCount);
} else if (aggregate == std::numeric_limits<double>::max()) {
tab_helper->SetAggregateAbsoluteFirstContentfulPaint(
web_performance_metrics::CalculateAbsoluteFirstContentfulPaint(
frame_navigation_start_time, relative_first_contentful_paint));
}
}
void WebPerformanceMetricsJavaScriptFeature::LogRelativeFirstInputDelay(
double value,
bool is_main_frame,
bool loaded_from_cache) {
base::TimeDelta delta = base::Milliseconds(value);
if (is_main_frame) {
if (!loaded_from_cache) {
UmaHistogramCustomTimes("IOS.Frame.FirstInputDelay.MainFrame2", delta,
kTimeRangeInputDelayHistogramMin,
kTimeRangeInputDelayHistogramMax,
kTimeRangeInputDelayHistogramBucketCount);
} else if (loaded_from_cache) {
UmaHistogramCustomTimes(
"IOS.Frame.FirstInputDelay.MainFrame.AfterBackForwardCacheRestore2",
delta, kTimeRangeInputDelayHistogramMin,
kTimeRangeInputDelayHistogramMax,
kTimeRangeInputDelayHistogramBucketCount);
}
} else {
if (!loaded_from_cache) {
UmaHistogramCustomTimes("IOS.Frame.FirstInputDelay.SubFrame2", delta,
kTimeRangeInputDelayHistogramMin,
kTimeRangeInputDelayHistogramMax,
kTimeRangeInputDelayHistogramBucketCount);
} else if (loaded_from_cache) {
UmaHistogramCustomTimes(
"IOS.Frame.FirstInputDelay.SubFrame.AfterBackForwardCacheRestore2",
delta, kTimeRangeInputDelayHistogramMin,
kTimeRangeInputDelayHistogramMax,
kTimeRangeInputDelayHistogramBucketCount);
}
}
}
void WebPerformanceMetricsJavaScriptFeature::LogAggregateFirstInputDelay(
web::WebState* web_state,
double first_input_delay,
bool loaded_from_cache) {
WebPerformanceMetricsTabHelper* tab_helper =
WebPerformanceMetricsTabHelper::FromWebState(web_state);
if (!tab_helper || tab_helper->HasBeenHiddenSinceNavigationStarted()) {
return;
}
bool first_input_delay_has_been_logged =
tab_helper->GetFirstInputDelayLoggingStatus();
if (!first_input_delay_has_been_logged) {
base::TimeDelta delta = base::Milliseconds(first_input_delay);
if (loaded_from_cache) {
// This is an input metric for WebVitals.FirstInputDelay{2, 3} so should
// not be deleted while those metrics still exist.
UmaHistogramCustomTimes("PageLoad.InteractiveTiming.FirstInputDelay."
"AfterBackForwardCacheRestore",
delta, base::Milliseconds(10), base::Minutes(10),
100);
// This is a version of the above metric that uses the same bucketing as
// non-iOS platforms.
UmaHistogramCustomTimes("PageLoad.InteractiveTiming.FirstInputDelay."
"AfterBackForwardCacheRestore_iOSFixed",
delta, kTimeRangeInputDelayHistogramMin,
kTimeRangeInputDelayHistogramMax,
kTimeRangeInputDelayHistogramBucketCount);
} else {
// This is an input metric for WebVitals.FirstInputDelay{2, 3} so should
// not be deleted while those metrics still exist.
UmaHistogramCustomTimes("PageLoad.InteractiveTiming.FirstInputDelay4",
delta, base::Milliseconds(10), base::Minutes(10),
100);
// This is a version of the above metric that uses the same bucketing as
// non-iOS platforms.
UmaHistogramCustomTimes("PageLoad.InteractiveTiming."
"FirstInputDelay4_iOSFixed",
delta, kTimeRangeInputDelayHistogramMin,
kTimeRangeInputDelayHistogramMax,
kTimeRangeInputDelayHistogramBucketCount);
}
tab_helper->SetFirstInputDelayLoggingStatus(true);
}
}