Improve timing of frame attach/detach (see #3664)

- Move frame attachment from RenderFrameCreated to
  DidCommitProvisionalLoad. This has a number of advantages:
  - Significantly reduces the frequency of disconnects by avoiding
    the GetInterface/DidCommitNavigation race condition.
  - Stops connecting temporary frames (created during cross-origin
    navigation), making callback behavior more consistent.
- Split frame detach and destruction notifications into separate
  callbacks. OnFrameDetached now reflects a potentially recoverable
  state. Add a new OnFrameDestroyed callback for the unrecoverable
  destruction state.
This commit is contained in:
Marshall Greenblatt
2024-11-27 17:08:42 -05:00
parent 7f253f83a2
commit 35fc888c72
16 changed files with 484 additions and 220 deletions

View File

@ -131,6 +131,9 @@ void CefBrowserInfo::MaybeCreateFrame(content::RenderFrameHost* host) {
return;
}
DVLOG(1) << __func__ << ": "
<< frame_util::GetFrameDebugString(host->GetGlobalFrameToken());
const auto global_id = host->GetGlobalId();
const bool is_main_frame = (host->GetParent() == nullptr);
@ -160,7 +163,7 @@ void CefBrowserInfo::MaybeCreateFrame(content::RenderFrameHost* host) {
#endif
// Update the associated RFH, which may have changed.
info->frame_->MaybeReAttach(this, host, /*require_detached=*/false);
info->frame_->MaybeAttach(this, host);
if (info->is_speculative_ && !is_speculative) {
// Upgrade the frame info from speculative to non-speculative.
@ -208,17 +211,20 @@ void CefBrowserInfo::FrameHostStateChanged(
content::RenderFrameHost::LifecycleState new_state) {
CEF_REQUIRE_UIT();
DVLOG(1) << __func__ << ": "
<< frame_util::GetFrameDebugString(host->GetGlobalFrameToken());
if ((old_state == content::RenderFrameHost::LifecycleState::kPrerendering ||
old_state ==
content::RenderFrameHost::LifecycleState::kInBackForwardCache) &&
new_state == content::RenderFrameHost::LifecycleState::kActive) {
if (auto frame = GetFrameForHost(host)) {
// Update the associated RFH, which may have changed.
frame->MaybeReAttach(this, host, /*require_detached=*/true);
frame->MaybeAttach(this, host);
if (frame->IsMain()) {
// Update the main frame object.
NotificationStateLock lock_scope(this);
// Update the main frame object.
SetMainFrame(browser_, frame);
}
@ -251,6 +257,9 @@ void CefBrowserInfo::FrameHostStateChanged(
void CefBrowserInfo::RemoveFrame(content::RenderFrameHost* host) {
CEF_REQUIRE_UIT();
DVLOG(1) << __func__ << ": "
<< frame_util::GetFrameDebugString(host->GetGlobalFrameToken());
NotificationStateLock lock_scope(this);
const auto global_id = host->GetGlobalId();
@ -281,12 +290,17 @@ void CefBrowserInfo::RemoveFrame(content::RenderFrameHost* host) {
const auto& other_frame_info = *it2;
if (other_frame_info->frame_) {
const bool is_current_main_frame = other_frame_info->IsCurrentMainFrame();
if (other_frame_info->frame_->Detach(
const auto [frame_detached, frame_destroyed] =
other_frame_info->frame_->Detach(
CefFrameHostImpl::DetachReason::RENDER_FRAME_DELETED,
is_current_main_frame)) {
DCHECK(!is_current_main_frame);
is_current_main_frame);
if (frame_detached) {
MaybeNotifyFrameDetached(browser_, other_frame_info->frame_);
}
if (frame_destroyed) {
DCHECK(!is_current_main_frame);
MaybeNotifyFrameDestroyed(browser_, other_frame_info->frame_);
}
}
frame_info_set_.erase(it2);
@ -477,13 +491,21 @@ void CefBrowserInfo::SetMainFrame(CefRefPtr<CefBrowserHostBase> browser,
return;
}
DVLOG(1) << __func__ << ": "
<< (frame ? frame->GetIdentifier().ToString() : "null");
CefRefPtr<CefFrameHostImpl> old_frame;
if (main_frame_) {
old_frame = main_frame_;
if (old_frame->Detach(CefFrameHostImpl::DetachReason::NEW_MAIN_FRAME,
/*is_current_main_frame=*/false)) {
const auto [frame_detached, frame_destroyed] =
old_frame->Detach(CefFrameHostImpl::DetachReason::NEW_MAIN_FRAME,
/*is_current_main_frame=*/false);
if (frame_detached) {
MaybeNotifyFrameDetached(browser, old_frame);
}
if (frame_destroyed) {
MaybeNotifyFrameDestroyed(browser, old_frame);
}
}
main_frame_ = frame;
@ -526,6 +548,24 @@ void CefBrowserInfo::MaybeNotifyFrameDetached(
browser, frame));
}
// Passing in |browser| here because |browser_| may already be cleared.
void CefBrowserInfo::MaybeNotifyFrameDestroyed(
CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> frame) {
CEF_REQUIRE_UIT();
// Never notify for temporary objects.
DCHECK(!frame->is_temporary());
MaybeExecuteFrameNotification(base::BindOnce(
[](CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> frame,
CefRefPtr<CefFrameHandler> handler) {
handler->OnFrameDestroyed(browser, frame);
},
browser, frame));
}
// Passing in |browser| here because |browser_| may already be cleared.
void CefBrowserInfo::MaybeNotifyMainFrameChanged(
CefRefPtr<CefBrowserHostBase> browser,
@ -563,13 +603,13 @@ void CefBrowserInfo::RemoveAllFrames(
// Explicitly Detach everything.
for (auto& info : frame_info_set_) {
if (info->frame_) {
const bool is_current_main_frame = info->IsCurrentMainFrame();
if (info->frame_->Detach(
[[maybe_unused]] const auto [frame_detached, frame_destroyed] =
info->frame_->Detach(
CefFrameHostImpl::DetachReason::BROWSER_DESTROYED,
is_current_main_frame)) {
DCHECK(!is_current_main_frame);
MaybeNotifyFrameDetached(old_browser, info->frame_);
}
info->IsCurrentMainFrame());
// Shouldn't need to trigger any notifications at this point.
DCHECK(!frame_detached);
DCHECK(!frame_destroyed);
}
}

View File

@ -166,6 +166,8 @@ class CefBrowserInfo : public base::RefCountedThreadSafe<CefBrowserInfo> {
CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> frame,
std::vector<CefDraggableRegion> draggable_regions);
void MaybeNotifyFrameDetached(CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> frame);
private:
friend class base::RefCountedThreadSafe<CefBrowserInfo>;
@ -190,8 +192,8 @@ class CefBrowserInfo : public base::RefCountedThreadSafe<CefBrowserInfo> {
CefRefPtr<CefFrameHostImpl> frame);
void MaybeNotifyFrameCreated(CefRefPtr<CefFrameHostImpl> frame);
void MaybeNotifyFrameDetached(CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> frame);
void MaybeNotifyFrameDestroyed(CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> frame);
void MaybeNotifyMainFrameChanged(CefRefPtr<CefBrowserHostBase> browser,
CefRefPtr<CefFrameHostImpl> old_frame,
CefRefPtr<CefFrameHostImpl> new_frame);

View File

@ -500,35 +500,18 @@ bool CefFrameHostImpl::IsDetached() const {
return !GetRenderFrameHost();
}
bool CefFrameHostImpl::Detach(DetachReason reason, bool is_current_main_frame) {
std::pair<bool, bool> CefFrameHostImpl::Detach(DetachReason reason,
bool is_current_main_frame) {
CEF_REQUIRE_UIT();
// This method may be called multiple times (e.g. from CefBrowserInfo
// SetMainFrame and RemoveFrame).
bool is_first_complete_detach = false;
// Should not be called for temporary frames.
CHECK(!is_temporary());
// Must be a main frame if |is_current_main_frame| is true.
CHECK(!is_current_main_frame || is_main_frame_);
if (!is_current_main_frame) {
{
base::AutoLock lock_scope(state_lock_);
if (browser_info_) {
is_first_complete_detach = true;
browser_info_ = nullptr;
}
}
// In case we never attached, clean up.
while (!queued_renderer_actions_.empty()) {
queued_renderer_actions_.pop();
}
}
if (render_frame_.is_bound()) {
const bool is_bound = render_frame_.is_bound();
if (is_bound) {
if (VLOG_IS_ON(1)) {
std::string reason_str;
switch (reason) {
@ -554,7 +537,27 @@ bool CefFrameHostImpl::Detach(DetachReason reason, bool is_current_main_frame) {
render_frame_host_ = nullptr;
}
return is_first_complete_detach;
// This method may be called multiple times (e.g. from CefBrowserInfo
// SetMainFrame and RemoveFrame).
bool is_first_complete_detach = false;
if (!is_current_main_frame) {
{
base::AutoLock lock_scope(state_lock_);
if (browser_info_) {
DVLOG(1) << __func__ << ": " << GetDebugString() << " invalidated";
is_first_complete_detach = true;
browser_info_ = nullptr;
}
}
// In case we never attached, clean up.
while (!queued_renderer_actions_.empty()) {
queued_renderer_actions_.pop();
}
}
return std::make_pair(is_bound, is_first_complete_detach);
}
void CefFrameHostImpl::DetachRenderFrame() {
@ -564,12 +567,11 @@ void CefFrameHostImpl::DetachRenderFrame() {
static_cast<uint32_t>(frame_util::ResetReason::kDetached), "Detached");
}
void CefFrameHostImpl::MaybeReAttach(
void CefFrameHostImpl::MaybeAttach(
scoped_refptr<CefBrowserInfo> browser_info,
content::RenderFrameHost* render_frame_host,
bool require_detached) {
content::RenderFrameHost* render_frame_host) {
CEF_REQUIRE_UIT();
if (render_frame_.is_bound() && render_frame_host_ == render_frame_host) {
if (render_frame_host_ == render_frame_host) {
// Nothing to do here.
return;
}
@ -577,16 +579,13 @@ void CefFrameHostImpl::MaybeReAttach(
// Should not be called for temporary frames.
CHECK(!is_temporary());
// If |require_detached| then we expect that Detach() was called previously.
CHECK(!require_detached || !render_frame_.is_bound());
// We expect that either this frame has never attached (e.g. when swapping
// from speculative to non-speculative) or Detach() was called previously
// (e.g. when exiting the bfcache).
CHECK(!render_frame_.is_bound());
if (render_frame_.is_bound()) {
// Intentionally not clearing |queued_renderer_actions_|, as we may be
// changing RFH during initial browser navigation.
DVLOG(1) << __func__ << ": " << GetDebugString()
<< " detached (reason=RENDER_FRAME_CHANGED)";
DetachRenderFrame();
}
// Intentionally not clearing |queued_renderer_actions_|, as we may be
// changing RFH during initial browser navigation.
// The RFH may change but the frame token should remain the same.
CHECK(*frame_token_ == render_frame_host->GetGlobalFrameToken());
@ -653,6 +652,14 @@ void CefFrameHostImpl::SendToRenderFrame(const std::string& function_name,
void CefFrameHostImpl::OnRenderFrameDisconnect() {
CEF_REQUIRE_UIT();
DVLOG(1) << __func__ << ": " << GetDebugString();
if (auto browser_info = GetBrowserInfo()) {
if (auto browser = browser_info->browser()) {
browser_info->MaybeNotifyFrameDetached(browser, this);
}
}
// Reconnect, if any, will be triggered via FrameAttached().
render_frame_.reset();
}

View File

@ -10,6 +10,7 @@
#include <optional>
#include <queue>
#include <string>
#include <utility>
#include "base/memory/raw_ptr.h"
#include "base/synchronization/lock.h"
@ -140,15 +141,15 @@ class CefFrameHostImpl : public CefFrame, public cef::mojom::BrowserFrame {
// implicitly via CefBrowserInfo::browser() returning nullptr. If
// |is_current_main_frame| is true then only the RenderFrameHost references
// will be released as we want the frame object itself to remain valid.
// Returns true if the frame is completely detached for the first time.
bool Detach(DetachReason reason, bool is_current_main_frame);
// Returns (bool, bool) to indicate if frame detached and/or frame destroyed
// notifications should be triggered respectively.
std::pair<bool, bool> Detach(DetachReason reason, bool is_current_main_frame);
// A frame has swapped to active status from prerendering or the back-forward
// cache. We may need to re-attach if the RFH has changed. See
// https://crbug.com/1179502#c8 for additional background.
void MaybeReAttach(scoped_refptr<CefBrowserInfo> browser_info,
content::RenderFrameHost* render_frame_host,
bool require_detached);
// A new frame was created or a frame has swapped to active status from
// prerendering or the back-forward cache. Update internal state if the RFH
// has changed. See https://crbug.com/1179502#c8 for additional background.
void MaybeAttach(scoped_refptr<CefBrowserInfo> browser_info,
content::RenderFrameHost* render_frame_host);
// cef::mojom::BrowserFrame methods forwarded from CefBrowserFrame.
void SendMessage(const std::string& name,

View File

@ -69,7 +69,10 @@ class CefFrameServiceBase : public Interface,
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (render_frame_host == render_frame_host_) {
DVLOG(1) << __func__ << ": RenderFrameHost destroyed.";
DVLOG(1) << __func__ << ": "
<< frame_util::GetFrameDebugString(
render_frame_host->GetGlobalFrameToken())
<< " destroyed";
if (receiver_.is_bound()) {
receiver_.ResetWithReason(
static_cast<uint32_t>(frame_util::ResetReason::kDeleted),

View File

@ -316,20 +316,23 @@ void CefFrameImpl::SendProcessMessage(CefProcessId target_process,
}
}
void CefFrameImpl::OnAttached() {
// Called indirectly from RenderFrameCreated.
ConnectBrowserFrame(ConnectReason::RENDER_FRAME_CREATED);
}
void CefFrameImpl::OnWasShown() {
if (browser_connection_state_ == ConnectionState::DISCONNECTED) {
// Reconnect a frame that has exited the bfcache.
if (browser_connection_state_ == ConnectionState::DISCONNECTED &&
did_commit_provisional_load_) {
// Reconnect a frame that has exited the bfcache. We ignore temporary
// frames that have never called DidCommitProvisionalLoad.
ConnectBrowserFrame(ConnectReason::WAS_SHOWN);
}
}
void CefFrameImpl::OnDidCommitProvisionalLoad() {
did_commit_provisional_load_ = true;
if (browser_connection_state_ == ConnectionState::DISCONNECTED) {
// Connect after RenderFrameImpl::DidCommitNavigation has potentially
// reset the BrowserInterfaceBroker in the browser process. See related
// comments in OnDisconnect.
ConnectBrowserFrame(ConnectReason::DID_COMMIT);
}
MaybeInitializeScriptContext();
}
@ -473,8 +476,8 @@ void CefFrameImpl::ConnectBrowserFrame(ConnectReason reason) {
if (VLOG_IS_ON(1)) {
std::string reason_str;
switch (reason) {
case ConnectReason::RENDER_FRAME_CREATED:
reason_str = "RENDER_FRAME_CREATED";
case ConnectReason::DID_COMMIT:
reason_str = "DID_COMMIT";
break;
case ConnectReason::WAS_SHOWN:
reason_str = "WAS_SHOWN";

View File

@ -81,7 +81,6 @@ class CefFrameImpl
CefRefPtr<CefProcessMessage> message) override;
// Forwarded from CefRenderFrameObserver.
void OnAttached();
void OnWasShown();
void OnDidCommitProvisionalLoad();
void OnDidFinishLoad();
@ -105,7 +104,7 @@ class CefFrameImpl
LocalFrameAction action);
enum class ConnectReason {
RENDER_FRAME_CREATED,
DID_COMMIT,
WAS_SHOWN,
RETRY,
};

View File

@ -199,7 +199,6 @@ void CefRenderFrameObserver::AttachFrame(CefFrameImpl* frame) {
DCHECK(frame);
DCHECK(!frame_);
frame_ = frame;
frame_->OnAttached();
}
void CefRenderFrameObserver::OnLoadStart() {