| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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/callback_helpers.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "content/browser/renderer_host/render_view_host_impl.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "services/service_manager/public/cpp/interface_provider.h" |
| #include "third_party/blink/public/common/frame/event_page_show_persisted.h" |
| #include "third_party/blink/public/common/page/page_lifecycle_state_updater.h" |
| |
| namespace { |
| constexpr base::TimeDelta kBackForwardCacheTimeoutInSeconds = base::Seconds(3); |
| } |
| |
| 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(), |
| /*restoring_main_frame_from_back_forward_cache=*/false); |
| } |
| |
| void PageLifecycleStateManager::SetFrameTreeVisibility( |
| blink::mojom::PageVisibilityState visibility) { |
| if (frame_tree_visibility_ == visibility) |
| return; |
| |
| frame_tree_visibility_ = visibility; |
| SendUpdatesToRendererIfNeeded( |
| /*page_restore_params=*/nullptr, base::NullCallback(), |
| /*restoring_main_frame_from_back_forward_cache=*/false); |
| // 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, |
| bool restoring_main_frame_from_back_forward_cache) { |
| 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, kBackForwardCacheTimeoutInSeconds, |
| 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(), |
| restoring_main_frame_from_back_forward_cache); |
| } |
| |
| 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), |
| /*restoring_main_frame_from_back_forward_cache=*/false); |
| } |
| |
| 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, |
| bool restoring_main_frame_from_back_forward_cache) { |
| 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::ThreadTaskRunnerHandle::Get()->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. |
| } |
| |
| // TODO(https://crbug.com/1234634): Remove this |if|. |
| if (restoring_main_frame_from_back_forward_cache) { |
| DCHECK(last_state_sent_to_renderer_); |
| if (blink::IsRestoredFromBackForwardCache(last_state_sent_to_renderer_, |
| new_state)) { |
| // We see that IPCs are not received by the renderer. Check that we are |
| // about to send an IPC to a live RVH. |
| if (!render_view_host_impl_->IsRenderViewLive()) { |
| blink::RecordUMAEventPageShowPersisted( |
| blink::EventPageShowPersisted::kYesInBrowserRenderViewNotLive); |
| NOTREACHED(); |
| } |
| // And that the mojo interface is connected. |
| if (!render_view_host_impl_->GetAssociatedPageBroadcast() |
| .is_connected()) { |
| blink::RecordUMAEventPageShowPersisted( |
| blink::EventPageShowPersisted::kYesInBrowserDisconnected); |
| NOTREACHED(); |
| } else { |
| blink::RecordUMAEventPageShowPersisted( |
| blink::EventPageShowPersisted::kYesInBrowser); |
| } |
| new_state->should_dispatch_pageshow_for_debugging = true; |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| last_state_sent_to_renderer_ = new_state.Clone(); |
| auto state = new_state.Clone(); |
| |
| if (test_delegate_) |
| test_delegate_->OnUpdateSentToRenderer(*last_state_sent_to_renderer_); |
| |
| // TODO(https://crbug.com/1234634): Remove this. |
| // We record the time that we sent it so that if the process exits |
| // unexpectedly, we can know how long that took. |
| if (new_state->should_dispatch_pageshow_for_debugging) { |
| // We could send a second one of these before before |
| // receiving the first ack. This would require something more complicated to |
| // handle perfectly but it should be rare or even impossible, so instead we |
| // just record that it happened and clear the timestamp without setting a |
| // new one |
| persisted_pageshow_timestamp_bug_1234634_ = base::Time::Now(); |
| } |
| |
| 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_; |
| // TODO(https://crbug.com/1234634): Remove this. It's for temporary |
| // debugging. |
| // This may become true later. |
| state->should_dispatch_pageshow_for_debugging = false; |
| return state; |
| } |
| |
| void PageLifecycleStateManager::OnPageLifecycleChangedAck( |
| blink::mojom::PageLifecycleStatePtr acknowledged_state, |
| base::OnceClosure done_cb) { |
| blink::mojom::PageLifecycleStatePtr old_state = |
| std::move(last_acknowledged_state_); |
| // TODO(https://crbug.com/1234634): Remove this. |
| if (acknowledged_state->should_dispatch_pageshow_for_debugging) { |
| blink::RecordUMAEventPageShowPersisted( |
| blink::EventPageShowPersisted::kYesInBrowserAck); |
| // We have received the ack, no need to track info for failures. |
| persisted_pageshow_timestamp_bug_1234634_.reset(); |
| } |
| |
| 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 |