// 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/page_timing_metrics_sender.h"

#include <utility>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "components/page_load_metrics/common/page_load_metrics.mojom.h"
#include "components/page_load_metrics/common/page_load_metrics_util.h"
#include "components/page_load_metrics/common/page_load_timing.h"
#include "components/page_load_metrics/renderer/page_timing_sender.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/loader/javascript_framework_detection.mojom-forward.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h"
#include "third_party/blink/public/mojom/use_counter/use_counter_feature.mojom-shared.h"
#include "ui/gfx/geometry/rect.h"

namespace page_load_metrics {

namespace {
const int kInitialTimerDelayMillis = 50;

bool IsFirstFCP(const mojom::PageLoadTimingPtr& last_timing,
                const mojom::PageLoadTimingPtr& new_timing) {
  return (!last_timing->paint_timing ||
          !last_timing->paint_timing->first_contentful_paint.has_value()) &&
         new_timing->paint_timing &&
         new_timing->paint_timing->first_contentful_paint.has_value();
}

bool IsFirstParseStart(const mojom::PageLoadTimingPtr& last_timing,
                       const mojom::PageLoadTimingPtr& new_timing) {
  return (!last_timing->parse_timing ||
          !last_timing->parse_timing->parse_start.has_value()) &&
         new_timing->parse_timing &&
         new_timing->parse_timing->parse_start.has_value();
}

bool IsFirstDCL(const mojom::PageLoadTimingPtr& last_timing,
                const mojom::PageLoadTimingPtr& new_timing) {
  return (!last_timing->document_timing ||
          !last_timing->document_timing->dom_content_loaded_event_start
               .has_value()) &&
         new_timing->document_timing &&
         new_timing->document_timing->dom_content_loaded_event_start
             .has_value();
}

}  // namespace

PageTimingMetricsSender::PageTimingMetricsSender(
    std::unique_ptr<PageTimingSender> sender,
    std::unique_ptr<base::OneShotTimer> timer,
    mojom::PageLoadTimingPtr initial_timing,
    const PageTimingMetadataRecorder::MonotonicTiming& initial_monotonic_timing,
    std::unique_ptr<PageResourceDataUse> initial_request,
    bool is_main_frame)
    : sender_(std::move(sender)),
      timer_(std::move(timer)),
      last_timing_(std::move(initial_timing)),
      last_cpu_timing_(mojom::CpuTiming::New()),
      metadata_(mojom::FrameMetadata::New()),
      buffer_timer_delay_ms_(GetBufferTimerDelayMillis(TimerType::kRenderer)),
      metadata_recorder_(initial_monotonic_timing, is_main_frame) {
  if (initial_request) {
    int resource_id = initial_request->resource_id();
    page_resource_data_use_[resource_id] = std::move(initial_request);
  }
  if (!IsEmpty(*last_timing_)) {
    EnsureSendTimer();
  }
}

PageTimingMetricsSender::~PageTimingMetricsSender() {
  // Make sure we don't have any unsent data. If this assertion fails, then it
  // means metrics are somehow coming in between MetricsRenderFrameObserver's
  // ReadyToCommitNavigation and DidCommitProvisionalLoad.
  DCHECK(!timer_->IsRunning());
}

void PageTimingMetricsSender::DidObserveLoadingBehavior(
    blink::LoadingBehaviorFlag behavior) {
  if (behavior & metadata_->behavior_flags) {
    return;
  }
  metadata_->behavior_flags |= behavior;
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidObserveJavaScriptFrameworks(
    const blink::JavaScriptFrameworkDetectionResult& result) {
  metadata_->framework_detection_result = result;
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidObserveSubresourceLoad(
    const blink::SubresourceLoadMetrics& subresource_load_metrics) {
  if (subresource_load_metrics_ &&
      *subresource_load_metrics_ == subresource_load_metrics) {
    return;
  }
  subresource_load_metrics_ = subresource_load_metrics;
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidObserveNewFeatureUsage(
    const blink::UseCounterFeature& feature) {
  if (feature_tracker_.TestAndSet(feature))
    return;

  new_features_.push_back(feature);
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidObserveSoftNavigation(
    blink::SoftNavigationMetricsForReporting new_metrics) {
  CHECK_GT(new_metrics.performance_timeline_navigation_id, 1u);
  CHECK(!new_metrics.start_time.is_zero());
  CHECK(!new_metrics.same_document_metrics_token.is_empty());
  auto entry = mojom::SoftNavigationMetrics::New();
  entry->performance_timeline_navigation_id =
      new_metrics.performance_timeline_navigation_id;
  entry->commit = mojom::SoftNavigationCommit::New(
      new_metrics.start_time, new_metrics.soft_navigation_slicing_time,
      new_metrics.navigation_type, new_metrics.same_document_metrics_token);
  soft_navigation_metrics_.emplace_back(std::move(entry));

  EnsureSendTimer();
}

void PageTimingMetricsSender::DidObserveSoftNavigationFirstContentfulPaint(
    uint64_t performance_timeline_navigation_id,
    base::TimeDelta first_contentful_paint) {
  CHECK_GT(performance_timeline_navigation_id, 1u);
  CHECK(!first_contentful_paint.is_zero());
  // In Blink, FCP always arrives after the soft navigation commit has occurred.
  // If the commit is still buffered in `soft_navigation_metrics_`, attach the
  // FCP timestamp directly to the commit entry.
  for (const auto& metrics : soft_navigation_metrics_) {
    if (metrics->performance_timeline_navigation_id ==
        performance_timeline_navigation_id) {
      metrics->first_contentful_paint = first_contentful_paint;
      EnsureSendTimer();
      return;
    }
  }
  // Otherwise, the commit entry was already sent in a previous IPC batch;
  // create a standalone entry to transmit this FCP update.
  auto entry = mojom::SoftNavigationMetrics::New();
  entry->performance_timeline_navigation_id =
      performance_timeline_navigation_id;
  entry->first_contentful_paint = first_contentful_paint;
  soft_navigation_metrics_.emplace_back(std::move(entry));
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidObserveLayoutShift(
    double score,
    bool after_input_or_scroll,
    uint64_t performance_timeline_navigation_id) {
  DCHECK(score > 0);
  render_data_.new_layout_shifts.push_back(mojom::LayoutShift::New(
      base::TimeTicks::Now(), score, after_input_or_scroll,
      performance_timeline_navigation_id));
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidStartResponse(
    const url::SchemeHostPort& final_response_url,
    int resource_id,
    const network::mojom::URLResponseHead& response_head,
    network::mojom::RequestDestination request_destination,
    bool is_ad_resource) {
  // There can be multiple `DidStartResponse` for the same resource id
  // (crbug.com/1504430).
  FindOrInsertPageResourceDataUse(resource_id)
      ->DidStartResponse(final_response_url, resource_id, response_head,
                         request_destination, is_ad_resource);
}

void PageTimingMetricsSender::DidReceiveTransferSizeUpdate(
    int resource_id,
    base::ByteSize received_data_length) {
  // Transfer size updates are called in a throttled manner.
  auto resource_it = page_resource_data_use_.find(resource_id);

  // It is possible that resources are not in the map, if response headers were
  // not received or for failed/cancelled resources.
  if (resource_it == page_resource_data_use_.end()) {
    return;
  }

  resource_it->second->DidReceiveTransferSizeUpdate(received_data_length);
  modified_resources_.insert(resource_it->second.get());
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidCompleteResponse(
    int resource_id,
    const network::URLLoaderCompletionStatus& status) {
  PageResourceDataUse* data_use = FindOrInsertPageResourceDataUse(resource_id);
  data_use->DidCompleteResponse(status);
  modified_resources_.insert(data_use);
  EnsureSendTimer();
}

void PageTimingMetricsSender::DidCancelResponse(int resource_id) {
  auto resource_it = page_resource_data_use_.find(resource_id);
  if (resource_it == page_resource_data_use_.end()) {
    return;
  }
  resource_it->second->DidCancelResponse();
}

void PageTimingMetricsSender::DidLoadResourceFromMemoryCache(
    const GURL& response_url,
    int request_id,
    base::ByteSize encoded_body_length,
    const std::string& mime_type) {
  // In general, we should not observe the same resource being loaded twice in
  // the frame. This is possible due to an existing workaround in
  // ResourceFetcher::EmulateLoadStartedForInspector(). In this case, ignore
  // multiple resources being loaded in the document, as memory cache resources
  // are only reported once per context by design in all other cases.
  if (page_resource_data_use_.contains(request_id)) {
    return;
  }

  FindOrInsertPageResourceDataUse(request_id)
      ->DidLoadFromMemoryCache(response_url, encoded_body_length, mime_type);
}

void PageTimingMetricsSender::OnMainFrameRectangleChanged(
    const gfx::Rect& main_frame_rect) {
  metadata_->main_frame_rect = main_frame_rect;
  EnsureSendTimer();
}

void PageTimingMetricsSender::OnMainFrameViewportRectangleChanged(
    const gfx::Rect& main_frame_viewport_rect) {
  metadata_->main_frame_viewport_rect = main_frame_viewport_rect;
  EnsureSendTimer();
}

void PageTimingMetricsSender::OnMainFrameAdRectangleChanged(
    int element_id,
    const gfx::Rect& ad_rect) {
  metadata_->main_frame_ad_rects[element_id] = ad_rect;
  EnsureSendTimer();
}

void PageTimingMetricsSender::UpdateResourceMetadata(
    int resource_id,
    bool is_main_frame_resource) {
  auto it = page_resource_data_use_.find(resource_id);
  if (it == page_resource_data_use_.end())
    return;

  it->second->SetIsMainFrameResource(is_main_frame_resource);
}

void PageTimingMetricsSender::UpdateCustomUserTimings(
    mojom::CustomUserTimingMarkPtr custom_timing) {
  TRACE_EVENT("loading", "PageTimingMetricsSender::UpdateCustomUserTimings",
              "mark_name", custom_timing->mark_name, "custom_timings_count",
              custom_user_timings_.size());
  custom_user_timings_.push_back(std::move(custom_timing));
  EnsureSendTimer();
}

void PageTimingMetricsSender::Update(
    mojom::PageLoadTimingPtr timing,
    const PageTimingMetadataRecorder::MonotonicTiming& monotonic_timing,
    mojom::FontLoadingMetricsPtr font_loading_metrics) {
  bool timing_changed = !last_timing_->Equals(*timing);
  bool font_metrics_changed =
      font_loading_metrics &&
      (!last_font_loading_metrics_ ||
       !last_font_loading_metrics_->Equals(*font_loading_metrics));

  if (!timing_changed && !font_metrics_changed) {
    return;
  }

  // We want to make sure that each PageTimingMetricsSender is associated
  // with a distinct page navigation. Because we reset the object on commit,
  // we can trash last_timing_ on a provisional load before SendNow() fires.
  if (!last_timing_->navigation_start.is_null() &&
      last_timing_->navigation_start != timing->navigation_start) {
    return;
  }

  // We want to force sending the metrics quickly when some loading milestones
  // are reached (currently parse start, DCL, and FCP) so that the browser can
  // receive the accurate number of events. This accuracy is important to
  // measure the abandoned navigation.
  const bool send_urgently =
      timing_changed && (IsFirstFCP(last_timing_, timing) ||
                         IsFirstParseStart(last_timing_, timing) ||
                         IsFirstDCL(last_timing_, timing));

  if (timing_changed) {
    last_timing_ = std::move(timing);
  }
  if (font_metrics_changed) {
    last_font_loading_metrics_ = std::move(font_loading_metrics);
  }

  metadata_recorder_.UpdateMetadata(monotonic_timing);
  EnsureSendTimer(send_urgently);
}

void PageTimingMetricsSender::DidObserveSoftLargestContentfulPaint(
    mojom::LargestContentfulPaintTimingPtr lcp) {
  soft_largest_contentful_paint_.emplace_back(std::move(lcp));
  EnsureSendTimer();
}

void PageTimingMetricsSender::SendCustomUserTimingMark(
    mojom::CustomUserTimingMarkPtr custom_timing) {
  TRACE_EVENT("loading", "PageTimingMetricsSender::SendCustomUserTimingMark",
              "mark_name", custom_timing->mark_name, "custom_timings_count",
              custom_user_timings_.size());
  // `custom_timing` is sent to the browser to clarify when the abandoned
  // navigation happens. When the navigation is abandoned, the renderer may be
  // busy, so it's important to start IPC and report UMA immediately.
  CHECK(custom_timing);
  sender_->SendCustomUserTiming(std::move(custom_timing));
}

void PageTimingMetricsSender::SendLatest() {
  if (!timer_->IsRunning())
    return;

  timer_->Stop();
  SendNow();
}

void PageTimingMetricsSender::UpdateCpuTiming(base::TimeDelta task_time) {
  last_cpu_timing_->task_time += task_time;
  EnsureSendTimer();
}

void PageTimingMetricsSender::EnsureSendTimer(bool urgent) {
  if (urgent)
    timer_->Stop();
  else if (timer_->IsRunning())
    return;

  int delay_ms;
  if (urgent) {
    // Send as soon as possible, but not synchronously, so that all pending
    // presentation callbacks for the current frame can run first.
    delay_ms = 0;
  } else if (have_sent_ipc_) {
    // This is the typical case.
    delay_ms = buffer_timer_delay_ms_;
  } else {
    // Send the first IPC eagerly to make sure the receiving side knows we're
    // sending metrics as soon as possible.
    delay_ms = kInitialTimerDelayMillis;
  }

  timer_->Start(FROM_HERE, base::Milliseconds(delay_ms),
                base::BindOnce(&PageTimingMetricsSender::SendNow,
                               base::Unretained(this)));
}

void PageTimingMetricsSender::SendNow() {
  have_sent_ipc_ = true;
  std::vector<mojom::ResourceDataUpdatePtr> resources;
  for (PageResourceDataUse* resource : modified_resources_) {
    resources.push_back(resource->GetResourceDataUpdate());
    if (resource->IsFinishedLoading()) {
      page_resource_data_use_.erase(resource->resource_id());
    }
  }

  TRACE_EVENT("loading", "PageTimingMetricsSender::SendNow",
              "custom_user_timings_count", custom_user_timings_.size());

  sender_->SendTiming(
      last_timing_, metadata_, std::move(new_features_), std::move(resources),
      render_data_, last_cpu_timing_, std::move(event_timings_),
      subresource_load_metrics_, std::move(soft_navigation_metrics_),
      std::move(soft_largest_contentful_paint_),
      std::move(custom_user_timings_), last_font_loading_metrics_);

  event_timings_.clear();
  new_features_.clear();
  metadata_->main_frame_rect.reset();
  metadata_->main_frame_viewport_rect.reset();
  metadata_->main_frame_ad_rects.clear();
  last_cpu_timing_->task_time = base::TimeDelta();
  modified_resources_.clear();
  render_data_.new_layout_shifts.clear();
  soft_navigation_metrics_.clear();
  soft_largest_contentful_paint_.clear();
  custom_user_timings_.clear();
  // As PageTimingMetricsSender is owned by MetricsRenderFrameObserver, which is
  // instantiated for each frame, there's no need to make soft_navigation_count_
  // zero here, as its value only increments through the lifetime of the frame.
}

PageResourceDataUse* PageTimingMetricsSender::FindOrInsertPageResourceDataUse(
    int resource_id) {
  auto& data_use = page_resource_data_use_[resource_id];
  if (!data_use) {
    data_use = std::make_unique<PageResourceDataUse>(resource_id);
  }
  return data_use.get();
}

void PageTimingMetricsSender::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) {
  metadata_recorder_.AddInteractionDurationMetadata(max_event_start,
                                                    max_event_end);
  metadata_recorder_.AddInteractionDurationAfterQueueingMetadata(
      max_event_start, max_event_queued_main_thread, max_event_commit_finish,
      max_event_end);
  base::TimeDelta duration = max_event_end - max_event_start;
  event_timings_.push_back(mojom::EventTiming::New(
      duration, interaction_offset, max_event_start, max_event_processing_start,
      performance_timeline_navigation_id));
  EnsureSendTimer();
}
}  // namespace page_load_metrics
