| // 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 "chrome/browser/page_load_metrics/page_load_metrics_update_dispatcher.h" |
| |
| #include <ostream> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/optional.h" |
| #include "chrome/browser/page_load_metrics/browser_page_track_decider.h" |
| #include "chrome/browser/page_load_metrics/page_load_metrics_embedder_interface.h" |
| #include "chrome/browser/page_load_metrics/page_load_metrics_util.h" |
| #include "chrome/browser/page_load_metrics/page_load_tracker.h" |
| #include "chrome/common/page_load_metrics/page_load_metrics_constants.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "extensions/common/constants.h" |
| #include "url/gurl.h" |
| |
| namespace page_load_metrics { |
| |
| namespace internal { |
| |
| const char kPageLoadTimingStatus[] = "PageLoad.Internal.PageLoadTimingStatus"; |
| const char kPageLoadTimingDispatchStatus[] = |
| "PageLoad.Internal.PageLoadTimingStatus.AtTimingCallbackDispatch"; |
| const char kHistogramOutOfOrderTiming[] = |
| "PageLoad.Internal.OutOfOrderInterFrameTiming"; |
| const char kHistogramOutOfOrderTimingBuffered[] = |
| "PageLoad.Internal.OutOfOrderInterFrameTiming.AfterBuffering"; |
| |
| } // namespace internal |
| |
| namespace { |
| |
| // Helper to allow use of Optional<> values in LOG() messages. |
| std::ostream& operator<<(std::ostream& os, |
| const base::Optional<base::TimeDelta>& opt) { |
| if (opt) |
| os << opt.value(); |
| else |
| os << "(unset)"; |
| return os; |
| } |
| |
| // If second is non-zero, first must also be non-zero and less than or equal to |
| // second. |
| bool EventsInOrder(const base::Optional<base::TimeDelta>& first, |
| const base::Optional<base::TimeDelta>& second) { |
| if (!second) { |
| return true; |
| } |
| return first && first <= second; |
| } |
| |
| internal::PageLoadTimingStatus IsValidPageLoadTiming( |
| const mojom::PageLoadTiming& timing) { |
| if (page_load_metrics::IsEmpty(timing)) |
| return internal::INVALID_EMPTY_TIMING; |
| |
| // If we have a non-empty timing, it should always have a navigation start. |
| if (timing.navigation_start.is_null()) { |
| LOG(ERROR) << "Received null navigation_start."; |
| return internal::INVALID_NULL_NAVIGATION_START; |
| } |
| |
| // Verify proper ordering between the various timings. |
| |
| if (!EventsInOrder(timing.response_start, timing.parse_timing->parse_start)) { |
| // We sometimes get a zero response_start with a non-zero parse start. See |
| // crbug.com/590212. |
| LOG(ERROR) << "Invalid response_start " << timing.response_start |
| << " for parse_start " << timing.parse_timing->parse_start; |
| // When browser-side navigation is enabled, we sometimes encounter this |
| // error case. For now, we disable reporting of this error, since most |
| // PageLoadMetricsObservers don't care about response_start and we want to |
| // see how much closer fixing this error will get us to page load metrics |
| // being consistent with and without browser side navigation enabled. See |
| // crbug.com/716587 for more details. |
| // |
| // return internal::INVALID_ORDER_RESPONSE_START_PARSE_START; |
| } |
| |
| if (!EventsInOrder(timing.parse_timing->parse_start, |
| timing.parse_timing->parse_stop)) { |
| LOG(ERROR) << "Invalid parse_start " << timing.parse_timing->parse_start |
| << " for parse_stop " << timing.parse_timing->parse_stop; |
| return internal::INVALID_ORDER_PARSE_START_PARSE_STOP; |
| } |
| |
| if (timing.parse_timing->parse_stop) { |
| const base::TimeDelta parse_duration = |
| timing.parse_timing->parse_stop.value() - |
| timing.parse_timing->parse_start.value(); |
| if (timing.parse_timing->parse_blocked_on_script_load_duration > |
| parse_duration) { |
| LOG(ERROR) << "Invalid parse_blocked_on_script_load_duration " |
| << timing.parse_timing->parse_blocked_on_script_load_duration |
| << " for parse duration " << parse_duration; |
| return internal::INVALID_SCRIPT_LOAD_LONGER_THAN_PARSE; |
| } |
| if (timing.parse_timing->parse_blocked_on_script_execution_duration > |
| parse_duration) { |
| LOG(ERROR) |
| << "Invalid parse_blocked_on_script_execution_duration " |
| << timing.parse_timing->parse_blocked_on_script_execution_duration |
| << " for parse duration " << parse_duration; |
| return internal::INVALID_SCRIPT_EXEC_LONGER_THAN_PARSE; |
| } |
| } |
| |
| if (timing.parse_timing |
| ->parse_blocked_on_script_load_from_document_write_duration > |
| timing.parse_timing->parse_blocked_on_script_load_duration) { |
| LOG(ERROR) |
| << "Invalid parse_blocked_on_script_load_from_document_write_duration " |
| << timing.parse_timing |
| ->parse_blocked_on_script_load_from_document_write_duration |
| << " for parse_blocked_on_script_load_duration " |
| << timing.parse_timing->parse_blocked_on_script_load_duration; |
| return internal::INVALID_SCRIPT_LOAD_DOC_WRITE_LONGER_THAN_SCRIPT_LOAD; |
| } |
| |
| if (timing.parse_timing |
| ->parse_blocked_on_script_execution_from_document_write_duration > |
| timing.parse_timing->parse_blocked_on_script_execution_duration) { |
| LOG(ERROR) |
| << "Invalid " |
| "parse_blocked_on_script_execution_from_document_write_duration " |
| << timing.parse_timing |
| ->parse_blocked_on_script_execution_from_document_write_duration |
| << " for parse_blocked_on_script_execution_duration " |
| << timing.parse_timing->parse_blocked_on_script_execution_duration; |
| return internal::INVALID_SCRIPT_EXEC_DOC_WRITE_LONGER_THAN_SCRIPT_EXEC; |
| } |
| |
| if (!EventsInOrder(timing.parse_timing->parse_stop, |
| timing.document_timing->dom_content_loaded_event_start)) { |
| LOG(ERROR) << "Invalid parse_stop " << timing.parse_timing->parse_stop |
| << " for dom_content_loaded_event_start " |
| << timing.document_timing->dom_content_loaded_event_start; |
| return internal::INVALID_ORDER_PARSE_STOP_DOM_CONTENT_LOADED; |
| } |
| |
| if (!EventsInOrder(timing.document_timing->dom_content_loaded_event_start, |
| timing.document_timing->load_event_start)) { |
| LOG(ERROR) << "Invalid dom_content_loaded_event_start " |
| << timing.document_timing->dom_content_loaded_event_start |
| << " for load_event_start " |
| << timing.document_timing->load_event_start; |
| return internal::INVALID_ORDER_DOM_CONTENT_LOADED_LOAD; |
| } |
| |
| if (!EventsInOrder(timing.parse_timing->parse_start, |
| timing.document_timing->first_layout)) { |
| LOG(ERROR) << "Invalid parse_start " << timing.parse_timing->parse_start |
| << " for first_layout " << timing.document_timing->first_layout; |
| return internal::INVALID_ORDER_PARSE_START_FIRST_LAYOUT; |
| } |
| |
| if (!EventsInOrder(timing.paint_timing->first_paint, |
| timing.paint_timing->first_image_paint)) { |
| LOG(ERROR) << "Invalid first_paint " << timing.paint_timing->first_paint |
| << " for first_image_paint " |
| << timing.paint_timing->first_image_paint; |
| return internal::INVALID_ORDER_FIRST_PAINT_FIRST_IMAGE_PAINT; |
| } |
| |
| if (!EventsInOrder(timing.paint_timing->first_paint, |
| timing.paint_timing->first_contentful_paint)) { |
| LOG(ERROR) << "Invalid first_paint " << timing.paint_timing->first_paint |
| << " for first_contentful_paint " |
| << timing.paint_timing->first_contentful_paint; |
| return internal::INVALID_ORDER_FIRST_PAINT_FIRST_CONTENTFUL_PAINT; |
| } |
| |
| if (!EventsInOrder(timing.paint_timing->first_paint, |
| timing.paint_timing->first_meaningful_paint)) { |
| LOG(ERROR) << "Invalid first_paint " << timing.paint_timing->first_paint |
| << " for first_meaningful_paint " |
| << timing.paint_timing->first_meaningful_paint; |
| return internal::INVALID_ORDER_FIRST_PAINT_FIRST_MEANINGFUL_PAINT; |
| } |
| |
| if (!EventsInOrder(timing.paint_timing->first_meaningful_paint, |
| timing.interactive_timing->interactive)) { |
| LOG(ERROR) << "Invalid first_meaningful_paint " |
| << timing.paint_timing->first_meaningful_paint |
| << " for time_to_interactive " |
| << timing.interactive_timing->interactive; |
| return internal::INVALID_ORDER_FIRST_MEANINGFUL_PAINT_PAGE_INTERACTIVE; |
| } |
| |
| if (timing.interactive_timing->first_input_delay.has_value() && |
| !timing.interactive_timing->first_input_timestamp.has_value()) { |
| return internal::INVALID_NULL_FIRST_INPUT_TIMESTAMP; |
| } |
| |
| if (!timing.interactive_timing->first_input_delay.has_value() && |
| timing.interactive_timing->first_input_timestamp.has_value()) { |
| return internal::INVALID_NULL_FIRST_INPUT_DELAY; |
| } |
| |
| if (timing.interactive_timing->longest_input_delay.has_value() && |
| !timing.interactive_timing->longest_input_timestamp.has_value()) { |
| return internal::INVALID_NULL_LONGEST_INPUT_TIMESTAMP; |
| } |
| |
| if (!timing.interactive_timing->longest_input_delay.has_value() && |
| timing.interactive_timing->longest_input_timestamp.has_value()) { |
| return internal::INVALID_NULL_LONGEST_INPUT_DELAY; |
| } |
| |
| if (timing.interactive_timing->longest_input_delay.has_value() && |
| timing.interactive_timing->first_input_delay.has_value() && |
| timing.interactive_timing->longest_input_delay < |
| timing.interactive_timing->first_input_delay) { |
| return internal::INVALID_LONGEST_INPUT_DELAY_LESS_THAN_FIRST_INPUT_DELAY; |
| } |
| |
| if (timing.interactive_timing->longest_input_timestamp.has_value() && |
| timing.interactive_timing->first_input_timestamp.has_value() && |
| timing.interactive_timing->longest_input_timestamp < |
| timing.interactive_timing->first_input_timestamp) { |
| return internal:: |
| INVALID_LONGEST_INPUT_TIMESTAMP_LESS_THAN_FIRST_INPUT_TIMESTAMP; |
| } |
| |
| return internal::VALID; |
| } |
| |
| // If the updated value has an earlier time than the current value, log so we |
| // can keep track of how often this happens. |
| void LogIfOutOfOrderTiming(const base::Optional<base::TimeDelta>& current, |
| const base::Optional<base::TimeDelta>& update) { |
| if (!current || !update) |
| return; |
| |
| if (update < current) { |
| PAGE_LOAD_HISTOGRAM(internal::kHistogramOutOfOrderTimingBuffered, |
| current.value() - update.value()); |
| } |
| } |
| |
| // PageLoadTimingMerger merges timing values received from different frames |
| // together. |
| class PageLoadTimingMerger { |
| public: |
| explicit PageLoadTimingMerger(mojom::PageLoadTiming* target) |
| : target_(target) {} |
| |
| // Merge timing values from |new_page_load_timing| into the target |
| // PageLoadTiming; |
| void Merge(base::TimeDelta navigation_start_offset, |
| const mojom::PageLoadTiming& new_page_load_timing, |
| bool is_main_frame) { |
| MergePaintTiming(navigation_start_offset, |
| *new_page_load_timing.paint_timing, is_main_frame); |
| MergeInteractiveTiming(navigation_start_offset, |
| *new_page_load_timing.interactive_timing, |
| is_main_frame); |
| } |
| |
| // Whether we merged a new value. |
| bool should_buffer_timing_update_callback() const { |
| return should_buffer_timing_update_callback_; |
| } |
| |
| private: |
| // Updates *|inout_existing_value| with |optional_candidate_new_value|, if |
| // either *|inout_existing_value| isn't set, or |optional_candidate_new_value| |
| // < |inout_existing_value|. Set should_buffer_timing_update_callback_ to true |
| // if a new value was merged. Returns true if an update occurred. Note that |
| // |inout_existing_value| is relative to the main frame's navigation start. |
| // |navigation_start_offset| contains the delta in navigation start time |
| // between the main frame and the frame for |optional_candidate_new_value|. |
| bool MaybeUpdateTimeDelta( |
| base::Optional<base::TimeDelta>* inout_existing_value, |
| base::TimeDelta navigation_start_offset, |
| const base::Optional<base::TimeDelta>& optional_candidate_new_value) { |
| // If we don't get a new value, there's nothing to do |
| if (!optional_candidate_new_value) |
| return false; |
| |
| // optional_candidate_new_value is relative to navigation start in its |
| // frame. We need to adjust it to be relative to navigation start in the |
| // main frame, so offset it by navigation_start_offset. |
| base::TimeDelta candidate_new_value = |
| navigation_start_offset + optional_candidate_new_value.value(); |
| |
| DCHECK_NE(nullptr, inout_existing_value); |
| if (inout_existing_value->has_value()) { |
| // If we have a new value, but it is after the existing value, then keep |
| // the existing value. |
| if (*inout_existing_value <= candidate_new_value) |
| return false; |
| |
| // We received a new timing event, but with a timestamp before the |
| // timestamp of a timing update we had received previously. We expect this |
| // to happen occasionally, as inter-frame updates can arrive out of order. |
| // Record a histogram to track how frequently it happens, along with the |
| // magnitude of the delta. |
| PAGE_LOAD_HISTOGRAM(internal::kHistogramOutOfOrderTiming, |
| inout_existing_value->value() - candidate_new_value); |
| } else { |
| // We only want to set this for new updates. If there's already a value, |
| // then the window during which we buffer updates is over. We'll still |
| // update the value. |
| // TODO(811752): should we just throw the data out if we're past the |
| // buffering window? |
| should_buffer_timing_update_callback_ = true; |
| } |
| |
| *inout_existing_value = candidate_new_value; |
| return true; |
| } |
| |
| // Merge paint timing values from |new_paint_timing| into the target |
| // PaintTiming. |
| void MergePaintTiming(base::TimeDelta navigation_start_offset, |
| const mojom::PaintTiming& new_paint_timing, |
| bool is_main_frame) { |
| mojom::PaintTiming* target_paint_timing = target_->paint_timing.get(); |
| MaybeUpdateTimeDelta(&target_paint_timing->first_paint, |
| navigation_start_offset, new_paint_timing.first_paint); |
| MaybeUpdateTimeDelta(&target_paint_timing->first_image_paint, |
| navigation_start_offset, |
| new_paint_timing.first_image_paint); |
| MaybeUpdateTimeDelta(&target_paint_timing->first_contentful_paint, |
| navigation_start_offset, |
| new_paint_timing.first_contentful_paint); |
| if (is_main_frame) { |
| // FMP and FCP++ are only tracked in the main frame. |
| target_paint_timing->first_meaningful_paint = |
| new_paint_timing.first_meaningful_paint; |
| |
| target_paint_timing->largest_image_paint = |
| new_paint_timing.largest_image_paint; |
| target_paint_timing->largest_image_paint_size = |
| new_paint_timing.largest_image_paint_size; |
| target_paint_timing->last_image_paint = new_paint_timing.last_image_paint; |
| target_paint_timing->last_image_paint_size = |
| new_paint_timing.last_image_paint_size; |
| target_paint_timing->largest_text_paint = |
| new_paint_timing.largest_text_paint; |
| target_paint_timing->largest_text_paint_size = |
| new_paint_timing.largest_text_paint_size; |
| target_paint_timing->last_text_paint = new_paint_timing.last_text_paint; |
| target_paint_timing->last_text_paint_size = |
| new_paint_timing.last_text_paint_size; |
| } |
| } |
| |
| void MergeInteractiveTiming( |
| base::TimeDelta navigation_start_offset, |
| const mojom::InteractiveTiming& new_interactive_timing, |
| bool is_main_frame) { |
| mojom::InteractiveTiming* target_interactive_timing = |
| target_->interactive_timing.get(); |
| |
| if (is_main_frame) { |
| // TTI is only tracked in the main frame. |
| target_interactive_timing->interactive = |
| new_interactive_timing.interactive; |
| target_interactive_timing->first_invalidating_input = |
| new_interactive_timing.first_invalidating_input; |
| target_interactive_timing->interactive_detection = |
| new_interactive_timing.interactive_detection; |
| } |
| |
| if (MaybeUpdateTimeDelta(&target_interactive_timing->first_input_timestamp, |
| navigation_start_offset, |
| new_interactive_timing.first_input_timestamp)) { |
| // If we updated the first input timestamp, also update the |
| // associated first input delay. |
| target_interactive_timing->first_input_delay = |
| new_interactive_timing.first_input_delay; |
| } |
| |
| if (new_interactive_timing.longest_input_delay.has_value()) { |
| base::TimeDelta new_longest_input_timestamp = |
| navigation_start_offset + |
| new_interactive_timing.longest_input_timestamp.value(); |
| if (!target_interactive_timing->longest_input_delay.has_value() || |
| new_interactive_timing.longest_input_delay.value() > |
| target_interactive_timing->longest_input_delay.value()) { |
| target_interactive_timing->longest_input_delay = |
| new_interactive_timing.longest_input_delay; |
| target_interactive_timing->longest_input_timestamp = |
| new_longest_input_timestamp; |
| } |
| } |
| } |
| |
| // The target PageLoadTiming we are merging values into. |
| mojom::PageLoadTiming* const target_; |
| |
| // Whether we merged a new value into |target_|. |
| bool should_buffer_timing_update_callback_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(PageLoadTimingMerger); |
| }; |
| |
| } // namespace |
| |
| PageLoadMetricsUpdateDispatcher::PageLoadMetricsUpdateDispatcher( |
| PageLoadMetricsUpdateDispatcher::Client* client, |
| content::NavigationHandle* navigation_handle, |
| PageLoadMetricsEmbedderInterface* embedder_interface) |
| : client_(client), |
| timer_(embedder_interface->CreateTimer()), |
| navigation_start_(navigation_handle->NavigationStart()), |
| current_merged_page_timing_(CreatePageLoadTiming()), |
| pending_merged_page_timing_(CreatePageLoadTiming()), |
| main_frame_metadata_(mojom::PageLoadMetadata::New()), |
| subframe_metadata_(mojom::PageLoadMetadata::New()), |
| main_frame_render_data_(mojom::PageRenderData::New()) {} |
| |
| PageLoadMetricsUpdateDispatcher::~PageLoadMetricsUpdateDispatcher() { |
| ShutDown(); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::ShutDown() { |
| bool should_dispatch = false; |
| if (timer_ && timer_->IsRunning()) { |
| timer_->Stop(); |
| should_dispatch = true; |
| } |
| timer_ = nullptr; |
| |
| if (should_dispatch) { |
| DispatchTimingUpdates(); |
| } |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateMetrics( |
| content::RenderFrameHost* render_frame_host, |
| mojom::PageLoadTimingPtr new_timing, |
| mojom::PageLoadMetadataPtr new_metadata, |
| mojom::PageLoadFeaturesPtr new_features, |
| const std::vector<mojom::ResourceDataUpdatePtr>& resources, |
| mojom::PageRenderDataPtr render_data) { |
| if (render_frame_host->GetLastCommittedURL().SchemeIs( |
| extensions::kExtensionScheme)) { |
| // Extensions can inject child frames into a page. We don't want to track |
| // these as they could skew metrics. See http://crbug.com/761037 |
| return; |
| } |
| |
| // Report data usage before new timing and metadata for messages that have |
| // both updates. |
| client_->UpdateResourceDataUse(render_frame_host->GetFrameTreeNodeId(), |
| resources); |
| if (render_frame_host->GetParent() == nullptr) { |
| UpdateMainFrameMetadata(std::move(new_metadata)); |
| UpdateMainFrameTiming(std::move(new_timing)); |
| UpdateMainFrameRenderData(std::move(render_data)); |
| } else { |
| UpdateSubFrameMetadata(std::move(new_metadata)); |
| UpdateSubFrameTiming(render_frame_host, std::move(new_timing)); |
| // TODO: Handle subframe PageRenderData. |
| } |
| client_->UpdateFeaturesUsage(render_frame_host, *new_features); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateFeatures( |
| content::RenderFrameHost* render_frame_host, |
| const mojom::PageLoadFeatures& new_features) { |
| if (render_frame_host->GetLastCommittedURL().SchemeIs( |
| extensions::kExtensionScheme)) { |
| // Extensions can inject child frames into a page. We don't want to track |
| // these as they could skew metrics. See http://crbug.com/761037 |
| return; |
| } |
| client_->UpdateFeaturesUsage(render_frame_host, new_features); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::DidFinishSubFrameNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->HasCommitted()) |
| return; |
| |
| // We have a new committed navigation, so discard information about the |
| // previously committed navigation. |
| subframe_navigation_start_offset_.erase( |
| navigation_handle->GetFrameTreeNodeId()); |
| |
| if (navigation_start_ > navigation_handle->NavigationStart()) { |
| RecordInternalError(ERR_SUBFRAME_NAVIGATION_START_BEFORE_MAIN_FRAME); |
| return; |
| } |
| base::TimeDelta navigation_delta = |
| navigation_handle->NavigationStart() - navigation_start_; |
| subframe_navigation_start_offset_.insert(std::make_pair( |
| navigation_handle->GetFrameTreeNodeId(), navigation_delta)); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateSubFrameTiming( |
| content::RenderFrameHost* render_frame_host, |
| mojom::PageLoadTimingPtr new_timing) { |
| const auto it = subframe_navigation_start_offset_.find( |
| render_frame_host->GetFrameTreeNodeId()); |
| if (it == subframe_navigation_start_offset_.end()) { |
| // We received timing information for an untracked load. Ignore it. |
| return; |
| } |
| |
| client_->OnSubFrameTimingChanged(render_frame_host, *new_timing); |
| |
| base::TimeDelta navigation_start_offset = it->second; |
| PageLoadTimingMerger merger(pending_merged_page_timing_.get()); |
| merger.Merge(navigation_start_offset, *new_timing, false /* is_main_frame */); |
| |
| MaybeDispatchTimingUpdates(merger.should_buffer_timing_update_callback()); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateSubFrameMetadata( |
| mojom::PageLoadMetadataPtr subframe_metadata) { |
| // Merge the subframe loading behavior flags with any we've already observed, |
| // possibly from other subframes. |
| const int last_subframe_loading_behavior_flags = |
| subframe_metadata_->behavior_flags; |
| subframe_metadata_->behavior_flags |= subframe_metadata->behavior_flags; |
| if (last_subframe_loading_behavior_flags == |
| subframe_metadata_->behavior_flags) |
| return; |
| |
| client_->OnSubframeMetadataChanged(); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateMainFrameTiming( |
| mojom::PageLoadTimingPtr new_timing) { |
| // Throw away IPCs that are not relevant to the current navigation. |
| // Two timing structures cannot refer to the same navigation if they indicate |
| // that a navigation started at different times, so a new timing struct with a |
| // different start time from an earlier struct is considered invalid. |
| const bool valid_timing_descendent = |
| pending_merged_page_timing_->navigation_start.is_null() || |
| pending_merged_page_timing_->navigation_start == |
| new_timing->navigation_start; |
| if (!valid_timing_descendent) { |
| RecordInternalError(ERR_BAD_TIMING_IPC_INVALID_TIMING_DESCENDENT); |
| return; |
| } |
| |
| internal::PageLoadTimingStatus status = IsValidPageLoadTiming(*new_timing); |
| UMA_HISTOGRAM_ENUMERATION(internal::kPageLoadTimingStatus, status, |
| internal::LAST_PAGE_LOAD_TIMING_STATUS); |
| if (status != internal::VALID) { |
| RecordInternalError(ERR_BAD_TIMING_IPC_INVALID_TIMING); |
| return; |
| } |
| |
| mojom::PaintTimingPtr last_paint_timing = |
| std::move(pending_merged_page_timing_->paint_timing); |
| |
| mojom::InteractiveTimingPtr last_interactive_timing = |
| std::move(pending_merged_page_timing_->interactive_timing); |
| |
| // Update the pending_merged_page_timing_, making sure to merge the previously |
| // observed |paint_timing| and |interactive_timing|, which are tracked across |
| // all frames in the page. |
| pending_merged_page_timing_ = new_timing->Clone(); |
| pending_merged_page_timing_->paint_timing = std::move(last_paint_timing); |
| pending_merged_page_timing_->interactive_timing = |
| std::move(last_interactive_timing); |
| |
| PageLoadTimingMerger merger(pending_merged_page_timing_.get()); |
| merger.Merge(base::TimeDelta(), *new_timing, true /* is_main_frame */); |
| MaybeDispatchTimingUpdates(merger.should_buffer_timing_update_callback()); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateMainFrameMetadata( |
| mojom::PageLoadMetadataPtr new_metadata) { |
| if (main_frame_metadata_->Equals(*new_metadata)) |
| return; |
| |
| // Ensure flags sent previously are still present in the new metadata fields. |
| const bool valid_behavior_descendent = |
| (main_frame_metadata_->behavior_flags & new_metadata->behavior_flags) == |
| main_frame_metadata_->behavior_flags; |
| if (!valid_behavior_descendent) { |
| RecordInternalError(ERR_BAD_TIMING_IPC_INVALID_BEHAVIOR_DESCENDENT); |
| return; |
| } |
| |
| main_frame_metadata_ = std::move(new_metadata); |
| client_->OnMainFrameMetadataChanged(); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::UpdateMainFrameRenderData( |
| mojom::PageRenderDataPtr render_data) { |
| main_frame_render_data_ = std::move(render_data); |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::MaybeDispatchTimingUpdates( |
| bool should_buffer_timing_update_callback) { |
| // If we merged a new timing value, then we should buffer updates for |
| // |kBufferTimerDelayMillis|, to allow for any other out of order timings to |
| // arrive before we dispatch the minimum observed timings to observers. |
| if (should_buffer_timing_update_callback) { |
| timer_->Start( |
| FROM_HERE, base::TimeDelta::FromMilliseconds(kBufferTimerDelayMillis), |
| base::Bind(&PageLoadMetricsUpdateDispatcher::DispatchTimingUpdates, |
| base::Unretained(this))); |
| } else if (!timer_->IsRunning()) { |
| DispatchTimingUpdates(); |
| } |
| } |
| |
| void PageLoadMetricsUpdateDispatcher::DispatchTimingUpdates() { |
| if (pending_merged_page_timing_->paint_timing->first_paint) { |
| if (!pending_merged_page_timing_->parse_timing->parse_start || |
| !pending_merged_page_timing_->document_timing->first_layout) { |
| // When merging paint events across frames, we can sometimes encounter |
| // cases where we've received a first paint event for a child frame before |
| // receiving required earlier events in the main frame, due to buffering |
| // in the render process which results in out of order delivery. For |
| // example, we may receive a notification for a first paint in a child |
| // frame before we've received a notification for parse start or first |
| // layout in the main frame. In these cases, we delay sending timing |
| // updates until we've received all expected events (e.g. wait to receive |
| // a parse or layout event before dispatching a paint event), so observers |
| // can make assumptions about ordering of these events in their callbacks. |
| return; |
| } |
| } |
| |
| if (current_merged_page_timing_->Equals(*pending_merged_page_timing_)) |
| return; |
| |
| LogIfOutOfOrderTiming(current_merged_page_timing_->paint_timing->first_paint, |
| pending_merged_page_timing_->paint_timing->first_paint); |
| LogIfOutOfOrderTiming( |
| current_merged_page_timing_->paint_timing->first_image_paint, |
| pending_merged_page_timing_->paint_timing->first_image_paint); |
| LogIfOutOfOrderTiming( |
| current_merged_page_timing_->paint_timing->first_contentful_paint, |
| pending_merged_page_timing_->paint_timing->first_contentful_paint); |
| |
| current_merged_page_timing_ = pending_merged_page_timing_->Clone(); |
| |
| internal::PageLoadTimingStatus status = |
| IsValidPageLoadTiming(*pending_merged_page_timing_); |
| UMA_HISTOGRAM_ENUMERATION(internal::kPageLoadTimingDispatchStatus, status, |
| internal::LAST_PAGE_LOAD_TIMING_STATUS); |
| |
| client_->OnTimingChanged(); |
| } |
| |
| } // namespace page_load_metrics |