chromium/fuchsia_web/webengine/browser/web_engine_memory_inspector.cc

// 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();
}