blob: bbd3bec8f440a323d7b277ef8a6a262ca6ccc41c [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/loader/interactive_detector.h"
#include "third_party/blink/public/platform/web_input_event.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/platform/histogram.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/wtf/time.h"
namespace blink {
// Required length of main thread and network quiet window for determining
// Time to Interactive.
constexpr auto kTimeToInteractiveWindow = TimeDelta::FromSeconds(5);
// Network is considered "quiet" if there are no more than 2 active network
// requests for this duration of time.
constexpr int kNetworkQuietMaximumConnections = 2;
const char kHistogramInputDelay[] = "PageLoad.InteractiveTiming.InputDelay";
const char kHistogramInputTimestamp[] =
"PageLoad.InteractiveTiming.InputTimestamp";
// static
const char InteractiveDetector::kSupplementName[] = "InteractiveDetector";
InteractiveDetector* InteractiveDetector::From(Document& document) {
InteractiveDetector* detector =
Supplement<Document>::From<InteractiveDetector>(document);
if (!detector) {
detector = MakeGarbageCollected<InteractiveDetector>(
document, new NetworkActivityChecker(&document));
Supplement<Document>::ProvideTo(document, detector);
}
return detector;
}
const char* InteractiveDetector::SupplementName() {
return "InteractiveDetector";
}
InteractiveDetector::InteractiveDetector(
Document& document,
NetworkActivityChecker* network_activity_checker)
: Supplement<Document>(document),
ContextLifecycleObserver(&document),
network_activity_checker_(network_activity_checker),
time_to_interactive_timer_(
document.GetTaskRunner(TaskType::kInternalDefault),
this,
&InteractiveDetector::TimeToInteractiveTimerFired),
initially_hidden_(document.hidden()) {}
void InteractiveDetector::SetNavigationStartTime(
TimeTicks navigation_start_time) {
// Should not set nav start twice.
DCHECK(page_event_times_.nav_start.is_null());
// Don't record TTI for OOPIFs (yet).
// TODO(crbug.com/808086): enable this case.
if (!GetSupplementable()->IsInMainFrame())
return;
LongTaskDetector::Instance().RegisterObserver(this);
page_event_times_.nav_start = navigation_start_time;
TimeTicks initial_timer_fire_time =
navigation_start_time + kTimeToInteractiveWindow;
active_main_thread_quiet_window_start_ = navigation_start_time;
active_network_quiet_window_start_ = navigation_start_time;
StartOrPostponeCITimer(initial_timer_fire_time);
}
int InteractiveDetector::NetworkActivityChecker::GetActiveConnections() {
DCHECK(document_);
ResourceFetcher* fetcher = document_->Fetcher();
return fetcher->BlockingRequestCount() + fetcher->NonblockingRequestCount();
}
int InteractiveDetector::ActiveConnections() {
return network_activity_checker_->GetActiveConnections();
}
void InteractiveDetector::StartOrPostponeCITimer(TimeTicks timer_fire_time) {
// This function should never be called after Time To Interactive is
// reached.
DCHECK(interactive_time_.is_null());
// We give 1ms extra padding to the timer fire time to prevent floating point
// arithmetic pitfalls when comparing window sizes.
timer_fire_time += TimeDelta::FromMilliseconds(1);
// Return if there is an active timer scheduled to fire later than
// |timer_fire_time|.
if (timer_fire_time < time_to_interactive_timer_fire_time_)
return;
TimeDelta delay = timer_fire_time - CurrentTimeTicks();
time_to_interactive_timer_fire_time_ = timer_fire_time;
if (delay <= TimeDelta()) {
// This argument of this function is never used and only there to fulfill
// the API contract. nullptr should work fine.
TimeToInteractiveTimerFired(nullptr);
} else {
time_to_interactive_timer_.StartOneShot(delay, FROM_HERE);
}
}
TimeTicks InteractiveDetector::GetInteractiveTime() const {
// TODO(crbug.com/808685) Simplify FMP and TTI input invalidation.
return page_event_times_.first_meaningful_paint_invalidated
? TimeTicks()
: interactive_time_;
}
TimeTicks InteractiveDetector::GetInteractiveDetectionTime() const {
// TODO(crbug.com/808685) Simplify FMP and TTI input invalidation.
return page_event_times_.first_meaningful_paint_invalidated
? TimeTicks()
: interactive_detection_time_;
}
TimeTicks InteractiveDetector::GetFirstInvalidatingInputTime() const {
return page_event_times_.first_invalidating_input;
}
TimeDelta InteractiveDetector::GetFirstInputDelay() const {
return page_event_times_.first_input_delay;
}
TimeTicks InteractiveDetector::GetFirstInputTimestamp() const {
return page_event_times_.first_input_timestamp;
}
TimeDelta InteractiveDetector::GetLongestInputDelay() const {
return page_event_times_.longest_input_delay;
}
TimeTicks InteractiveDetector::GetLongestInputTimestamp() const {
return page_event_times_.longest_input_timestamp;
}
bool InteractiveDetector::PageWasBackgroundedSinceEvent(TimeTicks event_time) {
DCHECK(GetSupplementable());
if (GetSupplementable()->hidden()) {
return true;
}
bool curr_hidden = initially_hidden_;
TimeTicks visibility_start = page_event_times_.nav_start;
for (auto change_event : visibility_change_events_) {
TimeTicks visibility_end = change_event.timestamp;
if (curr_hidden && event_time < visibility_end) {
// [event_time, now] intersects a backgrounded range.
return true;
}
curr_hidden = change_event.was_hidden;
visibility_start = visibility_end;
}
return false;
} // namespace blink
// This is called early enough in the pipeline that we don't need to worry about
// javascript dispatching untrusted input events.
void InteractiveDetector::HandleForInputDelay(const WebInputEvent& event) {
DCHECK(event.GetType() != WebInputEvent::kTouchStart);
// This only happens sometimes on tests unrelated to InteractiveDetector. It
// is safe to ignore events that are not properly initialized.
if (event.TimeStamp().is_null())
return;
// We can't report a pointerDown until the pointerUp, in case it turns into a
// scroll.
if (event.GetType() == WebInputEvent::kPointerDown) {
pending_pointerdown_delay_ = CurrentTimeTicks() - event.TimeStamp();
pending_pointerdown_timestamp_ = event.TimeStamp();
return;
}
bool event_is_meaningful =
event.GetType() == WebInputEvent::kMouseDown ||
event.GetType() == WebInputEvent::kKeyDown ||
event.GetType() == WebInputEvent::kRawKeyDown ||
// We need to explicitly include tap, as if there are no listeners, we
// won't receive the pointer events.
event.GetType() == WebInputEvent::kGestureTap ||
event.GetType() == WebInputEvent::kPointerUp;
if (!event_is_meaningful)
return;
TimeDelta delay;
TimeTicks event_timestamp;
if (event.GetType() == WebInputEvent::kPointerUp) {
// PointerUp by itself is not considered a significant input.
if (pending_pointerdown_timestamp_.is_null())
return;
// It is possible that this pointer up doesn't match with the pointer down
// whose delay is stored in pending_pointerdown_delay_. In this case, the
// user gesture started by this event contained some non-scroll input, so we
// consider it reasonable to use the delay of the initial event.
delay = pending_pointerdown_delay_;
event_timestamp = pending_pointerdown_timestamp_;
} else {
delay = CurrentTimeTicks() - event.TimeStamp();
event_timestamp = event.TimeStamp();
}
pending_pointerdown_delay_ = base::TimeDelta();
pending_pointerdown_timestamp_ = base::TimeTicks();
bool input_delay_metrics_changed = false;
if (page_event_times_.first_input_delay.is_zero()) {
page_event_times_.first_input_delay = delay;
page_event_times_.first_input_timestamp = event_timestamp;
input_delay_metrics_changed = true;
}
UMA_HISTOGRAM_CUSTOM_TIMES(kHistogramInputDelay, delay,
base::TimeDelta::FromMilliseconds(1),
base::TimeDelta::FromSeconds(60), 50);
UMA_HISTOGRAM_CUSTOM_TIMES(kHistogramInputTimestamp,
event_timestamp - page_event_times_.nav_start,
base::TimeDelta::FromMilliseconds(10),
base::TimeDelta::FromMinutes(10), 100);
// Only update longest input delay if page was not backgrounded while the
// input was queued.
if (delay > page_event_times_.longest_input_delay &&
!PageWasBackgroundedSinceEvent(event_timestamp)) {
page_event_times_.longest_input_delay = delay;
page_event_times_.longest_input_timestamp = event_timestamp;
input_delay_metrics_changed = true;
}
if (GetSupplementable()->Loader() && input_delay_metrics_changed)
GetSupplementable()->Loader()->DidChangePerformanceTiming();
}
void InteractiveDetector::BeginNetworkQuietPeriod(TimeTicks current_time) {
// Value of 0.0 indicates there is no currently actively network quiet window.
DCHECK(active_network_quiet_window_start_.is_null());
active_network_quiet_window_start_ = current_time;
StartOrPostponeCITimer(current_time + kTimeToInteractiveWindow);
}
void InteractiveDetector::EndNetworkQuietPeriod(TimeTicks current_time) {
DCHECK(!active_network_quiet_window_start_.is_null());
if (current_time - active_network_quiet_window_start_ >=
kTimeToInteractiveWindow) {
network_quiet_windows_.emplace_back(active_network_quiet_window_start_,
current_time);
}
active_network_quiet_window_start_ = TimeTicks();
}
// The optional opt_current_time, if provided, saves us a call to
// CurrentTimeTicksInSeconds.
void InteractiveDetector::UpdateNetworkQuietState(
double request_count,
base::Optional<TimeTicks> opt_current_time) {
if (request_count <= kNetworkQuietMaximumConnections &&
active_network_quiet_window_start_.is_null()) {
// Not using `value_or(CurrentTimeTicksInSeconds())` here because
// arguments to functions are eagerly evaluated, which always call
// CurrentTimeTicksInSeconds.
TimeTicks current_time =
opt_current_time ? opt_current_time.value() : CurrentTimeTicks();
BeginNetworkQuietPeriod(current_time);
} else if (request_count > kNetworkQuietMaximumConnections &&
!active_network_quiet_window_start_.is_null()) {
TimeTicks current_time =
opt_current_time ? opt_current_time.value() : CurrentTimeTicks();
EndNetworkQuietPeriod(current_time);
}
}
void InteractiveDetector::OnResourceLoadBegin(
base::Optional<TimeTicks> load_begin_time) {
if (!GetSupplementable())
return;
if (!interactive_time_.is_null())
return;
// The request that is about to begin is not counted in ActiveConnections(),
// so we add one to it.
UpdateNetworkQuietState(ActiveConnections() + 1, load_begin_time);
}
// The optional load_finish_time, if provided, saves us a call to
// CurrentTimeTicksInSeconds.
void InteractiveDetector::OnResourceLoadEnd(
base::Optional<TimeTicks> load_finish_time) {
if (!GetSupplementable())
return;
if (!interactive_time_.is_null())
return;
UpdateNetworkQuietState(ActiveConnections(), load_finish_time);
}
void InteractiveDetector::OnLongTaskDetected(TimeTicks start_time,
TimeTicks end_time) {
// We should not be receiving long task notifications after Time to
// Interactive has already been reached.
DCHECK(interactive_time_.is_null());
TimeDelta quiet_window_length =
start_time - active_main_thread_quiet_window_start_;
if (quiet_window_length >= kTimeToInteractiveWindow) {
main_thread_quiet_windows_.emplace_back(
active_main_thread_quiet_window_start_, start_time);
}
active_main_thread_quiet_window_start_ = end_time;
StartOrPostponeCITimer(end_time + kTimeToInteractiveWindow);
}
void InteractiveDetector::OnFirstMeaningfulPaintDetected(
TimeTicks fmp_time,
FirstMeaningfulPaintDetector::HadUserInput user_input_before_fmp) {
DCHECK(page_event_times_.first_meaningful_paint
.is_null()); // Should not set FMP twice.
page_event_times_.first_meaningful_paint = fmp_time;
page_event_times_.first_meaningful_paint_invalidated =
user_input_before_fmp == FirstMeaningfulPaintDetector::kHadUserInput;
if (CurrentTimeTicks() - fmp_time >= kTimeToInteractiveWindow) {
// We may have reached TTCI already. Check right away.
CheckTimeToInteractiveReached();
} else {
StartOrPostponeCITimer(page_event_times_.first_meaningful_paint +
kTimeToInteractiveWindow);
}
}
void InteractiveDetector::OnDomContentLoadedEnd(TimeTicks dcl_end_time) {
// InteractiveDetector should only receive the first DCL event.
DCHECK(page_event_times_.dom_content_loaded_end.is_null());
page_event_times_.dom_content_loaded_end = dcl_end_time;
CheckTimeToInteractiveReached();
}
void InteractiveDetector::OnInvalidatingInputEvent(
TimeTicks invalidation_time) {
if (!page_event_times_.first_invalidating_input.is_null())
return;
// In some edge cases (e.g. inaccurate input timestamp provided through remote
// debugging protocol) we might receive an input timestamp that is earlier
// than navigation start. Since invalidating input timestamp before navigation
// start in non-sensical, we clamp it at navigation start.
page_event_times_.first_invalidating_input =
std::max(invalidation_time, page_event_times_.nav_start);
if (GetSupplementable()->Loader())
GetSupplementable()->Loader()->DidChangePerformanceTiming();
}
void InteractiveDetector::OnPageHiddenChanged(bool is_hidden) {
visibility_change_events_.push_back({CurrentTimeTicks(), is_hidden});
}
void InteractiveDetector::TimeToInteractiveTimerFired(TimerBase*) {
if (!GetSupplementable() || !interactive_time_.is_null())
return;
// Value of 0.0 indicates there is currently no active timer.
time_to_interactive_timer_fire_time_ = TimeTicks();
CheckTimeToInteractiveReached();
}
void InteractiveDetector::AddCurrentlyActiveQuietIntervals(
TimeTicks current_time) {
// Network is currently quiet.
if (!active_network_quiet_window_start_.is_null()) {
if (current_time - active_network_quiet_window_start_ >=
kTimeToInteractiveWindow) {
network_quiet_windows_.emplace_back(active_network_quiet_window_start_,
current_time);
}
}
// Since this code executes on the main thread, we know that no task is
// currently running on the main thread. We can therefore skip checking.
// main_thread_quiet_window_being != 0.0.
if (current_time - active_main_thread_quiet_window_start_ >=
kTimeToInteractiveWindow) {
main_thread_quiet_windows_.emplace_back(
active_main_thread_quiet_window_start_, current_time);
}
}
void InteractiveDetector::RemoveCurrentlyActiveQuietIntervals() {
if (!network_quiet_windows_.empty() &&
network_quiet_windows_.back().Low() ==
active_network_quiet_window_start_) {
network_quiet_windows_.pop_back();
}
if (!main_thread_quiet_windows_.empty() &&
main_thread_quiet_windows_.back().Low() ==
active_main_thread_quiet_window_start_) {
main_thread_quiet_windows_.pop_back();
}
}
TimeTicks InteractiveDetector::FindInteractiveCandidate(TimeTicks lower_bound) {
// Main thread iterator.
auto it_mt = main_thread_quiet_windows_.begin();
// Network iterator.
auto it_net = network_quiet_windows_.begin();
while (it_mt < main_thread_quiet_windows_.end() &&
it_net < network_quiet_windows_.end()) {
if (it_mt->High() <= lower_bound) {
it_mt++;
continue;
}
if (it_net->High() <= lower_bound) {
it_net++;
continue;
}
// First handling the no overlap cases.
// [ main thread interval ]
// [ network interval ]
if (it_mt->High() <= it_net->Low()) {
it_mt++;
continue;
}
// [ main thread interval ]
// [ network interval ]
if (it_net->High() <= it_mt->Low()) {
it_net++;
continue;
}
// At this point we know we have a non-empty overlap after lower_bound.
TimeTicks overlap_start =
std::max({it_mt->Low(), it_net->Low(), lower_bound});
TimeTicks overlap_end = std::min(it_mt->High(), it_net->High());
TimeDelta overlap_duration = overlap_end - overlap_start;
if (overlap_duration >= kTimeToInteractiveWindow) {
return std::max(lower_bound, it_mt->Low());
}
// The interval with earlier end time will not produce any more overlap, so
// we move on from it.
if (it_mt->High() <= it_net->High()) {
it_mt++;
} else {
it_net++;
}
}
// Time To Interactive candidate not found.
return TimeTicks();
}
void InteractiveDetector::CheckTimeToInteractiveReached() {
// Already detected Time to Interactive.
if (!interactive_time_.is_null())
return;
// FMP and DCL have not been detected yet.
if (page_event_times_.first_meaningful_paint.is_null() ||
page_event_times_.dom_content_loaded_end.is_null())
return;
const TimeTicks current_time = CurrentTimeTicks();
if (current_time - page_event_times_.first_meaningful_paint <
kTimeToInteractiveWindow) {
// Too close to FMP to determine Time to Interactive.
return;
}
AddCurrentlyActiveQuietIntervals(current_time);
const TimeTicks interactive_candidate =
FindInteractiveCandidate(page_event_times_.first_meaningful_paint);
RemoveCurrentlyActiveQuietIntervals();
// No Interactive Candidate found.
if (interactive_candidate.is_null())
return;
interactive_time_ = std::max(
{interactive_candidate, page_event_times_.dom_content_loaded_end});
interactive_detection_time_ = CurrentTimeTicks();
OnTimeToInteractiveDetected();
}
void InteractiveDetector::OnTimeToInteractiveDetected() {
LongTaskDetector::Instance().UnregisterObserver(this);
main_thread_quiet_windows_.clear();
network_quiet_windows_.clear();
bool had_user_input_before_interactive =
!page_event_times_.first_invalidating_input.is_null() &&
page_event_times_.first_invalidating_input < interactive_time_;
// We log the trace event even if there is user input, but annotate the event
// with whether that happened.
TRACE_EVENT_MARK_WITH_TIMESTAMP2(
"loading,rail", "InteractiveTime", interactive_time_, "frame",
ToTraceValue(GetSupplementable()->GetFrame()),
"had_user_input_before_interactive", had_user_input_before_interactive);
// We only send TTI to Performance Timing Observers if FMP was not invalidated
// by input.
// TODO(crbug.com/808685) Simplify FMP and TTI input invalidation.
if (!page_event_times_.first_meaningful_paint_invalidated) {
if (GetSupplementable()->Loader())
GetSupplementable()->Loader()->DidChangePerformanceTiming();
}
}
void InteractiveDetector::ContextDestroyed(ExecutionContext*) {
LongTaskDetector::Instance().UnregisterObserver(this);
}
void InteractiveDetector::Trace(Visitor* visitor) {
Supplement<Document>::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
}
} // namespace blink