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

#include "content/renderer/renderer_navigation_metrics_manager.h"

#include "base/check.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/trace_id_helper.h"
#include "content/renderer/render_thread_impl.h"
#include "third_party/blink/public/common/tracing_support.h"

namespace content {

namespace {

// If a navigation hasn't completed within this timeout, its timeline data will
// be cleaned up. See also cleanup comments in GetOrCreateTimeline() below.
constexpr base::TimeDelta kLazyCleanupTimeout = base::Seconds(300);

// Kill switch for `RendererNavigationMetricsManager`'s generation of renderer
// trace events and metrics, in case they cause any unexpected overhead or other
// issues. See https://crbug.com/415821826.
BASE_FEATURE(kEnableRendererNavigationTimeline,
             base::FEATURE_ENABLED_BY_DEFAULT);

// Used to record how ready a renderer process is for an incoming
// CommitNavigation IPC. Please keep in sync with "RendererProcessReadiness" in
// tools/metrics/histograms/metadata/navigation/enums.xml. These values are
// persisted to logs. Entries should not be renumbered and numeric values should
// never be reused.
//
// LINT.IfChange(RendererProcessReadiness)
enum class RendererProcessReadiness {
  // The commit IPC was sent before the process was ready. This implies that
  // we should've started the renderer process earlier.
  kProcessNotReady = 0,
  // Renderer was ready, but the preparatory work of creating the
  // view/proxy/frame objects wasn't complete before the commit IPC was sent.
  // This implies that the speculative RenderFrameHost should've been created
  // earlier.
  kViewProxyFrameNotReady = 1,
  // Renderer was ready to process the commit IPC right away.
  kReadyToProcessCommitIPC = 2,

  kMaxValue = kReadyToProcessCommitIPC
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/navigation/enums.xml:RendererProcessReadiness)

void RecordProcessReadiness(bool is_main_frame,
                            RendererProcessReadiness readiness) {
  base::UmaHistogramEnumeration("Navigation.Renderer.ProcessReadiness",
                                readiness);
  if (is_main_frame) {
    base::UmaHistogramEnumeration(
        "Navigation.Renderer.ProcessReadiness.MainFrameOnly", readiness);
  }

  std::string readiness_description;
  switch (readiness) {
    case RendererProcessReadiness::kProcessNotReady:
      readiness_description = "Process not ready";
      break;
    case RendererProcessReadiness::kViewProxyFrameNotReady:
      readiness_description = "View/proxy/frame not ready";
      break;
    case RendererProcessReadiness::kReadyToProcessCommitIPC:
      readiness_description = "Ready to process commit IPC";
      break;
  }
  // Also emit an instant trace event to expose process readiness in traces.
  TRACE_EVENT_INSTANT(
      "navigation",
      "RendererNavigationMetricsManager::RecordTraceEventsAndMetrics",
      "ProcessReadiness", readiness_description);
}

}  // namespace

RendererNavigationMetricsManager& RendererNavigationMetricsManager::Instance() {
  static base::NoDestructor<RendererNavigationMetricsManager> manager;
  return *manager.get();
}

RendererNavigationMetricsManager::RendererNavigationMetricsManager() = default;
RendererNavigationMetricsManager::~RendererNavigationMetricsManager() = default;

RendererNavigationMetricsManager::Timeline::Timeline() = default;
RendererNavigationMetricsManager::Timeline::~Timeline() = default;

RendererNavigationMetricsManager::Timeline&
RendererNavigationMetricsManager::GetOrCreateTimeline(
    const base::UnguessableToken& navigation_metrics_token) {
  auto it = timelines_.find(navigation_metrics_token);
  if (it != timelines_.end()) {
    return it->second;
  }

  // Create a new Timeline object for this navigation.
  Timeline& timeline = timelines_[navigation_metrics_token];

  // Post a task to clean up the new timeline after a timeout, if the
  // corresponding navigation hasn't finished by then.
  //
  // Ideally, we would detect navigation cancellation and remove the timeline at
  // that point. Unfortunately, this is not as easy as it seems:
  // - IPCs for destroying previously created proxies, views, and provisional
  //   frames would all have to identify which navigation they pertained to, if
  //   any, which would add a lot of complexity.
  // - detecting NavigationClient interface disconnection seems promising, but
  //   unfortunately doesn't work for cross-process browser-initiated
  //   navigations where the commit NavigationClient isn't set up until
  //   ready-to-commit time, after proxy/view creation.
  //
  // As a longer-term solution, it might be possible to set up the commit
  // NavigationClient earlier and the corresponding `Timeline` directly on it.
  // However, this will also be tricky, since the current NavigationClient
  // lifetime also impacts web compatibility, by defining when a
  // renderer-initiated navigation may be canceled by JavaScript - see comments
  // on `RenderFrameImpl::navigation_client_impl_`.
  timeline.lazy_cleanup_timer_.Start(
      FROM_HERE, kLazyCleanupTimeout,
      base::BindOnce(
          [](const base::UnguessableToken& token) {
            RendererNavigationMetricsManager::Instance().timelines_.erase(
                token);
          },
          navigation_metrics_token));

  // Remember that this process has started at least one navigation.
  timeline.is_first_navigation_in_this_process = !has_first_navigation_started_;
  has_first_navigation_started_ = true;

  return timeline;
}

void RendererNavigationMetricsManager::AddCreateViewEvent(
    const std::optional<base::UnguessableToken>& navigation_metrics_token,
    const base::TimeTicks& start_time,
    const base::TimeDelta& elapsed_time) {
  if (!base::FeatureList::IsEnabled(kEnableRendererNavigationTimeline)) {
    return;
  }

  // Don't record any metrics if this event was not for a navigation.
  if (!navigation_metrics_token) {
    return;
  }

  auto& timeline = GetOrCreateTimeline(*navigation_metrics_token);
  timeline.create_view_events.emplace_back(start_time,
                                           start_time + elapsed_time);
}

void RendererNavigationMetricsManager::AddCreateRemoteChildrenEvent(
    const std::optional<base::UnguessableToken>& navigation_metrics_token,
    const base::TimeTicks& start_time,
    const base::TimeDelta& elapsed_time) {
  if (!base::FeatureList::IsEnabled(kEnableRendererNavigationTimeline)) {
    return;
  }

  // Don't record any metrics if this event was not for a navigation.
  if (!navigation_metrics_token) {
    return;
  }

  auto& timeline = GetOrCreateTimeline(*navigation_metrics_token);
  timeline.create_remote_children_events.emplace_back(
      start_time, start_time + elapsed_time);
}

void RendererNavigationMetricsManager::AddCreateFrameEvent(
    const std::optional<base::UnguessableToken>& navigation_metrics_token,
    const base::TimeTicks& start_time,
    const base::TimeDelta& elapsed_time) {
  if (!base::FeatureList::IsEnabled(kEnableRendererNavigationTimeline)) {
    return;
  }

  // Don't record any metrics if this event was not for a navigation.
  if (!navigation_metrics_token) {
    return;
  }

  auto& timeline = GetOrCreateTimeline(*navigation_metrics_token);

  // Typically, there's one CreateFrame call per navigation, corresponding to
  // the provisional frame that will eventually commit the navigation. However,
  // it's possible that a navigation will pick a different RenderFrame at
  // response time, which could end up being created in the same renderer
  // process. In this case, for now, capture the start/end times of the latest
  // CreateFrame call (which is more relevant for navigation latency), by
  // overwriting the start/end times if they already exist. In the future,
  // these calls could potentially be tracked as separate events.
  timeline.create_frame_event.emplace(start_time, start_time + elapsed_time);
}

void RendererNavigationMetricsManager::MarkCommitStart(
    const base::UnguessableToken& navigation_metrics_token) {
  if (!base::FeatureList::IsEnabled(kEnableRendererNavigationTimeline)) {
    return;
  }

  GetOrCreateTimeline(navigation_metrics_token).commit_start =
      base::TimeTicks().Now();
}

void RendererNavigationMetricsManager::RecordTraceEventsAndMetrics(
    const RendererNavigationMetricsManager::Timeline& timeline,
    const GURL& url,
    const blink::LocalFrameToken& frame_token) {
  CHECK(!timeline.navigation_start.is_null())
      << "Navigation start time not found for " << url;

  RenderThreadImpl* render_thread = RenderThreadImpl::current();
  // The `render_thread` may be null in tests.
  if (!render_thread) {
    return;
  }

  // Record these trace events in a child track of the frame track.
  perfetto::NamedTrack track(
      "Navigation: Timelines (Renderer)",
      base::trace_event::GetNextGlobalTraceId(),
      blink::GetLocalFrameTracingTrack(frame_token, timeline.is_main_frame));

  // Define a helper to log both a trace event slice and a corresponding metric
  // for one stage of a navigation.
  //
  // Note: A similar helper exists to log browser-side navigation timeline
  // events in RecordNavigationTraceEventsAndMetrics(). When adding new code
  // here, consider whether the browser-side helper also needs to be updated.
  // It might be desirable to merge the two helpers in the future.
  auto log_trace_event_and_uma =
      [&](perfetto::StaticString name, const base::TimeTicks& begin_time,
          const base::TimeTicks& end_time,
          const std::optional<std::string>& histogram_name = std::nullopt,
          const std::optional<std::string>& url = std::nullopt) {
        if (begin_time.is_null() || end_time.is_null()) {
          return;
        }

        TRACE_EVENT_BEGIN(
            "navigation", name, track, begin_time,
            [&](perfetto::EventContext& ctx) {
              if (!url.has_value()) {
                return;
              }
              perfetto::protos::pbzero::PageLoad* page_load =
                  ctx.event<perfetto::protos::pbzero::ChromeTrackEvent>()
                      ->set_page_load();
              page_load->set_url(*url);
            });
        TRACE_EVENT_END("navigation", track, end_time);

        // When provided, `histogram_name` is used to avoid including variable
        // or sensitive data in the reported metric name. For example, `name`
        // may include the navigation URL when measuring the start-to-finish
        // time, but we only want to use that for trace events and omit the
        // URL in metric names for UMA.
        base::UmaHistogramTimes(
            base::StrCat({"Navigation.Renderer.Timeline.",
                          histogram_name.value_or(std::string(name.value)),
                          ".Duration"}),
            end_time - begin_time);
      };

  // Actual navigation events are logged below in contiguous (or nested)
  // intervals.
  // TODO(crbug.com/405437928): Overlapping navigations may incorrectly appear
  // to be nested, using the wrong end times.
  log_trace_event_and_uma("Renderer Navigation", timeline.navigation_start,
                          timeline.commit_end,
                          /*histogram_name=*/"Total");

  // Emit a trace event with url in the name for convenience. Do this in a
  // separate trace event from the one above, since events with dynamic strings
  // are filtered out in some traces, and the event above would still be useful
  // in that case.
  // TODO(crbug.com/415720503): Remove once Perfetto navigation plugins surfaces
  // urls.
  std::string top_level_trace_event_name = "URL: " + url.spec();
  TRACE_EVENT_BEGIN("navigation",
                    perfetto::DynamicString(top_level_trace_event_name), track,
                    timeline.navigation_start);
  TRACE_EVENT_END("navigation", track, timeline.commit_end);

  // It's possible that the process was still starting when the navigation
  // started. In that case, record an event which measures the time for the
  // process to finish starting up and become ready for processing IPCs, and
  // treat that point as the starting point for the next event.
  base::TimeTicks process_ready_time = render_thread->run_loop_start_time();
  if (timeline.navigation_start < process_ready_time) {
    log_trace_event_and_uma("WaitingForProcessReady", timeline.navigation_start,
                            process_ready_time);
  } else if (timeline.is_first_navigation_in_this_process) {
    // If this was the first navigation in this renderer process, and the
    // process was ready before the navigation started, record a zero-sized
    // event for WaitingForProcessReady. This allows measuring how much of a
    // problem process startup costs are for navigations in a freshly created
    // process.
    log_trace_event_and_uma("WaitingForProcessReady", timeline.navigation_start,
                            timeline.navigation_start);
  }

  // Create an event for each CreateView IPC. There could be multiple if there
  // are multiple pages on the navigating frame's opener chain. There could also
  // be no CreateView IPCs at all, for example if a subframe navigates
  // same-origin.
  for (const auto& event : timeline.create_view_events) {
    log_trace_event_and_uma("CreateView", event.start, event.end);
  }

  // Create an event for each CreateRemoteChildren IPC, which creates all
  // subframe proxies for a particular page/frame tree. There could be multiple
  // of these IPCs per navigation if there are multiple pages on the navigating
  // frame's opener chain. Note that main frame proxies are created as part of
  // CreateView, and while there is also a CreateRemoteChild IPC to create
  // an individual proxy, it is not currently used in the navigation flow, so it
  // is not traced here.
  for (const auto& event : timeline.create_remote_children_events) {
    log_trace_event_and_uma("CreateChildProxies", event.start, event.end);
  }

  // Create an event for processing the CreateFrame IPC. Note that CreateFrame
  // may not happen if the navigation is staying in a previous RenderFrame, e.g.
  // for browser-initiated same-document navigations, navigations out of the
  // initial empty document, or same-site navigations when RenderDocument is
  // turned off.
  if (timeline.create_frame_event) {
    log_trace_event_and_uma("CreateFrame", timeline.create_frame_event->start,
                            timeline.create_frame_event->end);
  }

  // Record a metric to measure how ready the renderer process is to process a
  // navigation commit IPC.

  if (timeline.commit_sent < process_ready_time) {
    // The commit IPC was sent before the process was ready. This implies that
    // we should've started the renderer process earlier.
    RecordProcessReadiness(timeline.is_main_frame,
                           RendererProcessReadiness::kProcessNotReady);
  } else if (timeline.create_frame_event &&
             timeline.commit_sent < timeline.create_frame_event->end) {
    // Renderer was ready, but the prerequisite work of creating the
    // view/proxy/frame objects wasn't completed before the commit IPC was sent.
    // This implies that the speculative RenderFrameHost should've been created
    // earlier. Note that CreateFrame's end time is used here because it is the
    // last view/proxy/frame creation IPC to be processed; not having
    // CreateFrame for this navigation implies there also was no view or proxy
    // creation, and the navigation commit wasn't blocked waiting for any of
    // them.
    RecordProcessReadiness(timeline.is_main_frame,
                           RendererProcessReadiness::kViewProxyFrameNotReady);
  } else {
    // Renderer was ready to process the commit IPC without waiting for process
    // startup or prerequisite frame/proxy/view creation IPCs.
    RecordProcessReadiness(timeline.is_main_frame,
                           RendererProcessReadiness::kReadyToProcessCommitIPC);
  }

  log_trace_event_and_uma("CommitToDidCommit", timeline.commit_start,
                          timeline.commit_end);
}

void RendererNavigationMetricsManager::ProcessNavigationCommit(
    const base::UnguessableToken& navigation_metrics_token,
    const GURL& url,
    const base::TimeTicks& navigation_start_time,
    const base::TimeTicks& commit_sent_time,
    bool is_main_frame,
    const blink::LocalFrameToken& frame_token) {
  if (!base::FeatureList::IsEnabled(kEnableRendererNavigationTimeline)) {
    return;
  }

  auto it = timelines_.find(navigation_metrics_token);
  // The timeline may not exist for synchronous about:blank commits or
  // renderer-initiated same-document navigations. For now, do not record
  // anything for these cases.
  if (it == timelines_.end()) {
    return;
  }

  auto& timeline = it->second;
  timeline.navigation_start = navigation_start_time;
  timeline.commit_sent = commit_sent_time;
  timeline.commit_end = base::TimeTicks().Now();
  timeline.is_main_frame = is_main_frame;
  RecordTraceEventsAndMetrics(timeline, url, frame_token);

  // Remove the timeline from the map and cancel the cleanup timer.
  timeline.lazy_cleanup_timer_.Stop();
  timelines_.erase(it);
}

}  // namespace content
