| // 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 "base/metrics/histogram_macros.h" |
| #include "base/profiler/sample_metadata.h" |
| #include "base/time/default_tick_clock.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/events/event.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/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Used to generate a unique id when emitting the "Long Input Delay" trace |
| // event and metadata. |
| int g_num_long_input_events = 0; |
| |
| // The threshold to emit the "Long Input Delay" trace event is the 99th |
| // percentile of the histogram on Windows Stable as of Feb 25, 2020. |
| constexpr base::TimeDelta kInputDelayTraceEventThreshold = |
| base::TimeDelta::FromMilliseconds(250); |
| |
| // The threshold to emit the "Long First Input Delay" trace event is the 99th |
| // percentile of the histogram on Windows Stable as of Feb 27, 2020. |
| constexpr base::TimeDelta kFirstInputDelayTraceEventThreshold = |
| base::TimeDelta::FromMilliseconds(575); |
| |
| } // namespace |
| |
| // Required length of main thread and network quiet window for determining |
| // Time to Interactive. |
| constexpr auto kTimeToInteractiveWindow = base::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.InputDelay3"; |
| const char kHistogramInputTimestamp[] = |
| "PageLoad.InteractiveTiming.InputTimestamp3"; |
| |
| // 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), |
| ExecutionContextLifecycleObserver(document.GetExecutionContext()), |
| clock_(base::DefaultTickClock::GetInstance()), |
| network_activity_checker_(network_activity_checker), |
| time_to_interactive_timer_( |
| document.GetTaskRunner(TaskType::kInternalDefault), |
| this, |
| &InteractiveDetector::TimeToInteractiveTimerFired), |
| initially_hidden_(document.hidden()), |
| ukm_recorder_(document.UkmRecorder()) {} |
| |
| void InteractiveDetector::SetNavigationStartTime( |
| base::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; |
| base::TimeTicks initial_timer_fire_time = |
| navigation_start_time + kTimeToInteractiveWindow; |
| |
| 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( |
| base::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 += base::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; |
| |
| base::TimeDelta delay = timer_fire_time - clock_->NowTicks(); |
| time_to_interactive_timer_fire_time_ = timer_fire_time; |
| |
| if (delay <= base::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); |
| } |
| } |
| |
| base::Optional<base::TimeDelta> InteractiveDetector::GetFirstInputDelay() |
| const { |
| return page_event_times_.first_input_delay; |
| } |
| |
| WTF::Vector<base::Optional<base::TimeDelta>> |
| InteractiveDetector::GetFirstInputDelaysAfterBackForwardCacheRestore() const { |
| return page_event_times_.first_input_delays_after_back_forward_cache_restore; |
| } |
| |
| base::Optional<base::TimeTicks> InteractiveDetector::GetFirstInputTimestamp() |
| const { |
| return page_event_times_.first_input_timestamp; |
| } |
| |
| base::Optional<base::TimeDelta> InteractiveDetector::GetLongestInputDelay() |
| const { |
| return page_event_times_.longest_input_delay; |
| } |
| |
| base::Optional<base::TimeTicks> InteractiveDetector::GetLongestInputTimestamp() |
| const { |
| return page_event_times_.longest_input_timestamp; |
| } |
| |
| base::Optional<base::TimeDelta> |
| InteractiveDetector::GetFirstInputProcessingTime() const { |
| return page_event_times_.first_input_processing_time; |
| } |
| |
| base::Optional<base::TimeTicks> InteractiveDetector::GetFirstScrollTimestamp() |
| const { |
| return page_event_times_.first_scroll_timestamp; |
| } |
| |
| base::Optional<base::TimeDelta> InteractiveDetector::GetFirstScrollDelay() |
| const { |
| return page_event_times_.frist_scroll_delay; |
| } |
| |
| bool InteractiveDetector::PageWasBackgroundedSinceEvent( |
| base::TimeTicks event_time) { |
| DCHECK(GetSupplementable()); |
| if (GetSupplementable()->hidden()) { |
| return true; |
| } |
| |
| bool curr_hidden = initially_hidden_; |
| base::TimeTicks visibility_start = page_event_times_.nav_start; |
| for (auto change_event : visibility_change_events_) { |
| base::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; |
| } |
| |
| void InteractiveDetector::HandleForInputDelay( |
| const Event& event, |
| base::TimeTicks event_platform_timestamp, |
| base::TimeTicks processing_start) { |
| DCHECK(event.isTrusted()); |
| |
| // This only happens sometimes on tests unrelated to InteractiveDetector. It |
| // is safe to ignore events that are not properly initialized. |
| if (event_platform_timestamp.is_null()) |
| return; |
| |
| // We can't report a pointerDown until the pointerUp, in case it turns into a |
| // scroll. |
| if (event.type() == event_type_names::kPointerdown) { |
| pending_pointerdown_delay_ = processing_start - event_platform_timestamp; |
| pending_pointerdown_timestamp_ = event_platform_timestamp; |
| return; |
| } |
| |
| // We receive any event relevant for EventTiming, but we only care about |
| // events relevant for FirstInputDelay. |
| bool event_is_meaningful = event.type() == event_type_names::kPointerup || |
| event.type() == event_type_names::kClick || |
| event.type() == event_type_names::kKeydown || |
| event.type() == event_type_names::kMousedown; |
| |
| if (!event_is_meaningful) |
| return; |
| |
| // These variables track the values which will be reported to histograms. |
| base::TimeDelta delay; |
| base::TimeTicks event_timestamp; |
| if (event.type() == event_type_names::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 = processing_start - event_platform_timestamp; |
| event_timestamp = event_platform_timestamp; |
| } |
| pending_pointerdown_delay_ = base::TimeDelta(); |
| pending_pointerdown_timestamp_ = base::TimeTicks(); |
| bool interactive_timing_metrics_changed = false; |
| |
| if (!page_event_times_.first_input_delay.has_value()) { |
| page_event_times_.first_input_delay = delay; |
| page_event_times_.first_input_timestamp = event_timestamp; |
| interactive_timing_metrics_changed = true; |
| |
| if (delay > kFirstInputDelayTraceEventThreshold) { |
| // Emit a trace event to highlight long first input delays. |
| TRACE_EVENT_ASYNC_BEGIN_WITH_TIMESTAMP0( |
| "latency", "Long First Input Delay", |
| TRACE_ID_LOCAL(g_num_long_input_events), event_timestamp); |
| TRACE_EVENT_ASYNC_END_WITH_TIMESTAMP0( |
| "latency", "Long First Input Delay", |
| TRACE_ID_LOCAL(g_num_long_input_events), event_timestamp + delay); |
| g_num_long_input_events++; |
| } |
| } else if (delay > kInputDelayTraceEventThreshold) { |
| // Emit a trace event to highlight long input delays from second input and |
| // onwards. |
| TRACE_EVENT_ASYNC_BEGIN_WITH_TIMESTAMP0( |
| "latency", "Long Input Delay", TRACE_ID_LOCAL(g_num_long_input_events), |
| event_timestamp); |
| TRACE_EVENT_ASYNC_END_WITH_TIMESTAMP0( |
| "latency", "Long Input Delay", TRACE_ID_LOCAL(g_num_long_input_events), |
| event_timestamp + delay); |
| // Apply metadata on stack samples. |
| base::ApplyMetadataToPastSamples( |
| event_timestamp, event_timestamp + delay, |
| "PageLoad.InteractiveTiming.LongInputDelay", g_num_long_input_events, |
| 1); |
| g_num_long_input_events++; |
| } |
| |
| // ELements in |first_input_delays_after_back_forward_cache_restore| is |
| // allocated when the page is restored from the back-forward cache. If the |
| // last element exists and this is nullopt value, the first input has not come |
| // yet after the last time when the page is restored from the cache. |
| if (!page_event_times_.first_input_delays_after_back_forward_cache_restore |
| .IsEmpty() && |
| !page_event_times_.first_input_delays_after_back_forward_cache_restore |
| .back() |
| .has_value()) { |
| page_event_times_.first_input_delays_after_back_forward_cache_restore |
| .back() = delay; |
| } |
| |
| if (GetSupplementable()->Loader()) { |
| GetSupplementable()->Loader()->DidObserveInputDelay(delay); |
| } |
| |
| 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 ((!page_event_times_.longest_input_delay.has_value() || |
| 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; |
| interactive_timing_metrics_changed = true; |
| } |
| |
| if (GetSupplementable()->Loader() && interactive_timing_metrics_changed) { |
| GetSupplementable()->Loader()->DidChangePerformanceTiming(); |
| } |
| } |
| |
| void InteractiveDetector::BeginNetworkQuietPeriod( |
| base::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(base::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_ = base::TimeTicks(); |
| } |
| |
| // The optional opt_current_time, if provided, saves us a call to |
| // clock_->NowTicks(). |
| void InteractiveDetector::UpdateNetworkQuietState( |
| double request_count, |
| base::Optional<base::TimeTicks> opt_current_time) { |
| if (request_count <= kNetworkQuietMaximumConnections && |
| active_network_quiet_window_start_.is_null()) { |
| // Not using `value_or(clock_->NowTicks())` here because arguments to |
| // functions are eagerly evaluated, which always call clock_->NowTicks. |
| base::TimeTicks current_time = |
| opt_current_time ? opt_current_time.value() : clock_->NowTicks(); |
| BeginNetworkQuietPeriod(current_time); |
| } else if (request_count > kNetworkQuietMaximumConnections && |
| !active_network_quiet_window_start_.is_null()) { |
| base::TimeTicks current_time = |
| opt_current_time ? opt_current_time.value() : clock_->NowTicks(); |
| EndNetworkQuietPeriod(current_time); |
| } |
| } |
| |
| void InteractiveDetector::OnResourceLoadBegin( |
| base::Optional<base::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 |
| // clock_->NowTicks. |
| void InteractiveDetector::OnResourceLoadEnd( |
| base::Optional<base::TimeTicks> load_finish_time) { |
| if (!GetSupplementable()) |
| return; |
| if (!interactive_time_.is_null()) |
| return; |
| UpdateNetworkQuietState(ActiveConnections(), load_finish_time); |
| } |
| |
| void InteractiveDetector::OnLongTaskDetected(base::TimeTicks start_time, |
| base::TimeTicks end_time) { |
| // We should not be receiving long task notifications after Time to |
| // Interactive has already been reached. |
| DCHECK(interactive_time_.is_null()); |
| long_tasks_.emplace_back(start_time, end_time); |
| StartOrPostponeCITimer(end_time + kTimeToInteractiveWindow); |
| } |
| |
| void InteractiveDetector::OnFirstContentfulPaint( |
| base::TimeTicks first_contentful_paint) { |
| // Should not set FCP twice. |
| DCHECK(page_event_times_.first_contentful_paint.is_null()); |
| page_event_times_.first_contentful_paint = first_contentful_paint; |
| if (clock_->NowTicks() - first_contentful_paint >= kTimeToInteractiveWindow) { |
| // We may have reached TTI already. Check right away. |
| CheckTimeToInteractiveReached(); |
| } else { |
| StartOrPostponeCITimer(page_event_times_.first_contentful_paint + |
| kTimeToInteractiveWindow); |
| } |
| } |
| |
| void InteractiveDetector::OnDomContentLoadedEnd(base::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( |
| base::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( |
| VisibilityChangeEvent{clock_->NowTicks(), 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_ = base::TimeTicks(); |
| CheckTimeToInteractiveReached(); |
| } |
| |
| void InteractiveDetector::AddCurrentlyActiveNetworkQuietInterval( |
| base::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); |
| } |
| } |
| } |
| |
| void InteractiveDetector::RemoveCurrentlyActiveNetworkQuietInterval() { |
| if (!network_quiet_windows_.IsEmpty() && |
| network_quiet_windows_.back().Low() == |
| active_network_quiet_window_start_) { |
| network_quiet_windows_.pop_back(); |
| } |
| } |
| |
| base::TimeTicks InteractiveDetector::FindInteractiveCandidate( |
| base::TimeTicks lower_bound, |
| base::TimeTicks current_time) { |
| // Network iterator. |
| auto* it_net = network_quiet_windows_.begin(); |
| // Long tasks iterator. |
| auto* it_lt = long_tasks_.begin(); |
| |
| base::TimeTicks main_quiet_start = page_event_times_.nav_start; |
| |
| while (main_quiet_start < current_time && |
| it_net < network_quiet_windows_.end()) { |
| base::TimeTicks main_quiet_end = |
| it_lt == long_tasks_.end() ? current_time : it_lt->Low(); |
| base::TimeTicks next_main_quiet_start = |
| it_lt == long_tasks_.end() ? current_time : it_lt->High(); |
| if (main_quiet_end - main_quiet_start < kTimeToInteractiveWindow) { |
| // The main thread quiet window is too short. |
| ++it_lt; |
| main_quiet_start = next_main_quiet_start; |
| continue; |
| } |
| if (main_quiet_end <= lower_bound) { |
| // The main thread quiet window is before |lower_bound|. |
| ++it_lt; |
| main_quiet_start = next_main_quiet_start; |
| continue; |
| } |
| if (it_net->High() <= lower_bound) { |
| // The network quiet window is before |lower_bound|. |
| ++it_net; |
| continue; |
| } |
| |
| // First handling the no overlap cases. |
| // [ main thread interval ] |
| // [ network interval ] |
| if (main_quiet_end <= it_net->Low()) { |
| ++it_lt; |
| main_quiet_start = next_main_quiet_start; |
| continue; |
| } |
| // [ main thread interval ] |
| // [ network interval ] |
| if (it_net->High() <= main_quiet_start) { |
| ++it_net; |
| continue; |
| } |
| |
| // At this point we know we have a non-empty overlap after lower_bound. |
| base::TimeTicks overlap_start = |
| std::max({main_quiet_start, it_net->Low(), lower_bound}); |
| base::TimeTicks overlap_end = std::min(main_quiet_end, it_net->High()); |
| base::TimeDelta overlap_duration = overlap_end - overlap_start; |
| if (overlap_duration >= kTimeToInteractiveWindow) { |
| return std::max(lower_bound, main_quiet_start); |
| } |
| |
| // The interval with earlier end time will not produce any more overlap, so |
| // we move on from it. |
| if (main_quiet_end <= it_net->High()) { |
| ++it_lt; |
| main_quiet_start = next_main_quiet_start; |
| } else { |
| ++it_net; |
| } |
| } |
| |
| // Time To Interactive candidate not found. |
| return base::TimeTicks(); |
| } |
| |
| void InteractiveDetector::CheckTimeToInteractiveReached() { |
| // Already detected Time to Interactive. |
| if (!interactive_time_.is_null()) |
| return; |
| |
| // FCP and DCL have not been detected yet. |
| if (page_event_times_.first_contentful_paint.is_null() || |
| page_event_times_.dom_content_loaded_end.is_null()) |
| return; |
| |
| const base::TimeTicks current_time = clock_->NowTicks(); |
| if (current_time - page_event_times_.first_contentful_paint < |
| kTimeToInteractiveWindow) { |
| // Too close to FCP to determine Time to Interactive. |
| return; |
| } |
| |
| AddCurrentlyActiveNetworkQuietInterval(current_time); |
| const base::TimeTicks interactive_candidate = FindInteractiveCandidate( |
| page_event_times_.first_contentful_paint, current_time); |
| RemoveCurrentlyActiveNetworkQuietInterval(); |
| |
| // 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_ = clock_->NowTicks(); |
| OnTimeToInteractiveDetected(); |
| } |
| |
| void InteractiveDetector::OnTimeToInteractiveDetected() { |
| LongTaskDetector::Instance().UnregisterObserver(this); |
| network_quiet_windows_.clear(); |
| |
| TRACE_EVENT_MARK_WITH_TIMESTAMP2( |
| "loading,rail", "InteractiveTime", interactive_time_, "frame", |
| ToTraceValue(GetSupplementable()->GetFrame()), "args", |
| [&](perfetto::TracedValue context) { |
| // We log the trace event even if there is user input, but annotate the |
| // event with whether that happened. |
| bool had_user_input_before_interactive = |
| !page_event_times_.first_invalidating_input.is_null() && |
| page_event_times_.first_invalidating_input < interactive_time_; |
| |
| auto dict = std::move(context).WriteDictionary(); |
| dict.Add("had_user_input_before_interactive", |
| had_user_input_before_interactive); |
| dict.Add("total_blocking_time_ms", |
| ComputeTotalBlockingTime().InMillisecondsF()); |
| }); |
| |
| long_tasks_.clear(); |
| } |
| |
| base::TimeDelta InteractiveDetector::ComputeTotalBlockingTime() { |
| // We follow the same logic as the lighthouse computation in |
| // https://github.com/GoogleChrome/lighthouse/blob/f150573b5970cc90c8d0c2214f5738df5cde8a31/lighthouse-core/computed/metrics/total-blocking-time.js#L60-L74. |
| // In particular, tasks are clipped [FCP, TTI], and then all positive values |
| // of (task_length - 50) are added to the blocking time. |
| base::TimeDelta total_blocking_time; |
| for (const auto& long_task : long_tasks_) { |
| base::TimeTicks clipped_start = |
| std::max(long_task.Low(), page_event_times_.first_contentful_paint); |
| base::TimeTicks clipped_end = std::min(long_task.High(), interactive_time_); |
| total_blocking_time += |
| std::max(base::TimeDelta(), clipped_end - clipped_start - |
| base::TimeDelta::FromMilliseconds(50)); |
| } |
| return total_blocking_time; |
| } |
| |
| void InteractiveDetector::ContextDestroyed() { |
| LongTaskDetector::Instance().UnregisterObserver(this); |
| } |
| |
| void InteractiveDetector::Trace(Visitor* visitor) const { |
| visitor->Trace(time_to_interactive_timer_); |
| Supplement<Document>::Trace(visitor); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| } |
| |
| void InteractiveDetector::SetTickClockForTesting(const base::TickClock* clock) { |
| clock_ = clock; |
| } |
| |
| void InteractiveDetector::SetTaskRunnerForTesting( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner_for_testing) { |
| time_to_interactive_timer_.MoveToNewTaskRunner(task_runner_for_testing); |
| } |
| |
| ukm::UkmRecorder* InteractiveDetector::GetUkmRecorder() const { |
| return ukm_recorder_; |
| } |
| |
| void InteractiveDetector::SetUkmRecorderForTesting( |
| ukm::UkmRecorder* test_ukm_recorder) { |
| ukm_recorder_ = test_ukm_recorder; |
| } |
| |
| void InteractiveDetector::RecordInputEventTimingUKM( |
| base::TimeDelta input_delay, |
| base::TimeDelta processing_time, |
| base::TimeDelta time_to_next_paint, |
| WTF::AtomicString event_type) { |
| ukm::SourceId source_id = GetSupplementable()->UkmSourceID(); |
| |
| DCHECK_NE(source_id, ukm::kInvalidSourceId); |
| static const WTF::HashMap<WTF::AtomicString, blink::InputEventType>& |
| event_type_to_enum = {{"mousedown", blink::InputEventType::kMousedown}, |
| {"click", blink::InputEventType::kClick}, |
| {"keydown", blink::InputEventType::kKeydown}, |
| {"pointerup", blink::InputEventType::kPointerup}}; |
| ukm::builders::InputEvent(source_id) |
| .SetEventType(static_cast<int>(event_type_to_enum.at(event_type))) |
| .SetInteractiveTiming_InputDelay(input_delay.InMilliseconds()) |
| .SetInteractiveTiming_ProcessingTime(processing_time.InMilliseconds()) |
| .SetInteractiveTiming_ProcessingFinishedToNextPaint( |
| time_to_next_paint.InMilliseconds()) |
| .Record(GetUkmRecorder()); |
| |
| if (!page_event_times_.first_input_processing_time) { |
| page_event_times_.first_input_processing_time = processing_time; |
| if (GetSupplementable()->Loader()) { |
| GetSupplementable()->Loader()->DidChangePerformanceTiming(); |
| } |
| } |
| } |
| |
| void InteractiveDetector::DidObserveFirstScrollDelay( |
| base::TimeDelta first_scroll_delay, |
| base::TimeTicks first_scroll_timestamp) { |
| if (!page_event_times_.frist_scroll_delay.has_value()) { |
| page_event_times_.frist_scroll_delay = first_scroll_delay; |
| page_event_times_.first_scroll_timestamp = first_scroll_timestamp; |
| if (GetSupplementable()->Loader()) { |
| GetSupplementable()->Loader()->DidChangePerformanceTiming(); |
| } |
| } |
| } |
| |
| void InteractiveDetector::OnRestoredFromBackForwardCache() { |
| // Allocate the last element with 0, which indicates that the first input |
| // after this navigation doesn't happen yet. |
| page_event_times_.first_input_delays_after_back_forward_cache_restore |
| .push_back(base::nullopt); |
| } |
| |
| } // namespace blink |