blob: 55a44e31262abf22be87220f7d29d7d380b10281 [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include "lib/power-management/energy-model.h"
#include <zircon/errors.h>
// TODO(https://fxbug.dev/415033686): Stop using `syscalls-next.h` on host.
#define FUCHSIA_UNSUPPORTED_ALLOW_SYSCALLS_NEXT_ON_HOST
#include <zircon/syscalls-next.h>
#undef FUCHSIA_UNSUPPORTED_ALLOW_SYSCALLS_NEXT_ON_HOST
#include <lib/stdcompat/utility.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <fbl/ref_ptr.h>
#include <gtest/gtest.h>
#include "test-helper.h"
namespace {
using ffl::FromRatio;
using power_management::ControlInterface;
using power_management::EnergyModel;
using power_management::PowerDomain;
using power_management::PowerDomainRegistry;
using power_management::PowerDomainSet;
using power_management::PowerLevel;
using power_management::PowerLevelTransition;
using power_management::ProcessingRate;
using power_management::Utilization;
// Utility to test whether the given ranged object contains the given element.
template <typename Ranged, typename Element>
bool InRange(const Ranged& ranged, const Element& element) {
return std::ranges::find(ranged, element) != std::cend(ranged);
}
ProcessingRate ToProcessingRate(uint64_t unscaled) { return FromRatio<uint64_t>(unscaled, 1000); }
TEST(PowerLevelTest, Ctor) {
constexpr zx_processor_power_level_t kLevel = {
.options = 0,
.processing_rate = 456,
.power_coefficient_nw = 789,
.control_interface = cpp23::to_underlying(ControlInterface::kArmPsci),
.control_argument = 12345,
.diagnostic_name = "foobar one two three",
};
PowerLevel level(0, kLevel);
EXPECT_EQ(level.level(), 0);
EXPECT_EQ(level.processing_rate(), ToProcessingRate(kLevel.processing_rate));
EXPECT_EQ(level.power_coefficient_nw(), kLevel.power_coefficient_nw);
EXPECT_EQ(level.control(), static_cast<ControlInterface>(kLevel.control_interface));
EXPECT_EQ(level.control_argument(), kLevel.control_argument);
EXPECT_EQ(level.type(), PowerLevel::Type::kActive);
EXPECT_EQ(level.name(), std::string_view(kLevel.diagnostic_name));
EXPECT_TRUE(level.TargetsPowerDomain());
EXPECT_FALSE(level.TargetsCpus());
}
TEST(PowerLevelTest, Ctor2) {
constexpr zx_processor_power_level_t kLevel = {
.options = ZX_PROCESSOR_POWER_LEVEL_OPTIONS_DOMAIN_INDEPENDENT,
.processing_rate = 123,
.power_coefficient_nw = 789,
.control_interface = cpp23::to_underlying(ControlInterface::kArmPsci),
.control_argument = 12345,
.diagnostic_name = "foobar one two three",
};
PowerLevel level(123, kLevel);
EXPECT_EQ(level.level(), 123);
EXPECT_EQ(level.processing_rate(), ToProcessingRate(kLevel.processing_rate));
EXPECT_EQ(level.power_coefficient_nw(), kLevel.power_coefficient_nw);
EXPECT_EQ(level.control(), static_cast<ControlInterface>(kLevel.control_interface));
EXPECT_EQ(level.control_argument(), kLevel.control_argument);
EXPECT_EQ(level.type(), PowerLevel::Type::kActive);
EXPECT_EQ(level.name(), std::string_view(kLevel.diagnostic_name));
EXPECT_FALSE(level.TargetsPowerDomain());
EXPECT_TRUE(level.TargetsCpus());
}
TEST(PowerLevelTransitionTest, Ctor) {
static constexpr zx_processor_power_level_transition_t kTransition = {
.latency = 456,
.energy_nj = 1234,
.from = 0,
.to = 1,
};
PowerLevelTransition transition(kTransition);
EXPECT_EQ(transition.latency(), zx_duration_from_nsec(kTransition.latency));
EXPECT_EQ(transition.energy_cost_nj(), kTransition.energy_nj);
}
TEST(PowerModelTest, Create) {
static constexpr auto kPowerLevels = std::to_array<zx_processor_power_level_t>({
{
.options = 0,
.processing_rate = 0,
.power_coefficient_nw = 1,
.control_interface = cpp23::to_underlying(ControlInterface::kArmPsci),
.control_argument = 1,
.diagnostic_name = "0",
},
{
.options = 0,
.processing_rate = 4,
.power_coefficient_nw = 8,
.control_interface = cpp23::to_underlying(ControlInterface::kCpuDriver),
.control_argument = 3,
.diagnostic_name = "1",
},
{
.options = 0,
.processing_rate = 0,
.power_coefficient_nw = 2,
.control_interface = cpp23::to_underlying(ControlInterface::kArmWfi),
.control_argument = 0,
.diagnostic_name = "2",
},
{
.options = 0,
.processing_rate = 4,
.power_coefficient_nw = 10,
.control_interface = cpp23::to_underlying(ControlInterface::kCpuDriver),
.control_argument = 1,
.diagnostic_name = "3",
},
});
static constexpr auto kTransitions = std::to_array<zx_processor_power_level_transition_t>({
{
.latency = 1,
.energy_nj = 2,
.from = 1,
.to = 0,
},
{
.latency = 2,
.energy_nj = 3,
.from = 2,
.to = 0,
},
{
.latency = 3,
.energy_nj = 4,
.from = 3,
.to = 0,
},
{
.latency = 1,
.energy_nj = 2,
.from = 0,
.to = 1,
},
{
.latency = 3,
.energy_nj = 4,
.from = 2,
.to = 1,
},
{
.latency = 4,
.energy_nj = 5,
.from = 3,
.to = 1,
},
{
.latency = 2,
.energy_nj = 3,
.from = 0,
.to = 2,
},
{
.latency = 3,
.energy_nj = 4,
.from = 1,
.to = 2,
},
{
.latency = 5,
.energy_nj = 6,
.from = 3,
.to = 2,
},
{
.latency = 3,
.energy_nj = 4,
.from = 0,
.to = 3,
},
{
.latency = 4,
.energy_nj = 5,
.from = 1,
.to = 3,
},
{
.latency = 5,
.energy_nj = 6,
.from = 2,
.to = 3,
},
});
auto energy_model = EnergyModel::Create(kPowerLevels, kTransitions);
ASSERT_TRUE(energy_model.is_ok());
// Proper transformation of the model and the transition table.
auto check_level = [](const PowerLevel& actual, const PowerLevel& expected) {
EXPECT_EQ(actual.level(), expected.level());
EXPECT_EQ(actual.control(), expected.control());
EXPECT_EQ(actual.control_argument(), expected.control_argument());
EXPECT_EQ(actual.name(), expected.name());
EXPECT_EQ(actual.power_coefficient_nw(), expected.power_coefficient_nw());
EXPECT_EQ(actual.processing_rate(), expected.processing_rate());
EXPECT_EQ(actual.type(), expected.type());
EXPECT_EQ(actual.TargetsCpus(), expected.TargetsCpus());
EXPECT_EQ(actual.TargetsPowerDomain(), expected.TargetsPowerDomain());
};
ASSERT_EQ(energy_model->levels().size(), 4u);
auto levels = energy_model->levels();
for (size_t i = 0; i < levels.size() - 1; ++i) {
size_t j = i + 1;
EXPECT_LE(levels[i].processing_rate(), levels[i].processing_rate());
if (levels[i].processing_rate() == levels[j].processing_rate()) {
EXPECT_LE(levels[i].power_coefficient_nw(), levels[j].power_coefficient_nw());
}
check_level(levels[i], PowerLevel(levels[i].level(), kPowerLevels[levels[i].level()]));
check_level(levels[j], PowerLevel(levels[j].level(), kPowerLevels[levels[j].level()]));
}
auto get_original_transition = [&levels](size_t i, size_t j) {
size_t og_i = levels[i].level();
size_t og_j = levels[j].level();
for (const auto& transition : kTransitions) {
if (transition.from == og_i && transition.to == og_j) {
return PowerLevelTransition(transition);
}
}
return PowerLevelTransition::Invalid();
};
auto transitions = energy_model->transitions();
for (size_t i = 0; i < levels.size(); ++i) {
for (size_t j = 0; j < levels.size(); ++j) {
auto transition = transitions[i][j];
auto og_transition = get_original_transition(i, j);
EXPECT_EQ(transition.latency(), og_transition.latency());
EXPECT_EQ(transition.energy_cost_nj(), og_transition.energy_cost_nj());
}
}
// Properly partitioned.
ASSERT_EQ(energy_model->idle_levels().size(), 2u);
EXPECT_EQ(energy_model->idle_levels()[0].level(), 0u);
EXPECT_EQ(energy_model->idle_levels()[1].level(), 2u);
ASSERT_EQ(energy_model->active_levels().size(), 2u);
EXPECT_EQ(energy_model->active_levels()[0].level(), 1u);
EXPECT_EQ(energy_model->active_levels()[1].level(), 3u);
// Sorter by tuple <Control Interface, Control Argument>
for (size_t i = 0; i < levels.size(); ++i) {
auto& level = levels[i];
EXPECT_EQ(energy_model->FindPowerLevel(level.control(), level.control_argument()), i);
}
EXPECT_FALSE(energy_model->FindPowerLevel(ControlInterface::kArmPsci, 495));
EXPECT_FALSE(energy_model->FindPowerLevel(static_cast<ControlInterface>(495), 0));
}
TEST(PowerModelTest, CreateWithEmptyTransitionsIsOk) {
static constexpr auto kPowerLevels = std::to_array<zx_processor_power_level_t>({
{
.options = 0,
.processing_rate = 0,
.power_coefficient_nw = 1,
.control_interface = cpp23::to_underlying(ControlInterface::kArmPsci),
.control_argument = 1,
.diagnostic_name = "0",
},
{
.options = 0,
.processing_rate = 4,
.power_coefficient_nw = 8,
.control_interface = cpp23::to_underlying(ControlInterface::kCpuDriver),
.control_argument = 3,
.diagnostic_name = "1",
},
{
.options = 0,
.processing_rate = 0,
.power_coefficient_nw = 2,
.control_interface = cpp23::to_underlying(ControlInterface::kArmWfi),
.control_argument = 0,
.diagnostic_name = "2",
},
{
.options = 0,
.processing_rate = 4,
.power_coefficient_nw = 10,
.control_interface = cpp23::to_underlying(ControlInterface::kCpuDriver),
.control_argument = 1,
.diagnostic_name = "3",
},
});
auto energy_model = EnergyModel::Create(kPowerLevels, {});
ASSERT_TRUE(energy_model.is_ok());
// Proper transformation of the model and the transition table.
auto check_level = [](const PowerLevel& actual, const PowerLevel& expected) {
EXPECT_EQ(actual.level(), expected.level());
EXPECT_EQ(actual.control(), expected.control());
EXPECT_EQ(actual.control_argument(), expected.control_argument());
EXPECT_EQ(actual.name(), expected.name());
EXPECT_EQ(actual.power_coefficient_nw(), expected.power_coefficient_nw());
EXPECT_EQ(actual.processing_rate(), expected.processing_rate());
EXPECT_EQ(actual.type(), expected.type());
EXPECT_EQ(actual.TargetsCpus(), expected.TargetsCpus());
EXPECT_EQ(actual.TargetsPowerDomain(), expected.TargetsPowerDomain());
};
ASSERT_EQ(energy_model->levels().size(), 4u);
auto levels = energy_model->levels();
for (size_t i = 0; i < levels.size() - 1; ++i) {
size_t j = i + 1;
EXPECT_LE(levels[i].processing_rate(), levels[i].processing_rate());
if (levels[i].processing_rate() == levels[j].processing_rate()) {
EXPECT_LE(levels[i].power_coefficient_nw(), levels[j].power_coefficient_nw());
}
check_level(levels[i], PowerLevel(levels[i].level(), kPowerLevels[levels[i].level()]));
check_level(levels[j], PowerLevel(levels[j].level(), kPowerLevels[levels[j].level()]));
}
for (size_t row = 0; row < energy_model->levels().size(); ++row) {
for (size_t column = 0; column < energy_model->levels().size(); ++column) {
const PowerLevelTransition& transition = energy_model->transitions()[row][column];
EXPECT_EQ(transition.energy_cost_nj(), PowerLevelTransition::Zero().energy_cost_nj());
EXPECT_EQ(transition.latency(), PowerLevelTransition::Zero().latency());
}
}
ASSERT_EQ(energy_model->idle_levels().size(), 2u);
EXPECT_EQ(energy_model->idle_levels()[0].level(), 0u);
EXPECT_EQ(energy_model->idle_levels()[1].level(), 2u);
ASSERT_EQ(energy_model->active_levels().size(), 2u);
EXPECT_EQ(energy_model->active_levels()[0].level(), 1u);
EXPECT_EQ(energy_model->active_levels()[1].level(), 3u);
// Sorter by tuple <Control Interface, Control Argument>
for (size_t i = 0; i < levels.size(); ++i) {
auto& level = levels[i];
EXPECT_EQ(energy_model->FindPowerLevel(level.control(), level.control_argument()), i);
}
EXPECT_FALSE(energy_model->FindPowerLevel(ControlInterface::kArmPsci, 495));
EXPECT_FALSE(energy_model->FindPowerLevel(static_cast<ControlInterface>(495), 0));
}
TEST(PowerDomainRegistryTest, FindDomain) {
std::array domains_to_register{
MakePowerDomainHelper(0, 1, 2, 3),
MakePowerDomainHelper(1, 4, 5, 6),
MakePowerDomainHelper(2, 7, 8, 9),
};
PowerDomainRegistry registry;
for (const auto& domain : domains_to_register) {
ASSERT_TRUE(registry.Register(domain).is_ok());
}
for (const auto& domain : domains_to_register) {
EXPECT_EQ(registry.Find(domain->id()), domain.get());
}
EXPECT_EQ(registry.Find(112345567), nullptr);
}
TEST(PowerDomainRegistryTest, RegisterUpdateUnregisterPowerDomains) {
std::array unique_domains_to_register = {
MakePowerDomainHelper(0, 0, 1, 2),
MakePowerDomainHelper(1, 4, 5, 6),
MakePowerDomainHelper(2, 8, 9, 10),
};
std::array unique_domains_to_update = {
MakePowerDomainHelper(0, 0, 1, 2, 3),
MakePowerDomainHelper(1, 4, 5, 6, 7),
MakePowerDomainHelper(2, 8, 9, 10, 11),
};
std::array conflicting_domains_to_register = {
MakePowerDomainHelper(3, 12, 13, 14, 0),
MakePowerDomainHelper(4, 16, 17, 18, 1),
MakePowerDomainHelper(5, 20, 21, 22, 2),
};
std::array conflicting_domains_to_update = {
MakePowerDomainHelper(0, 0, 1, 2, 4),
MakePowerDomainHelper(1, 4, 5, 7, 8),
MakePowerDomainHelper(2, 8, 9, 10, 0),
};
// Called by the registry after each update to the power domain set maintained
// by the registry. On each update, the power domain set must only contain a
// subset of the unique power domains that are registered by the test.
const auto update_callback = [&](const PowerDomainSet& domain_set) {
domain_set.Visit([&](const fbl::RefPtr<PowerDomain>& domain) {
EXPECT_TRUE(InRange(unique_domains_to_register, domain) ||
InRange(unique_domains_to_update, domain))
<< "domain " << domain->id();
});
};
PowerDomainRegistry registry{update_callback};
for (const fbl::RefPtr<PowerDomain>& domain : unique_domains_to_register) {
EXPECT_EQ(domain->total_normalized_utilization(), Utilization{0});
ASSERT_TRUE(registry.Register(domain).is_ok());
}
// All of the domains registered so far should be present in the registry.
EXPECT_EQ(registry.power_domain_set().count(), unique_domains_to_register.size());
registry.Visit([&](const fbl::RefPtr<PowerDomain>& domain) {
EXPECT_TRUE(InRange(unique_domains_to_register, domain));
});
// Update each registered domain.
for (const fbl::RefPtr<PowerDomain>& domain : unique_domains_to_update) {
EXPECT_EQ(domain->total_normalized_utilization(), Utilization{0});
ASSERT_TRUE(registry.Register(domain).is_ok());
}
// All of the updated domains should be present in the registry.
EXPECT_EQ(registry.power_domain_set().count(), unique_domains_to_update.size());
registry.Visit([&](const fbl::RefPtr<PowerDomain>& domain) {
EXPECT_TRUE(InRange(unique_domains_to_update, domain));
});
// Attempt and fail to register new domain ids with CPU sets that intersect
// with the already registered domains.
for (const fbl::RefPtr<PowerDomain>& domain : conflicting_domains_to_register) {
EXPECT_EQ(domain->total_normalized_utilization(), Utilization{0});
ASSERT_FALSE(registry.Register(domain).is_ok());
}
// The domain set should remain unchanged from the last successful updates.
EXPECT_EQ(registry.power_domain_set().count(), unique_domains_to_update.size());
registry.Visit([&](const fbl::RefPtr<PowerDomain>& domain) {
EXPECT_TRUE(InRange(unique_domains_to_update, domain));
});
// Attempt and fail to update existing domain ids with CPU sets that intersect
// with other registered domains.
for (const fbl::RefPtr<PowerDomain>& domain : conflicting_domains_to_update) {
EXPECT_EQ(domain->total_normalized_utilization(), Utilization{0});
ASSERT_FALSE(registry.Register(domain).is_ok());
}
// The domain set should remain unchanged from the last successful updates.
EXPECT_EQ(registry.power_domain_set().count(), unique_domains_to_update.size());
registry.Visit([&](const fbl::RefPtr<PowerDomain>& domain) {
EXPECT_TRUE(InRange(unique_domains_to_update, domain));
});
// Unregister each domain and ensure that the updated domain set reflects the
// change.
for (const fbl::RefPtr<PowerDomain>& domain : unique_domains_to_update) {
EXPECT_TRUE(registry.Unregister(domain->id()).is_ok());
EXPECT_EQ(registry.Find(domain->id()), nullptr);
}
EXPECT_EQ(registry.power_domain_set().count(), 0u);
}
} // namespace