blob: c16a03bccac5c017e54c31b9f3bbfb0e7808b331 [file] [log] [blame]
// Copyright 2018 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 "chrome/browser/ui/webui/discards/graph_dump_impl.h"
#include <map>
#include <set>
#include <utility>
#include "base/containers/contains.h"
#include "base/json/json_reader.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "chrome/browser/ui/webui/discards/discards.mojom.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/test_support/graph_impl.h"
#include "components/performance_manager/test_support/mock_graphs.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using performance_manager::NodeBase;
const char kHtmlMimeType[] = "text/html";
class TestChangeStream : public discards::mojom::GraphChangeStream {
public:
using FrameMap = std::map<int64_t, discards::mojom::FrameInfoPtr>;
using PageMap = std::map<int64_t, discards::mojom::PageInfoPtr>;
using ProcessMap = std::map<int64_t, discards::mojom::ProcessInfoPtr>;
using WorkerMap = std::map<int64_t, discards::mojom::WorkerInfoPtr>;
using IdSet = std::set<int64_t>;
TestChangeStream() {}
mojo::PendingRemote<discards::mojom::GraphChangeStream> GetRemote() {
mojo::PendingRemote<discards::mojom::GraphChangeStream> remote;
receiver_.Bind(remote.InitWithNewPipeAndPassReceiver());
return remote;
}
// discards::mojom::GraphChangeStream implementation
void FrameCreated(discards::mojom::FrameInfoPtr frame) override {
EXPECT_FALSE(HasId(frame->id));
// If the node has a parent frame, we must have heard of it.
EXPECT_TRUE(HasIdIfValid(frame->parent_frame_id));
EXPECT_TRUE(HasId(frame->page_id));
EXPECT_TRUE(HasId(frame->process_id));
id_set_.insert(frame->id);
frame_map_.insert(std::make_pair(frame->id, std::move(frame)));
}
void PageCreated(discards::mojom::PageInfoPtr page) override {
EXPECT_FALSE(HasId(page->id));
id_set_.insert(page->id);
page_map_.insert(std::make_pair(page->id, std::move(page)));
}
void ProcessCreated(discards::mojom::ProcessInfoPtr process) override {
EXPECT_FALSE(HasId(process->id));
id_set_.insert(process->id);
process_map_.insert(std::make_pair(process->id, std::move(process)));
}
void WorkerCreated(discards::mojom::WorkerInfoPtr worker) override {
EXPECT_FALSE(HasId(worker->id));
id_set_.insert(worker->id);
worker_map_.insert(std::make_pair(worker->id, std::move(worker)));
}
void FrameChanged(discards::mojom::FrameInfoPtr frame) override {
EXPECT_TRUE(HasId(frame->id));
frame_map_[frame->id] = std::move(frame);
++num_changes_;
}
void PageChanged(discards::mojom::PageInfoPtr page) override {
EXPECT_TRUE(HasId(page->id));
page_map_[page->id] = std::move(page);
++num_changes_;
}
void ProcessChanged(discards::mojom::ProcessInfoPtr process) override {
EXPECT_TRUE(HasId(process->id));
process_map_[process->id] = std::move(process);
++num_changes_;
}
void WorkerChanged(discards::mojom::WorkerInfoPtr worker) override {
EXPECT_TRUE(HasId(worker->id));
worker_map_[worker->id] = std::move(worker);
++num_changes_;
}
void FavIconDataAvailable(discards::mojom::FavIconInfoPtr favicon) override {}
void NodeDeleted(int64_t node_id) override {
EXPECT_EQ(1u, id_set_.erase(node_id));
size_t erased = frame_map_.erase(node_id) + page_map_.erase(node_id) +
process_map_.erase(node_id);
EXPECT_EQ(1u, erased);
}
const FrameMap& frame_map() const { return frame_map_; }
const PageMap& page_map() const { return page_map_; }
const ProcessMap& process_map() const { return process_map_; }
const WorkerMap& worker_map() const { return worker_map_; }
const IdSet& id_set() const { return id_set_; }
size_t num_changes() const { return num_changes_; }
private:
bool HasId(int64_t id) { return base::Contains(id_set_, id); }
bool HasIdIfValid(int64_t id) { return id == 0u || HasId(id); }
FrameMap frame_map_;
PageMap page_map_;
ProcessMap process_map_;
WorkerMap worker_map_;
IdSet id_set_;
size_t num_changes_ = 0;
mojo::Receiver<discards::mojom::GraphChangeStream> receiver_{this};
};
class DiscardsGraphDumpImplTest : public testing::Test {
public:
void TearDown() override { graph_.TearDown(); }
protected:
performance_manager::TestGraphImpl graph_;
};
class TestNodeDataDescriber : public performance_manager::NodeDataDescriber {
public:
// NodeDataDescriber implementations:
base::Value DescribeFrameNodeData(
const performance_manager::FrameNode* node) const override {
return base::Value("frame");
}
base::Value DescribePageNodeData(
const performance_manager::PageNode* node) const override {
return base::Value("page");
}
base::Value DescribeProcessNodeData(
const performance_manager::ProcessNode* node) const override {
return base::Value("process");
}
base::Value DescribeSystemNodeData(
const performance_manager::SystemNode* node) const override {
return base::Value("system");
}
base::Value DescribeWorkerNodeData(
const performance_manager::WorkerNode* node) const override {
return base::Value("worker");
}
};
} // namespace
TEST_F(DiscardsGraphDumpImplTest, ChangeStream) {
using performance_manager::TestNodeWrapper;
using performance_manager::WorkerNodeImpl;
content::BrowserTaskEnvironment task_environment;
performance_manager::MockMultiplePagesWithMultipleProcessesGraph mock_graph(
&graph_);
TestNodeWrapper<WorkerNodeImpl> worker(
TestNodeWrapper<WorkerNodeImpl>::Create(
&graph_, performance_manager::WorkerNode::WorkerType::kDedicated,
mock_graph.process.get()));
worker->AddClientFrame(mock_graph.frame.get());
base::TimeTicks now = base::TimeTicks::Now();
const GURL kExampleUrl("http://www.example.org");
int64_t next_navigation_id = 1;
mock_graph.page->OnMainFrameNavigationCommitted(
false, now, next_navigation_id++, kExampleUrl, kHtmlMimeType);
mock_graph.other_page->OnMainFrameNavigationCommitted(
false, now, next_navigation_id++, kExampleUrl, kHtmlMimeType);
auto* main_frame = mock_graph.page->GetMainFrameNodeImpl();
main_frame->OnNavigationCommitted(kExampleUrl, /* same_document */ false);
std::unique_ptr<DiscardsGraphDumpImpl> impl =
std::make_unique<DiscardsGraphDumpImpl>();
DiscardsGraphDumpImpl* impl_raw = impl.get();
// Create a mojo remote to the impl.
mojo::Remote<discards::mojom::GraphDump> graph_dump_remote;
impl->BindWithGraph(&graph_, graph_dump_remote.BindNewPipeAndPassReceiver());
graph_.PassToGraph(std::move(impl));
TestNodeDataDescriber describer;
graph_.GetNodeDataDescriberRegistry()->RegisterDescriber(&describer, "test");
TestChangeStream change_stream;
graph_dump_remote->SubscribeToChanges(change_stream.GetRemote());
task_environment.RunUntilIdle();
// Validate that the initial graph state dump is complete. Note that there is
// an update for each node as part of the initial state dump.
EXPECT_EQ(8u, change_stream.num_changes());
EXPECT_EQ(8u, change_stream.id_set().size());
EXPECT_EQ(2u, change_stream.process_map().size());
for (const auto& kv : change_stream.process_map()) {
const auto* process_info = kv.second.get();
EXPECT_NE(0u, process_info->id);
EXPECT_EQ(base::JSONReader::Read("{\"test\":\"process\"}"),
base::JSONReader::Read(process_info->description_json));
}
EXPECT_EQ(3u, change_stream.frame_map().size());
for (const auto& kv : change_stream.frame_map()) {
EXPECT_EQ(base::JSONReader::Read("{\"test\":\"frame\"}"),
base::JSONReader::Read(kv.second->description_json));
}
EXPECT_EQ(1u, change_stream.worker_map().size());
for (const auto& kv : change_stream.worker_map()) {
EXPECT_EQ(base::JSONReader::Read("{\"test\":\"worker\"}"),
base::JSONReader::Read(kv.second->description_json));
}
// Count the top-level frames as we go.
size_t top_level_frames = 0;
for (const auto& kv : change_stream.frame_map()) {
const auto& frame = kv.second;
if (frame->parent_frame_id == 0) {
++top_level_frames;
// Top level frames should have a page ID.
EXPECT_NE(0u, frame->page_id);
// The page's main frame should have an URL.
if (frame->id == impl_raw->GetNodeIdForTesting(main_frame))
EXPECT_EQ(kExampleUrl, frame->url);
}
EXPECT_NE(0u, frame->id);
EXPECT_NE(0u, frame->process_id);
}
// Make sure we have one top-level frame per page.
EXPECT_EQ(change_stream.page_map().size(), top_level_frames);
EXPECT_EQ(2u, change_stream.page_map().size());
for (const auto& kv : change_stream.page_map()) {
const auto& page = kv.second;
EXPECT_NE(0u, page->id);
EXPECT_EQ(kExampleUrl, page->main_frame_url);
EXPECT_EQ(base::JSONReader::Read("{\"test\":\"page\"}"),
base::JSONReader::Read(kv.second->description_json));
}
// Test change notifications.
const GURL kAnotherURL("http://www.google.com/");
mock_graph.page->OnMainFrameNavigationCommitted(
false, now, next_navigation_id++, kAnotherURL, kHtmlMimeType);
size_t child_frame_id =
impl_raw->GetNodeIdForTesting(mock_graph.child_frame.get());
mock_graph.child_frame.reset();
task_environment.RunUntilIdle();
// Main frame navigation results in a notification for the url.
EXPECT_EQ(9u, change_stream.num_changes());
EXPECT_FALSE(base::Contains(change_stream.id_set(), child_frame_id));
const auto main_page_it = change_stream.page_map().find(
impl_raw->GetNodeIdForTesting(mock_graph.page.get()));
ASSERT_TRUE(main_page_it != change_stream.page_map().end());
EXPECT_EQ(kAnotherURL, main_page_it->second->main_frame_url);
task_environment.RunUntilIdle();
// Test RequestNodeDescriptions.
std::vector<int64_t> descriptions_requested;
for (int64_t node_id : change_stream.id_set()) {
descriptions_requested.push_back(node_id);
}
// Increase the last ID by one. As the entries are in increasing order, this
// results in a request for all but one nodes, and one non-existent node id.
descriptions_requested.back() += 1;
{
base::RunLoop run_loop;
base::RepeatingClosure quit_closure = run_loop.QuitClosure();
graph_dump_remote->RequestNodeDescriptions(
descriptions_requested,
base::BindLambdaForTesting(
[&descriptions_requested,
&quit_closure](const base::flat_map<int64_t, std::string>&
node_descriptions_json) {
std::vector<int64_t> keys_received;
// Check that the descriptions make sense.
for (auto kv : node_descriptions_json) {
keys_received.push_back(kv.first);
base::Optional<base::Value> v =
base::JSONReader::Read(kv.second);
EXPECT_TRUE(v->is_dict());
std::string* str = v->FindStringKey("test");
EXPECT_TRUE(str);
if (str) {
EXPECT_TRUE(*str == "frame" || *str == "page" ||
*str == "process" || *str == "worker");
}
}
EXPECT_THAT(keys_received,
::testing::UnorderedElementsAreArray(
descriptions_requested.data(),
descriptions_requested.size() - 1));
quit_closure.Run();
}));
run_loop.Run();
}
task_environment.RunUntilIdle();
// Make sure the Dump impl is torn down when the proxy closes.
graph_dump_remote.reset();
task_environment.RunUntilIdle();
EXPECT_EQ(nullptr, graph_.TakeFromGraph(impl_raw));
worker->RemoveClientFrame(mock_graph.frame.get());
graph_.GetNodeDataDescriberRegistry()->UnregisterDescriber(&describer);
}