| // Copyright 2023 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/loader/resource_cache_manager.h" |
| |
| #include "base/containers/contains.h" |
| #include "base/ranges/algorithm.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "third_party/blink/public/mojom/loader/resource_cache.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Calculates a resource cache key from a committed render frame host. |
| ResourceCacheKey CalculateResourceCacheKeyFromCommittedFrame( |
| RenderFrameHostImpl& render_frame_host) { |
| CHECK(!render_frame_host.HasPendingCommitForCrossDocumentNavigation()); |
| return ResourceCacheKey(render_frame_host.GetProcess()->GetProcessLock(), |
| render_frame_host.GetLastCommittedOrigin()); |
| } |
| |
| // Returns a RenderFrameHostImpl associated with `render_frame_host_id` if |
| // it can host or use a remote cache. Otherwise, return nullptr. |
| RenderFrameHostImpl* GetEligibleRenderFrameHostFromID( |
| GlobalRenderFrameHostId render_frame_host_id, |
| bool check_pending_navigation = true) { |
| RenderFrameHostImpl* render_frame_host = |
| RenderFrameHostImpl::FromID(render_frame_host_id); |
| if (!render_frame_host || !render_frame_host->IsActive() || |
| !render_frame_host->IsRenderFrameLive() || |
| render_frame_host->is_render_frame_deleted()) { |
| return nullptr; |
| } |
| |
| if (check_pending_navigation && |
| render_frame_host->HasPendingCommitForCrossDocumentNavigation()) { |
| // TODO(https://crbug.com/141426): Consider handling pending navigation. |
| // If there is a pending cross origin navigation, the frame isn't ready |
| // to use or host a remote cache due to initialization ordering in blink. |
| return nullptr; |
| } |
| |
| return render_frame_host; |
| } |
| |
| // Creates a mojo pending remote from `hosting_render_frame_host` and passes |
| // it to `target_render_frame_host`. |
| void PassResourceCacheRemote(RenderFrameHostImpl& hosting_render_frame_host, |
| RenderFrameHostImpl& target_render_frame_host) { |
| CHECK_NE(hosting_render_frame_host.GetGlobalId(), |
| target_render_frame_host.GetGlobalId()); |
| mojo::PendingRemote<blink::mojom::ResourceCache> pending_remote; |
| hosting_render_frame_host.GetRemoteInterfaces()->GetInterface( |
| pending_remote.InitWithNewPipeAndPassReceiver()); |
| target_render_frame_host.SetResourceCacheRemote(std::move(pending_remote)); |
| } |
| |
| } // namespace |
| |
| // A WebContentsObserver implementation to track state changes in a |
| // RenderFrameHost that hosts or has hosted a ResourceCache. Used to manage |
| // lifecycle changes of RenderFrameHost. |
| class ResourceCacheHostObserver : public WebContentsObserver { |
| public: |
| ResourceCacheHostObserver(ResourceCacheManager* manager, |
| WebContentsImpl* web_contents) |
| : WebContentsObserver(web_contents), manager(manager) { |
| CHECK(manager); |
| } |
| |
| // WebContentsObserver implementations: |
| void RenderFrameHostStateChanged( |
| RenderFrameHost* render_frame_host, |
| RenderFrameHost::LifecycleState old_state, |
| RenderFrameHost::LifecycleState new_state) override { |
| manager->RenderFrameHostStateChanged( |
| *static_cast<RenderFrameHostImpl*>(render_frame_host), old_state, |
| new_state); |
| } |
| |
| void WebContentsDestroyed() override { |
| manager->DestroyHostObserver(this); |
| // `manager` deleted `this`. |
| } |
| |
| private: |
| // `manager` owns `this`. |
| const raw_ptr<ResourceCacheManager> manager; |
| }; |
| |
| ResourceCacheKey::ResourceCacheKey(const ProcessLock& process_lock, |
| const url::Origin& origin) |
| : process_lock(process_lock), origin(origin) {} |
| |
| bool ResourceCacheKey::operator==(const ResourceCacheKey& rhs) const { |
| return this->process_lock == rhs.process_lock && this->origin == rhs.origin; |
| } |
| |
| bool ResourceCacheKey::operator<(const ResourceCacheKey& rhs) const { |
| return this->process_lock < rhs.process_lock || this->origin < rhs.origin; |
| } |
| |
| ResourceCacheManager::ResourceCacheEntry::ResourceCacheEntry( |
| mojo::Remote<blink::mojom::ResourceCache> remote, |
| GlobalRenderFrameHostId render_frame_host_id) |
| : remote(std::move(remote)), render_frame_host_id(render_frame_host_id) {} |
| |
| ResourceCacheManager::ResourceCacheEntry::~ResourceCacheEntry() = default; |
| |
| ResourceCacheManager::ResourceCacheManager() = default; |
| |
| ResourceCacheManager::~ResourceCacheManager() = default; |
| |
| void ResourceCacheManager::MaybeInitializeResourceCacheRemoteOnCommitNavigation( |
| mojo::PendingRemote<blink::mojom::ResourceCache>& pending_remote, |
| NavigationRequest& navigation_request) { |
| CHECK_EQ(navigation_request.state(), |
| NavigationRequest::NavigationState::READY_TO_COMMIT); |
| CHECK(!pending_remote.is_valid()); |
| if (!navigation_request.GetURL().SchemeIsHTTPOrHTTPS()) { |
| return; |
| } |
| |
| RenderFrameHostImpl* render_frame_host = |
| navigation_request.GetRenderFrameHost(); |
| CHECK(render_frame_host); |
| |
| // At this point there could be a renderer that can host a ResourceCache but |
| // doesn't host it yet. Find an existing ResourceCache to connect to the |
| // committing RenderFrameHost. If none exists, try to create one. |
| const ResourceCacheKey key(render_frame_host->GetProcess()->GetProcessLock(), |
| *navigation_request.GetOriginToCommit()); |
| |
| RenderFrameHostImpl* hosting_render_frame_host = |
| FindOrMaybeCreateResourceCache(key); |
| if (hosting_render_frame_host) { |
| hosting_render_frame_host->GetRemoteInterfaces()->GetInterface( |
| pending_remote.InitWithNewPipeAndPassReceiver()); |
| |
| // It's possible that the RenderFrameHost is already hosting a |
| // ResourceCache, when doing a same-RenderFrameHost navigation. |
| if (hosting_render_frame_host->GetGlobalId() == |
| render_frame_host->GetGlobalId()) { |
| return; |
| } |
| } |
| |
| // The RenderFrameHost we're about to commit in doesn't host a ResourceCache, |
| // so put it in non hosting hosts. |
| // TODO(https://crbug.com/141426): When two renderers that have the same |
| // process lock are about to commit at the same time, one may not get a |
| // ResourceCache remote. Figure out how to ensure renderers will get a |
| // ResourceCache remote eventually. |
| InsertNonHostingRenderFrameId(key, render_frame_host->GetGlobalId()); |
| } |
| |
| void ResourceCacheManager::RenderFrameHostBecameIneligible( |
| RenderFrameHostImpl& render_frame_host) { |
| CHECK(!GetEligibleRenderFrameHostFromID(render_frame_host.GetGlobalId())); |
| |
| const ResourceCacheKey key = |
| CalculateResourceCacheKeyFromCommittedFrame(render_frame_host); |
| auto non_hosting_it = non_hosting_frame_hosts_.find(key); |
| if (non_hosting_it != non_hosting_frame_hosts_.end()) { |
| non_hosting_it->second.erase(render_frame_host.GetGlobalId()); |
| } |
| |
| auto remote_cache_it = remote_caches_.find(key); |
| if (remote_cache_it != remote_caches_.end() && |
| remote_cache_it->second.render_frame_host_id == |
| render_frame_host.GetGlobalId()) { |
| remote_caches_.erase(key); |
| FindOrMaybeCreateResourceCache(key); |
| } |
| } |
| |
| void ResourceCacheManager::RenderFrameHostStateChanged( |
| RenderFrameHostImpl& render_frame_host, |
| RenderFrameHost::LifecycleState old_state, |
| RenderFrameHost::LifecycleState new_state) { |
| if (new_state == RenderFrameHost::LifecycleState::kPendingDeletion || |
| new_state == RenderFrameHost::LifecycleState::kInBackForwardCache) { |
| // * When a RenderFrameHost is pending deletion, it's no longer eligible to |
| // host a ResourceCache. |
| // * When a LocalFrame in a renderer process was stored in the BFCache, the |
| // frame got frozen. The frame unbound all ResourceCache receivers when it |
| // got frozen. To reflect that, make the corresponding RenderFrameHost |
| // ineligible to host ResourceCache. |
| RenderFrameHostBecameIneligible(render_frame_host); |
| return; |
| } |
| |
| if (old_state == RenderFrameHost::LifecycleState::kInBackForwardCache && |
| new_state == RenderFrameHost::LifecycleState::kActive) { |
| // This RenderFrameHost was restored from the BFCache. Treat the |
| // RenderFrameHost as eligible to host/use ResourceCache. |
| const ResourceCacheKey key = |
| CalculateResourceCacheKeyFromCommittedFrame(render_frame_host); |
| auto remote_cache_it = remote_caches_.find(key); |
| if (remote_cache_it == remote_caches_.end()) { |
| HostResourceCache(render_frame_host, key); |
| } else { |
| InsertNonHostingRenderFrameId(key, render_frame_host.GetGlobalId()); |
| auto* hosting_render_frame = GetEligibleRenderFrameHostFromID( |
| remote_cache_it->second.render_frame_host_id); |
| CHECK(hosting_render_frame); |
| PassResourceCacheRemote(*hosting_render_frame, render_frame_host); |
| } |
| return; |
| } |
| |
| // TODO(https://crbug.com/141426): Consider handling prerenders. |
| } |
| |
| void ResourceCacheManager::DestroyHostObserver( |
| ResourceCacheHostObserver* observer) { |
| auto it = host_observers_.find(observer); |
| CHECK(it != host_observers_.end()); |
| host_observers_.erase(it); |
| } |
| |
| bool ResourceCacheManager::IsRenderFrameHostHostingRemoteCache( |
| RenderFrameHostImpl& render_frame_host) { |
| const ResourceCacheKey key = |
| CalculateResourceCacheKeyFromCommittedFrame(render_frame_host); |
| auto remote_cache_it = remote_caches_.find(key); |
| if (remote_cache_it == remote_caches_.end()) { |
| // There is no remote for the key. No renderer hosts a remote cache. |
| return false; |
| } |
| |
| return remote_cache_it->second.render_frame_host_id == |
| render_frame_host.GetGlobalId(); |
| } |
| |
| void ResourceCacheManager::InsertNonHostingRenderFrameId( |
| const ResourceCacheKey& key, |
| GlobalRenderFrameHostId render_frame_host_id) { |
| auto non_hosting_it = non_hosting_frame_hosts_.find(key); |
| if (non_hosting_it == non_hosting_frame_hosts_.end()) { |
| std::set<GlobalRenderFrameHostId> hosts = {render_frame_host_id}; |
| non_hosting_frame_hosts_.emplace(key, std::move(hosts)); |
| } else { |
| CHECK(!base::Contains(non_hosting_it->second, render_frame_host_id)); |
| non_hosting_it->second.insert(render_frame_host_id); |
| } |
| } |
| |
| RenderFrameHostImpl* ResourceCacheManager::FindOrMaybeCreateResourceCache( |
| const ResourceCacheKey& key) { |
| auto remote_cache_it = remote_caches_.find(key); |
| if (remote_cache_it != remote_caches_.end()) { |
| return RenderFrameHostImpl::FromID( |
| remote_cache_it->second.render_frame_host_id); |
| } |
| |
| // Try to create a new ResourceCache. |
| |
| auto non_hosting_it = non_hosting_frame_hosts_.find(key); |
| if (non_hosting_it == non_hosting_frame_hosts_.end()) { |
| return nullptr; |
| } |
| |
| // Find an eligible RenderFrameHost to host a new ResourceCache. |
| RenderFrameHostImpl* new_hosting_render_frame = nullptr; |
| auto render_frame_host_it = non_hosting_it->second.begin(); |
| while (render_frame_host_it != non_hosting_it->second.end()) { |
| RenderFrameHostImpl* render_frame_host = |
| GetEligibleRenderFrameHostFromID(*render_frame_host_it); |
| if (!render_frame_host) { |
| ++render_frame_host_it; |
| continue; |
| } |
| |
| // Create a ResourceCache on the first eligible RenderFrameHost. |
| if (!new_hosting_render_frame) { |
| HostResourceCache(*render_frame_host, key); |
| |
| render_frame_host_it = non_hosting_it->second.erase(render_frame_host_it); |
| new_hosting_render_frame = render_frame_host; |
| continue; |
| } |
| |
| // The first eligible RenderFrameHost was selected to host a new |
| // ResourceCache. We will connect the newly created one to the remaining |
| // renderers. |
| PassResourceCacheRemote(*new_hosting_render_frame, *render_frame_host); |
| |
| ++render_frame_host_it; |
| } |
| |
| return new_hosting_render_frame; |
| } |
| |
| void ResourceCacheManager::HostResourceCache( |
| RenderFrameHostImpl& render_frame_host, |
| const ResourceCacheKey& key) { |
| CHECK_EQ(key.process_lock, render_frame_host.GetProcess()->GetProcessLock()); |
| CHECK(!render_frame_host.HasPendingCommitForCrossDocumentNavigation()); |
| |
| mojo::Remote<blink::mojom::ResourceCache> remote = |
| CreateResourceCacheRemote(render_frame_host, key); |
| auto [it, inserted] = remote_caches_.try_emplace( |
| std::move(key), std::move(remote), render_frame_host.GetGlobalId()); |
| CHECK(inserted); |
| auto* web_contents = static_cast<WebContentsImpl*>( |
| WebContentsImpl::FromRenderFrameHostID(render_frame_host.GetGlobalId())); |
| auto observer_it = base::ranges::find_if( |
| host_observers_, |
| [&web_contents]( |
| const std::unique_ptr<ResourceCacheHostObserver>& observer) { |
| return web_contents == observer->web_contents(); |
| }); |
| if (observer_it == host_observers_.end()) { |
| host_observers_.insert( |
| std::make_unique<ResourceCacheHostObserver>(this, web_contents)); |
| } |
| } |
| |
| mojo::Remote<blink::mojom::ResourceCache> |
| ResourceCacheManager::CreateResourceCacheRemote( |
| RenderFrameHostImpl& render_frame_host, |
| const ResourceCacheKey& key) { |
| mojo::PendingRemote<blink::mojom::ResourceCache> pending_remote; |
| render_frame_host.GetRemoteInterfaces()->GetInterface( |
| pending_remote.InitWithNewPipeAndPassReceiver()); |
| mojo::Remote<blink::mojom::ResourceCache> remote(std::move(pending_remote)); |
| remote.set_disconnect_handler(base::BindOnce( |
| &ResourceCacheManager::OnRemoteCacheDisconnected, |
| weak_ptr_factory_.GetWeakPtr(), key, render_frame_host.GetGlobalId())); |
| return remote; |
| } |
| |
| void ResourceCacheManager::OnRemoteCacheDisconnected( |
| const ResourceCacheKey& key, |
| GlobalRenderFrameHostId render_frame_host_id) { |
| auto it = remote_caches_.find(key); |
| if (it == remote_caches_.end()) { |
| return; |
| } |
| |
| // `render_frame_host` could be nullptr when the frame is about to shutdown, |
| // the browser is shutting down, or the renderer process was crashed. |
| // Also bypass pending navigation check because `render_frame_host` could |
| // have a pending navigation when same-origin-same-process navigation |
| // happened. |
| auto* render_frame_host = GetEligibleRenderFrameHostFromID( |
| render_frame_host_id, /*check_pending_navigation=*/false); |
| if (!render_frame_host) { |
| remote_caches_.erase(key); |
| FindOrMaybeCreateResourceCache(key); |
| return; |
| } |
| |
| CHECK(!render_frame_host->GetProcess()->ShutdownRequested()); |
| |
| // Same-origin-same-process navigation happened. The previous mojo remote was |
| // disconnected but the frame can continue hosting the remote cache. Recreate |
| // a new mojo connection. |
| mojo::Remote<blink::mojom::ResourceCache> remote = |
| CreateResourceCacheRemote(*render_frame_host, key); |
| it->second.remote = std::move(remote); |
| |
| // Plumb the new remote to eligible non hosting frames. |
| auto non_hosting_it = non_hosting_frame_hosts_.find(key); |
| if (non_hosting_it == non_hosting_frame_hosts_.end()) { |
| return; |
| } |
| for (auto& id : non_hosting_it->second) { |
| RenderFrameHostImpl* eligible_non_hosting_frame_host = |
| GetEligibleRenderFrameHostFromID(id); |
| if (eligible_non_hosting_frame_host) { |
| PassResourceCacheRemote(*render_frame_host, |
| *eligible_non_hosting_frame_host); |
| } |
| } |
| } |
| |
| } // namespace content |