blob: 73231765a16b87945b2df6dab2f99b24c75d9e09 [file]
// Copyright 2024 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 "kernel_sampler.h"
#include <fidl/fuchsia.kernel/cpp/fidl.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fit/defer.h>
#include <lib/trace/event.h>
#include <zircon/syscalls-next.h>
#include <zircon/syscalls.h>
#include <unordered_set>
#include <trace-reader/reader.h>
#include <trace-reader/records.h>
zx::result<std::unique_ptr<profiler::KernelSamplerSession>>
profiler::KernelSamplerSession::CreateAndInit(const zx_sampler_config_t& config) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
auto sampling_client_end = component::Connect<fuchsia_kernel::SamplingResource>();
if (sampling_client_end.is_error()) {
FX_PLOGS(ERROR, sampling_client_end.error_value())
<< "Failed to get connect to sampling resource";
return zx::error(sampling_client_end.status_value());
}
auto sampling_result = fidl::SyncClient(std::move(*sampling_client_end))->Get();
if (!sampling_result.is_ok()) {
FX_LOGS(ERROR) << sampling_result.error_value() << " Failed to get sampling resource";
return zx::error(sampling_result.error_value().status());
}
zx::resource sampling_resource = std::move(sampling_result->resource());
zx::handle sampler;
FX_LOGS(DEBUG) << "Creating kernel sampler.";
if (zx_status_t init_status =
zx_sampler_create(sampling_resource.get(), 0, &config, sampler.reset_and_get_address());
init_status != ZX_OK) {
FX_PLOGS(ERROR, init_status) << "Failed to create the kernel sampler.";
return zx::error(init_status);
}
return zx::ok(std::make_unique<profiler::KernelSamplerSession>(std::move(sampler)));
}
zx::result<> profiler::KernelSamplerSession::Start() {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (running_) {
return zx::error(ZX_ERR_BAD_STATE);
}
FX_LOGS(DEBUG) << "Starting kernel sampler.";
running_ = true;
return zx::make_result(zx_sampler_start(sampler_.get()));
}
void profiler::KernelSampler::ServiceBuffers() {
if (session_ && session_->is_running()) {
service_buffers_task_.set_handler([this]() {
if (session_ && session_->is_running()) {
if (zx::result<> res = ForwardBuffers(); res.is_error()) {
FX_PLOGS(WARNING, res.error_value()) << "Failed to forward buffers";
return;
}
ServiceBuffers();
}
});
service_buffers_task_.PostDelayed(dispatcher_, zx::sec(1));
}
}
zx::result<> profiler::KernelSamplerSession::Stop() {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (!running_) {
return zx::error(ZX_ERR_BAD_STATE);
}
running_ = false;
return zx::make_result(zx_sampler_stop(sampler_.get()));
}
zx::result<> profiler::KernelSampler::Start(size_t buffer_size_mb) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
// Verify we support the requested samples
// We currently only support 1 samplespec, and that's backtraces via frame pointers
if (sample_specs_.size() != 1) {
FX_LOGS(ERROR) << "Kernel sampling currently only supports one sampling approach at a time. ("
<< sample_specs_.size() << " approaches specified)";
return zx::error(ZX_ERR_INVALID_ARGS);
}
if (sample_specs_[0].timebase() != fuchsia_cpu_profiler::Counter::WithPlatformIndependent(
fuchsia_cpu_profiler::CounterId::kNanoseconds)) {
FX_LOGS(ERROR) << "Sampling currently only supports timer based sampling";
return zx::error(ZX_ERR_INVALID_ARGS);
}
if (!sample_specs_[0].sample()->callgraph() ||
!sample_specs_[0].sample()->callgraph()->strategy() ||
sample_specs_[0].sample()->callgraph()->strategy() !=
fuchsia_cpu_profiler::CallgraphStrategy::kFramePointer) {
FX_LOGS(ERROR) << "Sampling currently only supports framepointer based sampling";
return zx::error(ZX_ERR_INVALID_ARGS);
}
buffer_size_bytes_ = (1 << 20) * buffer_size_mb;
zx_sampler_config_t config{
.period =
zx::nsec(static_cast<int64_t>(sample_specs_[0].period().value_or(10'000'000))).get(),
.buffer_size = buffer_size_bytes_};
zx::result session_result = KernelSamplerSession::CreateAndInit(config);
if (session_result.is_error()) {
return session_result.take_error();
}
session_ = std::move(session_result).value();
// Passing a nullptr to zx_sampler_read queries the required buffer size to read out data.
size_t max_size = 0;
if (zx_status_t status = zx_sampler_read(session_->BorrowSampler()->get(), nullptr, 0, &max_size);
status != ZX_OK) {
return zx::error(status);
}
if (max_size == 0) {
return zx::error(ZX_ERR_BAD_STATE);
}
sample_buffer_.resize((max_size + sizeof(uint64_t) - 1) / sizeof(uint64_t));
FX_LOGS(DEBUG) << "Attaching to known tasks and watching for new ones.";
zx::result known_threads_res = targets_.ForEachProcess(
[this](std::span<const zx_koid_t> job_path, const ProcessTarget& p) -> zx::result<> {
TRACE_DURATION("cpu_profiler", "KernelSampler::Start/ForEachProcess");
// Before we start sampling the thread, make sure we've recorded information about its
// process
CacheModules(p);
std::vector<zx_koid_t> saved_path{job_path.begin(), job_path.end()};
auto process_watcher = std::make_unique<ProcessWatcher>(
p.handle.borrow(),
[saved_path, this](zx_koid_t pid, zx_koid_t tid, zx::thread t) {
AddThread(saved_path, pid, tid, std::move(t));
},
[saved_path, this](zx_koid_t pid, zx_koid_t tid) {
RemoveThread(saved_path, pid, tid);
});
auto [it, emplaced] = process_watchers_.emplace(p.pid, std::move(process_watcher));
if (emplaced) {
zx::result watch_result = it->second->Watch(dispatcher_);
if (watch_result.is_error()) {
if (watch_result.error_value() == ZX_ERR_BAD_STATE) {
FX_LOGS(DEBUG) << "Process terminated before being watched.";
} else {
FX_PLOGS(ERROR, watch_result.status_value()) << "Failed to watch process: " << p.pid;
job_watchers_.clear();
process_watchers_.clear();
return watch_result.take_error();
}
}
}
return zx::ok();
});
if (known_threads_res.is_error()) {
FX_PLOGS(ERROR, known_threads_res.error_value()) << "Failed to set up all known processes.";
return known_threads_res;
}
// If a watched job launches a new process, we want to add it to the set
zx::result watch_result =
targets_.ForEachJob([this](const JobTarget& target) { return WatchTarget(target); });
if (watch_result.is_error()) {
return watch_result;
}
zx::result<> res = session_->Start();
if (res.is_ok()) {
ServiceBuffers();
}
return res;
}
zx::result<> profiler::KernelSampler::AddTarget(JobTarget&& target) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (session_) {
if (zx::result<> watch_res = WatchTarget(target); watch_res.is_error()) {
return watch_res;
}
zx::result<> res = target.ForEachProcess(
[this](std::span<const zx_koid_t> job_path, const ProcessTarget& p) -> zx::result<> {
TRACE_DURATION("cpu_profiler", "KernelSampler::AddTarget/ForEachProcess");
// Before we start sampling the thread, make sure we've recorded information about its
// process
CacheModules(p);
return zx::ok();
});
if (res.is_error()) {
return res;
}
}
return targets_.AddJob(std::move(target));
}
zx::result<> profiler::KernelSampler::ForwardBuffers() {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
if (!session_) {
return zx::ok();
}
zx::unowned_handle sampler = session_->BorrowSampler();
// Flatten the watched threads so that we can filter out the records that aren't relevant.
std::unordered_set<zx_koid_t> profiled_threads;
// This will always return zx::ok;
auto _ = targets_.ForEachProcess(
[&profiled_threads](std::span<const zx_koid_t> job_path, const ProcessTarget& p) {
for (const auto& [koid, _] : p.threads) {
profiled_threads.insert(koid);
}
return zx::ok();
});
trace::TraceReader::RecordConsumer consume_record = [this, &profiled_threads](trace::Record rec) {
if (rec.type() != trace::RecordType::kProfiler) {
FX_LOGS(WARNING) << "Unhandled record type: " << static_cast<uint64_t>(rec.type());
return;
}
const trace::Record::Profiler& profiler = rec.GetProfiler();
if (profiler.type() != trace::ProfilerRecordType::kBacktrace) {
FX_LOGS(WARNING) << "Unhandled profiler record type: "
<< static_cast<uint64_t>(profiler.type());
return;
}
const trace::Record::Profiler::Backtrace& backtrace = profiler.backtrace();
const zx_koid_t pid = backtrace.process_thread.process_koid();
const zx_koid_t tid = backtrace.process_thread.thread_koid();
if (profiled_threads.contains(tid)) {
if (sample_cb_) {
sample_cb_({pid, tid, backtrace.backtrace, zx::ticks{backtrace.timestamp}, {}});
}
}
};
zx_status_t encountered_error = ZX_OK;
trace::TraceReader::ErrorHandler handle_error = [&encountered_error](std::string_view err) {
FX_LOGS(ERROR) << "Encountered malformed data: " << err;
encountered_error = ZX_ERR_BAD_STATE;
};
trace::TraceReader reader{std::move(consume_record), std::move(handle_error)};
size_t bytes_read = 0;
if (zx_status_t status = zx_sampler_read(sampler->get(), sample_buffer_.data(),
sample_buffer_.size() * sizeof(uint64_t), &bytes_read);
status != ZX_OK) {
return zx::error(status);
}
if (bytes_read == 0) {
return zx::ok();
}
trace::Chunk chunk{sample_buffer_.data(), bytes_read / sizeof(uint64_t)};
if (!reader.ReadRecords(chunk)) {
FX_LOGS(ERROR) << "Buffer data corrupted";
encountered_error = ZX_ERR_BAD_STATE;
}
return zx::make_result(encountered_error);
}
profiler::KernelSampler::~KernelSampler() { std::ignore = Stop(); }
zx::result<> profiler::KernelSampler::Stop() {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__);
FX_LOGS(DEBUG) << "Stopping kernel sampler.";
service_buffers_task_.Cancel();
job_watchers_.clear();
process_watchers_.clear();
if (!session_) {
sample_cb_ = nullptr;
return zx::ok();
}
if (zx::result res = session_->Stop(); res.is_error()) {
FX_PLOGS(WARNING, res.error_value()) << "Failed to stop";
session_.reset();
sample_buffer_.clear();
sample_buffer_.shrink_to_fit();
sample_cb_ = nullptr;
return res;
}
zx::result res = ForwardBuffers();
session_.reset();
sample_buffer_.clear();
sample_buffer_.shrink_to_fit();
sample_cb_ = nullptr;
return res;
}
void profiler::KernelSampler::AddThread(std::vector<zx_koid_t> job_path, zx_koid_t pid,
zx_koid_t tid, zx::thread t) {
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__, "pid", pid, "tid", tid);
// Before we start sampling the thread, make sure we've recorded information about its process
if (!contexts_.contains(pid)) {
zx::result<ProcessTarget*> target = targets_.GetProcess(job_path, pid);
if (target.is_ok()) {
CacheModules(**target);
} else {
FX_PLOGS(ERROR, target.status_value()) << "Failed to search up process: " << pid;
}
}
// Add the thread so we can later grab its address space and module information for
// symbolization purposes.
std::string thread_name = profiler::GetThreadName(t);
if (zx::result res = targets_.AddThread(
job_path, pid,
ThreadTarget{.handle = std::move(t), .tid = tid, .name = std::move(thread_name)});
res.is_error()) {
FX_PLOGS(ERROR, res.status_value()) << "Failed to add thread to session: " << tid;
}
}
void profiler::KernelSampler::RemoveThread(std::vector<zx_koid_t> job_path, zx_koid_t pid,
zx_koid_t tid) {
// Skip removing the thread, we need its metadata to remain around for when we read through the
// samples. Once we have streaming and we won't have any more samplings coming from the thread any
// more, we can properly remove it.
TRACE_DURATION("cpu_profiler", __PRETTY_FUNCTION__, "pid", pid, "tid", tid);
}