blob: 057b960fd79b71d35cf6c8447653c40b252b3d10 [file] [log] [blame]
// 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 "profiler_controller_impl.h"
#include <elf.h>
#include <fidl/fuchsia.kernel/cpp/fidl.h>
#include <fidl/fuchsia.sys2/cpp/fidl.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fit/result.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/handle.h>
#include <lib/zx/job.h>
#include <lib/zx/process.h>
#include <lib/zx/result.h>
#include <lib/zx/thread.h>
#include <lib/zx/time.h>
#include <zircon/errors.h>
#include <zircon/syscalls.h>
#include <zircon/system/ulib/elf-search/include/elf-search.h>
#include <zircon/types.h>
#include <algorithm>
#include <cstddef>
#include <memory>
#include <unordered_map>
#include <utility>
#include <vector>
#include <src/lib/fsl/socket/strings.h>
#include <src/lib/unwinder/module.h>
#include "component.h"
#include "component_watcher.h"
#include "kernel_sampler.h"
#include "sampler.h"
#include "symbolization_context.h"
#include "symbolizer_markup.h"
#include "targets.h"
#include "taskfinder.h"
#include "unowned_component.h"
#ifdef EXPERIMENTAL_THREAD_SAMPLER_ENABLED
constexpr bool kSamplerKernelSupport = EXPERIMENTAL_THREAD_SAMPLER_ENABLED;
#else
#error "EXPERIMENTAL_THREAD_SAMPLER_ENABLED should always be defined"
#endif
zx::result<> PopulateTargets(profiler::TargetTree& tree, TaskFinder::FoundTasks&& tasks) {
for (auto&& [koid, job] : tasks.jobs) {
zx::result<profiler::JobTarget> job_target = profiler::MakeJobTarget(std::move(job));
if (job_target.is_error()) {
// A job might exit in the time between us walking the tree and attempting to find its
// children. Skip it in this case.
continue;
}
if (zx::result res = tree.AddJob(std::move(*job_target)); res.is_error()) {
FX_PLOGS(ERROR, res.status_value()) << "Failed to add job target";
return res;
}
}
for (auto&& [koid, process] : tasks.processes) {
zx_info_handle_basic_t handle_info;
if (process.get_info(ZX_INFO_HANDLE_BASIC, &handle_info, sizeof(handle_info), nullptr,
nullptr) != ZX_OK) {
// A process might exit in the time between us walking the tree and attempting to find its
// children. Skip it in this case.
continue;
}
zx::result<profiler::ProcessTarget> process_target =
profiler::MakeProcessTarget(std::move(process));
if (process_target.is_error()) {
continue;
}
if (zx::result res = tree.AddProcess(std::move(*process_target)); res.is_error()) {
FX_PLOGS(ERROR, res.status_value()) << "Failed to add process target";
return res;
}
}
for (auto&& [koid, thread] : tasks.threads) {
zx_info_handle_basic_t handle_info;
if (thread.get_info(ZX_INFO_HANDLE_BASIC, &handle_info, sizeof(handle_info), nullptr,
nullptr) != ZX_OK) {
continue;
}
// If we have a thread, we need to know what its parent process is
// and then get a handle to it. Unfortunately, the "easiest" way to
// do this is to walk the job tree again.
TaskFinder finder;
finder.AddProcess(handle_info.related_koid);
zx::result<TaskFinder::FoundTasks> found_tasks = finder.FindHandles();
if (found_tasks.is_error()) {
continue;
}
if (found_tasks->processes.size() != 1) {
FX_LOGS(ERROR) << "Found the wrong number of processes for thread: " << thread.get();
return zx::error(ZX_ERR_NOT_FOUND);
}
auto [pid, process] = std::move(found_tasks->processes[0]);
profiler::ProcessTarget process_target{std::move(process), pid,
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
elf_search::ForEachModule(process_target.handle,
[&process_target](const elf_search::ModuleInfo& info) {
process_target.unwinder_data->modules.emplace_back(
info.vaddr, &process_target.unwinder_data->memory,
unwinder::Module::AddressMode::kProcess);
});
if (zx::result<> res = tree.AddProcess(std::move(process_target)); res.is_error()) {
// If the process already exists, then we'll just append to the existing one below
if (res.status_value() != ZX_ERR_ALREADY_EXISTS) {
FX_PLOGS(ERROR, res.status_value()) << "Failed to add process target";
return res;
}
}
if (zx::result res = tree.AddThread(pid, profiler::ThreadTarget{std::move(thread), koid});
res.is_error()) {
FX_PLOGS(ERROR, res.status_value()) << "Failed to add thread target: " << koid;
return res;
}
}
return zx::ok();
}
zx::result<zx_koid_t> ReadElfJobId(const fidl::SyncClient<fuchsia_io::Directory>& directory) {
zx::result<fidl::Endpoints<fuchsia_io::File>> endpoints =
fidl::CreateEndpoints<fuchsia_io::File>();
if (endpoints.is_error()) {
return endpoints.take_error();
}
fit::result<fidl::OneWayStatus> res = directory->Open(
{{.flags = fuchsia_io::OpenFlags::kRightReadable,
.mode = {},
.path = "elf/job_id",
.object = fidl::ServerEnd<fuchsia_io::Node>(endpoints->server.TakeChannel())}});
if (res.is_error()) {
return zx::error(ZX_ERR_IO);
}
fidl::SyncClient<fuchsia_io::File> job_id_file{std::move(endpoints->client)};
fidl::Result<fuchsia_io::File::Read> read_res =
job_id_file->Read({{.count = fuchsia_io::kMaxTransferSize}});
if (read_res.is_error()) {
return zx::error(ZX_ERR_IO);
}
std::string job_id_str(reinterpret_cast<const char*>(read_res->data().data()),
read_res->data().size());
char* end;
zx_koid_t job_id = std::strtoull(job_id_str.c_str(), &end, 10);
if (end != job_id_str.c_str() + job_id_str.size()) {
return zx::error(ZX_ERR_INVALID_ARGS);
}
return zx::ok(job_id);
}
zx::result<zx_koid_t> MonikerToJobId(const std::string& moniker) {
zx::result<fidl::ClientEnd<fuchsia_sys2::RealmQuery>> client_end =
component::Connect<fuchsia_sys2::RealmQuery>("/svc/fuchsia.sys2.RealmQuery.root");
if (client_end.is_error()) {
FX_LOGS(WARNING) << "Unable to connect to RealmQuery. Attaching by moniker isn't supported!";
return client_end.take_error();
}
zx::result<fidl::Endpoints<fuchsia_io::Directory>> directory_endpoints =
fidl::CreateEndpoints<fuchsia_io::Directory>();
if (directory_endpoints.is_error()) {
FX_LOGS(WARNING) << "Unable to create directory endpoints";
return directory_endpoints.take_error();
}
fidl::SyncClient<fuchsia_io::Directory> directory_client{std::move(directory_endpoints->client)};
fidl::SyncClient realm_query_client{std::move(*client_end)};
fidl::Result<fuchsia_sys2::RealmQuery::Open> open_result = realm_query_client->Open({{
.moniker = moniker,
.dir_type = fuchsia_sys2::OpenDirType::kRuntimeDir,
.flags = fuchsia_io::OpenFlags::kRightReadable,
.mode = {},
.path = ".",
.object = fidl::ServerEnd<fuchsia_io::Node>{directory_endpoints->server.TakeChannel()},
}});
if (open_result.is_error()) {
FX_LOGS(WARNING) << "Unable to open the runtime directory of " << moniker << ": "
<< open_result.error_value();
return zx::error(ZX_ERR_BAD_PATH);
}
zx::result<zx_koid_t> job_id = ReadElfJobId(directory_client);
if (job_id.is_error()) {
FX_LOGS(WARNING) << "Unable to read component directory";
}
return job_id;
}
void profiler::ProfilerControllerImpl::Configure(ConfigureRequest& request,
ConfigureCompleter::Sync& completer) {
if (state_ != ProfilingState::Unconfigured) {
completer.Reply(fit::error(fuchsia_cpu_profiler::SessionConfigureError::kBadState));
return;
}
if (!request.output()) {
completer.Reply(fit::error(fuchsia_cpu_profiler::SessionConfigureError::kBadSocket));
return;
}
socket_ = std::move(*request.output());
if (!request.config() || !request.config()->target()) {
FX_LOGS(ERROR) << "No Target Specified and System Wide profiling isn't yet implemented!";
completer.Reply(fit::error(fuchsia_cpu_profiler::SessionConfigureError::kMissingTargetConfigs));
return;
}
if (!request.config()->configs()) {
FX_LOGS(ERROR) << "No sampling configs specified!";
completer.Reply(fit::error(fuchsia_cpu_profiler::SessionConfigureError::kMissingSampleConfigs));
return;
}
sample_specs_.clear();
for (auto&& sampling_config : request.config()->configs().value()) {
sample_specs_.push_back(std::move(sampling_config));
}
// We're given pids/tids/jobids for each of our targets. We'll need handles to each of these
// targets in order to suspend them and read their memory. We'll walk the root job tree looking
// for anything that has a koid that matches the ones we've been given.
TaskFinder finder;
switch (request.config()->target()->Which()) {
case fuchsia_cpu_profiler::TargetConfig::Tag::kTasks: {
for (auto& t : request.config()->target()->tasks().value()) {
switch (t.Which()) {
case fuchsia_cpu_profiler::Task::Tag::kProcess:
finder.AddProcess(t.process().value());
break;
case fuchsia_cpu_profiler::Task::Tag::kThread:
finder.AddThread(t.thread().value());
break;
case fuchsia_cpu_profiler::Task::Tag::kJob:
finder.AddJob(t.job().value());
break;
default:
FX_LOGS(ERROR) << "Invalid task!";
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
}
break;
}
case fuchsia_cpu_profiler::TargetConfig::Tag::kComponent: {
const auto& attach_config = request.config()->target()->component();
switch (attach_config->Which()) {
case fuchsia_cpu_profiler::AttachConfig::Tag::kLaunchComponent: {
auto& launch_config = attach_config->launch_component();
if (!launch_config->url()) {
FX_LOGS(ERROR) << "Cannot launch a component without a specified url!";
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kMissingComponentUrl));
return;
}
auto url = launch_config->url().value();
std::string moniker;
if (launch_config->moniker()) {
moniker = launch_config->moniker().value();
} else {
// If we are launching the component and a moniker isn't specified, default to
// core/ffx-laboratory.
// url: fuchsia-pkg://fuchsia.com/package#meta/component.cm
const size_t name_start = url.find_last_of('/');
const size_t name_end = url.find_last_of('.');
if (name_start == std::string::npos || name_end == std::string::npos) {
FX_LOGS(ERROR) << "Invalid url: " << url;
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
// name: component
const std::string name = url.substr(name_start + 1, name_end - (name_start + 1));
moniker = "./core/ffx-laboratory:" + name;
}
zx::result<std::unique_ptr<profiler::Component>> res =
profiler::Component::Create(dispatcher_, url, moniker);
if (res.is_error()) {
FX_PLOGS(INFO, res.error_value())
<< "No access to fuchsia.sys2.LifecycleController.root. Component launching and attaching is disabled";
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
component_target_ = std::move(*res);
break;
}
case fuchsia_cpu_profiler::AttachConfig::Tag::kAttachToComponentMoniker: {
auto& attach_moniker = attach_config->attach_to_component_moniker();
zx::result<std::unique_ptr<profiler::Component>> res =
profiler::UnownedComponent::Create(dispatcher_, attach_moniker, std::nullopt);
if (res.is_error()) {
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
component_target_ = std::move(*res);
break;
}
case fuchsia_cpu_profiler::AttachConfig::Tag::kAttachToComponentUrl: {
auto& attach_url = attach_config->attach_to_component_url();
zx::result<std::unique_ptr<profiler::Component>> res =
profiler::UnownedComponent::Create(dispatcher_, std::nullopt, attach_url);
if (res.is_error()) {
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
component_target_ = std::move(*res);
break;
}
default: {
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
}
break;
}
case fuchsia_cpu_profiler::TargetConfig::Tag::kTest:
default:
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
if (!component_target_) {
zx::result<TaskFinder::FoundTasks> handles_result = finder.FindHandles();
if (handles_result.is_error()) {
FX_PLOGS(ERROR, handles_result.error_value()) << "Failed to walk job tree";
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
if (handles_result->empty()) {
FX_LOGS(ERROR) << "Found no relevant handles";
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
targets_.Clear();
if (PopulateTargets(targets_, std::move(*handles_result)).is_error()) {
completer.Reply(
fit::error(fuchsia_cpu_profiler::SessionConfigureError::kInvalidConfiguration));
return;
}
}
state_ = ProfilingState::Stopped;
completer.Reply(fit::ok());
}
void profiler::ProfilerControllerImpl::Start(StartRequest& request,
StartCompleter::Sync& completer) {
if (state_ != ProfilingState::Stopped) {
completer.Reply(fit::error(fuchsia_cpu_profiler::SessionStartError::kBadState));
return;
}
if constexpr (kSamplerKernelSupport) {
sampler_ =
std::make_unique<KernelSampler>(dispatcher_, std::move(targets_), std::move(sample_specs_));
} else {
FX_LOGS(WARNING)
<< "Kernel assisted sampling is not enabled. Falling back to zx_process_read_memory based sampling.\n"
<< "Set the build arg \"experimental_thread_sampler_enabled = true\" to enable kernel assisted sampling";
sampler_ =
std::make_unique<Sampler>(dispatcher_, std::move(targets_), std::move(sample_specs_));
}
sample_specs_.clear();
targets_.Clear();
if (component_target_) {
ComponentWatcher::ComponentEventHandler on_start_handler = [this](std::string moniker,
std::string) {
zx::result<zx_koid_t> job_id = MonikerToJobId(moniker);
if (job_id.is_error()) {
FX_PLOGS(ERROR, job_id.error_value()) << "Failed to get moniker from Job ID";
return;
}
TaskFinder tf;
tf.AddJob(*job_id);
zx::result<TaskFinder::FoundTasks> handles = tf.FindHandles();
if (handles.is_error()) {
FX_PLOGS(ERROR, handles.error_value()) << "Failed to find handle for: " << moniker;
return;
}
for (auto& [koid, handle] : handles->jobs) {
if (koid == job_id) {
zx::result<JobTarget> target = MakeJobTarget(zx::job(handle.release()));
if (target.is_error()) {
FX_PLOGS(ERROR, target.status_value()) << "Failed to make target for: " << moniker;
return;
}
zx::result<> target_result = sampler_->AddTarget(std::move(*target));
if (target_result.is_error()) {
FX_PLOGS(ERROR, target_result.error_value()) << "Failed to add target for: " << moniker;
return;
}
break;
}
}
};
if (zx::result<> res = component_target_->Start(std::move(on_start_handler)); res.is_error()) {
FX_PLOGS(ERROR, res.error_value()) << "Failed to start!";
completer.Close(res.error_value());
Reset();
return;
}
};
size_t buffer_size_mb = request.buffer_size_mb().value_or(8);
zx::result<> start_res = sampler_->Start(buffer_size_mb);
if (start_res.is_error()) {
FX_PLOGS(ERROR, start_res.status_value()) << "Failed to start sampler";
Reset();
completer.Close(start_res.status_value());
return;
}
state_ = ProfilingState::Running;
completer.Reply(fit::ok());
}
void profiler::ProfilerControllerImpl::Stop(StopCompleter::Sync& completer) {
zx::result<profiler::SymbolizationContext> modules = sampler_->GetContexts();
if (modules.is_error()) {
Reset();
completer.Close(modules.status_value());
return;
}
zx::result<> stop_res = sampler_->Stop();
if (stop_res.is_error()) {
FX_PLOGS(ERROR, stop_res.status_value()) << "Sampler failed to stop";
completer.Close(stop_res.status_value());
Reset();
return;
}
if (component_target_) {
if (zx::result<> res = component_target_->Stop(); res.is_error()) {
FX_PLOGS(WARNING, res.error_value()) << "Failed to stop launched components";
}
if (zx::result<> res = component_target_->Destroy(); res.is_error()) {
FX_PLOGS(WARNING, res.error_value()) << "Failed to destroy launched components";
}
}
std::vector<zx::ticks> inspecting_durations = sampler_->SamplingDurations();
fuchsia_cpu_profiler::SessionStopResponse stats;
stats.samples_collected() = inspecting_durations.size();
if (!inspecting_durations.empty()) {
auto ticks_per_second = zx::ticks::per_second();
auto ticks_per_us = ticks_per_second / 1000000;
zx::ticks total_ticks_inspecting;
for (zx::ticks ticks : inspecting_durations) {
total_ticks_inspecting += ticks;
}
std::sort(inspecting_durations.begin(), inspecting_durations.end(),
[](zx::ticks a, zx::ticks b) { return a < b; });
zx::ticks mean_inspecting = total_ticks_inspecting / inspecting_durations.size();
stats.mean_sample_time() = mean_inspecting / ticks_per_us;
stats.median_sample_time() =
inspecting_durations[inspecting_durations.size() / 2] / ticks_per_us;
stats.min_sample_time() = inspecting_durations.front() / ticks_per_us;
stats.max_sample_time() = inspecting_durations.back() / ticks_per_us;
}
// Allow the caller to move on before writing to the socket. This way, if the caller is
// synchronous, they can start reading from the socket without worrying about deadlocking due to a
// full socket.
completer.Reply(std::move(stats));
for (const auto& [pid, samples] : sampler_->GetSamples()) {
if (!fsl::BlockingCopyFromString(profiler::symbolizer_markup::kReset, socket_)) {
FX_LOGS(ERROR) << "Failed to write symbolizer markup to socket";
return;
}
auto process_modules = modules->process_contexts[pid];
for (const profiler::Module& mod : process_modules) {
if (!fsl::BlockingCopyFromString(profiler::symbolizer_markup::FormatModule(mod), socket_)) {
FX_LOGS(ERROR) << "Failed to write modules to socket";
return;
}
}
for (const Sample& sample : samples) {
if (!fsl::BlockingCopyFromString(profiler::symbolizer_markup::FormatSample(sample),
socket_)) {
FX_LOGS(ERROR) << "Failed to write samples to socket";
return;
}
}
}
Reset();
}
void profiler::ProfilerControllerImpl::Reset(ResetCompleter::Sync& completer) {
Reset();
completer.Reply();
}
void profiler::ProfilerControllerImpl::OnUnbound(
fidl::UnbindInfo info, fidl::ServerEnd<fuchsia_cpu_profiler::Session> server_end) {
Reset();
}
void profiler::ProfilerControllerImpl::Reset() {
sampler_.reset();
socket_.reset();
targets_.Clear();
state_ = ProfilingState::Unconfigured;
component_target_.reset();
sample_specs_.clear();
}