blob: 3163ab70798ec8e7ce983683f7e5040531ed97e5 [file] [log] [blame]
// Copyright 2017 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/profiling/json_exporter.h"
#include <sstream>
#include "base/gtest_prod_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/process/process.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/profiling/backtrace_storage.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/os_metrics.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace profiling {
namespace {
const size_t kNoSizeThreshold = 0;
const size_t kNoCountThreshold = 0;
const size_t kSizeThreshold = 1500;
const size_t kCountThreshold = 1000;
using MemoryMap = std::vector<memory_instrumentation::mojom::VmRegionPtr>;
static constexpr int kNoParent = -1;
// Finds the first period_interval trace event in the given JSON trace.
// Returns null on failure.
const base::Value* FindFirstPeriodicInterval(const base::Value& root) {
const base::Value* found_trace_events =
root.FindKeyOfType("traceEvents", base::Value::Type::LIST);
if (!found_trace_events)
return nullptr;
for (const base::Value& cur : found_trace_events->GetList()) {
const base::Value* found_name =
cur.FindKeyOfType("name", base::Value::Type::STRING);
if (!found_name)
return nullptr;
if (found_name->GetString() == "periodic_interval")
return &cur;
}
return nullptr;
}
// Finds the first vm region in the given periodic interval. Returns null on
// failure.
const base::Value* FindFirstRegionWithAnyName(
const base::Value* periodic_interval) {
const base::Value* found_args =
periodic_interval->FindKeyOfType("args", base::Value::Type::DICTIONARY);
if (!found_args)
return nullptr;
const base::Value* found_dumps =
found_args->FindKeyOfType("dumps", base::Value::Type::DICTIONARY);
if (!found_dumps)
return nullptr;
const base::Value* found_mmaps = found_dumps->FindKeyOfType(
"process_mmaps", base::Value::Type::DICTIONARY);
if (!found_mmaps)
return nullptr;
const base::Value* found_regions =
found_mmaps->FindKeyOfType("vm_regions", base::Value::Type::LIST);
if (!found_regions)
return nullptr;
for (const base::Value& cur : found_regions->GetList()) {
const base::Value* found_name =
cur.FindKeyOfType("mf", base::Value::Type::STRING);
if (!found_name)
return nullptr;
if (found_name->GetString() != "")
return &cur;
}
return nullptr;
}
int GetStringFromStringTable(const base::Value* strings, const char* text) {
for (const auto& string : strings->GetList()) {
const base::Value* string_id =
string.FindKeyOfType("id", base::Value::Type::INTEGER);
const base::Value* string_text =
string.FindKeyOfType("string", base::Value::Type::STRING);
if (string_id != nullptr && string_text != nullptr &&
string_text->GetString() == text)
return string_id->GetInt();
}
return -1;
}
int GetNodeWithNameID(const base::Value* nodes, int sid) {
for (const auto& node : nodes->GetList()) {
const base::Value* node_id =
node.FindKeyOfType("id", base::Value::Type::INTEGER);
const base::Value* node_name_sid =
node.FindKeyOfType("name_sid", base::Value::Type::INTEGER);
if (node_id != nullptr && node_name_sid != nullptr &&
node_name_sid->GetInt() == sid)
return node_id->GetInt();
}
return -1;
}
int GetOffsetForBacktraceID(const base::Value* nodes, int id) {
int offset = 0;
for (const auto& node : nodes->GetList()) {
if (node.GetInt() == id)
return offset;
offset++;
}
return -1;
}
bool IsBacktraceInList(const base::Value* backtraces, int id, int parent) {
for (const auto& backtrace : backtraces->GetList()) {
const base::Value* backtrace_id =
backtrace.FindKeyOfType("id", base::Value::Type::INTEGER);
if (backtrace_id == nullptr)
continue;
const base::Value* backtrace_parent =
backtrace.FindKeyOfType("parent", base::Value::Type::INTEGER);
int backtrace_parent_int = kNoParent;
if (backtrace_parent)
backtrace_parent_int = backtrace_parent->GetInt();
if (backtrace_id->GetInt() == id && backtrace_parent_int == parent)
return true;
}
return false;
}
} // namespace
TEST(ProfilingJsonExporterTest, DumpsHeader) {
BacktraceStorage backtrace_storage;
AllocationEventSet events;
std::ostringstream stream;
ExportAllocationEventSetToJSON(1234, events, MemoryMap(), stream, nullptr,
kNoSizeThreshold, kNoCountThreshold);
std::string json = stream.str();
// JSON should parse.
base::JSONReader reader(base::JSON_PARSE_RFC);
std::unique_ptr<base::Value> root = reader.ReadToValue(stream.str());
ASSERT_EQ(base::JSONReader::JSON_NO_ERROR, reader.error_code())
<< reader.GetErrorMessage();
ASSERT_TRUE(root);
// Find the periodic_interval event.
const base::Value* periodic_interval = FindFirstPeriodicInterval(*root);
ASSERT_TRUE(periodic_interval) << "Array contains no periodic_interval";
// The following fields are mandatory to make a trace viewable in
// chrome://tracing UI.
const base::Value* pid =
periodic_interval->FindKeyOfType("pid", base::Value::Type::INTEGER);
ASSERT_TRUE(pid);
EXPECT_NE(0, pid->GetInt());
const base::Value* ts =
periodic_interval->FindKeyOfType("ts", base::Value::Type::INTEGER);
ASSERT_TRUE(ts);
EXPECT_NE(0, ts->GetInt());
const base::Value* id =
periodic_interval->FindKeyOfType("id", base::Value::Type::STRING);
ASSERT_TRUE(id);
EXPECT_NE("", id->GetString());
const base::Value* args =
periodic_interval->FindKeyOfType("args", base::Value::Type::DICTIONARY);
ASSERT_TRUE(args);
}
TEST(ProfilingJsonExporterTest, Simple) {
BacktraceStorage backtrace_storage;
std::vector<Address> stack1;
stack1.push_back(Address(0x1234));
stack1.push_back(Address(0x5678));
const Backtrace* bt1 = backtrace_storage.Insert(std::move(stack1));
std::vector<Address> stack2;
stack2.push_back(Address(0x1234));
stack2.push_back(Address(0x9012));
stack2.push_back(Address(0x9013));
const Backtrace* bt2 = backtrace_storage.Insert(std::move(stack2));
AllocationEventSet events;
events.insert(AllocationEvent(Address(0x1), 20, bt1));
events.insert(AllocationEvent(Address(0x2), 32, bt2));
events.insert(AllocationEvent(Address(0x3), 20, bt1));
std::ostringstream stream;
ExportAllocationEventSetToJSON(1234, events, MemoryMap(), stream, nullptr,
kNoSizeThreshold, kNoCountThreshold);
std::string json = stream.str();
// JSON should parse.
base::JSONReader reader(base::JSON_PARSE_RFC);
std::unique_ptr<base::Value> root = reader.ReadToValue(stream.str());
ASSERT_EQ(base::JSONReader::JSON_NO_ERROR, reader.error_code())
<< reader.GetErrorMessage();
ASSERT_TRUE(root);
// The trace array contains two items, a process_name one and a
// periodic_interval one. Find the latter.
const base::Value* periodic_interval = FindFirstPeriodicInterval(*root);
ASSERT_TRUE(periodic_interval) << "Array contains no periodic_interval";
const base::Value* heaps_v2 =
periodic_interval->FindPath({"args", "dumps", "heaps_v2"});
ASSERT_TRUE(heaps_v2);
// Retrieve maps and validate their structure.
const base::Value* nodes = heaps_v2->FindPath({"maps", "nodes"});
const base::Value* strings = heaps_v2->FindPath({"maps", "strings"});
ASSERT_TRUE(nodes);
ASSERT_TRUE(strings);
// Validate the strings table.
EXPECT_EQ(5u, strings->GetList().size());
int sid_unknown = GetStringFromStringTable(strings, "[unknown]");
int sid_1234 = GetStringFromStringTable(strings, "pc:1234");
int sid_5678 = GetStringFromStringTable(strings, "pc:5678");
int sid_9012 = GetStringFromStringTable(strings, "pc:9012");
int sid_9013 = GetStringFromStringTable(strings, "pc:9013");
EXPECT_NE(-1, sid_unknown);
EXPECT_NE(-1, sid_1234);
EXPECT_NE(-1, sid_5678);
EXPECT_NE(-1, sid_9012);
EXPECT_NE(-1, sid_9013);
// Validate the nodes table.
// Nodes should be a list with 4 items.
// [0] => address: 1234 parent: none
// [1] => address: 5678 parent: 0
// [2] => address: 9012 parent: 0
// [3] => address: 9013 parent: 2
EXPECT_EQ(4u, nodes->GetList().size());
int id0 = GetNodeWithNameID(nodes, sid_1234);
int id1 = GetNodeWithNameID(nodes, sid_5678);
int id2 = GetNodeWithNameID(nodes, sid_9012);
int id3 = GetNodeWithNameID(nodes, sid_9013);
EXPECT_NE(-1, id0);
EXPECT_NE(-1, id1);
EXPECT_NE(-1, id2);
EXPECT_NE(-1, id3);
EXPECT_TRUE(IsBacktraceInList(nodes, id0, kNoParent));
EXPECT_TRUE(IsBacktraceInList(nodes, id1, id0));
EXPECT_TRUE(IsBacktraceInList(nodes, id2, id0));
EXPECT_TRUE(IsBacktraceInList(nodes, id3, id2));
// Retrieve the allocations and valid their structure.
const base::Value* counts =
heaps_v2->FindPath({"allocators", "malloc", "counts"});
const base::Value* types =
heaps_v2->FindPath({"allocators", "malloc", "types"});
const base::Value* sizes =
heaps_v2->FindPath({"allocators", "malloc", "sizes"});
const base::Value* backtraces =
heaps_v2->FindPath({"allocators", "malloc", "nodes"});
ASSERT_TRUE(counts);
ASSERT_TRUE(types);
ASSERT_TRUE(sizes);
ASSERT_TRUE(backtraces);
// Counts should be a list of two items, a 1 and a 2. The two matching 20-byte
// allocations should be coalesced to produce the 2.
EXPECT_EQ(2u, counts->GetList().size());
EXPECT_EQ(2u, types->GetList().size());
EXPECT_EQ(2u, sizes->GetList().size());
int node1 = GetOffsetForBacktraceID(backtraces, id1);
int node3 = GetOffsetForBacktraceID(backtraces, id3);
EXPECT_NE(-1, node1);
EXPECT_NE(-1, node3);
// Validate node allocated with |stack1|.
EXPECT_EQ(2, counts->GetList()[node1].GetInt());
EXPECT_EQ(0, types->GetList()[node1].GetInt());
EXPECT_EQ(20, sizes->GetList()[node1].GetInt());
EXPECT_EQ(id1, backtraces->GetList()[node1].GetInt());
// Validate node allocated with |stack2|.
EXPECT_EQ(1, counts->GetList()[node3].GetInt());
EXPECT_EQ(0, types->GetList()[node3].GetInt());
EXPECT_EQ(32, sizes->GetList()[node3].GetInt());
EXPECT_EQ(id3, backtraces->GetList()[node3].GetInt());
}
TEST(ProfilingJsonExporterTest, SimpleWithFilteredAllocations) {
BacktraceStorage backtrace_storage;
std::vector<Address> stack1;
stack1.push_back(Address(0x1234));
const Backtrace* bt1 = backtrace_storage.Insert(std::move(stack1));
std::vector<Address> stack2;
stack2.push_back(Address(0x5678));
const Backtrace* bt2 = backtrace_storage.Insert(std::move(stack2));
std::vector<Address> stack3;
stack3.push_back(Address(0x9999));
const Backtrace* bt3 = backtrace_storage.Insert(std::move(stack3));
AllocationEventSet events;
events.insert(AllocationEvent(Address(0x1), 16, bt1));
events.insert(AllocationEvent(Address(0x2), 32, bt1));
events.insert(AllocationEvent(Address(0x3), 1000, bt2));
events.insert(AllocationEvent(Address(0x4), 1000, bt2));
for (size_t i = 0; i < kCountThreshold + 1; ++i)
events.insert(AllocationEvent(Address(0x5 + i), 1, bt3));
// Validate filtering by size and count.
std::ostringstream stream;
ExportAllocationEventSetToJSON(1234, events, MemoryMap(), stream, nullptr,
kSizeThreshold, kCountThreshold);
std::string json = stream.str();
// JSON should parse.
base::JSONReader reader(base::JSON_PARSE_RFC);
std::unique_ptr<base::Value> root = reader.ReadToValue(stream.str());
ASSERT_EQ(base::JSONReader::JSON_NO_ERROR, reader.error_code())
<< reader.GetErrorMessage();
ASSERT_TRUE(root);
// The trace array contains two items, a process_name one and a
// periodic_interval one. Find the latter.
const base::Value* periodic_interval = FindFirstPeriodicInterval(*root);
ASSERT_TRUE(periodic_interval) << "Array contains no periodic_interval";
const base::Value* heaps_v2 =
periodic_interval->FindPath({"args", "dumps", "heaps_v2"});
ASSERT_TRUE(heaps_v2);
const base::Value* nodes = heaps_v2->FindPath({"maps", "nodes"});
const base::Value* strings = heaps_v2->FindPath({"maps", "strings"});
ASSERT_TRUE(nodes);
ASSERT_TRUE(strings);
// Validate the strings table.
EXPECT_EQ(3u, strings->GetList().size());
int sid_unknown = GetStringFromStringTable(strings, "[unknown]");
int sid_1234 = GetStringFromStringTable(strings, "pc:1234");
int sid_5678 = GetStringFromStringTable(strings, "pc:5678");
int sid_9999 = GetStringFromStringTable(strings, "pc:9999");
EXPECT_NE(-1, sid_unknown);
EXPECT_EQ(-1, sid_1234); // Must be filtered.
EXPECT_NE(-1, sid_5678);
EXPECT_NE(-1, sid_9999);
// Validate the nodes table.
// Nodes should be a list with 4 items.
// [0] => address: 5678 parent: none
// [1] => address: 9999 parent: none
EXPECT_EQ(2u, nodes->GetList().size());
int id0 = GetNodeWithNameID(nodes, sid_5678);
int id1 = GetNodeWithNameID(nodes, sid_9999);
EXPECT_NE(-1, id0);
EXPECT_NE(-1, id1);
EXPECT_TRUE(IsBacktraceInList(nodes, id0, kNoParent));
EXPECT_TRUE(IsBacktraceInList(nodes, id1, kNoParent));
// Counts should be a list with one item. Items with |bt1| are filtered.
// For |stack2|, there are two allocations of 1000 bytes. which is above the
// 1500 bytes threshold. For |stack3|, there are 1001 allocations of 1 bytes,
// which is above the 1000 allocations threshold.
const base::Value* backtraces =
heaps_v2->FindPath({"allocators", "malloc", "nodes"});
ASSERT_TRUE(backtraces);
EXPECT_EQ(2u, backtraces->GetList().size());
int node_bt2 = GetOffsetForBacktraceID(backtraces, id0);
int node_bt3 = GetOffsetForBacktraceID(backtraces, id1);
EXPECT_NE(-1, node_bt2);
EXPECT_NE(-1, node_bt3);
}
TEST(ProfilingJsonExporterTest, MemoryMaps) {
AllocationEventSet events;
std::vector<memory_instrumentation::mojom::VmRegionPtr> memory_maps =
memory_instrumentation::OSMetrics::GetProcessMemoryMaps(
base::Process::Current().Pid());
ASSERT_GT(memory_maps.size(), 2u);
std::ostringstream stream;
ExportAllocationEventSetToJSON(1234, events, memory_maps, stream, nullptr,
kNoSizeThreshold, kNoCountThreshold);
std::string json = stream.str();
// JSON should parse.
base::JSONReader reader(base::JSON_PARSE_RFC);
std::unique_ptr<base::Value> root = reader.ReadToValue(stream.str());
ASSERT_EQ(base::JSONReader::JSON_NO_ERROR, reader.error_code())
<< reader.GetErrorMessage();
ASSERT_TRUE(root);
const base::Value* periodic_interval = FindFirstPeriodicInterval(*root);
ASSERT_TRUE(periodic_interval) << "Array contains no periodic_interval";
const base::Value* region = FindFirstRegionWithAnyName(periodic_interval);
ASSERT_TRUE(region) << "Array contains no named vm regions";
const base::Value* start_address =
region->FindKeyOfType("sa", base::Value::Type::STRING);
ASSERT_TRUE(start_address);
EXPECT_NE(start_address->GetString(), "");
EXPECT_NE(start_address->GetString(), "0");
const base::Value* size =
region->FindKeyOfType("sz", base::Value::Type::STRING);
ASSERT_TRUE(size);
EXPECT_NE(size->GetString(), "");
EXPECT_NE(size->GetString(), "0");
}
TEST(ProfilingJsonExporterTest, Metadata) {
BacktraceStorage backtrace_storage;
std::vector<Address> stack1;
stack1.push_back(Address(1234));
const Backtrace* bt1 = backtrace_storage.Insert(std::move(stack1));
AllocationEventSet events;
events.insert(AllocationEvent(Address(0x1), 16, bt1));
// Generate metadata to pass in.
std::unique_ptr<base::DictionaryValue> metadata_dict(
new base::DictionaryValue);
metadata_dict->SetKey("product-version", base::Value("asdf1"));
metadata_dict->SetKey("user-agent", base::Value("\"\"\"9283hfa--+,/asdf2"));
base::Value metadata_dict_copy = metadata_dict->Clone();
std::ostringstream stream;
ExportAllocationEventSetToJSON(1234, events, MemoryMap(), stream,
std::move(metadata_dict), kNoSizeThreshold,
kNoCountThreshold);
std::string json = stream.str();
// JSON should parse.
base::JSONReader reader(base::JSON_PARSE_RFC);
std::unique_ptr<base::Value> root = reader.ReadToValue(stream.str());
ASSERT_EQ(base::JSONReader::JSON_NO_ERROR, reader.error_code())
<< reader.GetErrorMessage();
ASSERT_TRUE(root);
base::Value* found_metadatas =
root->FindKeyOfType("metadata", base::Value::Type::DICTIONARY);
ASSERT_TRUE(found_metadatas) << "Array contains no metadata";
EXPECT_EQ(metadata_dict_copy, *found_metadatas);
}
} // namespace profiling
OSZAR »