blob: 1a64e651e0530eb2ec5d4f55b1c2919c5fc8d5c0 [file] [log] [blame]
// Copyright 2019 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/sys/appmgr/cpu_watcher.h"
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/executor.h>
#include <lib/inspect/cpp/inspect.h>
#include <lib/inspect/cpp/reader.h>
#include <gtest/gtest.h>
namespace component {
namespace {
inspect::Hierarchy GetHierarchy(const inspect::Inspector& inspector) {
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
async::Executor executor(loop.dispatcher());
fit::result<inspect::Hierarchy> hierarchy;
executor.schedule_task(inspect::ReadFromInspector(inspector).then(
[&](fit::result<inspect::Hierarchy>& res) { hierarchy = std::move(res); }));
while (!hierarchy) {
loop.Run(zx::deadline_after(zx::sec(1)), true);
}
return hierarchy.take_value();
}
TEST(CpuWatcher, EmptyTasks) {
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), zx::job());
watcher.Measure();
// Ensure that we do not record any measurements for an invalid job handle.
auto hierarchy = GetHierarchy(inspector);
const auto* node = hierarchy.GetByPath({"test", "measurements", "root"});
ASSERT_NE(nullptr, node);
EXPECT_TRUE(node->children().empty());
EXPECT_TRUE(node->node().properties().empty());
}
int64_t PropertyIntValueOr(const inspect::Hierarchy* hierarchy, const std::string& name,
int64_t default_value) {
const auto* prop = hierarchy->node().get_property<inspect::IntPropertyValue>(name);
if (!prop) {
return default_value;
}
return prop->value();
}
uint64_t PropertyUintValueOr(const inspect::Hierarchy* hierarchy, const std::string& name,
uint64_t default_value) {
const auto* prop = hierarchy->node().get_property<inspect::UintPropertyValue>(name);
if (!prop) {
return default_value;
}
return prop->value();
}
TEST(CpuWatcher, BadTask) {
zx::job self;
zx_info_handle_basic_t basic;
ASSERT_EQ(ZX_OK, zx::job::default_job()->get_info(ZX_INFO_HANDLE_BASIC, &basic, sizeof(basic),
nullptr, nullptr));
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(basic.rights & ~ZX_RIGHT_INSPECT, &self));
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), zx::job());
watcher.AddTask({"test_invalid"}, std::move(self));
watcher.Measure();
// Ensure that we do not record any measurements for a task that cannot be read.
auto hierarchy = GetHierarchy(inspector);
const auto* node = hierarchy.GetByPath({"test", "measurements", "root", "test_invalid"});
ASSERT_NE(nullptr, node);
EXPECT_TRUE(node->children().empty());
EXPECT_TRUE(node->node().properties().empty());
}
// Returns the number of valid samples under the given hierarchy, or -1 if any sample is invalid.
int64_t GetValidSampleCount(const inspect::Hierarchy* hierarchy) {
if (!hierarchy) {
printf("hierarchy is null!\n");
return -1;
}
hierarchy = hierarchy->GetByPath({"@samples"});
if (!hierarchy) {
return 0;
}
size_t ret = 0;
for (const auto& child : hierarchy->children()) {
if (!std::all_of(child.name().begin(), child.name().end(),
[](char c) { return std::isdigit(c); })) {
printf("name '%s' is not entirely numeric!\n", child.name().c_str());
return -1;
}
auto check_int_nonzero = [&](const std::string& name) -> bool {
const auto* prop = child.node().get_property<inspect::IntPropertyValue>(name);
if (!prop) {
printf("missing %s\n", name.c_str());
return false;
}
if (prop->value() == 0) {
printf("property %s is 0\n", name.c_str());
return false;
}
return true;
};
if (!check_int_nonzero("timestamp") || !check_int_nonzero("cpu_time") ||
!check_int_nonzero("queue_time")) {
printf("invalid entry found at %s\n", child.name().c_str());
return -1;
}
ret++;
}
return ret;
}
TEST(CpuWatcher, SampleSingle) {
zx::job self;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), zx::job(), 3 /* max_samples */);
watcher.AddTask({"test_valid"}, std::move(self));
// Ensure that we record measurements up to the limit.
auto hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
1, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
watcher.Measure();
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
2, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
watcher.Measure();
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
3, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
// One measurement rolled out.
watcher.Measure();
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
3, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
// Remove the task, the value is still there for now.
watcher.RemoveTask({"test_valid"});
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
3, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
// Measurements roll out now.
watcher.Measure();
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
2, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
watcher.Measure();
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(
1, GetValidSampleCount(hierarchy.GetByPath({"test", "measurements", "root", "test_valid"})));
// After the last measurement rolls out, the node is deleted.
watcher.Measure();
hierarchy = GetHierarchy(inspector);
EXPECT_EQ(nullptr, hierarchy.GetByPath({"test", "measurements", "root", "test_valid"}));
}
TEST(CpuWatcher, SampleMultiple) {
zx::job self;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), zx::job(), 3 /* max_samples */);
watcher.AddTask({"test_valid"}, std::move(self));
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
watcher.AddTask({"test_valid", "nested"}, std::move(self));
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
watcher.RemoveTask({"test_valid"});
watcher.Measure();
watcher.AddTask({"separate", "nested"}, std::move(self));
watcher.Measure();
watcher.Measure();
// Ensure total CPU rotates
watcher.Measure();
// Expected hierarchy:
// root:
// test_valid: 0 samples
// nested: 3 samples
// separate: 0 samples
// nested: 3 samples
auto hierarchy = GetHierarchy(inspector);
inspect::Hierarchy *test_valid = nullptr, *test_valid_nested = nullptr,
*separate_nested = nullptr, *separate = nullptr;
hierarchy.Visit([&](const std::vector<std::string>& path, inspect::Hierarchy* hierarchy) {
if (path == std::vector<std::string>({"root", "test", "measurements", "root", "test_valid"})) {
test_valid = hierarchy;
} else if (path == std::vector<std::string>(
{"root", "test", "measurements", "root", "test_valid", "nested"})) {
test_valid_nested = hierarchy;
} else if (path ==
std::vector<std::string>({"root", "test", "measurements", "root", "separate"})) {
separate = hierarchy;
} else if (path == std::vector<std::string>(
{"root", "test", "measurements", "root", "separate", "nested"})) {
separate_nested = hierarchy;
}
return true;
});
EXPECT_EQ(0, GetValidSampleCount(test_valid));
EXPECT_EQ(3, GetValidSampleCount(test_valid_nested));
EXPECT_EQ(0, GetValidSampleCount(separate));
EXPECT_EQ(3, GetValidSampleCount(separate_nested));
// Check that total CPU contains the right number of measurements.
auto* total = hierarchy.GetByPath({"test", "@total"});
ASSERT_NE(nullptr, total);
EXPECT_EQ(3u, total->children().size());
}
TEST(CpuWatcher, RecentCpu) {
zx::job self;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), std::move(self));
auto hierarchy = GetHierarchy(inspector);
const auto* node = hierarchy.GetByPath({"test", "recent_usage"});
ASSERT_NE(nullptr, node);
EXPECT_EQ(0u, PropertyIntValueOr(node, "recent_timestamp", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "recent_cpu_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "recent_queue_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_timestamp", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_cpu_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_queue_time", -1));
watcher.Measure();
hierarchy = GetHierarchy(inspector);
node = hierarchy.GetByPath({"test", "recent_usage"});
ASSERT_NE(nullptr, node);
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_timestamp", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_cpu_time", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_queue_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_timestamp", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_cpu_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_queue_time", -1));
watcher.Measure();
hierarchy = GetHierarchy(inspector);
node = hierarchy.GetByPath({"test", "recent_usage"});
ASSERT_NE(nullptr, node);
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_timestamp", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_cpu_time", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_queue_time", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "previous_timestamp", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "previous_cpu_time", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "previous_queue_time", -1));
}
TEST(CpuWatcher, TotalCpuIncludesEndedJobs) {
zx::job self;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), zx::job());
watcher.Measure();
// This sample calculates 0 as the queue and CPU totals since there are no jobs.
auto hierarchy = GetHierarchy(inspector);
const auto* node = hierarchy.GetByPath({"test", "recent_usage"});
ASSERT_NE(nullptr, node);
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_timestamp", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "recent_cpu_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "recent_queue_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_timestamp", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_cpu_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_queue_time", -1));
watcher.AddTask({"testing"}, std::move(self));
watcher.RemoveTask({"testing"});
watcher.Measure();
// This sample collects the runtime from the exited job.
hierarchy = GetHierarchy(inspector);
node = hierarchy.GetByPath({"test", "recent_usage"});
ASSERT_NE(nullptr, node);
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_timestamp", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_cpu_time", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "recent_queue_time", -1));
EXPECT_LT(0u, PropertyIntValueOr(node, "previous_timestamp", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_cpu_time", -1));
EXPECT_EQ(0u, PropertyIntValueOr(node, "previous_queue_time", -1));
}
// This test generates enough measurements to fill the output VMO.
// Note that it will need to be updated if the output size is increased of
// if future optimizations make Inspect space usage more efficient.
TEST(CpuWatcher, StressSize) {
inspect::Inspector inspector;
CpuWatcher watcher(inspector.GetRoot().CreateChild("test"), zx::job());
// Make 1k tasks.
for (size_t i = 0; i < 1000; i++) {
zx::job self;
ASSERT_EQ(ZX_OK, zx::job::default_job()->duplicate(ZX_RIGHT_SAME_RIGHTS, &self));
watcher.AddTask({"test_entries", std::to_string(i)}, std::move(self));
}
// Sample 60 times
for (size_t i = 0; i < 60; i++) {
watcher.Measure();
}
// Get the hierarchy and confirm it is out of measurement space.
auto hierarchy = GetHierarchy(inspector);
const auto* node = hierarchy.GetByPath({"test", "measurements", "@inspect"});
ASSERT_NE(nullptr, node);
EXPECT_NE(0u, PropertyUintValueOr(node, "maximum_size", 0));
// Give a 100 byte margin of error on filling up the buffer.
EXPECT_GT(PropertyUintValueOr(node, "current_size", 0),
PropertyUintValueOr(node, "maximum_size", 0) - 100);
}
} // namespace
} // namespace component