blob: 409d62419ec1e06ad5d000a5a45ce03cdf6b6b13 [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.
#include "src/performance/cpu_profiler/targets.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 <algorithm>
#include <cstddef>
#include <set>
#include <span>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "zircon/types.h"
TEST(TargetsTest, OwnJob) {
profiler::TargetTree tree;
zx::job parent;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &parent));
elf_search::Searcher searcher;
zx::result<profiler::JobTarget> target = profiler::MakeJobTarget(std::move(parent), searcher);
ASSERT_TRUE(target.is_ok());
// We don't expect to have any sub jobs
EXPECT_TRUE(target->child_jobs.empty());
// And the only process should be this process
zx::unowned_process self = zx::process::self();
zx_info_handle_basic_t info;
ASSERT_EQ(ZX_OK, self->get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr));
EXPECT_EQ(target->processes.size(), size_t{1});
EXPECT_EQ(target->processes.begin()->second.pid, info.koid);
// There might be multiple threads running, but we should be one of them
zx::unowned_thread thread_self = zx::thread::self();
zx_info_handle_basic_t thread_info;
ASSERT_EQ(ZX_OK, thread_self->get_info(ZX_INFO_HANDLE_BASIC, &thread_info, sizeof(thread_info),
nullptr, nullptr));
ASSERT_GT(target->processes.begin()->second.threads.size(), size_t{0});
bool found_self = false;
for (const auto& [_, target] : target->processes.begin()->second.threads) {
if (target.tid == thread_info.koid) {
found_self = true;
}
}
EXPECT_TRUE(found_self);
}
// Ensure that if a sub job exits, we are able to get the remaining jobs that still exist
TEST(TargetsTest, JobsDone) {
elf_search::Searcher searcher;
std::atomic<bool> keep_going = true;
// In the background, create and destroy some siblings
std::thread sibling_maker{[&keep_going]() {
while (keep_going.load(std::memory_order::relaxed)) {
zx::job sibling;
zx_status_t result = zx::job::create(*zx::job::default_job(), 0, &sibling);
ASSERT_EQ(result, ZX_OK);
}
}};
// Run a bunch of times to give the race condition a chance to occur.
// Experimentally, the thread above is enough to trigger the race condition about 20% of the time.
profiler::TargetTree tree;
for (size_t i = 0; i < 100; i++) {
zx::job parent;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &parent));
zx::result<profiler::JobTarget> target = profiler::MakeJobTarget(std::move(parent), searcher);
// This should succeed regardless of if a job disappeared from under us or not. In the log for
// the test, we'll likely see some warnings about jobs not being found.
ASSERT_TRUE(target.is_ok());
// We should have 1 or zero sub jobs.
size_t num_subjobs = target->child_jobs.size();
EXPECT_TRUE(num_subjobs == 0 || num_subjobs == 1);
}
keep_going.store(false, std::memory_order_relaxed);
sibling_maker.join();
}
TEST(TargetsTest, TargetTreeAddProcessTopLevel) {
profiler::TargetTree tree;
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, 1, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
EXPECT_TRUE(tree.AddProcess(std::move(p1)).is_ok());
profiler::ProcessTarget p2{zx::process{ZX_HANDLE_INVALID}, 1, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
zx::result res = tree.AddProcess(std::move(p2));
ASSERT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_ALREADY_EXISTS);
}
TEST(TargetsTest, TargetTreeAddJobTopLevel) {
profiler::TargetTree tree;
profiler::JobTarget j1{zx::job{ZX_HANDLE_INVALID}, 2, std::span<const zx_koid_t>{}};
EXPECT_TRUE(tree.AddJob(std::move(j1)).is_ok());
profiler::JobTarget j2{zx::job{ZX_HANDLE_INVALID}, 2, std::span<const zx_koid_t>{}};
zx::result res = tree.AddJob(std::move(j2));
ASSERT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_ALREADY_EXISTS);
}
TEST(TargetsTest, TargetTreeAddThreadTopLevel) {
profiler::TargetTree tree;
profiler::ThreadTarget t1{.handle = zx::thread{ZX_HANDLE_INVALID}, .tid = 3, .name = ""};
zx::result res = tree.AddThread(4, std::move(t1));
ASSERT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_NOT_FOUND);
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, 4, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
EXPECT_TRUE(tree.AddProcess(std::move(p1)).is_ok());
profiler::ThreadTarget t2{.handle = zx::thread{ZX_HANDLE_INVALID}, .tid = 3, .name = ""};
res = tree.AddThread(4, std::move(t2));
ASSERT_TRUE(res.is_ok());
}
TEST(TargetsTest, TargetTreeAddTargetsNestedNonExist) {
profiler::TargetTree tree;
profiler::ThreadTarget t1{.handle = zx::thread{ZX_HANDLE_INVALID}, .tid = 1, .name = ""};
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, 2, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::JobTarget j1{zx::job{ZX_HANDLE_INVALID}, 3, std::vector<zx_koid_t>{}};
profiler::JobTarget j2{zx::job{ZX_HANDLE_INVALID}, 4, std::vector<zx_koid_t>{3}};
profiler::JobTarget j3{zx::job{ZX_HANDLE_INVALID}, 5, std::vector<zx_koid_t>{3, 4}};
zx::result res = tree.AddThread(std::vector<zx_koid_t>{3, 4, 5}, 2, std::move(t1));
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_NOT_FOUND);
res = tree.AddProcess(std::vector<zx_koid_t>{3, 4, 5}, std::move(p1));
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_NOT_FOUND);
res = tree.AddJob(std::vector<zx_koid_t>{3, 4}, std::move(j3));
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_NOT_FOUND);
res = tree.AddJob(std::vector<zx_koid_t>{3}, std::move(j2));
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_NOT_FOUND);
}
TEST(TargetsTest, TargetTreeAddTargetsNested) {
profiler::TargetTree tree;
profiler::ThreadTarget t1{.handle = zx::thread{ZX_HANDLE_INVALID}, .tid = 1, .name = ""};
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, 2, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::JobTarget j1{zx::job{ZX_HANDLE_INVALID}, 3, std::vector<zx_koid_t>{}};
profiler::JobTarget j2{zx::job{ZX_HANDLE_INVALID}, 4, std::vector<zx_koid_t>{3}};
profiler::JobTarget j3{zx::job{ZX_HANDLE_INVALID}, 5, std::vector<zx_koid_t>{3, 4}};
zx::result res = tree.AddJob(std::vector<zx_koid_t>{}, std::move(j1));
EXPECT_TRUE(res.is_ok());
res = tree.AddJob(std::vector<zx_koid_t>{3}, std::move(j2));
EXPECT_TRUE(res.is_ok());
res = tree.AddJob(std::vector<zx_koid_t>{3, 4}, std::move(j3));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{3, 4, 5}, std::move(p1));
EXPECT_TRUE(res.is_ok());
res = tree.AddThread(std::vector<zx_koid_t>{3, 4, 5}, 2, std::move(t1));
EXPECT_TRUE(res.is_ok());
}
TEST(TargetsTest, TargetTreeForEachProcess) {
profiler::TargetTree tree;
constexpr zx_koid_t top_level_pid1 = 4;
constexpr zx_koid_t top_level_pid2 = 5;
constexpr zx_koid_t nested_pid1 = 6;
constexpr zx_koid_t nested_pid2 = 7;
constexpr zx_koid_t nested_pid3 = 8;
constexpr zx_koid_t nested_pid4 = 9;
constexpr zx_koid_t nested_pid5 = 10;
constexpr zx_koid_t nested_pid6 = 11;
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, top_level_pid1, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p2{zx::process{ZX_HANDLE_INVALID}, top_level_pid2, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p3{zx::process{ZX_HANDLE_INVALID}, nested_pid1, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p4{zx::process{ZX_HANDLE_INVALID}, nested_pid2, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p5{zx::process{ZX_HANDLE_INVALID}, nested_pid3, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p6{zx::process{ZX_HANDLE_INVALID}, nested_pid4, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p7{zx::process{ZX_HANDLE_INVALID}, nested_pid5, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::ProcessTarget p8{zx::process{ZX_HANDLE_INVALID}, nested_pid6, "",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
profiler::JobTarget j1{zx::job{ZX_HANDLE_INVALID}, 1, std::vector<zx_koid_t>{}};
profiler::JobTarget j2{zx::job{ZX_HANDLE_INVALID}, 2, std::vector<zx_koid_t>{1}};
profiler::JobTarget j3{zx::job{ZX_HANDLE_INVALID}, 3, std::vector<zx_koid_t>{1, 2}};
zx::result res = tree.AddJob(std::move(j1));
EXPECT_TRUE(res.is_ok());
res = tree.AddJob(std::vector<zx_koid_t>{1}, std::move(j2));
EXPECT_TRUE(res.is_ok());
res = tree.AddJob(std::vector<zx_koid_t>{1, 2}, std::move(j3));
EXPECT_TRUE(res.is_ok());
// Add a bunch of processes in the tree at various levels
res = tree.AddProcess(std::move(p1));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{}, std::move(p2));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{1}, std::move(p3));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{1}, std::move(p4));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{1, 2}, std::move(p5));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{1, 2}, std::move(p6));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{1, 2, 3}, std::move(p7));
EXPECT_TRUE(res.is_ok());
res = tree.AddProcess(std::vector<zx_koid_t>{1, 2, 3}, std::move(p8));
EXPECT_TRUE(res.is_ok());
std::set<zx_koid_t> seen;
res = tree.ForEachProcess([&seen](std::span<const zx_koid_t> job_path,
const profiler::ProcessTarget& process) -> zx::result<> {
EXPECT_EQ(seen.find(process.pid), seen.end());
seen.insert(process.pid);
switch (process.pid) {
case top_level_pid1:
case top_level_pid2: {
std::vector<zx_koid_t> expected{};
EXPECT_TRUE(std::equal(job_path.begin(), job_path.end(), expected.begin()));
break;
}
case nested_pid1:
case nested_pid2: {
std::vector<zx_koid_t> expected{1};
EXPECT_TRUE(std::equal(job_path.begin(), job_path.end(), expected.begin()));
break;
}
case nested_pid3:
case nested_pid4: {
std::vector<zx_koid_t> expected{1, 2};
EXPECT_TRUE(std::equal(job_path.begin(), job_path.end(), expected.begin()));
break;
}
case nested_pid5:
case nested_pid6: {
std::vector<zx_koid_t> expected{1, 2, 3};
EXPECT_TRUE(std::equal(job_path.begin(), job_path.end(), expected.begin()));
break;
}
default:
return zx::error(ZX_ERR_OUT_OF_RANGE);
}
return zx::ok();
});
EXPECT_EQ(seen.size(), size_t{8});
EXPECT_TRUE(res.is_ok());
// Check that a top level error gets propagated
res = tree.ForEachProcess([](std::span<const zx_koid_t> job_path,
const profiler::ProcessTarget& process) -> zx::result<> {
return zx::make_result(process.pid == top_level_pid2 ? ZX_ERR_BAD_STATE : ZX_OK);
});
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_BAD_STATE);
// Check that a nested error gets propagated
res = tree.ForEachProcess([](std::span<const zx_koid_t> job_path,
const profiler::ProcessTarget& process) -> zx::result<> {
return zx::make_result(process.pid == nested_pid6 ? ZX_ERR_BAD_STATE : ZX_OK);
});
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_BAD_STATE);
}
TEST(TargetsTest, TargetTreeForEachJob) {
profiler::TargetTree tree;
profiler::JobTarget j1{zx::job{ZX_HANDLE_INVALID}, 1, std::vector<zx_koid_t>{}};
profiler::JobTarget j2{zx::job{ZX_HANDLE_INVALID}, 2, std::vector<zx_koid_t>{1}};
profiler::JobTarget j3{zx::job{ZX_HANDLE_INVALID}, 3, std::vector<zx_koid_t>{1, 2}};
zx::result res = tree.AddJob(std::move(j1));
EXPECT_TRUE(res.is_ok());
res = tree.AddJob(std::vector<zx_koid_t>{1}, std::move(j2));
EXPECT_TRUE(res.is_ok());
res = tree.AddJob(std::vector<zx_koid_t>{1, 2}, std::move(j3));
EXPECT_TRUE(res.is_ok());
std::set<zx_koid_t> seen;
res = tree.ForEachJob([&seen](const profiler::JobTarget& job) -> zx::result<> {
EXPECT_EQ(seen.find(job.job_id), seen.end());
seen.insert(job.job_id);
return zx::ok();
});
EXPECT_EQ(seen.size(), size_t{3});
EXPECT_EQ(size_t{1}, seen.count(1));
EXPECT_EQ(size_t{1}, seen.count(2));
EXPECT_EQ(size_t{1}, seen.count(3));
EXPECT_TRUE(res.is_ok());
res = tree.ForEachJob([](const profiler::JobTarget& job) -> zx::result<> {
return zx::make_result(job.job_id == 1 ? ZX_ERR_BAD_STATE : ZX_OK);
});
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_BAD_STATE);
res = tree.ForEachJob([](const profiler::JobTarget& job) -> zx::result<> {
return zx::make_result(job.job_id == 3 ? ZX_ERR_BAD_STATE : ZX_OK);
});
EXPECT_TRUE(res.is_error());
EXPECT_EQ(res.status_value(), ZX_ERR_BAD_STATE);
}
TEST(TargetsTest, TargetTreeGetProcessTopLevel) {
profiler::TargetTree tree;
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, 42, "test_process",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
EXPECT_TRUE(tree.AddProcess(std::move(p1)).is_ok());
zx::result<profiler::ProcessTarget*> found = tree.GetProcess(std::span<const zx_koid_t>{}, 42);
ASSERT_TRUE(found.is_ok());
ASSERT_NE(*found, nullptr);
EXPECT_EQ((*found)->pid, zx_koid_t{42});
EXPECT_EQ((*found)->name, "test_process");
zx::result<profiler::ProcessTarget*> not_found =
tree.GetProcess(std::span<const zx_koid_t>{}, 99);
EXPECT_TRUE(not_found.is_error());
EXPECT_EQ(not_found.status_value(), ZX_ERR_NOT_FOUND);
}
TEST(TargetsTest, TargetTreeGetProcessNested) {
profiler::TargetTree tree;
profiler::JobTarget j1{zx::job{ZX_HANDLE_INVALID}, 1, std::vector<zx_koid_t>{}};
profiler::JobTarget j2{zx::job{ZX_HANDLE_INVALID}, 2, std::vector<zx_koid_t>{1}};
profiler::JobTarget j3{zx::job{ZX_HANDLE_INVALID}, 3, std::vector<zx_koid_t>{1, 2}};
ASSERT_TRUE(tree.AddJob(std::move(j1)).is_ok());
ASSERT_TRUE(tree.AddJob(std::vector<zx_koid_t>{1}, std::move(j2)).is_ok());
ASSERT_TRUE(tree.AddJob(std::vector<zx_koid_t>{1, 2}, std::move(j3)).is_ok());
profiler::ProcessTarget p1{zx::process{ZX_HANDLE_INVALID}, 100, "nested_process",
std::unordered_map<zx_koid_t, profiler::ThreadTarget>{}};
ASSERT_TRUE(tree.AddProcess(std::vector<zx_koid_t>{1, 2, 3}, std::move(p1)).is_ok());
const std::vector<zx_koid_t> path{1, 2, 3};
zx::result<profiler::ProcessTarget*> found = tree.GetProcess(path, 100);
ASSERT_TRUE(found.is_ok());
ASSERT_NE(*found, nullptr);
EXPECT_EQ((*found)->pid, zx_koid_t{100});
EXPECT_EQ((*found)->name, "nested_process");
zx::result<profiler::ProcessTarget*> not_found_pid = tree.GetProcess(path, 101);
EXPECT_TRUE(not_found_pid.is_error());
EXPECT_EQ(not_found_pid.status_value(), ZX_ERR_NOT_FOUND);
const std::vector<zx_koid_t> invalid_path{1, 2, 99};
zx::result<profiler::ProcessTarget*> not_found_path = tree.GetProcess(invalid_path, 100);
EXPECT_TRUE(not_found_path.is_error());
EXPECT_EQ(not_found_path.status_value(), ZX_ERR_NOT_FOUND);
}