blob: cd7b875578aa0070f5d3869d3cb02b550f096788 [file] [log] [blame] [edit]
// Copyright 2023 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 "targets.h"
#include <lib/fit/function.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <lib/zx/job.h>
#include <lib/zx/process.h>
#include <lib/zx/result.h>
#include <lib/zx/thread.h>
#include <zircon/errors.h>
#include <zircon/rights.h>
#include <zircon/system/ulib/elf-search/include/elf-search.h>
#include <zircon/types.h>
#include <algorithm>
#include <cstddef>
#include <unordered_map>
#include <utility>
#include <vector>
#include <span>
#include <src/lib/unwinder/module.h>
zx::result<> profiler::JobTarget::ForEachProcess(
const fit::function<zx::result<>(std::span<const zx_koid_t> job_path,
const ProcessTarget& target)>& f) const {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
std::vector<zx_koid_t> job_path{ancestry.begin(), ancestry.end()};
job_path.push_back(job_id);
for (const auto& [_, process] : processes) {
zx::result<> res = f(job_path, process);
if (res.is_error()) {
return res;
}
}
for (const auto& [_, job] : child_jobs) {
zx::result<> res = job.ForEachProcess(f);
if (res.is_error()) {
return res;
}
}
return zx::ok();
}
zx::result<> profiler::JobTarget::ForEachJob(
const fit::function<zx::result<>(const JobTarget& target)>& f) const {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
zx::result res = f(*this);
if (res.is_error()) {
return res;
}
for (const auto& [_, job] : child_jobs) {
zx::result res = job.ForEachJob(f);
if (res.is_error()) {
return res;
}
}
return zx::ok();
}
zx::result<> profiler::JobTarget::AddJob(std::span<const zx_koid_t> ancestry, JobTarget&& job) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (ancestry.empty()) {
zx_koid_t job_id = job.job_id;
auto [_, emplaced] = child_jobs.try_emplace(job_id, std::move(job));
return zx::make_result(emplaced ? ZX_OK : ZX_ERR_ALREADY_EXISTS);
}
auto it = child_jobs.find(ancestry[0]);
if (it == child_jobs.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
return it->second.AddJob(ancestry.subspan(1), std::move(job));
}
zx::result<> profiler::JobTarget::AddProcess(std::span<const zx_koid_t> job_path,
ProcessTarget&& process) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
zx_koid_t pid = process.pid;
auto [_, emplaced] = processes.try_emplace(pid, std::move(process));
return zx::make_result(emplaced ? ZX_OK : ZX_ERR_ALREADY_EXISTS);
}
auto next_child = child_jobs.find(job_path[0]);
if (next_child == child_jobs.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
return next_child->second.AddProcess(job_path.subspan(1), std::move(process));
}
zx::result<profiler::ProcessTarget*> profiler::JobTarget::GetProcess(
std::span<const zx_koid_t> job_path, zx_koid_t pid) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
if (processes.contains(pid)) {
auto it = processes.find(pid);
return zx::ok(&it->second);
}
return zx::error(ZX_ERR_NOT_FOUND);
}
auto next_child = child_jobs.find(job_path[0]);
if (next_child == child_jobs.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
return next_child->second.GetProcess(job_path.subspan(1), pid);
}
zx::result<> profiler::JobTarget::AddThread(std::span<const zx_koid_t> job_path, zx_koid_t pid,
ThreadTarget&& thread) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
auto process = processes.find(pid);
if (process == processes.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
zx_koid_t tid = thread.tid;
auto [_, emplaced] = process->second.threads.try_emplace(tid, std::move(thread));
return zx::make_result(emplaced ? ZX_OK : ZX_ERR_ALREADY_EXISTS);
}
auto next_child = child_jobs.find(job_path[0]);
if (next_child == child_jobs.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
return next_child->second.AddThread(job_path.subspan(1), pid, std::move(thread));
}
zx::result<> profiler::JobTarget::RemoveThread(std::span<const zx_koid_t> job_path, zx_koid_t pid,
zx_koid_t tid) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
auto process = processes.find(pid);
if (process == processes.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
size_t num_removed = process->second.threads.erase(tid);
return zx::make_result(num_removed == 1 ? ZX_OK : ZX_ERR_NOT_FOUND);
}
auto next_child = child_jobs.find(job_path[0]);
if (next_child == child_jobs.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
return next_child->second.RemoveThread(job_path.subspan(1), pid, tid);
}
zx::result<std::vector<zx_koid_t>> GetChildrenTids(const zx::process& process) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
size_t num_threads;
zx_status_t status = process.get_info(ZX_INFO_PROCESS_THREADS, nullptr, 0, nullptr, &num_threads);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "failed to get process thread info (#threads)";
return zx::error(status);
}
if (num_threads < 1) {
// A job or process in early initialization may not have threads yet.
// That's okay, we'll attach to them when they are created.
return zx::ok(std::vector<zx_koid_t>{});
}
auto threads = std::make_unique<zx_koid_t[]>(num_threads);
size_t records_read;
status = process.get_info(ZX_INFO_PROCESS_THREADS, threads.get(), num_threads * sizeof(zx_koid_t),
&records_read, nullptr);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "failed to get process thread info";
return zx::error(status);
}
if (records_read != num_threads) {
FX_LOGS(ERROR) << "records_read != num_threads";
return zx::error(ZX_ERR_BAD_STATE);
}
std::vector<zx_koid_t> children{threads.get(), threads.get() + num_threads};
return zx::ok(children);
}
zx::result<profiler::ProcessTarget> profiler::MakeProcessTarget(zx::process process,
elf_search::Searcher& searcher) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
zx_info_handle_basic_t handle_info;
zx_status_t res =
process.get_info(ZX_INFO_HANDLE_BASIC, &handle_info, sizeof(handle_info), nullptr, nullptr);
if (res != ZX_OK) {
return zx::error(res);
}
FX_LOGS(DEBUG) << "Creating process target for " << handle_info.koid << ".";
zx::result<std::vector<zx_koid_t>> children = GetChildrenTids(process);
if (children.is_error()) {
return children.take_error();
}
std::unordered_map<zx_koid_t, profiler::ThreadTarget> threads;
for (auto child_tid : *children) {
zx::thread child_thread;
zx_status_t res = process.get_child(child_tid, ZX_DEFAULT_THREAD_RIGHTS, &child_thread);
if (res != ZX_OK) {
FX_PLOGS(ERROR, res) << "Failed to get handle for child (tid: " << child_tid << ")";
continue;
}
threads.try_emplace(
child_tid, profiler::ThreadTarget{.handle = std::move(child_thread), .tid = child_tid});
}
profiler::ProcessTarget process_target{std::move(process), handle_info.koid, std::move(threads)};
zx::result<std::map<std::vector<std::byte>, profiler::Module>> modules =
GetProcessModules(*zx::unowned_process{process_target.handle}, searcher);
if (modules.is_error()) {
return zx::error(modules.error_value());
}
for (const auto& [build_id, module] : *modules) {
process_target.unwinder_data->modules.emplace_back(module.vaddr,
&process_target.unwinder_data->memory,
unwinder::Module::AddressMode::kProcess);
}
return zx::ok(std::move(process_target));
}
zx::result<profiler::JobTarget> profiler::MakeJobTarget(zx::job job,
std::span<const zx_koid_t> ancestry,
elf_search::Searcher& searcher) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
zx_info_handle_basic_t info;
if (zx_status_t status =
job.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to make process_target";
return zx::error(status);
}
zx_koid_t job_id = info.koid;
FX_LOGS(DEBUG) << "Creating job target for " << job_id << ".";
size_t num_child_jobs;
if (zx_status_t status = job.get_info(ZX_INFO_JOB_CHILDREN, nullptr, 0, nullptr, &num_child_jobs);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to query number of job children";
return zx::error(status);
}
// Provide each of this job's children their ancestry, which is this job's ancestry, prepended to
// this job's job id.
std::vector<zx_koid_t> child_job_ancestry{ancestry.begin(), ancestry.end()};
child_job_ancestry.push_back(job_id);
// A job can contain child jobs as well as processes directly. We're going to scan through both
// here to build the job tree.
//
// We do need to be a little bit careful here: If a job has short lived processes or child jobs,
// or we simply get unlucky when a process/job exits, we could query the list child jobs, but find
// that one or more of the children is gone by the time we query the child itself for its handle.
// This is especially likely when we're doing system wide profiling and traversing the whole job
// tree.
//
// As such, we want to distinguish between failing an operation on the job we're trying to build
// the JobTarget for, and failing for a child. If we fail an operation on the job itself, we
// should abort, the job is no longer accessible to us. However if we fail to query a child, the
// overall job may still be alive so we want to be resilient and continue on, trying the remaining
// children.
std::unordered_map<zx_koid_t, profiler::JobTarget> child_job_targets;
if (num_child_jobs > 0) {
auto child_jobs = std::make_unique<zx_koid_t[]>(num_child_jobs);
if (zx_status_t status = job.get_info(ZX_INFO_JOB_CHILDREN, child_jobs.get(),
num_child_jobs * sizeof(zx_koid_t), nullptr, nullptr);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to get job children";
return zx::error(status);
}
for (size_t i = 0; i < num_child_jobs; i++) {
zx_koid_t child_koid = child_jobs[i];
zx::job child_job;
if (zx_status_t status = job.get_child(child_koid, ZX_DEFAULT_JOB_RIGHTS, &child_job);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to get job: " << child_koid;
continue;
}
zx::result<profiler::JobTarget> child_job_target =
MakeJobTarget(std::move(child_job), child_job_ancestry, searcher);
if (child_job_target.is_error()) {
FX_PLOGS(WARNING, child_job_target.status_value()) << "failed to make job_target";
continue;
}
child_job_target->ancestry = std::move(child_job_ancestry);
child_job_targets.try_emplace(child_koid, std::move(*child_job_target));
}
}
size_t num_processes;
if (zx_status_t status = job.get_info(ZX_INFO_JOB_PROCESSES, nullptr, 0, nullptr, &num_processes);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to query number of job processes";
return zx::error(status);
}
std::unordered_map<zx_koid_t, profiler::ProcessTarget> process_targets;
if (num_processes > 0) {
auto processes = std::make_unique<zx_koid_t[]>(num_processes);
if (zx_status_t status = job.get_info(ZX_INFO_JOB_PROCESSES, processes.get(),
num_processes * sizeof(zx_koid_t), nullptr, nullptr);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to get job processes";
return zx::error(status);
}
for (size_t i = 0; i < num_processes; i++) {
zx_koid_t process_koid = processes[i];
zx::process process;
if (zx_status_t status = job.get_child(process_koid, ZX_DEFAULT_PROCESS_RIGHTS, &process);
status != ZX_OK) {
FX_PLOGS(WARNING, status) << "failed to get process: " << process_koid;
continue;
}
zx::result<profiler::ProcessTarget> process_target =
MakeProcessTarget(std::move(process), searcher);
if (process_target.is_error()) {
FX_PLOGS(WARNING, process_target.status_value()) << "failed to make process_target";
continue;
}
process_targets.try_emplace(process_koid, std::move(*process_target));
}
}
return zx::ok(profiler::JobTarget{std::move(job), job_id, std::move(process_targets),
std::move(child_job_targets), ancestry});
}
zx::result<profiler::JobTarget> profiler::MakeJobTarget(zx::job job,
elf_search::Searcher& searcher) {
return MakeJobTarget(std::move(job), std::span<const zx_koid_t>{}, searcher);
}
zx::result<> profiler::TargetTree::AddJob(JobTarget&& job) {
return AddJob(std::span<const zx_koid_t>{}, std::move(job));
}
zx::result<> profiler::TargetTree::AddJob(std::span<const zx_koid_t> ancestry, JobTarget&& job) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (ancestry.empty()) {
zx_koid_t job_id = job.job_id;
auto [it, emplaced] = jobs_.try_emplace(job_id, std::move(job));
return zx::make_result(emplaced ? ZX_OK : ZX_ERR_ALREADY_EXISTS);
}
zx_koid_t next_child_koid = ancestry[0];
auto it = jobs_.find(next_child_koid);
return it == jobs_.end() ? zx::error(ZX_ERR_NOT_FOUND)
: it->second.AddJob(ancestry.subspan(1), std::move(job));
}
zx::result<> profiler::TargetTree::AddProcess(ProcessTarget&& process) {
return AddProcess(std::span<const zx_koid_t>{}, std::move(process));
}
zx::result<> profiler::TargetTree::AddProcess(std::span<const zx_koid_t> job_path,
ProcessTarget&& process) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
zx_koid_t pid = process.pid;
auto [it, emplaced] = processes_.try_emplace(pid, std::move(process));
return zx::make_result(emplaced ? ZX_OK : ZX_ERR_ALREADY_EXISTS);
}
zx_koid_t next_child_koid = job_path[0];
auto it = jobs_.find(next_child_koid);
return it == jobs_.end() ? zx::error(ZX_ERR_NOT_FOUND)
: it->second.AddProcess(job_path.subspan(1), std::move(process));
}
zx::result<profiler::ProcessTarget*> profiler::TargetTree::GetProcess(
std::span<const zx_koid_t> job_path, zx_koid_t pid) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
if (processes_.contains(pid)) {
auto it = processes_.find(pid);
zx::ok(&it->second);
}
return zx::error(ZX_ERR_NOT_FOUND);
}
zx_koid_t next_child_koid = job_path[0];
auto it = jobs_.find(next_child_koid);
return it == jobs_.end() ? zx::error(ZX_ERR_NOT_FOUND)
: it->second.GetProcess(job_path.subspan(1), pid);
}
zx::result<> profiler::TargetTree::AddThread(zx_koid_t pid, ThreadTarget&& thread) {
return AddThread(std::span<const zx_koid_t>{}, pid, std::move(thread));
}
zx::result<> profiler::TargetTree::RemoveThread(zx_koid_t pid, zx_koid_t tid) {
return RemoveThread(std::span<const zx_koid_t>{}, pid, tid);
}
zx::result<> profiler::TargetTree::AddThread(std::span<const zx_koid_t> job_path, zx_koid_t pid,
ThreadTarget&& thread) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
auto it = processes_.find(pid);
if (it == processes_.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
zx_koid_t tid = thread.tid;
auto [_, emplaced] = it->second.threads.try_emplace(tid, std::move(thread));
return zx::make_result(emplaced ? ZX_OK : ZX_ERR_ALREADY_EXISTS);
}
zx_koid_t next_child_koid = job_path[0];
auto it = jobs_.find(next_child_koid);
return it == jobs_.end() ? zx::error(ZX_ERR_NOT_FOUND)
: it->second.AddThread(job_path.subspan(1), pid, std::move(thread));
}
zx::result<> profiler::TargetTree::RemoveThread(std::span<const zx_koid_t> job_path,
zx_koid_t pid, zx_koid_t tid) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (job_path.empty()) {
auto it = processes_.find(pid);
if (it == processes_.end()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
size_t num_erased = it->second.threads.erase(tid);
return zx::make_result(num_erased == 1 ? ZX_OK : ZX_ERR_NOT_FOUND);
}
zx_koid_t next_child_koid = job_path[0];
auto it = jobs_.find(next_child_koid);
return it == jobs_.end() ? zx::error(ZX_ERR_NOT_FOUND)
: it->second.RemoveThread(job_path.subspan(1), pid, tid);
}
void profiler::TargetTree::Clear() {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
jobs_.clear();
processes_.clear();
}
zx::result<> profiler::TargetTree::ForEachJob(
const fit::function<zx::result<>(const JobTarget& target)>& f) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
for (const auto& [_, job] : jobs_) {
zx::result<> res = job.ForEachJob(f);
if (res.is_error()) {
return res;
}
}
return zx::ok();
}
zx::result<> profiler::TargetTree::ForEachProcess(
const fit::function<zx::result<>(std::span<const zx_koid_t> job_path,
const ProcessTarget& target)>& f) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
for (const auto& [_, process] : processes_) {
zx::result res = f(std::span<const zx_koid_t>{}, process);
if (res.is_error()) {
return res;
}
}
for (const auto& [_, job] : jobs_) {
zx::result<> res = job.ForEachProcess(f);
if (res.is_error()) {
return res;
}
}
return zx::ok();
}
zx::result<std::map<std::vector<std::byte>, profiler::Module>> profiler::GetProcessModules(
const zx::process& process, elf_search::Searcher& searcher) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
std::map<std::vector<std::byte>, profiler::Module> modules;
zx_status_t search_result =
searcher.ForEachModule(process, [&modules](const elf_search::ModuleInfo& info) mutable {
TRACE_DURATION("cpu_profiler", "ForEachModule");
std::vector<std::byte> build_id;
std::ranges::transform(info.build_id, std::back_inserter(build_id),
[](const uint8_t byte) { return std::byte{byte}; });
auto [it, inserted] = modules.try_emplace(build_id);
if (inserted) {
it->second.module_name = info.name;
it->second.vaddr = info.vaddr;
for (const auto& phdr : info.phdrs) {
if (phdr.p_type != PT_LOAD) {
continue;
}
it->second.loads.push_back({phdr.p_vaddr, phdr.p_memsz, phdr.p_flags});
}
}
});
return zx::make_result(search_result, std::move(modules));
}