// Copyright 2020 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/browser/renderer_host/page_lifecycle_state_manager.h"

#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/render_process_host.h"
#include "services/service_manager/public/cpp/interface_provider.h"

namespace {
// ASAN builds are slow and we see flakes caused by reaching this timeout. 6s
// was not enough to stop the flakes. Trying 12s just to ensure that there isn't
// something else going on that we don't understand.
// See https://crbug.com/1224355.
#if defined(ADDRESS_SANITIZER)
constexpr base::TimeDelta kBackForwardCacheTimeout = base::Seconds(12);
#else
constexpr base::TimeDelta kBackForwardCacheTimeout = base::Seconds(3);
#endif
base::TimeDelta GetBackForwardCacheEntryTimeout() {
  if (base::FeatureList::IsEnabled(features::kBackForwardCacheEntryTimeout)) {
    return kBackForwardCacheTimeout;
  } else {
    return base::TimeDelta::Max();
  }
}
}

namespace content {

PageLifecycleStateManager::TestDelegate::TestDelegate() = default;

PageLifecycleStateManager::TestDelegate::~TestDelegate() = default;

void PageLifecycleStateManager::TestDelegate::OnLastAcknowledgedStateChanged(
    const blink::mojom::PageLifecycleState& old_state,
    const blink::mojom::PageLifecycleState& new_state) {}

void PageLifecycleStateManager::TestDelegate::OnUpdateSentToRenderer(
    const blink::mojom::PageLifecycleState& new_state) {}

void PageLifecycleStateManager::TestDelegate::OnDeleted() {}

PageLifecycleStateManager::PageLifecycleStateManager(
    RenderViewHostImpl* render_view_host_impl,
    blink::mojom::PageVisibilityState frame_tree_visibility)
    : frame_tree_visibility_(frame_tree_visibility),
      render_view_host_impl_(render_view_host_impl) {
  last_acknowledged_state_ = CalculatePageLifecycleState();
  last_state_sent_to_renderer_ = last_acknowledged_state_.Clone();
}

PageLifecycleStateManager::~PageLifecycleStateManager() {
  if (test_delegate_)
    test_delegate_->OnDeleted();
}

void PageLifecycleStateManager::SetIsFrozen(bool frozen) {
  if (is_set_frozen_called_ == frozen)
    return;
  is_set_frozen_called_ = frozen;

  SendUpdatesToRendererIfNeeded(
      /*page_restore_params=*/nullptr, base::NullCallback());
}

void PageLifecycleStateManager::SetFrameTreeVisibility(
    blink::mojom::PageVisibilityState visibility) {
  if (frame_tree_visibility_ == visibility)
    return;

  frame_tree_visibility_ = visibility;
  SendUpdatesToRendererIfNeeded(
      /*page_restore_params=*/nullptr, base::NullCallback());
  // TODO(yuzus): When a page is frozen and made visible, the page should
  // automatically resume.
}

void PageLifecycleStateManager::SetIsInBackForwardCache(
    bool is_in_back_forward_cache,
    blink::mojom::PageRestoreParamsPtr page_restore_params) {
  if (is_in_back_forward_cache_ == is_in_back_forward_cache)
    return;
  // Prevent races by waiting for confirmation that the renderer will no longer
  // evict the page before allowing it to exit the back-forward cache
  DCHECK(is_in_back_forward_cache ||
         !last_acknowledged_state_->eviction_enabled);
  is_in_back_forward_cache_ = is_in_back_forward_cache;
  eviction_enabled_ = is_in_back_forward_cache;
  if (is_in_back_forward_cache) {
    // When a page is put into BackForwardCache, the page can run a busy loop.
    // Set a timeout monitor to check that the transition finishes within the
    // time limit.
    back_forward_cache_timeout_monitor_.Start(
        FROM_HERE, GetBackForwardCacheEntryTimeout(),
        base::BindOnce(&PageLifecycleStateManager::OnBackForwardCacheTimeout,
                       weak_ptr_factory_.GetWeakPtr()));
    pagehide_dispatch_ = blink::mojom::PagehideDispatch::kDispatchedPersisted;
  } else {
    DCHECK(page_restore_params);
    // When a page is restored from the back-forward cache, we should reset the
    // |pagehide_dispatch_| state so that we'd dispatch the
    // events again the next time we navigate away from the page.
    pagehide_dispatch_ = blink::mojom::PagehideDispatch::kNotDispatched;
  }

  SendUpdatesToRendererIfNeeded(std::move(page_restore_params),
                                base::NullCallback());
}

blink::mojom::PageLifecycleStatePtr
PageLifecycleStateManager::SetPagehideDispatchDuringNewPageCommit(
    bool persisted) {
  pagehide_dispatch_ =
      persisted ? blink::mojom::PagehideDispatch::kDispatchedPersisted
                : blink::mojom::PagehideDispatch::kDispatchedNotPersisted;
  // We've only modified |pagehide_dispatch_| here, but the "visibility"
  // property of |last_state_sent_to_renderer_| calculated from
  // CalculatePageLifecycleState() below will be set to kHidden because it
  // depends on the value of |pagehide_dispatch_|.
  last_state_sent_to_renderer_ = CalculatePageLifecycleState();
  DCHECK_EQ(last_state_sent_to_renderer_->visibility,
            blink::mojom::PageVisibilityState::kHidden);

  // We don't need to call SendUpdatesToRendererIfNeeded() because the update
  // will be sent through an OldPageInfo parameter in the CommitNavigation IPC.
  return last_state_sent_to_renderer_.Clone();
}

void PageLifecycleStateManager::DidSetPagehideDispatchDuringNewPageCommit(
    blink::mojom::PageLifecycleStatePtr acknowledged_state) {
  DCHECK_EQ(acknowledged_state->visibility,
            blink::mojom::PageVisibilityState::kHidden);
  DCHECK_NE(acknowledged_state->pagehide_dispatch,
            blink::mojom::PagehideDispatch::kNotDispatched);
  OnPageLifecycleChangedAck(std::move(acknowledged_state),
                            base::NullCallback());
}

void PageLifecycleStateManager::SetIsLeavingBackForwardCache(
    base::OnceClosure done_cb) {
  DCHECK(is_in_back_forward_cache_);
  eviction_enabled_ = false;
  SendUpdatesToRendererIfNeeded(nullptr, std::move(done_cb));
}

bool PageLifecycleStateManager::RendererExpectedToSendChannelAssociatedIpcs()
    const {
  // eviction_enabled_ => is_in_back_forward_cache_
  DCHECK(!eviction_enabled_ || is_in_back_forward_cache_);
  return !eviction_enabled_ || !last_acknowledged_state_->eviction_enabled;
}

void PageLifecycleStateManager::SendUpdatesToRendererIfNeeded(
    blink::mojom::PageRestoreParamsPtr page_restore_params,
    base::OnceClosure done_cb) {
  if (!render_view_host_impl_->GetAssociatedPageBroadcast()) {
    // TODO(https://crbug.com/1153155): For some tests, |render_view_host_impl_|
    // does not have the associated page.
    if (done_cb) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(done_cb));
    }
    return;
  }

  auto new_state = CalculatePageLifecycleState();
  if (last_state_sent_to_renderer_ &&
      last_state_sent_to_renderer_.Equals(new_state)) {
    // TODO(yuzus): Send updates to renderer only when the effective state (per
    // page lifecycle state) has changed since last sent to renderer. It is
    // possible that the web contents state has changed but the effective state
    // has not.
  }

  last_state_sent_to_renderer_ = new_state.Clone();
  auto state = new_state.Clone();

  if (test_delegate_)
    test_delegate_->OnUpdateSentToRenderer(*last_state_sent_to_renderer_);

  render_view_host_impl_->GetAssociatedPageBroadcast()->SetPageLifecycleState(
      std::move(state), std::move(page_restore_params),
      base::BindOnce(&PageLifecycleStateManager::OnPageLifecycleChangedAck,
                     weak_ptr_factory_.GetWeakPtr(), std::move(new_state),
                     std::move(done_cb)));
}

blink::mojom::PageLifecycleStatePtr
PageLifecycleStateManager::CalculatePageLifecycleState() {
  auto state = blink::mojom::PageLifecycleState::New();
  state->is_in_back_forward_cache = is_in_back_forward_cache_;
  state->is_frozen = is_in_back_forward_cache_ ? true : is_set_frozen_called_;
  state->pagehide_dispatch = pagehide_dispatch_;
  // If a page is stored in the back-forward cache, or we have already
  // dispatched/are dispatching pagehide for the page, it should be treated as
  // "hidden" regardless of what |frame_tree_visibility_| is set to.
  state->visibility =
      (is_in_back_forward_cache_ ||
       pagehide_dispatch_ != blink::mojom::PagehideDispatch::kNotDispatched)
          ? blink::mojom::PageVisibilityState::kHidden
          : frame_tree_visibility_;
  state->eviction_enabled = eviction_enabled_;
  return state;
}

void PageLifecycleStateManager::OnPageLifecycleChangedAck(
    blink::mojom::PageLifecycleStatePtr acknowledged_state,
    base::OnceClosure done_cb) {
  blink::mojom::PageLifecycleStatePtr old_state =
      std::move(last_acknowledged_state_);

  last_acknowledged_state_ = std::move(acknowledged_state);

  if (last_acknowledged_state_->is_in_back_forward_cache)
    did_receive_back_forward_cache_ack_ = true;

  // Call |MaybeEvictFromBackForwardCache| after setting
  // |last_acknowledged_state_|.
  // Features which can be cleaned by the page are taken into account only
  // after the 'pagehide' handlers have run. As we might have just received
  // an acknowledgement from the renderer that these handlers have run, call
  // |MaybeEvictFromBackForwardCache| in case we need to start taking these
  // features into account.
  render_view_host_impl_->MaybeEvictFromBackForwardCache();

  // A page that has not yet received an acknowledgement from renderer is not
  // counted against the cache size limit because it might still be ineligible
  // for caching after the ack, i.e., after running handlers. After it receives
  // the ack and we call |MaybeEvictFromBackForwardCache()|, we know whether it
  // is eligible for caching and we should reconsider the cache size limits
  // again.
  render_view_host_impl_->EnforceBackForwardCacheSizeLimit();

  if (last_acknowledged_state_->is_in_back_forward_cache) {
    back_forward_cache_timeout_monitor_.Stop();
  }

  if (test_delegate_) {
    test_delegate_->OnLastAcknowledgedStateChanged(*old_state,
                                                   *last_acknowledged_state_);
  }
  if (done_cb)
    std::move(done_cb).Run();
}

void PageLifecycleStateManager::OnBackForwardCacheTimeout() {
  DCHECK(!last_acknowledged_state_->is_in_back_forward_cache);
  render_view_host_impl_->OnBackForwardCacheTimeout();
  back_forward_cache_timeout_monitor_.Stop();
}

void PageLifecycleStateManager::SetDelegateForTesting(
    PageLifecycleStateManager::TestDelegate* test_delegate) {
  DCHECK(!test_delegate_ || !test_delegate);
  test_delegate_ = test_delegate;
}

}  // namespace content