// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/page_load_metrics/renderer/metrics_render_frame_observer.h"

#include <map>
#include <string>
#include <utility>

#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/page_load_metrics/common/features.h"
#include "components/page_load_metrics/renderer/features.h"
#include "components/page_load_metrics/renderer/page_timing_metrics_sender.h"
#include "components/page_load_metrics/renderer/page_timing_sender.h"
#include "content/public/renderer/render_frame.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/loader/resource_type_util.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_document_loader.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_local_frame_client.h"
#include "third_party/blink/public/web/web_performance_metrics_for_reporting.h"
#include "third_party/icu/source/common/unicode/uscript.h"
#include "url/gurl.h"

namespace page_load_metrics {

namespace {

// This method creates a TimeDelta from two doubles that represents timestamps
// in seconds.
base::TimeDelta CreateTimeDeltaFromTimestampsInSeconds(
    double event_time_in_seconds,
    double start_time_in_seconds) {
  if (event_time_in_seconds - start_time_in_seconds < 0) {
    event_time_in_seconds = start_time_in_seconds;
  }
  return base::Time::FromSecondsSinceUnixEpoch(event_time_in_seconds) -
         base::Time::FromSecondsSinceUnixEpoch(start_time_in_seconds);
}

base::TimeTicks ClampToStart(base::TimeTicks event, base::TimeTicks start) {
  return event < start ? start : event;
}

mojom::ScriptType MapToMojoScriptType(UScriptCode script_code, bool is_emoji) {
  if (is_emoji) {
    return mojom::ScriptType::kEmoji;
  }
  switch (script_code) {
    case USCRIPT_LATIN:
      return mojom::ScriptType::kLatin;
    case USCRIPT_HAN:
      return mojom::ScriptType::kHan;
    case USCRIPT_HANGUL:
      return mojom::ScriptType::kHangul;
    case USCRIPT_HIRAGANA:
      return mojom::ScriptType::kHiragana;
    case USCRIPT_KATAKANA:
      return mojom::ScriptType::kKatakana;
    case USCRIPT_ARABIC:
      return mojom::ScriptType::kArabic;
    case USCRIPT_BENGALI:
      return mojom::ScriptType::kBengali;
    case USCRIPT_DEVANAGARI:
      return mojom::ScriptType::kDevanagari;
    case USCRIPT_CYRILLIC:
      return mojom::ScriptType::kCyrillic;
    case USCRIPT_COMMON:
      return mojom::ScriptType::kCommon;
    default:
      return mojom::ScriptType::kOther;
  }
}

class MojoPageTimingSender : public PageTimingSender {
 public:
  explicit MojoPageTimingSender(content::RenderFrame* render_frame,
                                bool limited_sending_mode)
      : limited_sending_mode_(limited_sending_mode) {
    DCHECK(render_frame);
    render_frame->GetRemoteAssociatedInterfaces()->GetInterface(
        &page_load_metrics_);
  }

  ~MojoPageTimingSender() override = default;

  void SendTiming(
      const mojom::PageLoadTimingPtr& timing,
      const mojom::FrameMetadataPtr& metadata,
      const std::vector<blink::UseCounterFeature>& new_features,
      std::vector<mojom::ResourceDataUpdatePtr> resources,
      const mojom::FrameRenderDataUpdate& render_data,
      const mojom::CpuTimingPtr& cpu_timing,
      std::vector<mojom::EventTimingPtr> event_timings,
      const std::optional<blink::SubresourceLoadMetrics>&
          subresource_load_metrics,
      std::vector<mojom::SoftNavigationMetricsPtr> soft_navigation_metrics,
      std::vector<mojom::LargestContentfulPaintTimingPtr>
          soft_largest_contentful_paint,
      std::vector<mojom::CustomUserTimingMarkPtr> user_timings,
      const mojom::FontLoadingMetricsPtr& font_loading_metrics) override {
    DCHECK(page_load_metrics_);
    page_load_metrics_->UpdateTiming(
        limited_sending_mode_ ? CreatePageLoadTiming() : timing->Clone(),
        metadata->Clone(), new_features, std::move(resources),
        render_data.Clone(), cpu_timing->Clone(), std::move(event_timings),
        subresource_load_metrics, std::move(soft_navigation_metrics),
        std::move(soft_largest_contentful_paint), std::move(user_timings),
        font_loading_metrics.Clone());
  }

  void SendCustomUserTiming(mojom::CustomUserTimingMarkPtr timing) override {
    CHECK(timing);
    CHECK(page_load_metrics_);
    page_load_metrics_->AddCustomUserTiming(std::move(timing));
  }

 private:
  // Indicates that this sender should not send timing updates or frame render
  // data updates.
  // TODO(crbug.com/40136524): When timing updates are handled for cases
  // where we have a subframe document and no committed navigation, this can be
  // removed.
  bool limited_sending_mode_ = false;

  // Use associated interface to make sure mojo messages are ordered with regard
  // to legacy IPC messages.
  mojo::AssociatedRemote<mojom::PageLoadMetrics> page_load_metrics_;
};
}  //  namespace

MetricsRenderFrameObserver::MetricsRenderFrameObserver(
    content::RenderFrame* render_frame)
    : content::RenderFrameObserver(render_frame),
      blink::WebLocalFrameObserver(render_frame ? render_frame->GetWebFrame()
                                                : nullptr) {
  if (base::FeatureList::IsEnabled(
          features::kMetricsRenderFrameObserverImprovement) &&
      render_frame) {
    // If the optimization is enabled, `DidObserveNewFeatureUsage()` will be
    // called as a callback instead of the observer interface.
    render_frame->SetNewFeatureUsageCallback(base::BindRepeating(
        &MetricsRenderFrameObserver::DidObserveNewFeatureUsage,
        weak_factory_.GetWeakPtr()));
    // If the optimization is enabled, `DidObserveSubresourceLoad()` will be
    // called as a callback instead of the observer interface.
    render_frame->SetSubresourceLoadCallback(base::BindRepeating(
        &MetricsRenderFrameObserver::DidObserveSubresourceLoad,
        weak_factory_.GetWeakPtr()));
    // If the optimization is enabled, `DidLoadResourceFromMemoryCache()` will
    // be called as a callback instead of the observer interface.
    render_frame->SetLoadFromMemoryCacheCallback(base::BindRepeating(
        &MetricsRenderFrameObserver::DidLoadResourceFromMemoryCache,
        weak_factory_.GetWeakPtr()));

    render_frame->SetDidStartResponseCallback(
        base::BindRepeating(&MetricsRenderFrameObserver::DidStartResponse,
                            weak_factory_.GetWeakPtr()));
    render_frame->SetDidCompleteResponseCallback(
        base::BindRepeating(&MetricsRenderFrameObserver::DidCompleteResponse,
                            weak_factory_.GetWeakPtr()));
    render_frame->SetDidCancelResponseCallback(
        base::BindRepeating(&MetricsRenderFrameObserver::DidCancelResponse,
                            weak_factory_.GetWeakPtr()));
  }
}

MetricsRenderFrameObserver::~MetricsRenderFrameObserver() {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->SendLatest();
  }
}

void MetricsRenderFrameObserver::DidChangePerformanceTiming() {
  SendMetrics();
}

void MetricsRenderFrameObserver::DidObserveUserInteraction(
    base::TimeTicks max_event_start,
    base::TimeTicks max_event_queued_main_thread,
    base::TimeTicks max_event_processing_start,
    base::TimeTicks max_event_commit_finish,
    base::TimeTicks max_event_end,
    uint64_t interaction_offset,
    uint64_t performance_timeline_navigation_id) {
  if (!page_timing_metrics_sender_ || HasNoRenderFrame()) {
    return;
  }
  page_timing_metrics_sender_->DidObserveUserInteraction(
      max_event_start, max_event_queued_main_thread, max_event_processing_start,
      max_event_commit_finish, max_event_end, interaction_offset,
      performance_timeline_navigation_id);
}

void MetricsRenderFrameObserver::DidChangeCpuTiming(base::TimeDelta time) {
  if (!page_timing_metrics_sender_) {
    return;
  }
  if (HasNoRenderFrame()) {
    return;
  }
  page_timing_metrics_sender_->UpdateCpuTiming(time);
}

void MetricsRenderFrameObserver::DidObserveLoadingBehavior(
    blink::LoadingBehaviorFlag behavior) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidObserveLoadingBehavior(behavior);
  }
}

void MetricsRenderFrameObserver::DidObserveJavaScriptFrameworks(
    const blink::JavaScriptFrameworkDetectionResult& result) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidObserveJavaScriptFrameworks(result);
  }
}

void MetricsRenderFrameObserver::DidObserveSubresourceLoad(
    const blink::SubresourceLoadMetrics& subresource_load_metrics) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidObserveSubresourceLoad(
        subresource_load_metrics);
  }
}

void MetricsRenderFrameObserver::DidObserveNewFeatureUsage(
    const blink::UseCounterFeature& feature) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidObserveNewFeatureUsage(feature);
  }
}

void MetricsRenderFrameObserver::DidObserveSoftNavigation(
    blink::SoftNavigationMetricsForReporting soft_nav_metrics) {
  if (page_timing_metrics_sender_) {
    // Make soft navigation start time relative to navigation start.
    soft_nav_metrics.start_time = CreateTimeDeltaFromTimestampsInSeconds(
        soft_nav_metrics.start_time.InSecondsF(), GetNavigationStart());
    page_timing_metrics_sender_->DidObserveSoftNavigation(soft_nav_metrics);
  }
}

void MetricsRenderFrameObserver::DidObserveSoftNavigationFirstContentfulPaint(
    uint64_t performance_timeline_navigation_id,
    base::TimeDelta first_contentful_paint) {
  if (page_timing_metrics_sender_) {
    // Make soft navigation FCP relative to navigation start.
    first_contentful_paint = CreateTimeDeltaFromTimestampsInSeconds(
        first_contentful_paint.InSecondsF(), GetNavigationStart());
    page_timing_metrics_sender_->DidObserveSoftNavigationFirstContentfulPaint(
        performance_timeline_navigation_id, first_contentful_paint);
  }
}

void MetricsRenderFrameObserver::DidObserveSoftLargestContentfulPaint(
    const blink::LargestContentfulPaintDetailsForReporting& lcp) {
  if (page_timing_metrics_sender_) {
    // The lcp object we pass to the sender is a mojom type that is relative
    // to the (hard) navigation start time.
    mojom::LargestContentfulPaintTimingPtr relative_lcp =
        CreateLargestContentfulPaintTiming();
    relative_lcp->performance_timeline_navigation_id =
        lcp.performance_timeline_navigation_id;

    if (lcp.image_paint_size > 0) {
      // Set largest image time.
      // Note that size can be nonzero while the time is 0 since a time of 0 is
      // sent when the image is painting. We assign the time even when it is 0
      // so that it's not ignored, but need to be careful when doing operations
      // on the value.
      if (lcp.image_paint_time == 0.0) {
        relative_lcp->largest_image_paint = base::TimeDelta();
      } else {
        relative_lcp->largest_image_paint =
            CreateTimeDeltaFromTimestampsInSeconds(lcp.image_paint_time,
                                                   GetNavigationStart());
      }
      // Set largest image size.
      relative_lcp->largest_image_paint_size = lcp.image_paint_size;

      // Set largest image load type.
      relative_lcp->type = LargestContentfulPaintTypeToUKMFlags(lcp.type);

      // Set largest image bpp value.
      relative_lcp->image_bpp = lcp.image_bpp;

      // Set largest image request priority.
      if (lcp.image_request_priority.has_value()) {
        relative_lcp->image_request_priority_valid = true;
        relative_lcp->image_request_priority_value =
            blink::WebURLRequest::ConvertToNetPriority(
                lcp.image_request_priority.value());
      } else {
        relative_lcp->image_request_priority_valid = false;
      }

      // Set largest image discovery time.
      if (lcp.resource_load_timings.discovery_time.has_value()) {
        relative_lcp->resource_load_timings->discovery_time =
            CreateTimeDeltaFromTimestampsInSeconds(
                lcp.resource_load_timings.discovery_time.value().InSecondsF(),
                GetNavigationStart());
      }

      // Set largest image load start.
      if (lcp.resource_load_timings.load_start.has_value()) {
        relative_lcp->resource_load_timings->load_start =
            CreateTimeDeltaFromTimestampsInSeconds(
                lcp.resource_load_timings.load_start.value().InSecondsF(),
                GetNavigationStart());
      }

      // Set largest image load end.
      if (lcp.resource_load_timings.load_end.has_value()) {
        relative_lcp->resource_load_timings->load_end =
            CreateTimeDeltaFromTimestampsInSeconds(
                lcp.resource_load_timings.load_end.value().InSecondsF(),
                GetNavigationStart());
      }
    }
    if (lcp.text_paint_size > 0) {
      // LargestTextPaint and LargestTextPaintSize should be available at the
      // same time. This is a renderer side DCHECK to ensure this.
      DCHECK(lcp.text_paint_time);

      relative_lcp->largest_text_paint = CreateTimeDeltaFromTimestampsInSeconds(
          lcp.text_paint_time, GetNavigationStart());

      relative_lcp->largest_text_paint_size = lcp.text_paint_size;

      relative_lcp->type = LargestContentfulPaintTypeToUKMFlags(lcp.type);
    }

    page_timing_metrics_sender_->DidObserveSoftLargestContentfulPaint(
        std::move(relative_lcp));
  }
}

void MetricsRenderFrameObserver::DidObserveLayoutShift(
    double score,
    bool after_input_or_scroll,
    uint64_t performance_timeline_navigation_id) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidObserveLayoutShift(
        score, after_input_or_scroll, performance_timeline_navigation_id);
  }
}

void MetricsRenderFrameObserver::DidStartResponse(
    const url::SchemeHostPort& final_response_url,
    int request_id,
    const network::mojom::URLResponseHead& response_head,
    network::mojom::RequestDestination request_destination,
    bool is_ad_resource) {
  if (provisional_frame_resource_data_use_ &&
      blink::IsRequestDestinationFrame(request_destination)) {
    provisional_frame_resource_data_use_->DidStartResponse(
        final_response_url, request_id, response_head, request_destination,
        is_ad_resource);
  } else if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidStartResponse(
        final_response_url, request_id, response_head, request_destination,
        is_ad_resource);
    UpdateResourceMetadata(request_id);
  }
}

void MetricsRenderFrameObserver::DidCompleteResponse(
    int request_id,
    const network::URLLoaderCompletionStatus& status) {
  if (provisional_frame_resource_data_use_ &&
      provisional_frame_resource_data_use_->resource_id() == request_id) {
    provisional_frame_resource_data_use_->DidCompleteResponse(status);
  } else if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidCompleteResponse(request_id, status);
    UpdateResourceMetadata(request_id);
  }
}

void MetricsRenderFrameObserver::DidCancelResponse(int request_id) {
  if (provisional_frame_resource_data_use_ &&
      provisional_frame_resource_data_use_->resource_id() == request_id) {
    provisional_frame_resource_data_use_.reset();
  } else if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidCancelResponse(request_id);
    UpdateResourceMetadata(request_id);
  }
}

void MetricsRenderFrameObserver::DidReceiveTransferSizeUpdate(
    int request_id,
    base::ByteSize received_data_length) {
  if (provisional_frame_resource_data_use_ &&
      provisional_frame_resource_data_use_->resource_id() == request_id) {
    provisional_frame_resource_data_use_->DidReceiveTransferSizeUpdate(
        received_data_length);
  } else if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidReceiveTransferSizeUpdate(
        request_id, received_data_length);
    UpdateResourceMetadata(request_id);
  }
}

void MetricsRenderFrameObserver::DidLoadResourceFromMemoryCache(
    const GURL& response_url,
    int request_id,
    base::ByteSize encoded_body_length,
    const std::string& mime_type,
    bool from_archive) {
  // Resources from archives, such as subresources from a MHTML archive, do not
  // have valid request ids and should not be reported to PLM.
  if (from_archive) {
    return;
  }

  // A provisional frame resource cannot be serviced from the memory cache, so
  // we do not need to check |provisional_frame_resource_data_use_|.
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->DidLoadResourceFromMemoryCache(
        response_url, request_id, encoded_body_length, mime_type);
    UpdateResourceMetadata(request_id);
  }
}

void MetricsRenderFrameObserver::WillDetach(blink::DetachReason detach_reason) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->SendLatest();
    page_timing_metrics_sender_.reset();
  }
}

void MetricsRenderFrameObserver::DidStartNavigation(
    const GURL& url,
    std::optional<blink::WebNavigationType> navigation_type) {
  // Send current metrics, as we might create a new RenderFrame later due to
  // this navigation (that might end up in a different process entirely, and
  // won't notify us until the current RenderFrameHost in the browser changed).
  // If that happens, it will be too late to send the metrics from WillDetach
  // or the destructor, because the browser ignores metrics update from
  // non-current RenderFrameHosts. See crbug.com/1150242 for more details.
  // TODO(crbug.com/40157795): Remove this when we have the full fix for the
  // bug.
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->SendLatest();
  }
}

void MetricsRenderFrameObserver::DidSetPageLifecycleState(
    blink::BFCacheStateChange bfcache_change) {
  // Send current metrics, as this RenderFrame might be replaced by a new
  // RenderFrame or its process might be killed, and this might be the last
  // point we can send the metrics to the browser. See crbug.com/1150242 for
  // more details.
  // TODO(crbug.com/40157795): Remove this when we have the full fix for the
  // bug.
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->SendLatest();
  }
}

void MetricsRenderFrameObserver::ReadyToCommitNavigation(
    blink::WebDocumentLoader* document_loader) {
  // Create a new data use tracker for the new document load.
  provisional_frame_resource_data_use_ = std::make_unique<PageResourceDataUse>(
      PageResourceDataUse::kUnknownResourceId);

  // Send current metrics before the next page load commits. Don't reset here
  // as it may be a same document load.
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->SendLatest();
  }
}

void MetricsRenderFrameObserver::DidFailProvisionalLoad() {
  // Clear the data use tracker for the provisional navigation that started.
  provisional_frame_resource_data_use_.reset();
}

void MetricsRenderFrameObserver::DidCreateDocumentElement() {
  // If we do not have a render frame or are already tracking this frame, ignore
  // the new document element.
  if (HasNoRenderFrame() || page_timing_metrics_sender_) {
    return;
  }

  // We should only track committed navigations for the main frame so ignore new
  // document elements in the main frame.
  if (IsMainFrame()) {
    return;
  }

  // A new document element was created in a frame that did not commit a
  // provisional load. This can be due to a doc.write in the frame that aborted
  // a navigation. Create a page timing sender to track this load. This sender
  // will only send resource usage updates to the browser process. There
  // currently is not infrastructure in the browser process to monitor this case
  // and properly handle timing updates without a committed load.
  // TODO(crbug.com/40136524): Implement proper handling of timing
  // updates in the browser process and create a normal page timing sender.

  // It should not be possible to have a |provisional_frame_resource_data_use_|
  // object at this point. If we did, it means we reached
  // ReadyToCommitNavigation() and aborted prior to load commit which should not
  // be possible.
  DCHECK(!provisional_frame_resource_data_use_);

  Timing timing = GetTiming();
  page_timing_metrics_sender_ = std::make_unique<PageTimingMetricsSender>(
      CreatePageTimingSender(true /* limited_sending_mode */), CreateTimer(),
      std::move(timing.relative_timing), timing.monotonic_timing,
      /* initial_request=*/nullptr, /* is_main_frame=*/false);

  OnMetricsSenderCreated();
}

void MetricsRenderFrameObserver::DidCommitProvisionalLoad(
    ui::PageTransition transition) {
  // Make sure to release the sender for a previous navigation, if we have one.
  page_timing_metrics_sender_.reset();

  if (HasNoRenderFrame()) {
    return;
  }

  Timing timing = GetTiming();
  page_timing_metrics_sender_ = std::make_unique<PageTimingMetricsSender>(
      CreatePageTimingSender(false /* limited_sending_mode*/), CreateTimer(),
      std::move(timing.relative_timing), timing.monotonic_timing,
      std::move(provisional_frame_resource_data_use_), IsMainFrame());

  OnMetricsSenderCreated();
}

void MetricsRenderFrameObserver::OnMainFrameRectangleChanged(
    const gfx::Rect& main_frame_rect) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->OnMainFrameRectangleChanged(main_frame_rect);
    return;
  }

  main_frame_rect_before_metrics_sender_created_ = main_frame_rect;
}

void MetricsRenderFrameObserver::OnMainFrameViewportRectangleChanged(
    const gfx::Rect& main_frame_viewport_rect) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->OnMainFrameViewportRectangleChanged(
        main_frame_viewport_rect);
  }
}

void MetricsRenderFrameObserver::OnMainFrameAdRectangleChanged(
    int element_id,
    const gfx::Rect& ad_rect) {
  if (page_timing_metrics_sender_) {
    page_timing_metrics_sender_->OnMainFrameAdRectangleChanged(element_id,
                                                               ad_rect);
  }
}

void MetricsRenderFrameObserver::OnFrameDetached() {
  WillDetach(blink::DetachReason::kNavigation);
}

MetricsRenderFrameObserver::Timing::Timing(
    mojom::PageLoadTimingPtr relative_timing,
    const PageTimingMetadataRecorder::MonotonicTiming& monotonic_timing)
    : relative_timing(std::move(relative_timing)),
      monotonic_timing(monotonic_timing) {}

MetricsRenderFrameObserver::Timing::~Timing() = default;

MetricsRenderFrameObserver::Timing::Timing(Timing&&) = default;
MetricsRenderFrameObserver::Timing&
MetricsRenderFrameObserver::Timing::operator=(Timing&&) = default;

void MetricsRenderFrameObserver::UpdateResourceMetadata(int request_id) {
  DCHECK(page_timing_metrics_sender_);

  bool is_main_frame_resource = IsMainFrame();

  if (provisional_frame_resource_data_use_ &&
      provisional_frame_resource_data_use_->resource_id() == request_id) {
    provisional_frame_resource_data_use_->SetIsMainFrameResource(
        is_main_frame_resource);
    // Don't bother with before-fcp metrics for a provisional frame.
  } else {
    page_timing_metrics_sender_->UpdateResourceMetadata(request_id,
                                                        is_main_frame_resource);
  }
}

void MetricsRenderFrameObserver::SendMetrics() {
  if (!page_timing_metrics_sender_) {
    return;
  }
  if (HasNoRenderFrame()) {
    return;
  }
  Timing timing = GetTiming();
  mojom::FontLoadingMetricsPtr font_metrics = GetFontLoadingMetrics();
  page_timing_metrics_sender_->Update(std::move(timing.relative_timing),
                                      timing.monotonic_timing,
                                      std::move(font_metrics));

  mojom::CustomUserTimingMarkPtr user_timing = GetCustomUserTimingMark();
  if (user_timing) {
    if (base::FeatureList::IsEnabled(
            features::kThrottleSendingCustomUserTimings)) {
      page_timing_metrics_sender_->UpdateCustomUserTimings(
          std::move(user_timing));
    } else {
      page_timing_metrics_sender_->SendCustomUserTimingMark(
          std::move(user_timing));
    }
  }
}

void MetricsRenderFrameObserver::OnMetricsSenderCreated() {
  // Send the latest the frame intersection update, as otherwise we may miss
  // this information for a frame completely if there are no future updates.
  if (main_frame_rect_before_metrics_sender_created_) {
    page_timing_metrics_sender_->OnMainFrameRectangleChanged(
        *main_frame_rect_before_metrics_sender_created_);
    main_frame_rect_before_metrics_sender_created_.reset();
  }
}

double MetricsRenderFrameObserver::GetNavigationStart() const {
  return render_frame()
      ->GetWebFrame()
      ->PerformanceMetricsForReporting()
      .NavigationStart();
}

MetricsRenderFrameObserver::Timing MetricsRenderFrameObserver::GetTiming()
    const {
  const blink::WebPerformanceMetricsForReporting& perf =
      render_frame()->GetWebFrame()->PerformanceMetricsForReporting();

  mojom::PageLoadTimingPtr timing(CreatePageLoadTiming());
  PageTimingMetadataRecorder::MonotonicTiming monotonic_timing;
  double start = perf.NavigationStart();
  timing->navigation_start = base::Time::FromSecondsSinceUnixEpoch(start);
  monotonic_timing.navigation_start = perf.NavigationStartAsMonotonicTime();
  if (perf.InputForNavigationStart() > 0.0) {
    timing->input_to_navigation_start = CreateTimeDeltaFromTimestampsInSeconds(
        start, perf.InputForNavigationStart());
  }
  if (perf.FirstInputDelay().has_value()) {
    timing->interactive_timing->first_input_delay = *perf.FirstInputDelay();
    monotonic_timing.first_input_delay = perf.FirstInputDelay();
  }
  if (perf.FirstInputTimestamp().has_value()) {
    timing->interactive_timing->first_input_timestamp =
        CreateTimeDeltaFromTimestampsInSeconds(
            (*perf.FirstInputTimestamp()).InSecondsF(), start);
  }
  if (perf.FirstInputTimestampAsMonotonicTime()) {
    monotonic_timing.first_input_timestamp =
        perf.FirstInputTimestampAsMonotonicTime();
  }
  if (perf.FirstScrollDelay().has_value()) {
    timing->interactive_timing->first_scroll_delay = *perf.FirstScrollDelay();
  }
  if (perf.FirstScrollTimestamp().has_value()) {
    timing->interactive_timing->first_scroll_timestamp =
        CreateTimeDeltaFromTimestampsInSeconds(
            (*perf.FirstScrollTimestamp()).InSecondsF(), start);
  }
  if (perf.DomainLookupStart() > 0.0) {
    timing->domain_lookup_timing->domain_lookup_start =
        CreateTimeDeltaFromTimestampsInSeconds(perf.DomainLookupStart(), start);
  }
  if (perf.DomainLookupEnd() > 0.0) {
    timing->domain_lookup_timing->domain_lookup_end =
        CreateTimeDeltaFromTimestampsInSeconds(perf.DomainLookupEnd(), start);
  }
  if (perf.ConnectStart() > 0.0) {
    timing->connect_start =
        CreateTimeDeltaFromTimestampsInSeconds(perf.ConnectStart(), start);
  }
  if (perf.ConnectEnd() > 0.0) {
    timing->connect_end =
        CreateTimeDeltaFromTimestampsInSeconds(perf.ConnectEnd(), start);
  }
  if (perf.ResponseStart() > 0.0) {
    timing->response_start =
        CreateTimeDeltaFromTimestampsInSeconds(perf.ResponseStart(), start);
  }
  if (perf.DomContentLoadedEventStart() > 0.0) {
    timing->document_timing->dom_content_loaded_event_start =
        CreateTimeDeltaFromTimestampsInSeconds(
            perf.DomContentLoadedEventStart(), start);
  }
  if (perf.LoadEventStart() > 0.0) {
    timing->document_timing->load_event_start =
        CreateTimeDeltaFromTimestampsInSeconds(perf.LoadEventStart(), start);
  }
  if (perf.FirstPaint() > 0.0) {
    timing->paint_timing->first_paint =
        CreateTimeDeltaFromTimestampsInSeconds(perf.FirstPaint(), start);
  }
  if (!perf.BackForwardCacheRestore().empty()) {
    blink::WebPerformanceMetricsForReporting::BackForwardCacheRestoreTimings
        restore_timings = perf.BackForwardCacheRestore();
    for (const auto& restore_timing : restore_timings) {
      double navigation_start = restore_timing.navigation_start;
      double first_paint = restore_timing.first_paint;
      std::optional<base::TimeDelta> first_input_delay =
          restore_timing.first_input_delay;

      auto back_forward_cache_timing = mojom::BackForwardCacheTiming::New();
      if (first_paint) {
        back_forward_cache_timing
            ->first_paint_after_back_forward_cache_restore =
            CreateTimeDeltaFromTimestampsInSeconds(first_paint,
                                                   navigation_start);
      }
      for (double raf : restore_timing.request_animation_frames) {
        if (!raf) {
          break;
        }
        back_forward_cache_timing
            ->request_animation_frames_after_back_forward_cache_restore
            .push_back(
                CreateTimeDeltaFromTimestampsInSeconds(raf, navigation_start));
      }
      if (first_input_delay.has_value()) {
        back_forward_cache_timing
            ->first_input_delay_after_back_forward_cache_restore =
            first_input_delay;
      }
      timing->back_forward_cache_timings.push_back(
          std::move(back_forward_cache_timing));
    }
  }
  if (perf.FirstImagePaint() > 0.0) {
    timing->paint_timing->first_image_paint =
        CreateTimeDeltaFromTimestampsInSeconds(perf.FirstImagePaint(), start);
  }
  if (perf.FirstContentfulPaint() > 0.0) {
    DCHECK(perf.FirstEligibleToPaint() > 0);
    timing->paint_timing->first_contentful_paint =
        CreateTimeDeltaFromTimestampsInSeconds(perf.FirstContentfulPaint(),
                                               start);
    monotonic_timing.first_contentful_paint =
        ClampToStart(perf.FirstContentfulPaintAsMonotonicTime(),
                     perf.NavigationStartAsMonotonicTime());
  }
  if (perf.FirstMeaningfulPaint() > 0.0) {
    timing->paint_timing->first_meaningful_paint =
        CreateTimeDeltaFromTimestampsInSeconds(perf.FirstMeaningfulPaint(),
                                               start);
  }
  blink::LargestContentfulPaintDetailsForReporting
      largest_contentful_paint_details =
          perf.LargestContentfulDetailsForMetrics();
  monotonic_timing.frame_largest_contentful_paint =
      largest_contentful_paint_details.merged_unclamped_paint_time;

  if (largest_contentful_paint_details.image_paint_size > 0) {
    timing->paint_timing->largest_contentful_paint->largest_image_paint_size =
        largest_contentful_paint_details.image_paint_size;
    // Note that size can be nonzero while the time is 0 since a time of 0 is
    // sent when the image is painting. We assign the time even when it is 0 so
    // that it's not ignored, but need to be careful when doing operations on
    // the value.
    timing->paint_timing->largest_contentful_paint->largest_image_paint =
        largest_contentful_paint_details.image_paint_time == 0.0
            ? base::TimeDelta()
            : CreateTimeDeltaFromTimestampsInSeconds(
                  largest_contentful_paint_details.image_paint_time, start);

    timing->paint_timing->largest_contentful_paint->type =
        LargestContentfulPaintTypeToUKMFlags(
            largest_contentful_paint_details.type);

    timing->paint_timing->largest_contentful_paint->image_bpp =
        largest_contentful_paint_details.image_bpp;

    if (largest_contentful_paint_details.image_request_priority.has_value()) {
      timing->paint_timing->largest_contentful_paint
          ->image_request_priority_valid = true;
      timing->paint_timing->largest_contentful_paint
          ->image_request_priority_value =
          blink::WebURLRequest::ConvertToNetPriority(
              largest_contentful_paint_details.image_request_priority.value());
    } else {
      timing->paint_timing->largest_contentful_paint
          ->image_request_priority_valid = false;
    }

    // Set largest image load timings.
    if (largest_contentful_paint_details.resource_load_timings.discovery_time
            .has_value()) {
      timing->paint_timing->largest_contentful_paint->resource_load_timings
          ->discovery_time = CreateTimeDeltaFromTimestampsInSeconds(
          (largest_contentful_paint_details.resource_load_timings.discovery_time
               .value())
              .InSecondsF(),
          start);
    }

    if (largest_contentful_paint_details.resource_load_timings.load_start
            .has_value()) {
      timing->paint_timing->largest_contentful_paint->resource_load_timings
          ->load_start = CreateTimeDeltaFromTimestampsInSeconds(
          (largest_contentful_paint_details.resource_load_timings.load_start
               .value())
              .InSecondsF(),
          start);
    }

    if (largest_contentful_paint_details.resource_load_timings.load_end
            .has_value()) {
      timing->paint_timing->largest_contentful_paint->resource_load_timings
          ->load_end = CreateTimeDeltaFromTimestampsInSeconds(
          (largest_contentful_paint_details.resource_load_timings.load_end
               .value())
              .InSecondsF(),
          start);
    }
  }
  if (largest_contentful_paint_details.text_paint_size > 0) {
    // LargestTextPaint and LargestTextPaintSize should be available at the
    // same time. This is a renderer side DCHECK to ensure this.
    DCHECK(largest_contentful_paint_details.text_paint_time);

    timing->paint_timing->largest_contentful_paint->largest_text_paint =
        CreateTimeDeltaFromTimestampsInSeconds(
            largest_contentful_paint_details.text_paint_time, start);

    timing->paint_timing->largest_contentful_paint->largest_text_paint_size =
        largest_contentful_paint_details.text_paint_size;

    timing->paint_timing->largest_contentful_paint->type =
        LargestContentfulPaintTypeToUKMFlags(
            largest_contentful_paint_details.type);
  }

  if (largest_contentful_paint_details.image_paint_size > 0 ||
      largest_contentful_paint_details.text_paint_size > 0) {
    // Hard navigation LCP has an expected navigation_id of 1
    // (kHardNavigationId). Note: In principle, this should be the navigation ID
    // from the global WindowPerformance object. However, because hard
    // navigation LCP is read lazily (after presentation feedback), a subsequent
    // soft navigation might have already incremented the global navigation ID
    // before this is read. Therefore, we cannot rely on the current global
    // navigation ID here. Once LCP is captured at paint time, this can be
    // plumbed directly.
    timing->paint_timing->largest_contentful_paint
        ->performance_timeline_navigation_id =
        kHardNavigationPerformanceTimelineNavigationId;
  }

  // It is possible for a frame to switch from eligible for painting to
  // ineligible for it prior to the first paint. If this occurs, we need to
  // propagate the null value.
  if (perf.FirstEligibleToPaint() > 0) {
    timing->paint_timing->first_eligible_to_paint =
        CreateTimeDeltaFromTimestampsInSeconds(perf.FirstEligibleToPaint(),
                                               start);
  } else {
    timing->paint_timing->first_eligible_to_paint.reset();
  }
  if (perf.FirstInputOrScrollNotifiedTimestamp() > 0) {
    timing->paint_timing->first_input_or_scroll_notified_timestamp =
        CreateTimeDeltaFromTimestampsInSeconds(
            perf.FirstInputOrScrollNotifiedTimestamp(), start);
  }
  if (perf.ParseStart() > 0.0) {
    timing->parse_timing->parse_start =
        CreateTimeDeltaFromTimestampsInSeconds(perf.ParseStart(), start);
  }
  if (perf.ParseStop() > 0.0) {
    timing->parse_timing->parse_stop =
        CreateTimeDeltaFromTimestampsInSeconds(perf.ParseStop(), start);
  }
  if (timing->parse_timing->parse_start) {
    // If we started parsing, record all parser durations such as the amount of
    // time blocked on script load, even if those values are zero.
    timing->parse_timing->parse_blocked_on_script_load_duration =
        base::Seconds(perf.ParseBlockedOnScriptLoadDuration());
    timing->parse_timing
        ->parse_blocked_on_script_load_from_document_write_duration =
        base::Seconds(perf.ParseBlockedOnScriptLoadFromDocumentWriteDuration());
    timing->parse_timing->parse_blocked_on_script_execution_duration =
        base::Seconds(perf.ParseBlockedOnScriptExecutionDuration());
    timing->parse_timing
        ->parse_blocked_on_script_execution_from_document_write_duration =
        base::Seconds(
            perf.ParseBlockedOnScriptExecutionFromDocumentWriteDuration());
  }
  if (perf.PrerenderActivationStart().has_value()) {
    timing->activation_start = perf.PrerenderActivationStart();
  }

  if (perf.UserTimingMarkFullyLoaded().has_value()) {
    timing->user_timing_mark_fully_loaded = perf.UserTimingMarkFullyLoaded();
  }

  if (perf.UserTimingMarkFullyVisible().has_value()) {
    timing->user_timing_mark_fully_visible = perf.UserTimingMarkFullyVisible();
  }

  if (perf.UserTimingMarkInteractive().has_value()) {
    timing->user_timing_mark_interactive = perf.UserTimingMarkInteractive();
  }

  blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
  if (web_frame->Client()->IsForInitialWebUI()) {
    if (!perf.FirstPaintAsMonotonicTime().is_null()) {
      if (!timing->monotonic_paint_timing) {
        timing->monotonic_paint_timing = mojom::MonotonicPaintTiming::New();
      }
      timing->monotonic_paint_timing->first_paint =
          perf.FirstPaintAsMonotonicTime();
    }
    if (!perf.FirstContentfulPaintAsMonotonicTime().is_null()) {
      if (!timing->monotonic_paint_timing) {
        timing->monotonic_paint_timing = mojom::MonotonicPaintTiming::New();
      }
      timing->monotonic_paint_timing->first_contentful_paint =
          perf.FirstContentfulPaintAsMonotonicTime();
    }
    if (!perf.FirstContentfulPaintRenderedButNotPresentedAsMonotonicTime()
             .is_null()) {
      if (!timing->monotonic_paint_timing) {
        timing->monotonic_paint_timing = mojom::MonotonicPaintTiming::New();
      }
      timing->monotonic_paint_timing->first_contentful_paint_submitted =
          perf.FirstContentfulPaintRenderedButNotPresentedAsMonotonicTime();
    }
  }

  return Timing(std::move(timing), monotonic_timing);
}

mojom::FontLoadingMetricsPtr MetricsRenderFrameObserver::GetFontLoadingMetrics()
    const {
  const blink::WebPerformanceMetricsForReporting& perf =
      render_frame()->GetWebFrame()->PerformanceMetricsForReporting();
  if (perf.SystemFallbackFontCount() > 0 || perf.ShapeCacheHitCount() > 0 ||
      perf.ShapeCacheMissCount() > 0) {
    auto font_metrics = mojom::FontLoadingMetrics::New();
    font_metrics->fallback_duration = perf.SystemFallbackFontTime();
    font_metrics->fallback_count = perf.SystemFallbackFontCount();
    font_metrics->fallback_initial_duration =
        perf.SystemFallbackFontInitialDuration();
    font_metrics->shape_cache_hit_count = perf.ShapeCacheHitCount();
    font_metrics->shape_cache_miss_count = perf.ShapeCacheMissCount();
    std::map<mojom::ScriptType, size_t> aggregated;
    for (const auto& details : perf.GetScriptFontFallbackDetails()) {
      aggregated[MapToMojoScriptType(details.script_code, details.is_emoji)] +=
          details.fallback_count;
    }
    for (const auto& [type, count] : aggregated) {
      auto info = mojom::ScriptFallbackInfo::New();
      info->script_type = type;
      info->fallback_count = static_cast<uint32_t>(count);
      font_metrics->script_fallback_metrics.push_back(std::move(info));
    }
    return font_metrics;
  }
  return nullptr;
}

mojom::CustomUserTimingMarkPtr
MetricsRenderFrameObserver::GetCustomUserTimingMark() const {
  const blink::WebPerformanceMetricsForReporting& perf =
      render_frame()->GetWebFrame()->PerformanceMetricsForReporting();
  auto timing = perf.CustomUserTimingMark();
  if (!timing.has_value()) {
    return nullptr;
  }
  const auto [mark_name, start_time] = timing.value();
  mojom::CustomUserTimingMarkPtr custom_user_timing_mark =
      mojom::CustomUserTimingMark::New();
  custom_user_timing_mark->mark_name = mark_name;
  custom_user_timing_mark->start_time = start_time;

  return custom_user_timing_mark;
}

std::unique_ptr<base::OneShotTimer> MetricsRenderFrameObserver::CreateTimer() {
  return std::make_unique<base::OneShotTimer>();
}

std::unique_ptr<PageTimingSender>
MetricsRenderFrameObserver::CreatePageTimingSender(bool limited_sending_mode) {
  return base::WrapUnique<PageTimingSender>(
      new MojoPageTimingSender(render_frame(), limited_sending_mode));
}

bool MetricsRenderFrameObserver::HasNoRenderFrame() const {
  bool no_frame = !render_frame() || !render_frame()->GetWebFrame();
  DCHECK(!no_frame);
  return no_frame;
}

bool MetricsRenderFrameObserver::IsMainFrame() const {
  return render_frame()->IsMainFrame();
}

void MetricsRenderFrameObserver::OnDestruct() {
  delete this;
}

}  // namespace page_load_metrics
