// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fuchsia_web/webengine/browser/web_engine_memory_inspector.h"
#include <lib/fpromise/promise.h>
#include <lib/inspect/cpp/inspector.h>
#include <sstream>
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/memory_dump_request_args.h"
#include "components/fuchsia_component_support/config_reader.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/memory_instrumentation.h"
namespace {
std::vector<std::string> GetAllocatorDumpNamesFromConfig() {
const std::optional<base::Value::Dict>& config =
fuchsia_component_support::LoadPackageConfig();
if (!config)
return {};
const base::Value::List* names_list =
config->FindList("allocator-dump-names");
if (!names_list)
return {};
std::vector<std::string> names;
names.reserve(names_list->size());
for (auto& name : *names_list) {
names.push_back(name.GetString());
}
return names;
}
// List of allocator dump names to include.
const std::vector<std::string>& AllocatorDumpNames() {
static base::NoDestructor<std::vector<std::string>> allocator_dump_names(
GetAllocatorDumpNamesFromConfig());
return *allocator_dump_names;
}
// Returns true if every field in the supplied |dump|, and those of its
// children, are zero. Generally parent nodes summarize the total usage across
// all of their children, such that if the parent is all-zero then the children
// must also be all-zero. This implementation is optimized for the case in
// which that property holds, but also copes gracefully when it does not.
bool AreAllDumpEntriesZero(
const memory_instrumentation::mojom::AllocatorMemDump* dump) {
for (auto& it : dump->numeric_entries) {
if (it.second != 0u)
return false;
}
for (auto& it : dump->children) {
if (!AreAllDumpEntriesZero(it.second.get()))
return false;
}
return true;
}
// Creates a node |name|, under |parent|, populated recursively with the
// contents of |dump|. The returned tree of Nodes are emplace()d to be owned by
// the specified |owner|.
inspect::Node NodeFromAllocatorMemDump(
inspect::Inspector* owner,
inspect::Node* parent,
const std::string& name,
const memory_instrumentation::mojom::AllocatorMemDump* dump) {
auto node = parent->CreateChild(name);
// Add subordinate nodes for any children.
std::vector<const memory_instrumentation::mojom::AllocatorMemDump*> children;
children.reserve(dump->children.size());
for (auto& it : dump->children) {
// If a child contains no information then omit it.
if (AreAllDumpEntriesZero(it.second.get()))
continue;
children.push_back(it.second.get());
owner->emplace(
NodeFromAllocatorMemDump(owner, &node, it.first, it.second.get()));
}
// Publish the allocator-provided fields into the node. Entries are not
// published if there is a single child, with identical entries, to avoid
// redundancy in the emitted output.
bool same_as_child =
(children.size() == 1u &&
(*children.begin())->numeric_entries == dump->numeric_entries);
if (!same_as_child) {
for (auto& it : dump->numeric_entries) {
node.CreateUint(it.first, it.second, owner);
}
}
return node;
}
} // namespace
WebEngineMemoryInspector::WebEngineMemoryInspector(inspect::Node& parent_node) {
// Loading the allocator dump names from the config involves blocking I/O so
// trigger it to be done during construction, before the prohibition on
// blocking the main thread is applied.
AllocatorDumpNames();
node_ = parent_node.CreateLazyNode("memory", [this]() {
return fpromise::make_promise(fit::bind_member(
this, &WebEngineMemoryInspector::ResolveMemoryDumpPromise));
});
}
WebEngineMemoryInspector::~WebEngineMemoryInspector() = default;
fpromise::result<inspect::Inspector>
WebEngineMemoryInspector::ResolveMemoryDumpPromise(fpromise::context& context) {
// If there is a |dump_results_| then resolve the promise with it.
if (dump_results_) {
auto memory_dump = std::move(dump_results_);
return fpromise::ok(*memory_dump);
}
// If MemoryInstrumentation is not initialized then resolve an error.
auto* memory_instrumentation =
memory_instrumentation::MemoryInstrumentation::GetInstance();
if (!memory_instrumentation)
return fpromise::error();
// Request memory usage summaries for all processes, including details for
// any configured allocator dumps.
auto* coordinator = memory_instrumentation->GetCoordinator();
DCHECK(coordinator);
coordinator->RequestGlobalMemoryDump(
base::trace_event::MemoryDumpType::kSummaryOnly,
base::trace_event::MemoryDumpLevelOfDetail::kBackground,
base::trace_event::MemoryDumpDeterminism::kNone, AllocatorDumpNames(),
base::BindOnce(&WebEngineMemoryInspector::OnMemoryDumpComplete,
weak_this_.GetMutableWeakPtr(), base::TimeTicks::Now(),
context.suspend_task()));
return fpromise::pending();
}
void WebEngineMemoryInspector::OnMemoryDumpComplete(
base::TimeTicks requested_at,
fpromise::suspended_task task,
bool success,
memory_instrumentation::mojom::GlobalMemoryDumpPtr raw_dump) {
DCHECK(!dump_results_);
dump_results_ = std::make_unique<inspect::Inspector>();
// If capture failed then there is no data to report.
if (!success || !raw_dump) {
task.resume_task();
return;
}
// Note the delay between requesting the dump, and it being started.
dump_results_->GetRoot().CreateDouble(
"dump_queued_duration_ms",
(raw_dump->start_time - requested_at).InMillisecondsF(),
dump_results_.get());
// Note the delay between starting the dump, and it completing.
dump_results_->GetRoot().CreateDouble(
"dump_duration_ms",
(base::TimeTicks::Now() - raw_dump->start_time).InMillisecondsF(),
dump_results_.get());
for (const auto& process_dump : raw_dump->process_dumps) {
auto node = dump_results_->GetRoot().CreateChild(
base::NumberToString(process_dump->pid));
// Include details of each process' role in the web instance.
std::ostringstream type;
type << process_dump->process_type;
static const inspect::StringReference kTypeNodeName("type");
node.CreateString(kTypeNodeName, type.str(), dump_results_.get());
const auto service_name = process_dump->service_name;
if (service_name) {
static const inspect::StringReference kServiceNodeName("service");
node.CreateString(kServiceNodeName, *service_name, dump_results_.get());
}
// Include the summary of the process' memory usage.
const auto& os_dump = process_dump->os_dump;
static const inspect::StringReference kResidentKbNodeName("resident_kb");
node.CreateUint(kResidentKbNodeName, os_dump->resident_set_kb,
dump_results_.get());
static const inspect::StringReference kPrivateKbNodeName("private_kb");
node.CreateUint(kPrivateKbNodeName, os_dump->private_footprint_kb,
dump_results_.get());
static const inspect::StringReference kSharedKbNodeName("shared_kb");
node.CreateUint(kSharedKbNodeName, os_dump->shared_footprint_kb,
dump_results_.get());
// If provided, include detail from individual allocators.
if (!process_dump->chrome_allocator_dumps.empty()) {
static const inspect::StringReference kAllocatorDumpNodeName(
"allocator_dump");
auto detail_node = node.CreateChild(kAllocatorDumpNodeName);
for (auto& it : process_dump->chrome_allocator_dumps) {
dump_results_->emplace(NodeFromAllocatorMemDump(
dump_results_.get(), &detail_node, it.first, it.second.get()));
}
dump_results_->emplace(std::move(detail_node));
}
dump_results_->emplace(std::move(node));
}
task.resume_task();
}