blob: 1bcedae2f189a9f6728ce760d7844a7d0a9c2f20 [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 "components/heap_profiling/test_driver.h"
#include <string>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/process/process_handle.h"
#include "base/run_loop.h"
#include "base/sampling_heap_profiler/poisson_allocation_sampler.h"
#include "base/stl_util.h"
#include "base/task/post_task.h"
#include "base/threading/platform_thread.h"
#include "base/trace_event/heap_profiler_event_filter.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/heap_profiling/supervisor.h"
#include "components/services/heap_profiling/public/cpp/controller.h"
#include "components/services/heap_profiling/public/cpp/sampling_profiler_wrapper.h"
#include "components/services/heap_profiling/public/cpp/settings.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/tracing_controller.h"
#include "content/public/common/service_manager_connection.h"
namespace heap_profiling {
namespace {
constexpr const char kTestCategory[] = "kTestCategory";
const char kMallocEvent[] = "kMallocEvent";
const char kMallocTypeTag[] = "kMallocTypeTag";
const char kPAEvent[] = "kPAEvent";
const char kVariadicEvent[] = "kVariadicEvent";
const char kThreadName[] = "kThreadName";
// Note: When we test sampling with |sample_everything| = true, we set the
// sampling interval to 2. It's important that all allocations made in this file
// have size >> 2, so that the probability that they are sampled is
// exponentially close to 1.
//
// Make some specific allocations in Browser to do a deeper test of the
// allocation tracking.
constexpr int kMallocAllocSize = 7907;
constexpr int kMallocAllocCount = 157;
constexpr int kVariadicAllocCount = 157;
// The sample rate should not affect the sampled allocations. Intentionally
// choose an odd number.
constexpr int kSampleRate = 7777;
constexpr int kSamplingAllocSize = 100;
constexpr int kSamplingAllocCount = 10000;
const char kSamplingAllocTypeName[] = "kSamplingAllocTypeName";
// Test fixed-size partition alloc. The size must be aligned to system pointer
// size.
constexpr int kPartitionAllocSize = 8 * 23;
constexpr int kPartitionAllocCount = 107;
static const char* kPartitionAllocTypeName = "kPartitionAllocTypeName";
// Ideally, we'd check to see that at least one renderer exists, and all
// renderers are being profiled, but something odd seems to be happening with
// warm-up/spare renderers.
//
// Whether at least 1 renderer exists, and at least 1 renderer is being
// profiled.
bool RenderersAreBeingProfiled(
const std::vector<base::ProcessId>& profiled_pids) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
for (auto iter = content::RenderProcessHost::AllHostsIterator();
!iter.IsAtEnd(); iter.Advance()) {
if (iter.GetCurrentValue()->GetProcess().Handle() ==
base::kNullProcessHandle)
continue;
base::ProcessId pid = iter.GetCurrentValue()->GetProcess().Pid();
if (base::ContainsValue(profiled_pids, pid)) {
return true;
}
}
return false;
}
// On success, populates |pid|.
int NumProcessesWithName(base::Value* dump_json,
std::string name,
std::vector<int>* pids) {
int num_processes = 0;
base::Value* events = dump_json->FindKey("traceEvents");
for (const base::Value& event : events->GetList()) {
const base::Value* found_name =
event.FindKeyOfType("name", base::Value::Type::STRING);
if (!found_name)
continue;
if (found_name->GetString() != "process_name")
continue;
const base::Value* found_args =
event.FindKeyOfType("args", base::Value::Type::DICTIONARY);
if (!found_args)
continue;
const base::Value* found_process_name =
found_args->FindKeyOfType("name", base::Value::Type::STRING);
if (!found_process_name)
continue;
if (found_process_name->GetString() != name)
continue;
if (pids) {
const base::Value* found_pid =
event.FindKeyOfType("pid", base::Value::Type::INTEGER);
if (!found_pid) {
LOG(ERROR) << "Process missing pid.";
return 0;
}
pids->push_back(found_pid->GetInt());
}
++num_processes;
}
return num_processes;
}
base::Value* FindArgDump(base::ProcessId pid,
base::Value* dump_json,
const char* arg) {
base::Value* events = dump_json->FindKey("traceEvents");
base::Value* dumps = nullptr;
base::Value* heaps_v2 = nullptr;
for (base::Value& event : events->GetList()) {
const base::Value* found_name =
event.FindKeyOfType("name", base::Value::Type::STRING);
if (!found_name)
continue;
if (found_name->GetString() != "periodic_interval")
continue;
const base::Value* found_pid =
event.FindKeyOfType("pid", base::Value::Type::INTEGER);
if (!found_pid)
continue;
if (static_cast<base::ProcessId>(found_pid->GetInt()) != pid)
continue;
dumps = &event;
heaps_v2 = dumps->FindPath({"args", "dumps", arg});
if (heaps_v2)
return heaps_v2;
}
return nullptr;
}
constexpr uint64_t kNullParent = std::numeric_limits<int>::max();
struct Node {
int name_id;
std::string name;
int parent_id = kNullParent;
};
using NodeMap = std::unordered_map<uint64_t, Node>;
// Parses maps.nodes and maps.strings. Returns |true| on success.
bool ParseNodes(base::Value* heaps_v2, NodeMap* output) {
base::Value* nodes = heaps_v2->FindPath({"maps", "nodes"});
for (const base::Value& node_value : nodes->GetList()) {
const base::Value* id = node_value.FindKey("id");
const base::Value* name_sid = node_value.FindKey("name_sid");
if (!id || !name_sid) {
LOG(ERROR) << "Node missing id or name_sid field";
return false;
}
Node node;
node.name_id = name_sid->GetInt();
const base::Value* parent_id = node_value.FindKey("parent");
if (parent_id) {
node.parent_id = parent_id->GetInt();
}
(*output)[id->GetInt()] = node;
}
base::Value* strings = heaps_v2->FindPath({"maps", "strings"});
for (const base::Value& string_value : strings->GetList()) {
const base::Value* id = string_value.FindKey("id");
const base::Value* string = string_value.FindKey("string");
if (!id || !string) {
LOG(ERROR) << "String struct missing id or string field";
return false;
}
for (auto& pair : *output) {
if (pair.second.name_id == id->GetInt()) {
pair.second.name = string->GetString();
break;
}
}
}
return true;
}
// Parses maps.types and maps.strings. Returns |true| on success.
bool ParseTypes(base::Value* heaps_v2, NodeMap* output) {
base::Value* types = heaps_v2->FindPath({"maps", "types"});
for (const base::Value& type_value : types->GetList()) {
const base::Value* id = type_value.FindKey("id");
const base::Value* name_sid = type_value.FindKey("name_sid");
if (!id || !name_sid) {
LOG(ERROR) << "Node missing id or name_sid field";
return false;
}
Node node;
node.name_id = name_sid->GetInt();
(*output)[id->GetInt()] = node;
}
base::Value* strings = heaps_v2->FindPath({"maps", "strings"});
for (const base::Value& string_value : strings->GetList()) {
const base::Value* id = string_value.FindKey("id");
const base::Value* string = string_value.FindKey("string");
if (!id || !string) {
LOG(ERROR) << "String struct missing id or string field";
return false;
}
for (auto& pair : *output) {
if (pair.second.name_id == id->GetInt()) {
pair.second.name = string->GetString();
break;
}
}
}
return true;
}
// Verify expectations are present in heap dump.
bool ValidateDump(base::Value* heaps_v2,
int expected_alloc_size,
int expected_alloc_count,
const char* allocator_name,
const char* type_name,
const std::string& frame_name,
const std::string& thread_name) {
base::Value* sizes =
heaps_v2->FindPath({"allocators", allocator_name, "sizes"});
if (!sizes) {
LOG(ERROR) << "Failed to find path: 'allocators." << allocator_name
<< ".sizes' in heaps v2";
return false;
}
const base::Value::ListStorage& sizes_list = sizes->GetList();
if (sizes_list.empty()) {
LOG(ERROR) << "'allocators." << allocator_name
<< ".sizes' is an empty list";
return false;
}
base::Value* counts =
heaps_v2->FindPath({"allocators", allocator_name, "counts"});
if (!counts) {
LOG(ERROR) << "Failed to find path: 'allocators." << allocator_name
<< ".counts' in heaps v2";
return false;
}
const base::Value::ListStorage& counts_list = counts->GetList();
if (sizes_list.size() != counts_list.size()) {
LOG(ERROR)
<< "'allocators." << allocator_name
<< ".sizes' does not have the same number of elements as *.counts";
return false;
}
base::Value* types =
heaps_v2->FindPath({"allocators", allocator_name, "types"});
if (!types) {
LOG(ERROR) << "Failed to find path: 'allocators." << allocator_name
<< ".types' in heaps v2";
return false;
}
const base::Value::ListStorage& types_list = types->GetList();
if (types_list.empty()) {
LOG(ERROR) << "'allocators." << allocator_name
<< ".types' is an empty list";
return false;
}
if (sizes_list.size() != types_list.size()) {
LOG(ERROR)
<< "'allocators." << allocator_name
<< ".types' does not have the same number of elements as *.sizes";
return false;
}
base::Value* nodes =
heaps_v2->FindPath({"allocators", allocator_name, "nodes"});
if (!nodes) {
LOG(ERROR) << "Failed to find path: 'allocators." << allocator_name
<< ".nodes' in heaps v2";
return false;
}
const base::Value::ListStorage& nodes_list = nodes->GetList();
if (sizes_list.size() != nodes_list.size()) {
LOG(ERROR)
<< "'allocators." << allocator_name
<< ".sizes' does not have the same number of elements as *.nodes";
return false;
}
bool found_browser_alloc = false;
size_t browser_alloc_index = 0;
for (size_t i = 0; i < sizes_list.size(); i++) {
if (counts_list[i].GetInt() == expected_alloc_count &&
sizes_list[i].GetInt() != expected_alloc_size) {
LOG(WARNING) << "Allocation candidate (size:" << sizes_list[i].GetInt()
<< " count:" << counts_list[i].GetInt() << ")";
}
if (counts_list[i].GetInt() == expected_alloc_count &&
sizes_list[i].GetInt() == expected_alloc_size) {
browser_alloc_index = i;
found_browser_alloc = true;
break;
}
}
if (!found_browser_alloc) {
LOG(ERROR) << "Failed to find an allocation of the "
"appropriate size. Did the send buffer "
"not flush? (size: "
<< expected_alloc_size << " count:" << expected_alloc_count
<< ")";
return false;
}
// Find the type, if an expectation was passed in.
if (type_name) {
NodeMap node_map;
if (!ParseTypes(heaps_v2, &node_map)) {
LOG(ERROR) << "Failed to parse type and string structs";
return false;
}
int type = types_list[browser_alloc_index].GetInt();
auto it = node_map.find(type);
if (it == node_map.end()) {
LOG(ERROR) << "Failed to look up type.";
return false;
}
if (it->second.name != type_name) {
LOG(ERROR) << "actual name: " << it->second.name
<< " expected name: " << type_name;
return false;
}
}
// Check that the frame has the right name.
if (!frame_name.empty()) {
NodeMap node_map;
if (!ParseNodes(heaps_v2, &node_map)) {
LOG(ERROR) << "Failed to parse node and string structs";
return false;
}
int node_id = nodes_list[browser_alloc_index].GetInt();
auto it = node_map.find(node_id);
if (it == node_map.end()) {
LOG(ERROR) << "Failed to find frame for node with id: " << node_id;
return false;
}
if (it->second.name != frame_name) {
LOG(ERROR) << "Wrong name: " << it->second.name
<< " for frame with expected name: " << frame_name;
return false;
}
}
// Check that the thread [top frame] has the right name.
if (!thread_name.empty()) {
NodeMap node_map;
if (!ParseNodes(heaps_v2, &node_map)) {
LOG(ERROR) << "Failed to parse node and string structs";
return false;
}
int node_id = nodes_list[browser_alloc_index].GetInt();
auto it = node_map.find(node_id);
while (true) {
if (it == node_map.end() || it->second.parent_id == kNullParent)
break;
it = node_map.find(it->second.parent_id);
}
if (it == node_map.end()) {
LOG(ERROR) << "Failed to find root for node with id: " << node_id;
return false;
}
if (it->second.name != thread_name) {
LOG(ERROR) << "Wrong name: " << it->second.name
<< " for thread with expected name: " << thread_name;
return false;
}
}
return true;
}
// |expected_size| of 0 means no expectation.
bool GetAllocatorSubarray(base::Value* heaps_v2,
const char* allocator_name,
const char* subarray_name,
size_t expected_size,
const base::Value::ListStorage** output) {
base::Value* subarray =
heaps_v2->FindPath({"allocators", allocator_name, subarray_name});
if (!subarray) {
LOG(ERROR) << "Failed to find path: 'allocators." << allocator_name << "."
<< subarray_name << "' in heaps v2";
return false;
}
const base::Value::ListStorage& subarray_list = subarray->GetList();
if (expected_size && subarray_list.size() != expected_size) {
LOG(ERROR) << subarray_name << " has wrong size";
return false;
}
*output = &subarray_list;
return true;
}
bool ValidateSamplingAllocations(base::Value* heaps_v2,
const char* allocator_name,
int approximate_size,
int approximate_count,
const char* type_name) {
// Maps type ids to strings.
NodeMap type_map;
if (!ParseTypes(heaps_v2, &type_map))
return false;
bool found = false;
int id_of_type = 0;
for (auto& pair : type_map) {
if (pair.second.name == type_name) {
id_of_type = pair.first;
found = true;
}
}
if (!found) {
LOG(ERROR) << "Failed to find type with name: " << type_name;
return false;
}
// Find the type with the appropriate id.
const base::Value::ListStorage* types_list;
if (!GetAllocatorSubarray(heaps_v2, allocator_name, "types", 0,
&types_list)) {
return false;
}
found = false;
size_t index = 0;
for (size_t i = 0; i < types_list->size(); ++i) {
if ((*types_list)[i].GetInt() == id_of_type) {
index = i;
found = true;
break;
}
}
if (!found) {
LOG(ERROR) << "Failed to find type with correct sid";
return false;
}
// Look up the size.
const base::Value::ListStorage* sizes;
if (!GetAllocatorSubarray(heaps_v2, allocator_name, "sizes",
types_list->size(), &sizes)) {
return false;
}
if ((*sizes)[index].GetInt() < approximate_size / 2 ||
(*sizes)[index].GetInt() > approximate_size * 2) {
LOG(ERROR) << "sampling size " << (*sizes)[index].GetInt()
<< " was not within a factor of 2 of expected size "
<< approximate_size;
return false;
}
// Look up the count.
const base::Value::ListStorage* counts;
if (!GetAllocatorSubarray(heaps_v2, allocator_name, "counts",
types_list->size(), &counts)) {
return false;
}
if ((*counts)[index].GetInt() < approximate_count / 2 ||
(*counts)[index].GetInt() > approximate_count * 2) {
LOG(ERROR) << "sampling size " << (*counts)[index].GetInt()
<< " was not within a factor of 2 of expected count "
<< approximate_count;
return false;
}
return true;
}
bool ValidateProcessMmaps(base::Value* process_mmaps,
bool should_have_contents) {
base::Value* vm_regions = process_mmaps->FindKey("vm_regions");
size_t count = vm_regions->GetList().size();
if (!should_have_contents) {
if (count != 0) {
LOG(ERROR) << "vm_regions should be empty, but has contents";
return false;
}
return true;
}
if (count == 0) {
LOG(ERROR) << "vm_regions should have contents, but doesn't";
return false;
}
// File paths may contain PII. Make sure that "mf" entries only contain the
// basename, rather than a full path.
for (const base::Value& vm_region : vm_regions->GetList()) {
const base::Value* file_path_value = vm_region.FindKey("mf");
if (file_path_value) {
std::string file_path = file_path_value->GetString();
base::FilePath::StringType path(file_path.begin(), file_path.end());
if (base::FilePath(path).BaseName().AsUTF8Unsafe() != file_path) {
LOG(ERROR) << "vm_region should not contain file path: " << file_path;
return false;
}
}
}
return true;
}
} // namespace
TestDriver::TestDriver()
: wait_for_ui_thread_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
base::WaitableEvent::InitialState::NOT_SIGNALED) {
partition_allocator_.init();
}
TestDriver::~TestDriver() = default;
bool TestDriver::RunTest(const Options& options) {
options_ = options;
if (options_.should_sample)
base::PoissonAllocationSampler::Get()->SuppressRandomnessForTest(true);
running_on_ui_thread_ =
content::BrowserThread::CurrentlyOn(content::BrowserThread::UI);
// The only thing to test for Mode::kNone is that profiling hasn't started.
if (options_.mode == Mode::kNone) {
if (running_on_ui_thread_) {
has_started_ = Supervisor::GetInstance()->HasStarted();
} else {
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&TestDriver::GetHasStartedOnUIThread,
base::Unretained(this)));
wait_for_ui_thread_.Wait();
}
if (has_started_) {
LOG(ERROR) << "Profiling should not have started";
return false;
}
return true;
}
if (running_on_ui_thread_) {
if (!CheckOrStartProfilingOnUIThreadWithNestedRunLoops())
return false;
if (ShouldProfileRenderer())
WaitForProfilingToStartForAllRenderersUIThread();
if (ShouldProfileBrowser())
MakeTestAllocations();
CollectResults(true);
} else {
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&TestDriver::CheckOrStartProfilingOnUIThreadAndSignal,
base::Unretained(this)));
wait_for_ui_thread_.Wait();
if (!initialization_success_)
return false;
if (ShouldProfileRenderer()) {
base::PostTaskWithTraits(
FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(
&TestDriver::
WaitForProfilingToStartForAllRenderersUIThreadAndSignal,
base::Unretained(this)));
wait_for_ui_thread_.Wait();
}
if (ShouldProfileBrowser()) {
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&TestDriver::MakeTestAllocations,
base::Unretained(this)));
}
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(&TestDriver::CollectResults,
base::Unretained(this), false));
wait_for_ui_thread_.Wait();
}
std::unique_ptr<base::Value> dump_json =
base::JSONReader::ReadDeprecated(serialized_trace_);
if (!dump_json) {
LOG(ERROR) << "Failed to deserialize trace.";
return false;
}
if (!ValidateBrowserAllocations(dump_json.get())) {
LOG(ERROR) << "Failed to validate browser allocations";
return false;
}
if (!ValidateRendererAllocations(dump_json.get())) {
LOG(ERROR) << "Failed to validate renderer allocations";
return false;
}
return true;
}
void TestDriver::GetHasStartedOnUIThread() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
has_started_ = Supervisor::GetInstance()->HasStarted();
wait_for_ui_thread_.Signal();
}
void TestDriver::CheckOrStartProfilingOnUIThreadAndSignal() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
initialization_success_ =
CheckOrStartProfilingOnUIThreadWithAsyncSignalling();
// If the flag is true, then the WaitableEvent will be signaled after
// profiling has started.
if (!wait_for_profiling_to_start_)
wait_for_ui_thread_.Signal();
}
bool TestDriver::CheckOrStartProfilingOnUIThreadWithAsyncSignalling() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (options_.profiling_already_started) {
if (!Supervisor::GetInstance()->HasStarted()) {
LOG(ERROR) << "Profiling should have been started, but wasn't";
return false;
}
// Even if profiling has started, it's possible that the allocator shim
// has not yet been initialized. Wait for it.
if (ShouldProfileBrowser()) {
bool already_initialized = SetOnInitAllocatorShimCallbackForTesting(
base::BindOnce(&base::WaitableEvent::Signal,
base::Unretained(&wait_for_ui_thread_)),
base::ThreadTaskRunnerHandle::Get());
if (!already_initialized) {
wait_for_profiling_to_start_ = true;
}
}
return true;
}
content::ServiceManagerConnection* connection =
content::ServiceManagerConnection::GetForProcess();
if (!connection) {
LOG(ERROR) << "A ServiceManagerConnection was not available for the "
"current process.";
return false;
}
wait_for_profiling_to_start_ = true;
base::OnceClosure start_callback;
// If we're going to profile the browser, then wait for the allocator shim to
// start. Otherwise, wait for the Supervisor to start.
if (ShouldProfileBrowser()) {
SetOnInitAllocatorShimCallbackForTesting(
base::BindOnce(&base::WaitableEvent::Signal,
base::Unretained(&wait_for_ui_thread_)),
base::ThreadTaskRunnerHandle::Get());
} else {
start_callback = base::BindOnce(&base::WaitableEvent::Signal,
base::Unretained(&wait_for_ui_thread_));
}
uint32_t sampling_rate = options_.should_sample
? (options_.sample_everything ? 2 : kSampleRate)
: 1;
Supervisor::GetInstance()->Start(connection, options_.mode,
options_.stack_mode, sampling_rate,
std::move(start_callback));
return true;
}
bool TestDriver::CheckOrStartProfilingOnUIThreadWithNestedRunLoops() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (options_.profiling_already_started) {
if (!Supervisor::GetInstance()->HasStarted()) {
LOG(ERROR) << "Profiling should have been started, but wasn't";
return false;
}
// Even if profiling has started, it's possible that the allocator shim
// has not yet been initialized. Wait for it.
if (ShouldProfileBrowser()) {
std::unique_ptr<base::RunLoop> run_loop(new base::RunLoop);
bool already_initialized = SetOnInitAllocatorShimCallbackForTesting(
run_loop->QuitClosure(), base::ThreadTaskRunnerHandle::Get());
if (!already_initialized)
run_loop->Run();
}
return true;
}
content::ServiceManagerConnection* connection =
content::ServiceManagerConnection::GetForProcess();
if (!connection) {
LOG(ERROR) << "A ServiceManagerConnection was not available for the "
"current process.";
return false;
}
// When this is not-null, initialization should wait for the QuitClosure to be
// called.
std::unique_ptr<base::RunLoop> run_loop(new base::RunLoop);
base::OnceClosure start_callback;
// If we're going to profile the browser, then wait for the allocator shim to
// start. Otherwise, wait for the Supervisor to start.
if (ShouldProfileBrowser()) {
SetOnInitAllocatorShimCallbackForTesting(
run_loop->QuitClosure(), base::ThreadTaskRunnerHandle::Get());
} else {
start_callback = run_loop->QuitClosure();
}
uint32_t sampling_rate = options_.should_sample
? (options_.sample_everything ? 2 : kSampleRate)
: 1;
Supervisor::GetInstance()->Start(connection, options_.mode,
options_.stack_mode, sampling_rate,
std::move(start_callback));
run_loop->Run();
return true;
}
void TestDriver::MakeTestAllocations() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
base::PlatformThread::SetName(kThreadName);
// Warm up the sampler. Once enabled it may need to see up to 1MB of
// allocations to start sampling.
leaks_.push_back(new char[base::PoissonAllocationSampler::kWarmupInterval]);
// In sampling mode, only sampling allocations are relevant.
if (!IsRecordingAllAllocations()) {
leaks_.reserve(kSamplingAllocCount);
for (int i = 0; i < kSamplingAllocCount; ++i) {
leaks_.push_back(static_cast<char*>(partition_allocator_.root()->Alloc(
kSamplingAllocSize, kSamplingAllocTypeName)));
}
return;
}
leaks_.reserve(2 * kMallocAllocCount + 1 + kPartitionAllocSize);
{
TRACE_HEAP_PROFILER_API_SCOPED_TASK_EXECUTION event(kMallocTypeTag);
TRACE_EVENT0(kTestCategory, kMallocEvent);
for (int i = 0; i < kMallocAllocCount; ++i) {
leaks_.push_back(new char[kMallocAllocSize]);
}
}
{
TRACE_EVENT0(kTestCategory, kPAEvent);
for (int i = 0; i < kPartitionAllocCount; ++i) {
leaks_.push_back(static_cast<char*>(partition_allocator_.root()->Alloc(
kPartitionAllocSize, kPartitionAllocTypeName)));
}
}
{
TRACE_EVENT0(kTestCategory, kVariadicEvent);
for (int i = 0; i < kVariadicAllocCount; ++i) {
leaks_.push_back(new char[i + 8000]); // Variadic allocation.
total_variadic_allocations_ += i + 8000;
}
}
// // Navigate around to force allocations in the renderer.
// ASSERT_TRUE(embedded_test_server()->Start());
// ui_test_utils::NavigateToURL(
// browser(), embedded_test_server()->GetURL("/english_page.html"));
// // Vive la France!
// ui_test_utils::NavigateToURL(
// browser(), embedded_test_server()->GetURL("/french_page.html"));
}
void TestDriver::CollectResults(bool synchronous) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
base::Closure finish_tracing_closure;
std::unique_ptr<base::RunLoop> run_loop;
if (synchronous) {
run_loop.reset(new base::RunLoop);
finish_tracing_closure = run_loop->QuitClosure();
} else {
finish_tracing_closure = base::Bind(&base::WaitableEvent::Signal,
base::Unretained(&wait_for_ui_thread_));
}
Supervisor::GetInstance()->RequestTraceWithHeapDump(
base::BindOnce(&TestDriver::TraceFinished, base::Unretained(this),
std::move(finish_tracing_closure)),
/* anonymize= */ true);
if (synchronous)
run_loop->Run();
}
void TestDriver::TraceFinished(base::Closure closure,
bool success,
std::string trace_json) {
serialized_trace_.swap(trace_json);
std::move(closure).Run();
}
bool TestDriver::ValidateBrowserAllocations(base::Value* dump_json) {
base::Value* heaps_v2 =
FindArgDump(base::Process::Current().Pid(), dump_json, "heaps_v2");
if (!ShouldProfileBrowser()) {
if (heaps_v2) {
LOG(ERROR) << "There should be no heap dump for the browser.";
return false;
}
return true;
}
if (!heaps_v2) {
LOG(ERROR) << "Browser heap dump missing.";
return false;
}
bool result = false;
bool should_validate_dumps = true;
#if defined(OS_ANDROID) && !defined(OFFICIAL_BUILD)
// TODO(ajwong): This step fails on Nexus 5X devices running kit-kat. It works
// on Nexus 5X devices running oreo. The problem is that all allocations have
// the same [an effectively empty] backtrace and get glommed together. More
// investigation is necessary. For now, I'm turning this off for Android.
// https://crbug.com/786450.
if (!HasPseudoFrames())
should_validate_dumps = false;
#endif
std::string thread_name = ShouldIncludeNativeThreadNames() ? kThreadName : "";
if (IsRecordingAllAllocations()) {
if (should_validate_dumps) {
result = ValidateDump(heaps_v2, kMallocAllocSize * kMallocAllocCount,
kMallocAllocCount, "malloc", kMallocTypeTag,
HasPseudoFrames() ? kMallocEvent : "", thread_name);
if (!result) {
LOG(ERROR) << "Failed to validate malloc fixed allocations";
return false;
}
result = ValidateDump(
heaps_v2, total_variadic_allocations_, kVariadicAllocCount, "malloc",
nullptr, HasPseudoFrames() ? kVariadicEvent : "", thread_name);
if (!result) {
LOG(ERROR) << "Failed to validate malloc variadic allocations";
return false;
}
}
// TODO(ajwong): Like malloc, all Partition-Alloc allocations get glommed
// together for some Android device/OS configurations. However, since there
// is only one place that uses partition alloc in the browser process [this
// test], the count is still valid. This should still be made more robust by
// fixing backtrace. https://crbug.com/786450.
result = ValidateDump(heaps_v2, kPartitionAllocSize * kPartitionAllocCount,
kPartitionAllocCount, "partition_alloc",
kPartitionAllocTypeName,
HasPseudoFrames() ? kPAEvent : "", thread_name);
if (!result) {
LOG(ERROR) << "Failed to validate PA allocations";
return false;
}
} else {
bool result = ValidateSamplingAllocations(
heaps_v2, "partition_alloc", kSamplingAllocSize * kSamplingAllocCount,
kSamplingAllocCount, kSamplingAllocTypeName);
if (!result) {
LOG(ERROR) << "Failed to validate sampling allocations";
return false;
}
}
int process_count = NumProcessesWithName(dump_json, "Browser", nullptr);
if (process_count != 1) {
LOG(ERROR) << "Found " << process_count
<< " processes with name: Browser. Expected 1.";
return false;
}
base::Value* process_mmaps =
FindArgDump(base::Process::Current().Pid(), dump_json, "process_mmaps");
if (!ValidateProcessMmaps(process_mmaps, HasNativeFrames())) {
LOG(ERROR) << "Failed to validate browser process mmaps.";
return false;
}
return true;
}
bool TestDriver::ValidateRendererAllocations(base::Value* dump_json) {
// On Android Webview, there is may not be a separate Renderer process. If we
// are not asked to profile the Renderer, do not perform any Renderer checks.
if (!ShouldProfileRenderer())
return true;
std::vector<int> pids;
bool result = NumProcessesWithName(dump_json, "Renderer", &pids) >= 1;
if (!result) {
LOG(ERROR) << "Failed to find process with name Renderer";
return false;
}
for (int pid : pids) {
base::ProcessId renderer_pid = static_cast<base::ProcessId>(pid);
base::Value* heaps_v2 = FindArgDump(renderer_pid, dump_json, "heaps_v2");
if (!heaps_v2) {
LOG(ERROR) << "Failed to find heaps v2 for renderer";
return false;
}
base::Value* process_mmaps =
FindArgDump(renderer_pid, dump_json, "process_mmaps");
if (!ValidateProcessMmaps(process_mmaps, HasNativeFrames())) {
LOG(ERROR) << "Failed to validate renderer process mmaps.";
return false;
}
}
return true;
}
bool TestDriver::ShouldProfileBrowser() {
return options_.mode == Mode::kAll || options_.mode == Mode::kBrowser ||
options_.mode == Mode::kMinimal ||
options_.mode == Mode::kUtilityAndBrowser;
}
bool TestDriver::ShouldProfileRenderer() {
return options_.mode == Mode::kAll || options_.mode == Mode::kAllRenderers;
}
bool TestDriver::ShouldIncludeNativeThreadNames() {
return options_.stack_mode == mojom::StackMode::NATIVE_WITH_THREAD_NAMES;
}
bool TestDriver::HasPseudoFrames() {
return options_.stack_mode == mojom::StackMode::PSEUDO ||
options_.stack_mode == mojom::StackMode::MIXED;
}
bool TestDriver::HasNativeFrames() {
return options_.stack_mode == mojom::StackMode::NATIVE_WITH_THREAD_NAMES ||
options_.stack_mode == mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES ||
options_.stack_mode == mojom::StackMode::MIXED;
}
bool TestDriver::IsRecordingAllAllocations() {
return !options_.should_sample || options_.sample_everything;
}
void TestDriver::WaitForProfilingToStartForAllRenderersUIThread() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
while (true) {
std::vector<base::ProcessId> profiled_pids;
base::RunLoop run_loop;
auto callback = base::BindOnce(
[](std::vector<base::ProcessId>* results, base::OnceClosure finished,
std::vector<base::ProcessId> pids) {
results->swap(pids);
std::move(finished).Run();
},
&profiled_pids, run_loop.QuitClosure());
Supervisor::GetInstance()->GetProfiledPids(std::move(callback));
run_loop.Run();
if (RenderersAreBeingProfiled(profiled_pids))
break;
}
}
void TestDriver::WaitForProfilingToStartForAllRenderersUIThreadAndSignal() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
Supervisor::GetInstance()->GetProfiledPids(base::BindOnce(
&TestDriver::WaitForProfilingToStartForAllRenderersUIThreadCallback,
base::Unretained(this)));
}
void TestDriver::WaitForProfilingToStartForAllRenderersUIThreadCallback(
std::vector<base::ProcessId> results) {
if (RenderersAreBeingProfiled(results)) {
wait_for_ui_thread_.Signal();
return;
}
// Brief sleep to prevent spamming the task queue, since this code is called
// in a tight loop.
base::PlatformThread::Sleep(base::TimeDelta::FromMicroseconds(100));
WaitForProfilingToStartForAllRenderersUIThreadAndSignal();
}
} // namespace heap_profiling