| // 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 <fidl/fuchsia.cpu.profiler/cpp/fidl.h> | |
| #include <fidl/fuchsia.sys2/cpp/fidl.h> | |
| #include <lib/component/incoming/cpp/protocol.h> | |
| #include <lib/fdio/spawn.h> | |
| #include <lib/syslog/cpp/macros.h> | |
| #include <lib/zx/clock.h> | |
| #include <lib/zx/job.h> | |
| #include <lib/zx/process.h> | |
| #include <lib/zx/result.h> | |
| #include <lib/zx/socket.h> | |
| #include <lib/zx/thread.h> | |
| #include <lib/zx/time.h> | |
| #include <unistd.h> | |
| #include <zircon/errors.h> | |
| #include <zircon/rights.h> | |
| #include <zircon/syscalls.h> | |
| #include <zircon/threads.h> | |
| #include <zircon/types.h> | |
| #include <cctype> | |
| #include <condition_variable> | |
| #include <cstddef> | |
| #include <cstdlib> | |
| #include <mutex> | |
| #include <set> | |
| #include <sstream> | |
| #include <string> | |
| #include <thread> | |
| #include <utility> | |
| #include <vector> | |
| #include <gtest/gtest.h> | |
| #include <src/lib/fsl/socket/strings.h> | |
| #include <trace-reader/reader.h> | |
| #include <trace-reader/records.h> | |
| namespace fprofiler = fuchsia_cpu_profiler; | |
| namespace { | |
| void MakeWork() { | |
| for (;;) { | |
| // We need to have at least some side effect producing code or a release build will elide the | |
| // entire function | |
| FX_LOGS(TRACE) << "Working!"; | |
| } | |
| zx_thread_exit(); | |
| } | |
| class SocketReader { | |
| public: | |
| explicit SocketReader(zx::socket sock) : sock_(std::move(sock)) { | |
| thread_ = std::thread([this]() { ReadLoop(); }); | |
| } | |
| ~SocketReader() { | |
| sock_.reset(); | |
| if (thread_.joinable()) { | |
| thread_.join(); | |
| } | |
| } | |
| void ReadUntilRecordCount(size_t min_records = 11, size_t min_pids = 0, size_t min_tids = 0) { | |
| std::unique_lock<std::mutex> lock(mutex_); | |
| cv_.wait(lock, [this, min_records, min_pids, min_tids]() { | |
| return (record_count_ >= min_records && pids_.size() >= min_pids && | |
| tids_.size() >= min_tids) || | |
| closed_; | |
| }); | |
| } | |
| std::set<zx_koid_t> pids() { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| return pids_; | |
| } | |
| std::set<zx_koid_t> tids() { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| return tids_; | |
| } | |
| size_t record_count() { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| return record_count_; | |
| } | |
| private: | |
| void ReadLoop() { | |
| auto record_consumer = [this](trace::Record record) { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| if (record.type() == trace::RecordType::kProfiler) { | |
| const auto& profiler_record = record.GetProfiler(); | |
| if (profiler_record.type() == trace::ProfilerRecordType::kBacktrace) { | |
| record_count_++; | |
| const auto& pt = profiler_record.backtrace().process_thread; | |
| if (pt.process_koid() != 0) { | |
| pids_.insert(pt.process_koid()); | |
| } | |
| if (pt.thread_koid() != 0) { | |
| tids_.insert(pt.thread_koid()); | |
| } | |
| } | |
| } else if (record.type() == trace::RecordType::kLargeRecord) { | |
| const auto& large_record = record.GetLargeRecord(); | |
| if (large_record.type() == trace::LargeRecordType::kBlob) { | |
| const auto& blob = large_record.GetBlob(); | |
| if (std::holds_alternative<trace::LargeRecordData::BlobEvent>(blob)) { | |
| record_count_++; | |
| const auto& event = std::get<trace::LargeRecordData::BlobEvent>(blob); | |
| if (event.process_thread.process_koid() != 0) { | |
| pids_.insert(event.process_thread.process_koid()); | |
| } | |
| if (event.process_thread.thread_koid() != 0) { | |
| tids_.insert(event.process_thread.thread_koid()); | |
| } | |
| } | |
| } | |
| } | |
| cv_.notify_all(); | |
| }; | |
| trace::TraceReader reader(record_consumer, [](std::string_view) {}); | |
| std::vector<uint8_t> buffer; | |
| size_t buffer_end = 0; | |
| while (true) { | |
| if (buffer.size() - buffer_end < 65536) { | |
| buffer.resize(buffer_end + 65536); | |
| } | |
| size_t actual = 0; | |
| zx_status_t status = | |
| sock_.read(0, buffer.data() + buffer_end, buffer.size() - buffer_end, &actual); | |
| if (status == ZX_ERR_SHOULD_WAIT) { | |
| zx_signals_t pending = 0; | |
| status = sock_.wait_one(ZX_SOCKET_READABLE | ZX_SOCKET_PEER_CLOSED, zx::time::infinite(), | |
| &pending); | |
| if (status == ZX_OK && (pending & ZX_SOCKET_PEER_CLOSED) && | |
| !(pending & ZX_SOCKET_READABLE)) { | |
| break; | |
| } | |
| continue; | |
| } else if (status == ZX_ERR_PEER_CLOSED) { | |
| break; | |
| } else if (status != ZX_OK) { | |
| break; | |
| } | |
| if (actual > 0) { | |
| buffer_end += actual; | |
| size_t words_in_chunk = buffer_end / sizeof(uint64_t); | |
| trace::Chunk chunk(reinterpret_cast<const uint64_t*>(buffer.data()), words_in_chunk); | |
| reader.ReadRecords(chunk); | |
| size_t words_consumed = words_in_chunk - chunk.remaining_words(); | |
| size_t bytes_consumed = words_consumed * sizeof(uint64_t); | |
| if (bytes_consumed > 0) { | |
| memmove(buffer.data(), buffer.data() + bytes_consumed, buffer_end - bytes_consumed); | |
| buffer_end -= bytes_consumed; | |
| } | |
| } | |
| } | |
| { | |
| std::lock_guard<std::mutex> lock(mutex_); | |
| closed_ = true; | |
| } | |
| cv_.notify_all(); | |
| } | |
| zx::socket sock_; | |
| std::thread thread_; | |
| std::mutex mutex_; | |
| std::condition_variable cv_; | |
| std::set<zx_koid_t> pids_; | |
| std::set<zx_koid_t> tids_; | |
| size_t record_count_ = 0; | |
| bool closed_ = false; | |
| }; | |
| // Sample the callstack via frame pointer at 100hz. | |
| std::vector<fprofiler::SamplingConfig> default_sample_configs() { | |
| return std::vector{fprofiler::SamplingConfig{{ | |
| .period = 10'000'000, | |
| .timebase = fprofiler::Counter::WithPlatformIndependent(fprofiler::CounterId::kNanoseconds), | |
| .sample = fprofiler::Sample{{ | |
| .callgraph = | |
| fprofiler::CallgraphConfig{{.strategy = fprofiler::CallgraphStrategy::kFramePointer}}, | |
| .counters = std::vector<fprofiler::Counter>{}, | |
| }}, | |
| }}}; | |
| } | |
| // Sample the stack via DWARF at 100hz. | |
| std::vector<fprofiler::SamplingConfig> dwarf_sample_configs() { | |
| return std::vector{fprofiler::SamplingConfig{{ | |
| .period = 10'000'000, | |
| .timebase = fprofiler::Counter::WithPlatformIndependent(fprofiler::CounterId::kNanoseconds), | |
| .sample = fprofiler::Sample{{ | |
| .callgraph = | |
| fprofiler::CallgraphConfig{{.strategy = fprofiler::CallgraphStrategy::kDwarf}}, | |
| .counters = std::vector<fprofiler::Counter>{}, | |
| }}, | |
| }}}; | |
| } | |
| // Launch a component as a dynamic child in the tests's launchpad collection. | |
| // This will Create, Resolve, and Start the requested instance. | |
| zx::result<fidl::ClientEnd<fuchsia_component::Binder>> RunInstance( | |
| const fidl::SyncClient<fuchsia_sys2::LifecycleController>& lifecycle_client, | |
| const std::string& name, const std::string& url, const std::string& moniker) { | |
| auto [client, server] = fidl::Endpoints<fuchsia_component::Binder>::Create(); | |
| if (auto create_res = lifecycle_client->CreateInstance({{ | |
| .parent_moniker = ".", | |
| .collection = {"launchpad"}, | |
| .decl = {{ | |
| .name = name, | |
| .url = url, | |
| .startup = fuchsia_component_decl::StartupMode::kLazy, | |
| }}, | |
| }}); | |
| create_res.is_error()) { | |
| FX_LOGS(ERROR) << create_res.error_value(); | |
| return zx::error(ZX_ERR_BAD_STATE); | |
| } | |
| if (auto resolve_res = lifecycle_client->ResolveInstance({{ | |
| .moniker = moniker, | |
| }}); | |
| resolve_res.is_error()) { | |
| FX_LOGS(ERROR) << resolve_res.error_value(); | |
| return zx::error(ZX_ERR_BAD_STATE); | |
| } | |
| if (auto start_res = lifecycle_client->StartInstance({{ | |
| .moniker = moniker, | |
| .binder = std::move(server), | |
| }}); | |
| start_res.is_error()) { | |
| FX_LOGS(ERROR) << start_res.error_value(); | |
| return zx::error(ZX_ERR_BAD_STATE); | |
| } | |
| // Ensure component instance is fully registered in Component Manager before returning | |
| if (zx::result query_client_end = component::Connect<fuchsia_sys2::RealmQuery>(); | |
| query_client_end.is_ok()) { | |
| fidl::SyncClient realm_query{std::move(*query_client_end)}; | |
| for (int i = 0; i < 100; ++i) { | |
| if (realm_query->GetInstance(moniker).is_ok()) { | |
| break; | |
| } | |
| zx::nanosleep(zx::deadline_after(zx::msec(50))); | |
| } | |
| } | |
| return zx::ok(std::move(client)); | |
| } | |
| // Tear down a child component in the tests's launchpad collection. | |
| // This will Stop and Destroy the requested instance. | |
| zx::result<> TearDownInstance( | |
| const fidl::SyncClient<fuchsia_sys2::LifecycleController>& lifecycle_client, | |
| const std::string& name, const std::string& moniker) { | |
| if (auto stop_res = lifecycle_client->StopInstance({{ | |
| .moniker = moniker, | |
| }}); | |
| stop_res.is_error()) { | |
| FX_LOGS(ERROR) << stop_res.error_value(); | |
| return zx::error(ZX_ERR_BAD_STATE); | |
| } | |
| if (auto destroy_res = lifecycle_client->DestroyInstance({{.parent_moniker = ".", | |
| .child = {{ | |
| .name = name, | |
| .collection = "launchpad", | |
| }}}}); | |
| destroy_res.is_error()) { | |
| FX_LOGS(ERROR) << destroy_res.error_value(); | |
| return zx::error(ZX_ERR_BAD_STATE); | |
| } | |
| return zx::ok(); | |
| } | |
| } // namespace | |
| TEST(ProfilerIntegrationTest, EndToEnd) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::process self; | |
| zx::process::self()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self); | |
| std::thread child(MakeWork); | |
| const zx::unowned_thread child_handle{native_thread_get_zx_handle(child.native_handle())}; | |
| child.detach(); | |
| zx_status_t res = | |
| child_handle->wait_one(ZX_THREAD_RUNNING, zx::deadline_after(zx::sec(1)), nullptr); | |
| ASSERT_EQ(ZX_OK, res); | |
| zx_info_handle_basic_t info; | |
| res = child_handle->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); | |
| ASSERT_EQ(ZX_OK, res); | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithThread(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } | |
| // Monitor ourself and check that if we start new threads after the profiling session starts, that | |
| // one or more of them show up in the samples we take. | |
| TEST(ProfilerIntegrationTest, NewThreads) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::unowned_process self = zx::process::self(); | |
| zx_info_handle_basic_t info; | |
| zx_status_t info_result = | |
| self->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); | |
| ASSERT_EQ(ZX_OK, info_result); | |
| // We'll sample ourself. | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithProcess(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| // Start some threads; | |
| std::thread t1{MakeWork}; | |
| std::thread t2{MakeWork}; | |
| std::thread t3{MakeWork}; | |
| t1.detach(); | |
| t2.detach(); | |
| t3.detach(); | |
| reader.ReadUntilRecordCount(11, 1, 2); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should only have one pid | |
| EXPECT_EQ(size_t{1}, reader.pids().size()); | |
| // We should only have more than one thread | |
| EXPECT_GT(reader.tids().size(), size_t{1}); | |
| } | |
| // Monitor ourself via our job id | |
| TEST(ProfilerIntegrationTest, OwnJobId) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::unowned_job self = zx::job::default_job(); | |
| zx_info_handle_basic_t info; | |
| zx_status_t info_result = | |
| self->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); | |
| ASSERT_EQ(ZX_OK, info_result); | |
| // We'll sample ourself by our job id | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithJob(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| std::thread t1{MakeWork}; | |
| std::thread t2{MakeWork}; | |
| std::thread t3{MakeWork}; | |
| t1.detach(); | |
| t2.detach(); | |
| t3.detach(); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| ASSERT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should only have one pid | |
| EXPECT_EQ(size_t{1}, reader.pids().size()); | |
| // And that pid should be us | |
| zx::unowned_process process_self = zx::process::self(); | |
| zx_info_handle_basic_t process_info; | |
| ASSERT_EQ(ZX_OK, process_self->get_info(ZX_INFO_HANDLE_BASIC, &process_info, sizeof(process_info), | |
| nullptr, nullptr)); | |
| EXPECT_EQ(*reader.pids().begin(), process_info.koid); | |
| } | |
| // Monitor ourself via our job id and then launch a process as part of our job and check that it | |
| // gets added to the profiling set | |
| TEST(ProfilerIntegrationTest, LaunchedProcess) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::unowned_job self = zx::job::default_job(); | |
| zx_info_handle_basic_t info; | |
| zx_status_t info_result = | |
| self->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); | |
| ASSERT_EQ(ZX_OK, info_result); | |
| // We'll sample ourself by our job id | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithJob(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| // Launch an additional process before starting | |
| zx::process process1; | |
| const char* kArgs[] = {"/pkg/bin/demo_target", nullptr}; | |
| ASSERT_EQ(ZX_OK, fdio_spawn(self->get(), FDIO_SPAWN_CLONE_ALL, "/pkg/bin/demo_target", kArgs, | |
| process1.reset_and_get_address())); | |
| size_t num_processes; | |
| self->get_info(ZX_INFO_JOB_PROCESSES, nullptr, 0, nullptr, &num_processes); | |
| ASSERT_EQ(num_processes, size_t{2}); | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| // Launch a thread in our process to ensure we get samples that aren't | |
| // just this process sleeping | |
| std::thread t1{MakeWork}; | |
| t1.detach(); | |
| // Then launch another process after starting | |
| zx::process process2; | |
| ASSERT_EQ(ZX_OK, fdio_spawn(self->get(), FDIO_SPAWN_CLONE_ALL, "/pkg/bin/demo_target", kArgs, | |
| process2.reset_and_get_address())); | |
| self->get_info(ZX_INFO_JOB_PROCESSES, nullptr, 0, nullptr, &num_processes); | |
| ASSERT_EQ(num_processes, size_t{3}); | |
| reader.ReadUntilRecordCount(11, 3, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| ASSERT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should three pids, our pid, the pid of process1, and the pid of process2 | |
| zx_info_handle_basic_t pid_info; | |
| ASSERT_EQ(ZX_OK, zx::process::self()->get_info(ZX_INFO_HANDLE_BASIC, &pid_info, sizeof(pid_info), | |
| nullptr, nullptr)); | |
| zx_koid_t our_pid = pid_info.koid; | |
| ASSERT_EQ(ZX_OK, | |
| process1.get_info(ZX_INFO_HANDLE_BASIC, &pid_info, sizeof(pid_info), nullptr, nullptr)); | |
| zx_koid_t process1_pid = pid_info.koid; | |
| ASSERT_EQ(ZX_OK, | |
| process2.get_info(ZX_INFO_HANDLE_BASIC, &pid_info, sizeof(pid_info), nullptr, nullptr)); | |
| zx_koid_t process2_pid = pid_info.koid; | |
| EXPECT_EQ(size_t{3}, reader.pids().size()); | |
| EXPECT_TRUE(reader.pids().find(our_pid) != reader.pids().end()); | |
| EXPECT_TRUE(reader.pids().find(process1_pid) != reader.pids().end()); | |
| EXPECT_TRUE(reader.pids().find(process2_pid) != reader.pids().end()); | |
| process1.kill(); | |
| process2.kill(); | |
| } | |
| // Monitor ourself via our job id and then launch a process as part of our job and check that it we | |
| // see the threads it spawns | |
| TEST(ProfilerIntegrationTest, LaunchedProcessThreadSpawner) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::unowned_job self = zx::job::default_job(); | |
| zx_info_handle_basic_t info; | |
| zx_status_t info_result = | |
| self->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); | |
| ASSERT_EQ(ZX_OK, info_result); | |
| // We'll sample ourself by our job id | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithJob(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| // Launch the thread spawner process after starting | |
| zx::process process; | |
| const char* kArgs[] = {"/pkg/bin/thread_spawner", nullptr}; | |
| ASSERT_EQ(ZX_OK, fdio_spawn(self->get(), FDIO_SPAWN_CLONE_ALL, "/pkg/bin/thread_spawner", kArgs, | |
| process.reset_and_get_address())); | |
| reader.ReadUntilRecordCount(11, 1, 11); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| ASSERT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should have many sampled threads | |
| EXPECT_GT(reader.tids().size(), size_t{10}); | |
| process.kill(); | |
| } | |
| // Monitor a component via moniker. Since we're running in the test realm, we only have access to | |
| // our children components. | |
| TEST(ProfilerIntegrationTest, ComponentByMoniker) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker("demo_target")); | |
| fprofiler::Config demo_target_config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(demo_target_config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| ASSERT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should have only one thread and one process | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| } | |
| TEST(ProfilerIntegrationTest, LaunchedComponent) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithComponent(fprofiler::AttachConfig::WithLaunchComponent({{ | |
| .url = "demo_target#meta/demo_target.cm", | |
| .moniker = "./launchpad:demo_target", | |
| }})); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } | |
| TEST(ProfilerIntegrationTest, ChildComponents) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithComponent(fprofiler::AttachConfig::WithLaunchComponent({{ | |
| .url = "component_with_children#meta/component_with_children.cm", | |
| .moniker = "./launchpad:component_with_children", | |
| }})); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11, 4, 4); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should see 4 different pids and tids | |
| EXPECT_EQ(reader.tids().size(), size_t{4}); | |
| EXPECT_EQ(reader.pids().size(), size_t{4}); | |
| } | |
| TEST(ProfilerIntegrationTest, ChildComponentsByMoniker) { | |
| // Create and launch a component to attach to | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| const std::string name = "component_with_children"; | |
| const std::string url = "component_with_children#meta/component_with_children.cm"; | |
| const std::string moniker = "./launchpad:" + name; | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker(moniker)); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11, 4, 4); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should see 4 different pids and tids | |
| EXPECT_EQ(reader.tids().size(), size_t{4}); | |
| EXPECT_EQ(reader.pids().size(), size_t{4}); | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| } | |
| TEST(ProfilerIntegrationTest, DelayedConnectByMoniker) { | |
| // Start profiling targeting a moniker and check that we attach if it's launched after profiling | |
| // started | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "demo_target"; | |
| const std::string url = "demo_target#meta/demo_target.cm"; | |
| const std::string moniker = "./launchpad:demo_target"; | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker(moniker)); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should see 1 pid and tid from the demo target | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| } | |
| TEST(ProfilerIntegrationTest, DelayedConnectByUrl) { | |
| // Start profiling targeting a moniker and check that we attach if it's launched after profiling | |
| // started | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "demo_target"; | |
| const std::string url = "demo_target#meta/demo_target.cm"; | |
| const std::string moniker = "./launchpad:demo_target"; | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentUrl(url)); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should see 1 pid and tid from the demo target | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| } | |
| TEST(ProfilerIntegrationTest, DelayedConnectByUrlWithChildren) { | |
| // Start profiling targeting a url and check that we attach to the component and its children if | |
| // it's launched after profiling started | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "component_with_children"; | |
| const std::string url = "component_with_children#meta/component_with_children.cm"; | |
| const std::string moniker = "./launchpad:" + name; | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentUrl(url)); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| reader.ReadUntilRecordCount(11, 4, 4); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| // We should see 4 different pids and tids: the parent and its 3 children | |
| EXPECT_EQ(reader.tids().size(), size_t{4}); | |
| EXPECT_EQ(reader.pids().size(), size_t{4}); | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| } | |
| // If a process exits from underneath us, we should still be able to return the samples we got | |
| TEST(ProfilerIntegrationTest, ExitedProcess) { | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| const fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "demo_target"; | |
| const std::string url = "demo_target#meta/demo_target.cm"; | |
| const std::string moniker = "./launchpad:demo_target"; | |
| // TODO(https://fxbug.dev/476409475): Fix the profiler to handle the case where | |
| // a process starts between configuring and starting without race conditions. | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker(moniker)); | |
| ASSERT_TRUE(client | |
| ->Configure({{.output = std::move(outgoing_socket), | |
| .config = fprofiler::Config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}}}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| // Destroy the target before the profiler stops | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| auto stop_response = client->Stop(); | |
| if (stop_response.is_error()) { | |
| FX_LOGS(INFO) << "Stop response: " << stop_response.error_value(); | |
| } | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| // We should see 1 pid and tid from the demo target. | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| // We should still see the mapping we eagerly pulled. | |
| EXPECT_EQ(stop_response->missing_process_mappings()->size(), size_t{0}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } | |
| // If a process exits from underneath us, we should still be able to return the samples we got | |
| // | |
| // Attaching while a a session is in progress goes down a different path, so test that too here. | |
| TEST(ProfilerIntegrationTest, ExitedProcessLateAttach) { | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| const fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "demo_target"; | |
| const std::string url = "demo_target#meta/demo_target.cm"; | |
| const std::string moniker = "./launchpad:demo_target"; | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker(moniker)); | |
| ASSERT_TRUE(client | |
| ->Configure({{.output = std::move(outgoing_socket), | |
| .config = fprofiler::Config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}}}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| // Destroy the target before the profiler stops | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| auto stop_response = client->Stop(); | |
| if (stop_response.is_error()) { | |
| FX_LOGS(INFO) << "Stop response: " << stop_response.error_value(); | |
| } | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| // We should see 1 pid and tid from the demo target. | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| // We should still see the mapping we eagerly pulled. | |
| EXPECT_EQ(stop_response->missing_process_mappings()->size(), size_t{0}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } | |
| TEST(ProfilerIntegrationTest, ExitedAfterConfigure) { | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| const fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "demo_target"; | |
| const std::string url = "demo_target#meta/demo_target.cm"; | |
| const std::string moniker = "./launchpad:demo_target"; | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker(moniker)); | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| ASSERT_TRUE(client | |
| ->Configure({{.output = std::move(outgoing_socket), | |
| .config = fprofiler::Config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}}}) | |
| .is_ok()); | |
| // Destroy the instance after configuring, but before starting. We should still be able to run the | |
| // profiler, though we may not get any samples. | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| ASSERT_TRUE(client->Stop().is_ok()); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } | |
| TEST(ProfilerIntegrationTest, StackSampling) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket; | |
| zx::socket outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::process self; | |
| zx::process::self()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self); | |
| std::thread child(MakeWork); | |
| const zx::unowned_thread child_handle{native_thread_get_zx_handle(child.native_handle())}; | |
| child.detach(); | |
| zx_status_t res = | |
| child_handle->wait_one(ZX_THREAD_RUNNING, zx::deadline_after(zx::sec(1)), nullptr); | |
| ASSERT_EQ(ZX_OK, res); | |
| zx_info_handle_basic_t info; | |
| res = child_handle->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr); | |
| ASSERT_EQ(ZX_OK, res); | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithThread(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = dwarf_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } | |
| // A subjob and process created between Configure and Start, as well as a process created in that | |
| // subjob after Start, must be profiled and have their module mappings collected. | |
| TEST(ProfilerIntegrationTest, ProcessAndSubjobStartedBetweenConfigureAndStart) { | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| zx::unowned_job self = zx::job::default_job(); | |
| zx_info_handle_basic_t info; | |
| ASSERT_EQ(ZX_OK, self->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr)); | |
| fprofiler::TargetConfig target_config = | |
| fprofiler::TargetConfig::WithTasks(std::vector{fprofiler::Task::WithJob(info.koid)}); | |
| fprofiler::Config config{{ | |
| .configs = default_sample_configs(), | |
| .target = std::move(target_config), | |
| }}; | |
| ASSERT_TRUE(client | |
| ->Configure({{ | |
| .output = std::move(outgoing_socket), | |
| .config = std::move(config), | |
| }}) | |
| .is_ok()); | |
| // Between Configure and Start, create a nested subjob hierarchy and launch a process inside it. | |
| zx::job child_job, grandchild_job; | |
| ASSERT_EQ(ZX_OK, zx::job::create(*self, 0, &child_job)); | |
| ASSERT_EQ(ZX_OK, zx::job::create(child_job, 0, &grandchild_job)); | |
| const char* kArgs[] = {"/pkg/bin/demo_target", nullptr}; | |
| zx::process process_between; | |
| ASSERT_EQ(ZX_OK, fdio_spawn(grandchild_job.get(), FDIO_SPAWN_CLONE_ALL, "/pkg/bin/demo_target", | |
| kArgs, process_between.reset_and_get_address())); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| std::thread t1{MakeWork}; | |
| t1.detach(); | |
| // After Start, launch another process in the same grandchild job to verify that | |
| // AddProcessesStartedSinceConfigure also installed a JobWatcher on the new subjobs. | |
| zx::process process_after; | |
| ASSERT_EQ(ZX_OK, fdio_spawn(grandchild_job.get(), FDIO_SPAWN_CLONE_ALL, "/pkg/bin/demo_target", | |
| kArgs, process_after.reset_and_get_address())); | |
| reader.ReadUntilRecordCount(11, 3, 1); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| EXPECT_EQ(stop_response->missing_process_mappings()->size(), size_t{0}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| zx_info_handle_basic_t pid_info; | |
| ASSERT_EQ(ZX_OK, zx::process::self()->get_info(ZX_INFO_HANDLE_BASIC, &pid_info, sizeof(pid_info), | |
| nullptr, nullptr)); | |
| zx_koid_t our_pid = pid_info.koid; | |
| ASSERT_EQ(ZX_OK, process_between.get_info(ZX_INFO_HANDLE_BASIC, &pid_info, sizeof(pid_info), | |
| nullptr, nullptr)); | |
| zx_koid_t between_pid = pid_info.koid; | |
| ASSERT_EQ(ZX_OK, process_after.get_info(ZX_INFO_HANDLE_BASIC, &pid_info, sizeof(pid_info), | |
| nullptr, nullptr)); | |
| zx_koid_t after_pid = pid_info.koid; | |
| EXPECT_TRUE(reader.pids().contains(our_pid)); | |
| EXPECT_TRUE(reader.pids().contains(between_pid)); | |
| EXPECT_TRUE(reader.pids().contains(after_pid)); | |
| process_between.kill(); | |
| process_after.kill(); | |
| } | |
| // StackSampler must retain module mappings for a profiled process that exits before Stop(). | |
| TEST(ProfilerIntegrationTest, StackSamplingExitedProcess) { | |
| auto lifecycle_client_end = component::Connect<fuchsia_sys2::LifecycleController>(); | |
| ASSERT_TRUE(lifecycle_client_end.is_ok()); | |
| const fidl::SyncClient lifecycle_client{std::move(*lifecycle_client_end)}; | |
| zx::result client_end = component::Connect<fprofiler::Session>(); | |
| ASSERT_TRUE(client_end.is_ok()); | |
| const fidl::SyncClient client{std::move(*client_end)}; | |
| zx::socket in_socket, outgoing_socket; | |
| ASSERT_EQ(zx::socket::create(0u, &in_socket, &outgoing_socket), ZX_OK); | |
| const std::string name = "demo_target"; | |
| const std::string url = "demo_target#meta/demo_target.cm"; | |
| const std::string moniker = "./launchpad:demo_target"; | |
| ASSERT_TRUE(RunInstance(lifecycle_client, name, url, moniker).is_ok()); | |
| fprofiler::TargetConfig target_config = fprofiler::TargetConfig::WithComponent( | |
| fprofiler::AttachConfig::WithAttachToComponentMoniker(moniker)); | |
| ASSERT_TRUE(client | |
| ->Configure({{.output = std::move(outgoing_socket), | |
| .config = fprofiler::Config{{ | |
| .configs = dwarf_sample_configs(), | |
| .target = std::move(target_config), | |
| }}}}) | |
| .is_ok()); | |
| SocketReader reader(std::move(in_socket)); | |
| ASSERT_TRUE(client->Start({{.buffer_results = true}}).is_ok()); | |
| reader.ReadUntilRecordCount(11, 1, 1); | |
| ASSERT_TRUE(TearDownInstance(lifecycle_client, name, moniker).is_ok()); | |
| auto stop_response = client->Stop(); | |
| ASSERT_TRUE(stop_response.is_ok()); | |
| ASSERT_TRUE(stop_response.value().samples_collected().has_value()); | |
| EXPECT_GT(stop_response.value().samples_collected().value(), size_t{10}); | |
| EXPECT_EQ(reader.tids().size(), size_t{1}); | |
| EXPECT_EQ(reader.pids().size(), size_t{1}); | |
| EXPECT_EQ(stop_response->missing_process_mappings()->size(), size_t{0}); | |
| ASSERT_TRUE(client->Reset().is_ok()); | |
| } |