blob: cb265ca9f584b5a484434a37b0baffdd5be9d4d4 [file]
// 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.
#ifndef SRC_PERFORMANCE_CPU_PROFILER_TARGETS_H_
#define SRC_PERFORMANCE_CPU_PROFILER_TARGETS_H_
#include <elf-search.h>
#include <lib/fit/function.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/job.h>
#include <lib/zx/process.h>
#include <lib/zx/result.h>
#include <lib/zx/thread.h>
#include <zircon/types.h>
#include <memory>
#include <optional>
#include <unordered_map>
#include <vector>
#include <src/lib/unwinder/elf_module_cache.h>
#include <src/lib/unwinder/fp_unwinder.h>
#include <src/lib/unwinder/fuchsia.h>
#include <src/lib/unwinder/unwind.h>
#include "memory.h"
#include "symbolization_context.h"
namespace profiler {
inline std::string GetThreadName(const zx::thread& thread) {
char name_buf[ZX_MAX_NAME_LEN];
if (zx_status_t status = thread.get_property(ZX_PROP_NAME, name_buf, sizeof(name_buf));
status != ZX_OK) {
name_buf[0] = '\0';
}
return std::string(name_buf);
}
struct ThreadTarget {
zx::thread handle;
zx_koid_t tid;
std::string name;
mutable std::optional<uint64_t> restricted_state_addr;
};
// The unwinding library receives pointers and references. We place the relevant structs together to
// ensure they don't get copied/moved and invalidate the references.
struct UnwinderData {
explicit UnwinderData(const zx::unowned_process& process)
: memory(process->get()),
cached_memory(&memory),
elf_module_cache_(this->modules),
fp_unwinder(this->elf_module_cache_) {}
UnwinderData(const UnwinderData&) = delete;
UnwinderData(UnwinderData&&) = delete;
UnwinderData& operator=(const UnwinderData&) = delete;
UnwinderData& operator=(UnwinderData&&) = delete;
unwinder::FuchsiaMemory memory;
profiler::CachedModuleMemory cached_memory;
std::vector<unwinder::Module> modules;
// Must keep the cache of loaded modules available for the duration of unwinding.
unwinder::ElfModuleCache elf_module_cache_;
// mutable, because stepping the unwinder causes state to change
mutable unwinder::FramePointerUnwinder fp_unwinder;
};
struct ProcessTarget {
ProcessTarget(zx::process process, zx_koid_t pid, std::string name,
std::unordered_map<zx_koid_t, ThreadTarget> threads)
: handle(std::move(process)),
pid(pid),
name(std::move(name)),
threads(std::move(threads)),
unwinder_data(std::make_unique<UnwinderData>(handle.borrow())) {}
zx::process handle;
zx_koid_t pid;
std::string name;
std::unordered_map<zx_koid_t, ThreadTarget> threads;
std::unique_ptr<UnwinderData> unwinder_data;
mutable std::vector<zx_info_maps_t> cached_mappings;
};
zx::result<std::map<std::vector<std::byte>, profiler::Module>> GetProcessModules(
const zx::process&, elf_search::Searcher& searcher);
// Given a process, create a process target containing it and all its threads
zx::result<profiler::ProcessTarget> MakeProcessTarget(zx::process process,
elf_search::Searcher& searcher);
struct JobTarget {
explicit JobTarget(zx::job job, zx_koid_t job_id, std::span<const zx_koid_t> ancestry)
: job(std::move(job)), job_id(job_id), ancestry(ancestry.begin(), ancestry.end()) {}
JobTarget(zx::job job, zx_koid_t job_id, std::unordered_map<zx_koid_t, ProcessTarget> processes,
std::unordered_map<zx_koid_t, JobTarget> child_jobs,
std::span<const zx_koid_t> ancestry)
: job(std::move(job)),
job_id(job_id),
processes(std::move(processes)),
child_jobs(std::move(child_jobs)),
ancestry(ancestry.begin(), ancestry.end()) {}
JobTarget(const JobTarget&) = delete;
JobTarget(JobTarget&&) = default;
zx::job job;
zx_koid_t job_id;
std::unordered_map<zx_koid_t, ProcessTarget> processes;
std::unordered_map<zx_koid_t, JobTarget> child_jobs;
// The list of ancestor jobs encountered while traversing starting at root job to this job.
// Contains the root job, but does not contain this job.
std::vector<zx_koid_t> ancestry;
// Do a depth first search to call f on each process in the modeled job tree.
zx::result<> ForEachProcess(
const fit::function<zx::result<>(std::span<const zx_koid_t> job_path,
const ProcessTarget& target)>& f) const;
// Do a depth first search to call f on each child job in the modeled job tree.
zx::result<> ForEachJob(const fit::function<zx::result<>(const JobTarget& target)>& f) const;
// Add `job` into the job tree as a child to the nested jobs specified by `ancestry`.
//
// Returns zx::ok if the job was successfully added,
// ZX_ERR_NOT_FOUND if there is no matching ancestry
// ZX_ERR_ALREADY_EXISTS if there is already an existing job at the location with the same job_id
//
// Note: `ancestry` is the jobs that `job` will be placed under and does not include the job_id of
// `job` itself.
zx::result<> AddJob(std::span<const zx_koid_t> ancestry, JobTarget&& job);
// Add `process` into the job tree as a child to the job specified by `job_path`
//
// Returns zx::ok if the process was successfully added,
// ZX_ERR_NOT_FOUND if there is no matching job_path
// ZX_ERR_ALREADY_EXISTS if there is already an existing process at the location with the same pid
zx::result<> AddProcess(std::span<const zx_koid_t> job_path, ProcessTarget&& process);
// Get `process` from the job tree as a child to the job specified by `job_path`
//
// Returns zx::ok if the process was found,
// ZX_ERR_NOT_FOUND if there is no matching job_path or pid
zx::result<ProcessTarget*> GetProcess(std::span<const zx_koid_t> job_path, zx_koid_t pid);
// Add `thread` into the job tree as a child to process in job specified by `pid` and `job_path`.
//
// Returns zx::ok if the thread was successfully added,
// ZX_ERR_NOT_FOUND if there is no matching job_path
// ZX_ERR_ALREADY_EXISTS if there is already an existing thread at the location with the same tid
zx::result<> AddThread(std::span<const zx_koid_t> job_path, zx_koid_t pid, ThreadTarget&& thread);
zx::result<> RemoveThread(std::span<const zx_koid_t> job_path, zx_koid_t pid, zx_koid_t tid);
};
// Given a job, create a job target containing it, its processes, their threads, and its child
// jobs. Additionally, the created job will be given the ancestry specified by `ancestry`.
zx::result<profiler::JobTarget> MakeJobTarget(zx::job job, std::span<const zx_koid_t> ancestry,
elf_search::Searcher& searcher);
// Given a job, create a job target containing it, its processes, their threads, and its child
// jobs. The resulting job will have no parent or ancestors.
zx::result<profiler::JobTarget> MakeJobTarget(zx::job job, elf_search::Searcher& searcher);
class TargetTree {
public:
TargetTree() = default;
TargetTree(const TargetTree&) = delete;
TargetTree& operator=(const TargetTree&) = delete;
TargetTree(TargetTree&&) = default;
TargetTree& operator=(TargetTree&&) = default;
// Add `job` into top level set of jobs in the tree.
//
// Returns zx::ok if the job was successfully added,
// ZX_ERR_ALREADY_EXISTS if there is already an existing job with the same job_id
zx::result<> AddJob(JobTarget&& job);
// Add `thread` into the tree as a child to the process specified by `pid` with no parent job
//
// Returns zx::ok if the thread was successfully added,
// ZX_ERR_NOT_FOUND if there is no process matching `pid`
// ZX_ERR_ALREADY_EXISTS if there is already an existing thread at the location
zx::result<> AddThread(zx_koid_t pid, ThreadTarget&& thread);
zx::result<> RemoveThread(zx_koid_t pid, zx_koid_t tid);
// Add `process` into the job tree as a process with no parent job
//
// Returns zx::ok if the process was successfully added,
// ZX_ERR_ALREADY_EXISTS if there is already an existing process with the same pid
zx::result<> AddProcess(ProcessTarget&& process);
// Add `job` into the job tree as a child to the job specified by `ancestry`.
//
// Returns zx::ok if the job was successfully added,
// ZX_ERR_NOT_FOUND if there is no matching ancestry
// ZX_ERR_ALREADY_EXISTS if there is already an existing job at the location with the same job_id
//
// Note: `ancestry` is the jobs that `job` will be placed under and does not include the job_id of
// `job` itself.
zx::result<> AddJob(std::span<const zx_koid_t> ancestry, JobTarget&& job);
zx::result<> RemoveThread(std::span<const zx_koid_t> job_path, zx_koid_t pid, zx_koid_t tid);
// Add `thread` into the job tree as a child to the job specified by `job_path` in the process
// `pid`
//
// Returns zx::ok if the thread was successfully added,
// ZX_ERR_NOT_FOUND if there is no matching job_path
// ZX_ERR_ALREADY_EXISTS if there is already an existing thread at the location with the same tid
zx::result<> AddThread(std::span<const zx_koid_t> job_path, zx_koid_t pid, ThreadTarget&& thread);
// Add `process` into the job tree as a child in the job by `job_path`.
//
// Returns zx::ok if the process was successfully added,
// ZX_ERR_NOT_FOUND if there is no matching job_path
// ZX_ERR_ALREADY_EXISTS if there is already an existing thread at the location with the same tid
zx::result<> AddProcess(std::span<const zx_koid_t> job_path, ProcessTarget&& process);
// Search `pid` as a ProcessTarget from the job tree indexed by `job_path`.
//
// ZX_ERR_NOT_FOUND if there is no matching job_path and pid
zx::result<ProcessTarget*> GetProcess(std::span<const zx_koid_t> job_path, zx_koid_t pid);
void Clear();
// Call `f` on each Job in the TargetTree. The order each job is visited is unspecified. If `f`
// returns an error, the function will short circuit and immediately return the error code without
// visiting any remaining jobs.
zx::result<> ForEachJob(const fit::function<zx::result<>(const JobTarget& target)>& f);
// Call `f` on each top level unparented process as well as every process in each added job. The
// order each process is visited is unspecified. If `f` returns an error, the function will short
// circuit and immediately return the error code without visiting any remaining processes.
zx::result<> ForEachProcess(const fit::function<zx::result<>(std::span<const zx_koid_t> job_path,
const ProcessTarget& target)>& f);
private:
std::unordered_map<zx_koid_t, JobTarget> jobs_;
std::unordered_map<zx_koid_t, ProcessTarget> processes_;
};
} // namespace profiler
#endif // SRC_PERFORMANCE_CPU_PROFILER_TARGETS_H_