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

#include "third_party/blink/renderer/core/timing/soft_navigation_context.h"

#include "base/feature_list.h"
#include "base/trace_event/trace_event.h"
#include "third_party/blink/renderer/core/dom/container_node.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/paint/timing/largest_contentful_paint_calculator.h"
#include "third_party/blink/renderer/core/paint/timing/paint_timing_record.h"
#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
#include "third_party/blink/renderer/core/timing/interaction_contentful_paint.h"
#include "third_party/blink/renderer/core/timing/interaction_effects_monitor.h"
#include "third_party/blink/renderer/core/timing/largest_contentful_paint.h"
#include "third_party/blink/renderer/core/timing/soft_navigation_heuristics.h"
#include "third_party/blink/renderer/core/timing/window_performance.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"

namespace blink {

uint64_t SoftNavigationContext::last_context_id_ = 0;

SoftNavigationContext::SoftNavigationContext(
    LocalDOMWindow& window,
    PerformanceEventTiming* initial_event_timing)
    : window_(&window),
      lcp_calculator_(MakeGarbageCollected<LargestContentfulPaintCalculator>(
          DOMWindowPerformance::performance(window),
          this)),
      initial_event_timing_(initial_event_timing),
      track_(perfetto::NamedTrack::FromPointer("blink::SoftNavigation", this)) {
  CHECK(initial_event_timing_);
  CHECK(initial_event_timing_->IsInteraction());

  TRACE_EVENT_BEGIN("loading", "SoftNavigation", track_, TimeOrigin());

  TRACE_EVENT_INSTANT("loading", "SoftNavigationContextCreated", track_,
                      "context", *this);

  GetSoftNavigationHeuristics()->ForEachInteractionEffectsMonitor(
      [&](InteractionEffectsMonitor& monitor) {
        monitor.OnSoftNavigationContextCreated();
      });
}

PerformanceTimelineEntryIdInfo SoftNavigationContext::GetInteractionIdInfo()
    const {
  return initial_event_timing_->GetInteractionIdInfo().value();
}

SoftNavigationHeuristics* SoftNavigationContext::GetSoftNavigationHeuristics()
    const {
  // Before this context is Garbage-collected, it may become disposed, and
  // window_ may get cleared.
  if (HasBeenShutdown()) {
    return nullptr;
  }
  return window_->GetSoftNavigationHeuristics();
}

base::TimeTicks SoftNavigationContext::TimeOrigin() const {
  return initial_event_timing_->GetStartTime();
}

void SoftNavigationContext::AddUrl(
    const String& url,
    V8NavigationType::Enum navigation_type,
    base::UnguessableToken same_document_metrics_token) {
  // The navigation layer should never pass an empty URL.
  CHECK(!url.empty());

  // An interaction can lead to multiple URL changes, e.g. because of
  // client-side redirects. Subsequent URL changes are no-ops.
  if (!initial_url_.empty()) {
    return;
  }
  initial_url_ = url;
  navigation_type_ = navigation_type;
  same_document_metrics_token_ = same_document_metrics_token;
  url_change_time_ = base::TimeTicks::Now();
}

void SoftNavigationContext::AddModifiedNode(Node* node) {
  ++num_modified_dom_nodes_;
  TRACE_EVENT_INSTANT(
      "loading", "SoftNavigationContext::AddedModifiedNodeInAnimationFrame",
      track_, "context", this, "nodeId", node->GetDomNodeId(), "nodeDebugName",
      node->DebugName(), "domModificationsThisAnimationFrame",
      num_modified_dom_nodes_ - num_modified_dom_nodes_last_animation_frame_);
}

bool SoftNavigationContext::AddPaintedArea(PaintTimingRecord* record) {
  // Stop recording paints once we have next input/scroll.
  if (!first_input_or_scroll_time_.is_null()) {
    return false;
  }

  const gfx::RectF& rect = record->RootVisualRect();
  uint64_t painted_area = rect.size().GetArea();

  Node* node = record->GetNode();
  // TODO(crbug.com/441914208): `node` can be null here, which is unexpected.
  // Change this back to a CHECK when the root cause is understood and fixed.
  if (!node) {
    return false;
  }

  painted_area_ += painted_area;
  TRACE_EVENT_INSTANT(
      "loading", "SoftNavigationContext::AttributablePaintInAnimationFrame",
      track_, "context", this, "nodeId", node->GetDomNodeId(), "nodeDebugName",
      node->DebugName(), "rect_x", rect.x(), "rect_y", rect.y(), "rect_width",
      rect.width(), "rect_height", rect.height(),
      "paintedAreaThisAnimationFrame",
      painted_area_ - painted_area_last_animation_frame_);

  // TODO(crbug.com/434159332): This doesn't currently match hard-FCP semantics
  // because we aren't notified about images paints until they are "sufficiently
  // loaded", which is needed for LCP/ICP.
  if (!first_image_or_text_) {
    first_image_or_text_ = record;
  }

  return true;
}

bool SoftNavigationContext::SatisfiesSoftNavNonPaintCriteria() const {
  // TODO(crbug.com/490814752): Event StartTime value seems to be missing in
  // some unittests.  It should not be missing from any real events.
  if (TimeOrigin().is_null()) {
    return false;
  }
  // These start false, and become true as we observe effects.
  if (!HasDomModification() || !HasUrl()) {
    return false;
  }
  CHECK(!UrlChangeTime().is_null());
  CHECK(!TimeOrigin().is_null());
  return true;
}

bool SoftNavigationContext::SatisfiesSoftNavPaintCriteria(
    uint64_t required_paint_area) const {
  return painted_area_ >= required_paint_area;
}

bool SoftNavigationContext::OnPaintFinished() {
  auto num_modded_new_nodes =
      num_modified_dom_nodes_ - num_modified_dom_nodes_last_animation_frame_;
  auto new_painted_area = painted_area_ - painted_area_last_animation_frame_;

  // TODO(crbug.com/353218760): Consider reporting if any of the values change
  // if we have an extra loud tracing debug mode.
  if (num_modded_new_nodes || new_painted_area) {
    TRACE_EVENT_INSTANT("loading", "SoftNavigationContext::OnPaintFinished",
                        track_, "context", this, "numModdedNewNodes",
                        num_modded_new_nodes, "newPaintedArea",
                        new_painted_area);
  }

  if (new_painted_area > 0) {
    GetSoftNavigationHeuristics()->ForEachInteractionEffectsMonitor(
        [&](InteractionEffectsMonitor& monitor) {
          monitor.OnContentfulPaint(this, new_painted_area);
        });
  }

  num_modified_dom_nodes_last_animation_frame_ = num_modified_dom_nodes_;
  painted_area_last_animation_frame_ = painted_area_;

  return new_painted_area > 0;
}

void SoftNavigationContext::OnInputOrScroll() {
  if (!first_input_or_scroll_time_.is_null()) {
    return;
  }
  TRACE_EVENT_INSTANT("loading", "SoftNavigationContext::OnInputOrScroll",
                      "painted_area", painted_area_);
  // Between interaction and first painted area, we allow other inputs or
  // scrolling to happen.  Once we observe the first paint, we have to constrain
  // to that initial viewport, or else the viewport area and set of candidates
  // gets messy.
  if (!painted_area_) {
    return;
  }
  first_input_or_scroll_time_ = base::TimeTicks::Now();
}

void SoftNavigationContext::OnFramePresented(
    LargestContentfulPaintCalculator::LcpCandidates* candidates) {
  // TODO(crbug.com/454082773): Input should not invalidate pending presentation
  // feedback, but this can happen due to scheduling races.
  CHECK(IsRecordingLargestContentfulPaint());
  lcp_calculator_->OnFramePresented(candidates);
}

const LargestContentfulPaintDetails&
SoftNavigationContext::LatestLcpDetailsForUkm() {
  return lcp_calculator_->LatestLcpDetails();
}

void SoftNavigationContext::WriteIntoTrace(
    perfetto::TracedValue context) const {
  // Ensure we don't try to trace after shutdown has been called.  If you want
  // to trace the final values-- do so right before shutdown.
  CHECK(!HasBeenShutdown());
  perfetto::TracedDictionary dict = std::move(context).WriteDictionary();

  dict.Add("softNavContextId", context_id_);
  dict.Add("performanceTimelineNavigationId", navigation_id_.web_exposed_id);

  dict.Add("URL", AttributionUrl());
  dict.Add("timeOrigin", TimeOrigin());
  dict.Add("urlChangeTime", url_change_time_);
  dict.Add("processingEnd", initial_event_timing_->GetEventTimingReportingInfo()
                                ->processing_end_time);
  dict.Add("firstContentfulPaint", FirstContentfulPaint());

  dict.Add("domModifications", num_modified_dom_nodes_);
  dict.Add("paintedArea", painted_area_);
}

void SoftNavigationContext::Trace(Visitor* visitor) const {
  visitor->Trace(lcp_calculator_);
  visitor->Trace(first_image_or_text_);
  visitor->Trace(window_);
  visitor->Trace(largest_icp_entry_);
  visitor->Trace(current_lcp_entry_);
  visitor->Trace(initial_event_timing_);
}

void SoftNavigationContext::Shutdown() {
  TRACE_EVENT_END("loading", track_);

  lcp_calculator_ = nullptr;
  first_image_or_text_ = nullptr;
  window_ = nullptr;
  largest_icp_entry_ = nullptr;
  current_lcp_entry_ = nullptr;
  initial_event_timing_ = nullptr;
}

void SoftNavigationContext::EmitSoftNavigation() {
  CHECK(!HasBeenShutdown());
  CHECK(!WasEmitted());
  CHECK(HasFirstContentfulPaint());
  CHECK(SatisfiesSoftNavNonPaintCriteria());
  was_emitted_ = true;

  if (base::FeatureList::IsEnabled(kSoftNavigationTraceEvents)) {
    // This trace event reports the TimeOrigin() value which we already report
    // as part of the umbrella "SoftNavigation" trace, as an instant event.
    // However, that other event reports all new *potential* soft navs, while
    // this event only reports actually *emitted* soft navs.
    // This is used by DevTools performance profiler to mark the perf timeline.
    TRACE_EVENT_INSTANT("scheduler,devtools.timeline,loading",
                        "SoftNavigationStart", track_, TimeOrigin(), "context",
                        *this, "frame",
                        GetFrameIdForTracing(window_->GetFrame()));

    // This trace event reports the when the soft nav heuristics were observerd,
    // and thus when the new navigationId was created, and when the performance
    // timeline is logically "sliced" into soft-nav sub-timelines.
    TRACE_EVENT_INSTANT("scheduler,devtools.timeline,loading",
                        "SoftNavigationEmitted", track_,
                        soft_navigation_slicing_time_, "context", *this);
  }

  if (!RuntimeEnabledFeatures::SoftNavigationHeuristicsEnabled(window_)) {
    return;
  }

  WindowPerformance* performance = DOMWindowPerformance::performance(*window_);
  CHECK(performance);
  performance->AddSoftNavigation(TimeOrigin(), FirstContentfulPaintTimingInfo(),
                                 this);
}

void SoftNavigationContext::Dispose() {
  if (HasBeenShutdown()) {
    return;
  }
  // `heuristics` will be null if the `window_` was detached but this context
  // wasn't shut down by the associated `SoftNavigationHeuristics`, which
  // happens in some unit tests where the context isn't created by the SNH.
  SoftNavigationHeuristics* heuristics = GetSoftNavigationHeuristics();
  if (!heuristics) {
    return;
  }
  heuristics->OnContextDisposed(this);
}

void SoftNavigationContext::EmitLcpPerformanceEntry(
    const DOMPaintTimingInfo& paint_timing_info,
    uint64_t paint_size,
    base::TimeTicks load_time,
    const AtomicString& id,
    const String& url,
    Element* element) {
  if (!RuntimeEnabledFeatures::SoftNavigationHeuristicsEnabled(window_)) {
    return;
  }
  // This should not be called after we've been shut down.
  CHECK(!HasBeenShutdown());

  WindowPerformance* performance = DOMWindowPerformance::performance(*window_);

  auto* lcp_entry = MakeGarbageCollected<LargestContentfulPaint>(
      /*start_time=*/paint_timing_info.presentation_time,
      /*render_time=*/paint_timing_info.presentation_time, paint_size,
      performance->MonotonicTimeToDOMHighResTimeStamp(load_time), id, url,
      element, window_, performance->NavigationId().web_exposed_id);
  lcp_entry->SetPaintTimingInfo(paint_timing_info);

  current_lcp_entry_ = lcp_entry;

  auto* entry = MakeGarbageCollected<InteractionContentfulPaint>(
      /*start_time=*/performance->MonotonicTimeToDOMHighResTimeStamp(
          TimeOrigin()),
      /*render_time=*/paint_timing_info.presentation_time, current_lcp_entry_,
      window_, performance->NavigationId().web_exposed_id,
      initial_event_timing_->interactionId());
  entry->SetPaintTimingInfo(paint_timing_info);
  performance->OnInteractionContentfulPaintUpdated(entry);

  largest_icp_entry_ = entry;
}

void SoftNavigationContext::OnLcpMetricsForReportingChanged() {
  GetSoftNavigationHeuristics()->UpdateSoftLcpMetricsForContext(this);
}

}  // namespace blink
