blob: 2b574e9d9a7b1c6bfc09980d42ae99b617c02f7a [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/developer/memory/metrics/capture.h"
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fdio.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <lib/zx/channel.h>
#include <lib/zx/job.h>
#include <zircon/errors.h>
#include <zircon/process.h>
#include <zircon/rights.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <memory>
#include <optional>
#include <ranges>
#include <stack>
#include <task-utils/walker.h>
#include "src/developer/memory/metrics/capture_strategy.h"
namespace memory {
class OSImpl : public OS, public TaskEnumerator {
public:
zx_status_t GetKernelStats(fidl::WireSyncClient<fuchsia_kernel::Stats>* stats) override {
auto client_end = component::Connect<fuchsia_kernel::Stats>();
if (!client_end.is_ok()) {
return client_end.status_value();
}
*stats = fidl::WireSyncClient(std::move(*client_end));
return ZX_OK;
}
zx_handle_t ProcessSelf() override { return zx_process_self(); }
zx_instant_boot_t GetBoot() override { return zx_clock_get_boot(); }
zx_status_t GetProcesses(
fit::function<zx_status_t(int, zx::handle, zx_koid_t, zx_koid_t)> cb) override {
TRACE_DURATION("memory_metrics", "Capture::GetProcesses");
cb_ = [cb = std::move(cb)](int depth, zx_handle_t handle, zx_koid_t koid,
zx_koid_t parent_koid) {
// Each time |WalkJobTree| iterates over a new process, it closes the previously sent handle.
// However, we want to keep the handles around to avoid re-walking the tree for filtering. To
// do that, we use |zx_handle_duplicate| to have a handle that survives the callback call.
// Using a |zx::handle| object ensures the duplicated handle will be closed upon the object
// destruction.
zx::handle new_handle;
zx_status_t s =
zx_handle_duplicate(handle, ZX_RIGHT_SAME_RIGHTS, new_handle.reset_and_get_address());
if (s != ZX_OK) {
return s;
}
return cb(depth, std::move(new_handle), koid, parent_koid);
};
auto client_end = component::Connect<fuchsia_kernel::RootJobForInspect>();
if (!client_end.is_ok()) {
return client_end.status_value();
}
auto result = fidl::WireCall(*client_end)->Get();
if (result.status() != ZX_OK) {
return result.status();
}
return WalkJobTree(result->job.get());
}
zx_status_t OnProcess(int depth, zx_handle_t handle, zx_koid_t koid,
zx_koid_t parent_koid) override {
return cb_(depth, handle, koid, parent_koid);
}
zx_status_t GetProperty(zx_handle_t handle, uint32_t property, void* value,
size_t name_len) override {
return zx_object_get_property(handle, property, value, name_len);
}
zx_status_t GetInfo(zx_handle_t handle, uint32_t topic, void* buffer, size_t buffer_size,
size_t* actual, size_t* avail) override {
TRACE_DURATION("memory_metrics", "OSImpl::GetInfo", "topic", topic, "buffer_size", buffer_size);
return zx_object_get_info(handle, topic, buffer, buffer_size, actual, avail);
}
zx_status_t GetKernelMemoryStats(const fidl::WireSyncClient<fuchsia_kernel::Stats>& stats_client,
zx_info_kmem_stats_t& kmem) override {
TRACE_DURATION("memory_metrics", "Capture::GetKernelMemoryStats");
if (!stats_client.is_valid()) {
return ZX_ERR_BAD_STATE;
}
auto result = stats_client->GetMemoryStats();
if (result.status() != ZX_OK) {
return result.status();
}
const auto& stats = result->stats;
kmem.total_bytes = stats.total_bytes();
kmem.free_bytes = stats.free_bytes();
kmem.wired_bytes = stats.wired_bytes();
kmem.total_heap_bytes = stats.total_heap_bytes();
kmem.free_heap_bytes = stats.free_heap_bytes();
kmem.vmo_bytes = stats.vmo_bytes();
kmem.mmu_overhead_bytes = stats.mmu_overhead_bytes();
kmem.ipc_bytes = stats.ipc_bytes();
kmem.other_bytes = stats.other_bytes();
return ZX_OK;
}
zx_status_t GetKernelMemoryStatsExtended(
const fidl::WireSyncClient<fuchsia_kernel::Stats>& stats_client,
zx_info_kmem_stats_extended_t& kmem_ext, zx_info_kmem_stats_t* kmem) override {
TRACE_DURATION("memory_metrics", "Capture::GetKernelMemoryStatsExtended");
if (!stats_client.is_valid()) {
return ZX_ERR_BAD_STATE;
}
auto result = stats_client->GetMemoryStatsExtended();
if (result.status() != ZX_OK) {
return result.status();
}
const auto& stats = result->stats;
kmem_ext.total_bytes = stats.total_bytes();
kmem_ext.free_bytes = stats.free_bytes();
kmem_ext.wired_bytes = stats.wired_bytes();
kmem_ext.total_heap_bytes = stats.total_heap_bytes();
kmem_ext.free_heap_bytes = stats.free_heap_bytes();
kmem_ext.vmo_bytes = stats.vmo_bytes();
kmem_ext.vmo_pager_total_bytes = stats.vmo_pager_total_bytes();
kmem_ext.vmo_pager_newest_bytes = stats.vmo_pager_newest_bytes();
kmem_ext.vmo_pager_oldest_bytes = stats.vmo_pager_oldest_bytes();
kmem_ext.vmo_discardable_locked_bytes = stats.vmo_discardable_locked_bytes();
kmem_ext.vmo_discardable_unlocked_bytes = stats.vmo_discardable_unlocked_bytes();
kmem_ext.mmu_overhead_bytes = stats.mmu_overhead_bytes();
kmem_ext.ipc_bytes = stats.ipc_bytes();
kmem_ext.other_bytes = stats.other_bytes();
// Copy over shared fields from kmem_ext to kmem, if provided.
if (kmem) {
kmem->total_bytes = kmem_ext.total_bytes;
kmem->free_bytes = kmem_ext.free_bytes;
kmem->wired_bytes = kmem_ext.wired_bytes;
kmem->total_heap_bytes = kmem_ext.total_heap_bytes;
kmem->free_heap_bytes = kmem_ext.free_heap_bytes;
kmem->vmo_bytes = kmem_ext.vmo_bytes;
kmem->mmu_overhead_bytes = kmem_ext.mmu_overhead_bytes;
kmem->ipc_bytes = kmem_ext.ipc_bytes;
kmem->other_bytes = kmem_ext.other_bytes;
}
return ZX_OK;
}
zx_status_t GetKernelMemoryStatsCompression(
const fidl::WireSyncClient<fuchsia_kernel::Stats>& stats_client,
zx_info_kmem_stats_compression_t& kmem_compression) override {
TRACE_DURATION("memory_metrics", "Capture::GetKernelMemoryStatsCompression");
if (!stats_client.is_valid()) {
return ZX_ERR_BAD_STATE;
}
auto result = stats_client->GetMemoryStatsCompression();
if (result.status() != ZX_OK) {
return result.status();
}
kmem_compression.uncompressed_storage_bytes = result->uncompressed_storage_bytes();
kmem_compression.compressed_storage_bytes = result->compressed_storage_bytes();
kmem_compression.compressed_fragmentation_bytes = result->compressed_fragmentation_bytes();
kmem_compression.compression_time = result->compression_time();
kmem_compression.decompression_time = result->decompression_time();
kmem_compression.total_page_compression_attempts = result->total_page_compression_attempts();
kmem_compression.failed_page_compression_attempts = result->failed_page_compression_attempts();
kmem_compression.total_page_decompressions = result->total_page_decompressions();
kmem_compression.compressed_page_evictions = result->compressed_page_evictions();
kmem_compression.eager_page_compressions = result->eager_page_compressions();
kmem_compression.memory_pressure_page_compressions =
result->memory_pressure_page_compressions();
kmem_compression.critical_memory_page_compressions =
result->critical_memory_page_compressions();
kmem_compression.pages_decompressed_unit_ns = result->pages_decompressed_unit_ns();
std::copy(result->pages_decompressed_within_log_time().begin(),
result->pages_decompressed_within_log_time().end(),
std::begin(kmem_compression.pages_decompressed_within_log_time));
return ZX_OK;
}
protected:
bool has_on_process() const final { return true; }
private:
fit::function<zx_status_t(int /* depth */, zx_handle_t /* handle */, zx_koid_t /* koid */,
zx_koid_t /* parent_koid */)>
cb_;
};
// Returns an OS implementation querying Zircon Kernel.
std::unique_ptr<OS> CreateDefaultOS() { return std::make_unique<OSImpl>(); }
const std::unordered_set<std::string> Capture::kDefaultRootedVmoNames = {
"SysmemContiguousPool", "SysmemAmlogicProtectedPool", "Sysmem-core"};
CaptureMaker::CaptureMaker(fidl::WireSyncClient<fuchsia_kernel::Stats> stats_client,
std::unique_ptr<OS> os)
: stats_client_(std::move(stats_client)), os_(std::move(os)) {
FX_CHECK(os_);
}
zx_status_t CaptureMaker::GetCapture(Capture* capture, CaptureLevel level,
const std::unordered_set<std::string>& rooted_vmo_names) {
TRACE_DURATION("memory_metrics", "Capture::GetCapture");
capture->time_ = os_->GetBoot();
// Capture level KMEM only queries ZX_INFO_KMEM_STATS, as opposed to ZX_INFO_KMEM_STATS_EXTENDED
// which queries a more detailed set of kernel metrics. KMEM capture level is used to poll the
// free memory level every 10s in order to keep the highwater digest updated, so a lightweight
// syscall is preferable.
if (level == CaptureLevel::KMEM) {
return os_->GetKernelMemoryStats(stats_client_, capture->kmem_);
}
// ZX_INFO_KMEM_STATS_EXTENDED is more expensive to collect than ZX_INFO_KMEM_STATS, so only
// query it for the more detailed capture levels. Use kmem_extended_ to populate the shared
// fields in kmem_ (kmem_extended_ is a superset of kmem_), avoiding the need for a redundant
// syscall.
zx_status_t err = os_->GetKernelMemoryStatsExtended(
stats_client_, capture->kmem_extended_.emplace(), &capture->kmem_);
if (err != ZX_OK) {
return err;
}
// Some Fuchsia systems use ZRAM, ie. compressed RAM. ZX_INFO_KMEM_STATS_COMPRESSION retrieves
// information about this compression, so we can get an accurate view of the actual physical
// memory used.
err = os_->GetKernelMemoryStatsCompression(stats_client_, capture->kmem_compression_.emplace());
// Assume compression is disabled when there is no storage.
if (!capture->kmem_compression_->compressed_storage_bytes) {
capture->kmem_compression_ = std::nullopt;
}
if (err != ZX_OK) {
return err;
}
StarnixCaptureStrategy strategy;
// We don't have a guarantee on the iteration order of GetProcesses. To be able to filter jobs
// correctly based on the name of their processes, we need to go through all processes first. We
// extract the process handle and keep it to avoid walking the process tree a second time.
err = os_->GetProcesses(
[this, &strategy](int depth, zx::handle handle, zx_koid_t koid, zx_koid_t parent_koid) {
TRACE_DURATION("memory_metrics", "Capture::GetProcesses::Callback");
Process process{.koid = koid, .job = parent_koid};
zx_status_t s = os_->GetProperty(handle.get(), ZX_PROP_NAME, process.name, ZX_MAX_NAME_LEN);
if (s != ZX_OK) {
return s == ZX_ERR_BAD_STATE ? ZX_OK : s;
}
s = strategy.OnNewProcess(*os_, std::move(process), std::move(handle));
return s == ZX_ERR_BAD_STATE ? ZX_OK : s;
});
auto result = StarnixCaptureStrategy::Finalize(*os_, std::move(strategy));
if (result.is_error()) {
return result.error_value();
}
auto& [koid_to_process, koid_to_vmo] = result.value();
capture->koid_to_process_ = std::move(koid_to_process);
capture->koid_to_vmo_ = std::move(koid_to_vmo);
{
TRACE_DURATION("memory_metrics", "Capture::GetProcesses::ReallocateDescendants");
ReallocateDescendants(rooted_vmo_names, capture->koid_to_vmo_);
}
return err;
}
fit::result<zx_status_t, CaptureMaker> CaptureMaker::Create(std::unique_ptr<OS> os) {
TRACE_DURATION("memory_metrics", "Capture::GetCaptureState");
fidl::WireSyncClient<fuchsia_kernel::Stats> stats_client;
zx_status_t err = os->GetKernelStats(&stats_client);
if (err != ZX_OK) {
return fit::error(err);
}
return fit::ok(CaptureMaker{std::move(stats_client), std::move(os)});
}
// Descendants of this vmo will have their allocated_bytes treated as an allocation of their
// immediate parent. This supports a usage pattern where a potentially large allocation is done
// and then slices are given to read / write children. In this case the children have no
// committed_bytes of their own. For accounting purposes it gives more clarity to push the
// committed bytes to the lowest points in the tree, where the vmo names give more specific
// meanings.
void CaptureMaker::ReallocateDescendants(Vmo& parent,
std::unordered_map<zx_koid_t, Vmo>& koid_to_vmo) {
std::unordered_set<zx_koid_t> visited_vmos;
std::stack<Vmo*> vmos;
vmos.push(&parent);
while (!vmos.empty()) {
Vmo* current = vmos.top();
vmos.pop();
if (visited_vmos.contains(current->koid))
continue;
visited_vmos.insert(current->koid);
for (auto& child_koid : current->children) {
auto& child = koid_to_vmo.at(child_koid);
if (child.parent_koid == current->koid) {
uint64_t reallocated_bytes =
std::min(current->committed_scaled_bytes.integral, child.allocated_bytes);
current->committed_scaled_bytes.integral -= reallocated_bytes;
child.committed_scaled_bytes.integral = reallocated_bytes;
vmos.push(&child);
}
}
}
}
// See the above description of ReallocateDescendants(zx_koid_t) for the specific behavior for each
// vmo that has a name listed in rooted_vmo_names.
void CaptureMaker::ReallocateDescendants(const std::unordered_set<std::string>& rooted_vmo_names,
std::unordered_map<zx_koid_t, Vmo>& koid_to_vmo) {
TRACE_DURATION("memory_metrics", "Capture::ReallocateDescendants");
std::vector<std::reference_wrapper<Vmo>> root_vmos;
for (auto& child : koid_to_vmo | std::views::values) {
// Some VMOs are their own parents; no need to reallocate them.
if (child.parent_koid == child.koid) {
continue;
}
// Root VMOs don't have parents.
if (child.parent_koid == ZX_KOID_INVALID) {
root_vmos.emplace_back(child);
continue;
}
// Add references of VMOs to their children, to populate the descendance graph.
if (auto parent_it = koid_to_vmo.find(child.parent_koid); parent_it != koid_to_vmo.end()) {
parent_it->second.children.push_back(child.koid);
}
}
for (auto vmo : root_vmos | std::views::filter([&rooted_vmo_names](const auto& vmo) {
return rooted_vmo_names.contains(vmo.get().name);
})) {
ReallocateDescendants(vmo, koid_to_vmo);
}
}
} // namespace memory