| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/webui/discards/graph_dump_impl.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/cancelable_task_tracker.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "chrome/browser/favicon/favicon_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/webui/discards/discards.mojom.h" |
| #include "components/favicon/core/favicon_service.h" |
| #include "components/favicon_base/favicon_callback.h" |
| #include "components/performance_manager/public/graph/graph.h" |
| #include "components/performance_manager/public/graph/node_data_describer.h" |
| #include "components/performance_manager/public/graph/node_data_describer_registry.h" |
| #include "components/performance_manager/public/performance_manager.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| |
| namespace { |
| |
| // Best effort convert |value| to a string. |
| std::string ToJSON(const base::Value::Dict& value) { |
| std::string result; |
| JSONStringValueSerializer serializer(&result); |
| if (serializer.Serialize(value)) { |
| return result; |
| } |
| |
| return std::string(); |
| } |
| |
| } // namespace |
| |
| class DiscardsGraphDumpImpl::FaviconRequestHelper { |
| public: |
| FaviconRequestHelper() = default; |
| ~FaviconRequestHelper() = default; |
| |
| FaviconRequestHelper(const FaviconRequestHelper&) = delete; |
| FaviconRequestHelper& operator=(const FaviconRequestHelper&) = delete; |
| |
| void RequestFavicon(GURL page_url, |
| base::WeakPtr<content::WebContents> web_contents, |
| FaviconAvailableCallback on_favicon_available); |
| void FaviconDataAvailable(FaviconAvailableCallback on_favicon_available, |
| const favicon_base::FaviconRawBitmapResult& result); |
| |
| private: |
| base::CancelableTaskTracker cancelable_task_tracker_; |
| SEQUENCE_CHECKER(sequence_checker_); |
| }; |
| |
| void DiscardsGraphDumpImpl::FaviconRequestHelper::RequestFavicon( |
| GURL page_url, |
| base::WeakPtr<content::WebContents> web_contents, |
| FaviconAvailableCallback on_favicon_available) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!web_contents) { |
| return; |
| } |
| |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| if (!profile) { |
| return; |
| } |
| |
| favicon::FaviconService* favicon_service = |
| FaviconServiceFactory::GetForProfile(profile, |
| ServiceAccessType::EXPLICIT_ACCESS); |
| if (!favicon_service) { |
| return; |
| } |
| |
| constexpr size_t kIconSize = 16; |
| constexpr bool kFallbackToHost = true; |
| // It's safe to pass this unretained here, as the tasks are cancelled |
| // on deletion of the cancelable task tracker. |
| favicon_service->GetRawFaviconForPageURL( |
| page_url, {favicon_base::IconType::kFavicon}, kIconSize, kFallbackToHost, |
| base::BindOnce(&FaviconRequestHelper::FaviconDataAvailable, |
| base::Unretained(this), std::move(on_favicon_available)), |
| &cancelable_task_tracker_); |
| } |
| |
| void DiscardsGraphDumpImpl::FaviconRequestHelper::FaviconDataAvailable( |
| FaviconAvailableCallback on_favicon_available, |
| const favicon_base::FaviconRawBitmapResult& result) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!result.is_valid()) { |
| return; |
| } |
| std::move(on_favicon_available).Run(result.bitmap_data); |
| } |
| |
| DiscardsGraphDumpImpl::DiscardsGraphDumpImpl() = default; |
| |
| DiscardsGraphDumpImpl::~DiscardsGraphDumpImpl() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!change_subscriber_); |
| } |
| |
| // static |
| void DiscardsGraphDumpImpl::CreateAndBind( |
| mojo::PendingReceiver<discards::mojom::GraphDump> receiver, |
| performance_manager::Graph* graph) { |
| std::unique_ptr<DiscardsGraphDumpImpl> dump = |
| std::make_unique<DiscardsGraphDumpImpl>(); |
| |
| dump->BindWithGraph(graph, std::move(receiver)); |
| graph->PassToGraph(std::move(dump)); |
| } |
| |
| void DiscardsGraphDumpImpl::BindWithGraph( |
| performance_manager::Graph* graph, |
| mojo::PendingReceiver<discards::mojom::GraphDump> receiver) { |
| receiver_.Bind(std::move(receiver)); |
| receiver_.set_disconnect_handler(base::BindOnce( |
| &DiscardsGraphDumpImpl::OnConnectionError, base::Unretained(this))); |
| } |
| |
| int64_t DiscardsGraphDumpImpl::GetNodeIdForTesting( |
| const performance_manager::Node* node) { |
| return GetNodeId(node); |
| } |
| |
| namespace { |
| |
| template <typename FunctionType> |
| void ForFrameAndOffspring(const performance_manager::FrameNode* parent_frame, |
| FunctionType on_frame) { |
| on_frame(parent_frame); |
| |
| for (const performance_manager::FrameNode* child_frame : |
| parent_frame->GetChildFrameNodes()) { |
| ForFrameAndOffspring(child_frame, on_frame); |
| } |
| } |
| |
| } // namespace |
| |
| void DiscardsGraphDumpImpl::SubscribeToChanges( |
| mojo::PendingRemote<discards::mojom::GraphChangeStream> change_subscriber) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| change_subscriber_.Bind(std::move(change_subscriber)); |
| |
| // Give all existing nodes an ID. |
| performance_manager::Graph* graph = GetOwningGraph(); |
| for (const performance_manager::FrameNode* frame_node : |
| graph->GetAllFrameNodes()) { |
| AddNode(frame_node); |
| } |
| for (const performance_manager::PageNode* page_node : |
| graph->GetAllPageNodes()) { |
| AddNode(page_node); |
| } |
| for (const performance_manager::ProcessNode* process_node : |
| graph->GetAllProcessNodes()) { |
| AddNode(process_node); |
| } |
| for (const performance_manager::WorkerNode* worker_node : |
| graph->GetAllWorkerNodes()) { |
| AddNode(worker_node); |
| } |
| |
| // Send creation notifications for all existing nodes. |
| SendNotificationToAllNodes(/* created = */ true); |
| |
| // It is entirely possible for there to be circular link references between |
| // nodes that already existed at the point this object was created (the loop |
| // was closed after the two nodes themselves were created). We don't have the |
| // exact order of historical events that led to the current graph state, so we |
| // simply fire off a node changed notification for all nodes after the node |
| // creation. This ensures that all targets exist the second time through, and |
| // any loops are closed. Afterwards any newly created loops will be properly |
| // maintained as node creation/destruction/link events will be fed to the |
| // graph in the proper order. |
| SendNotificationToAllNodes(/* created = */ false); |
| |
| // Subscribe to subsequent notifications. |
| graph->AddFrameNodeObserver(this); |
| graph->AddPageNodeObserver(this); |
| graph->AddProcessNodeObserver(this); |
| graph->AddWorkerNodeObserver(this); |
| } |
| |
| void DiscardsGraphDumpImpl::RequestNodeDescriptions( |
| const std::vector<int64_t>& node_ids, |
| RequestNodeDescriptionsCallback callback) { |
| base::flat_map<int64_t, std::string> descriptions; |
| performance_manager::NodeDataDescriberRegistry* describer_registry = |
| GetOwningGraph()->GetNodeDataDescriberRegistry(); |
| for (int64_t node_id : node_ids) { |
| auto it = nodes_by_id_.find(NodeId::FromUnsafeValue(node_id)); |
| // The requested node may have been removed by the time the request arrives, |
| // in which case no description is returned for that node ID. |
| if (it != nodes_by_id_.end()) { |
| descriptions[node_id] = |
| ToJSON(describer_registry->DescribeNodeData(it->second)); |
| } |
| } |
| |
| std::move(callback).Run(descriptions); |
| } |
| |
| void DiscardsGraphDumpImpl::OnPassedToGraph(performance_manager::Graph* graph) { |
| // Do nothing. |
| } |
| |
| void DiscardsGraphDumpImpl::OnTakenFromGraph( |
| performance_manager::Graph* graph) { |
| if (change_subscriber_) { |
| graph->RemoveFrameNodeObserver(this); |
| graph->RemovePageNodeObserver(this); |
| graph->RemoveProcessNodeObserver(this); |
| graph->RemoveWorkerNodeObserver(this); |
| } |
| |
| change_subscriber_.reset(); |
| } |
| |
| void DiscardsGraphDumpImpl::OnFrameNodeAdded( |
| const performance_manager::FrameNode* frame_node) { |
| AddNode(frame_node); |
| SendFrameNotification(frame_node, true); |
| StartFrameFaviconRequest(frame_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeFrameNodeRemoved( |
| const performance_manager::FrameNode* frame_node) { |
| SendDeletionNotification(frame_node); |
| RemoveNode(frame_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnURLChanged( |
| const performance_manager::FrameNode* frame_node, |
| const GURL& previous_value) { |
| SendFrameNotification(frame_node, false); |
| StartFrameFaviconRequest(frame_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnPageNodeAdded( |
| const performance_manager::PageNode* page_node) { |
| AddNode(page_node); |
| SendPageNotification(page_node, true); |
| StartPageFaviconRequest(page_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforePageNodeRemoved( |
| const performance_manager::PageNode* page_node) { |
| SendDeletionNotification(page_node); |
| RemoveNode(page_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnOpenerFrameNodeChanged( |
| const performance_manager::PageNode* page_node, |
| const performance_manager::FrameNode* previous_opener) { |
| DCHECK(HasNode(page_node)); |
| SendPageNotification(page_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnEmbedderFrameNodeChanged( |
| const performance_manager::PageNode* page_node, |
| const performance_manager::FrameNode*) { |
| DCHECK(HasNode(page_node)); |
| SendPageNotification(page_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnFaviconUpdated( |
| const performance_manager::PageNode* page_node) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| StartPageFaviconRequest(page_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnMainFrameUrlChanged( |
| const performance_manager::PageNode* page_node) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| SendPageNotification(page_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnProcessNodeAdded( |
| const performance_manager::ProcessNode* process_node) { |
| AddNode(process_node); |
| SendProcessNotification(process_node, true); |
| } |
| |
| void DiscardsGraphDumpImpl::OnProcessLifetimeChange( |
| const performance_manager::ProcessNode* process_node) { |
| SendProcessNotification(process_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeProcessNodeRemoved( |
| const performance_manager::ProcessNode* process_node) { |
| SendDeletionNotification(process_node); |
| RemoveNode(process_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnWorkerNodeAdded( |
| const performance_manager::WorkerNode* worker_node) { |
| AddNode(worker_node); |
| SendWorkerNotification(worker_node, true); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeWorkerNodeRemoved( |
| const performance_manager::WorkerNode* worker_node) { |
| SendDeletionNotification(worker_node); |
| RemoveNode(worker_node); |
| } |
| |
| void DiscardsGraphDumpImpl::OnFinalResponseURLDetermined( |
| const performance_manager::WorkerNode* worker_node) { |
| SendWorkerNotification(worker_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeClientFrameAdded( |
| const performance_manager::WorkerNode* worker_node, |
| const performance_manager::FrameNode* client_frame_node) { |
| // Nothing to do. |
| } |
| |
| void DiscardsGraphDumpImpl::OnClientFrameAdded( |
| const performance_manager::WorkerNode* worker_node, |
| const performance_manager::FrameNode* client_frame_node) { |
| SendWorkerNotification(worker_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeClientFrameRemoved( |
| const performance_manager::WorkerNode* worker_node, |
| const performance_manager::FrameNode* client_frame_node) { |
| SendWorkerNotification(worker_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeClientWorkerAdded( |
| const performance_manager::WorkerNode* worker_node, |
| const performance_manager::WorkerNode* client_worker_node) { |
| // Nothing to do. |
| } |
| |
| void DiscardsGraphDumpImpl::OnClientWorkerAdded( |
| const performance_manager::WorkerNode* worker_node, |
| const performance_manager::WorkerNode* client_worker_node) { |
| SendWorkerNotification(worker_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::OnBeforeClientWorkerRemoved( |
| const performance_manager::WorkerNode* worker_node, |
| const performance_manager::WorkerNode* client_worker_node) { |
| SendWorkerNotification(worker_node, false); |
| } |
| |
| void DiscardsGraphDumpImpl::AddNode(const performance_manager::Node* node) { |
| DCHECK(node_ids_.find(node) == node_ids_.end()); |
| NodeId new_id = node_id_generator_.GenerateNextId(); |
| node_ids_.insert(std::make_pair(node, new_id)); |
| nodes_by_id_.insert(std::make_pair(new_id, node)); |
| } |
| |
| void DiscardsGraphDumpImpl::RemoveNode(const performance_manager::Node* node) { |
| auto it = node_ids_.find(node); |
| CHECK(it != node_ids_.end()); |
| NodeId node_id = it->second; |
| node_ids_.erase(it); |
| size_t erased = nodes_by_id_.erase(node_id); |
| DCHECK_EQ(1u, erased); |
| } |
| |
| bool DiscardsGraphDumpImpl::HasNode( |
| const performance_manager::Node* node) const { |
| return node_ids_.find(node) != node_ids_.end(); |
| } |
| |
| int64_t DiscardsGraphDumpImpl::GetNodeId( |
| const performance_manager::Node* node) const { |
| if (node == nullptr) { |
| return 0; |
| } |
| |
| auto it = node_ids_.find(node); |
| CHECK(it != node_ids_.end()); |
| return it->second.GetUnsafeValue(); |
| } |
| |
| base::SequenceBound<DiscardsGraphDumpImpl::FaviconRequestHelper>& |
| DiscardsGraphDumpImpl::EnsureFaviconRequestHelper() { |
| if (!favicon_request_helper_) { |
| favicon_request_helper_ = base::SequenceBound<FaviconRequestHelper>( |
| content::GetUIThreadTaskRunner({})); |
| } |
| return favicon_request_helper_; |
| } |
| |
| DiscardsGraphDumpImpl::FaviconAvailableCallback |
| DiscardsGraphDumpImpl::GetFaviconAvailableCallback(int64_t serialization_id) { |
| return base::BindPostTaskToCurrentDefault( |
| base::BindOnce(&DiscardsGraphDumpImpl::SendFaviconNotification, |
| weak_factory_.GetWeakPtr(), serialization_id)); |
| } |
| |
| void DiscardsGraphDumpImpl::StartPageFaviconRequest( |
| const performance_manager::PageNode* page_node) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!page_node->GetMainFrameUrl().is_valid()) { |
| return; |
| } |
| |
| EnsureFaviconRequestHelper() |
| .AsyncCall(&FaviconRequestHelper::RequestFavicon) |
| .WithArgs(page_node->GetMainFrameUrl(), page_node->GetWebContents(), |
| GetFaviconAvailableCallback(GetNodeId(page_node))); |
| } |
| |
| void DiscardsGraphDumpImpl::StartFrameFaviconRequest( |
| const performance_manager::FrameNode* frame_node) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!frame_node->GetURL().is_valid()) { |
| return; |
| } |
| |
| EnsureFaviconRequestHelper() |
| .AsyncCall(&FaviconRequestHelper::RequestFavicon) |
| .WithArgs(frame_node->GetURL(), |
| frame_node->GetPageNode()->GetWebContents(), |
| GetFaviconAvailableCallback(GetNodeId(frame_node))); |
| } |
| |
| void DiscardsGraphDumpImpl::SendNotificationToAllNodes(bool created) { |
| performance_manager::Graph* graph = GetOwningGraph(); |
| for (const performance_manager::ProcessNode* process_node : |
| graph->GetAllProcessNodes()) { |
| SendProcessNotification(process_node, created); |
| } |
| |
| for (const performance_manager::PageNode* page_node : |
| graph->GetAllPageNodes()) { |
| SendPageNotification(page_node, created); |
| if (created) { |
| StartPageFaviconRequest(page_node); |
| } |
| |
| // Dispatch preorder frame notifications. |
| for (const performance_manager::FrameNode* main_frame_node : |
| page_node->GetMainFrameNodes()) { |
| ForFrameAndOffspring( |
| main_frame_node, |
| [this, created](const performance_manager::FrameNode* frame_node) { |
| this->SendFrameNotification(frame_node, created); |
| if (created) { |
| this->StartFrameFaviconRequest(frame_node); |
| } |
| }); |
| } |
| } |
| |
| for (const performance_manager::WorkerNode* worker_node : |
| graph->GetAllWorkerNodes()) { |
| SendWorkerNotification(worker_node, created); |
| } |
| } |
| |
| void DiscardsGraphDumpImpl::SendFrameNotification( |
| const performance_manager::FrameNode* frame, |
| bool created) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // TODO(crbug.com/40109021): Add more frame properties. |
| discards::mojom::FrameInfoPtr frame_info = discards::mojom::FrameInfo::New(); |
| |
| frame_info->id = GetNodeId(frame); |
| |
| auto* parent_frame = frame->GetParentFrameNode(); |
| frame_info->parent_frame_id = GetNodeId(parent_frame); |
| |
| auto* process = frame->GetProcessNode(); |
| frame_info->process_id = GetNodeId(process); |
| |
| auto* page = frame->GetPageNode(); |
| frame_info->page_id = GetNodeId(page); |
| |
| frame_info->url = frame->GetURL(); |
| frame_info->description_json = |
| ToJSON(GetOwningGraph()->GetNodeDataDescriberRegistry()->DescribeNodeData( |
| frame)); |
| |
| if (created) { |
| change_subscriber_->FrameCreated(std::move(frame_info)); |
| } else { |
| change_subscriber_->FrameChanged(std::move(frame_info)); |
| } |
| } |
| |
| void DiscardsGraphDumpImpl::SendPageNotification( |
| const performance_manager::PageNode* page_node, |
| bool created) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // TODO(crbug.com/40109021): Add more page_node properties. |
| discards::mojom::PageInfoPtr page_info = discards::mojom::PageInfo::New(); |
| |
| page_info->id = GetNodeId(page_node); |
| page_info->main_frame_url = page_node->GetMainFrameUrl(); |
| page_info->opener_frame_id = GetNodeId(page_node->GetOpenerFrameNode()); |
| page_info->embedder_frame_id = GetNodeId(page_node->GetEmbedderFrameNode()); |
| page_info->description_json = |
| ToJSON(GetOwningGraph()->GetNodeDataDescriberRegistry()->DescribeNodeData( |
| page_node)); |
| |
| if (created) { |
| change_subscriber_->PageCreated(std::move(page_info)); |
| } else { |
| change_subscriber_->PageChanged(std::move(page_info)); |
| } |
| } |
| |
| void DiscardsGraphDumpImpl::SendProcessNotification( |
| const performance_manager::ProcessNode* process, |
| bool created) { |
| // TODO(crbug.com/40109021): Add more process properties. |
| discards::mojom::ProcessInfoPtr process_info = |
| discards::mojom::ProcessInfo::New(); |
| |
| process_info->id = GetNodeId(process); |
| process_info->pid = process->GetProcessId(); |
| process_info->private_footprint_kb = process->GetPrivateFootprint().InKiB(); |
| |
| process_info->description_json = |
| ToJSON(GetOwningGraph()->GetNodeDataDescriberRegistry()->DescribeNodeData( |
| process)); |
| |
| if (created) { |
| change_subscriber_->ProcessCreated(std::move(process_info)); |
| } else { |
| change_subscriber_->ProcessChanged(std::move(process_info)); |
| } |
| } |
| |
| void DiscardsGraphDumpImpl::SendWorkerNotification( |
| const performance_manager::WorkerNode* worker, |
| bool created) { |
| // TODO(crbug.com/40109021): Add more process properties. |
| discards::mojom::WorkerInfoPtr worker_info = |
| discards::mojom::WorkerInfo::New(); |
| |
| worker_info->id = GetNodeId(worker); |
| worker_info->url = worker->GetURL(); |
| worker_info->process_id = GetNodeId(worker->GetProcessNode()); |
| |
| for (const performance_manager::FrameNode* client_frame : |
| worker->GetClientFrames()) { |
| worker_info->client_frame_ids.push_back(GetNodeId(client_frame)); |
| } |
| for (const performance_manager::WorkerNode* client_worker : |
| worker->GetClientWorkers()) { |
| worker_info->client_worker_ids.push_back(GetNodeId(client_worker)); |
| } |
| for (const performance_manager::WorkerNode* child_worker : |
| worker->GetChildWorkers()) { |
| worker_info->child_worker_ids.push_back(GetNodeId(child_worker)); |
| } |
| |
| worker_info->description_json = |
| ToJSON(GetOwningGraph()->GetNodeDataDescriberRegistry()->DescribeNodeData( |
| worker)); |
| |
| if (created) { |
| change_subscriber_->WorkerCreated(std::move(worker_info)); |
| } else { |
| change_subscriber_->WorkerChanged(std::move(worker_info)); |
| } |
| } |
| |
| void DiscardsGraphDumpImpl::SendDeletionNotification( |
| const performance_manager::Node* node) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| change_subscriber_->NodeDeleted(GetNodeId(node)); |
| } |
| |
| void DiscardsGraphDumpImpl::SendFaviconNotification( |
| int64_t serialization_id, |
| scoped_refptr<base::RefCountedMemory> bitmap_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_NE(0u, bitmap_data->size()); |
| |
| discards::mojom::FavIconInfoPtr icon_info = |
| discards::mojom::FavIconInfo::New(); |
| icon_info->node_id = serialization_id; |
| icon_info->icon_data = base::Base64Encode(*bitmap_data); |
| |
| change_subscriber_->FavIconDataAvailable(std::move(icon_info)); |
| } |
| |
| // static |
| void DiscardsGraphDumpImpl::OnConnectionError(DiscardsGraphDumpImpl* impl) { |
| std::unique_ptr<GraphOwned> owned_impl = |
| impl->GetOwningGraph()->TakeFromGraph(impl); |
| } |