Media: Add power-systems-health utils
Used for audio power management and statistics.
This CL is library only.
Flag: com.android.media.audioserver.power_stats
Test: atest powerstats_collector_tests
Test: atest audio_powerstats_benchmark
Bug: 350114693
Change-Id: Iab8cae3f4418f86d43acef109ae7ce2776d627ca
diff --git a/media/psh_utils/Android.bp b/media/psh_utils/Android.bp
new file mode 100644
index 0000000..f67f63e
--- /dev/null
+++ b/media/psh_utils/Android.bp
@@ -0,0 +1,44 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_av_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_av_license"],
+}
+
+// libraries that are included whole_static for test apps
+ndk_libs = [
+ "android.hardware.power.stats-V1-ndk",
+]
+
+// Power, System, Health utils
+cc_library {
+ name: "libpshutils",
+ local_include_dirs: ["include"],
+ export_include_dirs: ["include"],
+ srcs: [
+ "PowerStats.cpp",
+ "PowerStatsCollector.cpp",
+ "PowerStatsProvider.cpp",
+ ],
+ shared_libs: [
+ "libaudioutils",
+ "libbase",
+ "libbinder_ndk",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wthread-safety",
+ ],
+ shared: {
+ shared_libs: ndk_libs,
+ },
+ static: {
+ whole_static_libs: ndk_libs,
+ },
+}
diff --git a/media/psh_utils/PowerStats.cpp b/media/psh_utils/PowerStats.cpp
new file mode 100644
index 0000000..c1f3d9a
--- /dev/null
+++ b/media/psh_utils/PowerStats.cpp
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <android-base/logging.h>
+#include <audio_utils/clock.h>
+#include <psh_utils/PowerStats.h>
+
+namespace android::media::psh_utils {
+
+// Determine the best start time from a and b, which is
+// min(a, b) if both exist, otherwise the one that exists.
+template <typename T>
+const T& choose_best_start_time(const T& a, const T& b) {
+ if (a) {
+ return b ? std::min(a, b) : a;
+ } else {
+ return b;
+ }
+}
+
+// subtract two time differences.
+template <typename T, typename U>
+const T sub_time_diff(const T& diff_a, const T& diff_b, const U& abs_c, const U& abs_d) {
+ if (diff_a) {
+ return diff_b ? (diff_a - diff_b) : diff_a;
+ } else if (diff_b) {
+ return diff_b;
+ } else { // no difference exists, use absolute time.
+ return abs_c - abs_d;
+ }
+}
+
+std::string PowerStats::Metadata::toString() const {
+ return std::string("start_time_since_boot_ms: ").append(
+ std::to_string(start_time_since_boot_ms))
+ .append(" start_time_monotonic_ms: ").append(std::to_string(start_time_monotonic_ms))
+ .append(audio_utils_time_string_from_ns(start_time_epoch_ms * 1'000'000).time)
+ .append(" duration_ms: ").append(std::to_string(duration_ms))
+ .append(" duration_monotonic_ms: ").append(std::to_string(duration_monotonic_ms));
+}
+
+PowerStats::Metadata PowerStats::Metadata::operator+=(const Metadata& other) {
+ start_time_since_boot_ms = choose_best_start_time(
+ start_time_since_boot_ms, other.start_time_since_boot_ms);
+ start_time_epoch_ms = choose_best_start_time(
+ start_time_epoch_ms, other.start_time_epoch_ms);
+ start_time_monotonic_ms = choose_best_start_time(
+ start_time_monotonic_ms, other.start_time_monotonic_ms);
+ duration_ms += other.duration_ms;
+ duration_monotonic_ms += other.duration_monotonic_ms;
+ return *this;
+}
+
+PowerStats::Metadata PowerStats::Metadata::operator-=(const Metadata& other) {
+ // here we calculate duration, if it makes sense.
+ duration_ms = sub_time_diff(duration_ms, other.duration_ms,
+ start_time_since_boot_ms, other.start_time_since_boot_ms);
+ duration_monotonic_ms = sub_time_diff(
+ duration_monotonic_ms, other.duration_monotonic_ms,
+ start_time_monotonic_ms, other.start_time_monotonic_ms);
+ start_time_since_boot_ms = choose_best_start_time(
+ start_time_since_boot_ms, other.start_time_since_boot_ms);
+ start_time_epoch_ms = choose_best_start_time(
+ start_time_epoch_ms, other.start_time_epoch_ms);
+ start_time_monotonic_ms = choose_best_start_time(
+ start_time_monotonic_ms, other.start_time_monotonic_ms);
+ return *this;
+}
+
+PowerStats::Metadata PowerStats::Metadata::operator+(const Metadata& other) const {
+ Metadata result = *this;
+ result += other;
+ return result;
+}
+
+PowerStats::Metadata PowerStats::Metadata::operator-(const Metadata& other) const {
+ Metadata result = *this;
+ result -= other;
+ return result;
+}
+
+std::string PowerStats::StateResidency::toString() const {
+ return std::string(entity_name).append(state_name)
+ .append(" ").append(std::to_string(time_ms))
+ .append(" ").append(std::to_string(entry_count));
+}
+
+PowerStats::StateResidency PowerStats::StateResidency::operator+=(const StateResidency& other) {
+ if (entity_name.empty()) entity_name = other.entity_name;
+ if (state_name.empty()) state_name = other.state_name;
+ time_ms += other.time_ms;
+ entry_count += other.entry_count;
+ return *this;
+}
+
+PowerStats::StateResidency PowerStats::StateResidency::operator-=(const StateResidency& other) {
+ if (entity_name.empty()) entity_name = other.entity_name;
+ if (state_name.empty()) state_name = other.state_name;
+ time_ms -= other.time_ms;
+ entry_count -= other.entry_count;
+ return *this;
+}
+
+PowerStats::StateResidency PowerStats::StateResidency::operator+(
+ const StateResidency& other) const {
+ StateResidency result = *this;
+ result += other;
+ return result;
+}
+
+PowerStats::StateResidency PowerStats::StateResidency::operator-(
+ const StateResidency& other) const {
+ StateResidency result = *this;
+ result -= other;
+ return result;
+}
+
+std::string PowerStats::RailEnergy::toString() const {
+ return std::string(subsystem_name)
+ .append(rail_name)
+ .append(" ")
+ .append(std::to_string(energy_uws));
+}
+
+PowerStats::RailEnergy PowerStats::RailEnergy::operator+=(const RailEnergy& other) {
+ if (subsystem_name.empty()) subsystem_name = other.subsystem_name;
+ if (rail_name.empty()) rail_name = other.rail_name;
+ energy_uws += other.energy_uws;
+ return *this;
+}
+
+PowerStats::RailEnergy PowerStats::RailEnergy::operator-=(const RailEnergy& other) {
+ if (subsystem_name.empty()) subsystem_name = other.subsystem_name;
+ if (rail_name.empty()) rail_name = other.rail_name;
+ energy_uws -= other.energy_uws;
+ return *this;
+}
+
+PowerStats::RailEnergy PowerStats::RailEnergy::operator+(const RailEnergy& other) const {
+ RailEnergy result = *this;
+ result += other;
+ return result;
+}
+
+PowerStats::RailEnergy PowerStats::RailEnergy::operator-(const RailEnergy& other) const {
+ RailEnergy result = *this;
+ result -= other;
+ return result;
+}
+
+std::string PowerStats::toString() const {
+ std::string result;
+ result.append(metadata.toString()).append("\n");
+ for (const auto &residency: power_entity_state_residency) {
+ result.append(residency.toString()).append("\n");
+ }
+ for (const auto &energy: rail_energy) {
+ result.append(energy.toString()).append("\n");
+ }
+ return result;
+}
+
+std::string PowerStats::normalizedEnergy() const {
+ if (metadata.duration_ms == 0) return {};
+
+ std::string result(audio_utils_time_string_from_ns(
+ metadata.start_time_epoch_ms * 1'000'000).time);
+ result.append(" duration_boottime: ")
+ .append(std::to_string(metadata.duration_ms * 1e-3f))
+ .append(" duration_monotonic: ")
+ .append(std::to_string(metadata.duration_monotonic_ms * 1e-3f))
+ .append("\n");
+ // energy_uws is converted to ave W using recip time in us.
+ const float recipTime = 1e-3 / metadata.duration_ms;
+ int64_t total_energy = 0;
+ for (const auto& energy: rail_energy) {
+ total_energy += energy.energy_uws;
+ result.append(energy.subsystem_name)
+ .append(energy.rail_name)
+ .append(" ")
+ .append(std::to_string(energy.energy_uws * 1e-6))
+ .append(" ")
+ .append(std::to_string(energy.energy_uws * recipTime))
+ .append("\n");
+ }
+ result.append("total J and ave W: ")
+ .append(std::to_string(total_energy * 1e-6))
+ .append(" ")
+ .append(std::to_string(total_energy * recipTime))
+ .append("\n");
+ return result;
+}
+
+// seconds, joules, watts
+std::tuple<float, float, float> PowerStats::energyFrom(const std::string& railMatcher) const {
+ if (metadata.duration_ms == 0) return {};
+
+ // energy_uws is converted to ave W using recip time in us.
+ const float recipTime = 1e-3 / metadata.duration_ms;
+ int64_t total_energy = 0;
+ for (const auto& energy: rail_energy) {
+ if (energy.subsystem_name.find(railMatcher) != std::string::npos
+ || energy.rail_name.find(railMatcher) != std::string::npos) {
+ total_energy += energy.energy_uws;
+ }
+ }
+ return {metadata.duration_ms * 1e-3, total_energy * 1e-6, total_energy * recipTime};
+}
+
+PowerStats PowerStats::operator+=(const PowerStats& other) {
+ metadata += other.metadata;
+ if (power_entity_state_residency.empty()) {
+ power_entity_state_residency = other.power_entity_state_residency;
+ } else {
+ for (size_t i = 0; i < power_entity_state_residency.size(); ++i) {
+ power_entity_state_residency[i] += other.power_entity_state_residency[i];
+ }
+ }
+ if (rail_energy.empty()) {
+ rail_energy = other.rail_energy;
+ } else {
+ for (size_t i = 0; i < rail_energy.size(); ++i) {
+ rail_energy[i] += other.rail_energy[i];
+ }
+ }
+ return *this;
+}
+
+PowerStats PowerStats::operator-=(const PowerStats& other) {
+ metadata -= other.metadata;
+ if (power_entity_state_residency.empty()) {
+ power_entity_state_residency = other.power_entity_state_residency;
+ } else {
+ for (size_t i = 0; i < power_entity_state_residency.size(); ++i) {
+ power_entity_state_residency[i] -= other.power_entity_state_residency[i];
+ }
+ }
+ if (rail_energy.empty()) {
+ rail_energy = other.rail_energy;
+ } else {
+ for (size_t i = 0; i < rail_energy.size(); ++i) {
+ rail_energy[i] -= other.rail_energy[i];
+ }
+ }
+ return *this;
+}
+
+PowerStats PowerStats::operator+(const PowerStats& other) const {
+ PowerStats result = *this;
+ result += other;
+ return result;
+}
+
+PowerStats PowerStats::operator-(const PowerStats& other) const {
+ PowerStats result = *this;
+ result -= other;
+ return result;
+}
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/PowerStatsCollector.cpp b/media/psh_utils/PowerStatsCollector.cpp
new file mode 100644
index 0000000..c166d85
--- /dev/null
+++ b/media/psh_utils/PowerStatsCollector.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <android-base/logging.h>
+#include <psh_utils/PowerStatsCollector.h>
+#include "PowerStatsProvider.h"
+#include <utils/Timers.h>
+
+namespace android::media::psh_utils {
+
+PowerStatsCollector::PowerStatsCollector() {
+ addProvider(std::make_unique<PowerEntityResidencyDataProvider>());
+ addProvider(std::make_unique<RailEnergyDataProvider>());
+}
+
+/* static */
+PowerStatsCollector& PowerStatsCollector::getCollector() {
+ [[clang::no_destroy]] static PowerStatsCollector psc;
+ return psc;
+}
+
+std::shared_ptr<PowerStats> PowerStatsCollector::getStats() const {
+ auto result = std::make_shared<PowerStats>();
+ (void)fill(result.get());
+ return result;
+}
+
+void PowerStatsCollector::addProvider(std::unique_ptr<PowerStatsProvider>&& powerStatsProvider) {
+ mPowerStatsProviders.emplace_back(std::move(powerStatsProvider));
+}
+
+int PowerStatsCollector::fill(PowerStats* stats) const {
+ if (!stats) {
+ LOG(ERROR) << __func__ << ": bad args; stat is null";
+ return 1;
+ }
+
+ for (const auto& provider : mPowerStatsProviders) {
+ if (provider->fill(stats) != 0) {
+ LOG(ERROR) << __func__ << ": a data provider failed";
+ continue;
+ }
+ }
+
+ // boot time follows wall clock time, but starts from boot.
+ stats->metadata.start_time_since_boot_ms = systemTime(SYSTEM_TIME_BOOTTIME) / 1'000'000;
+
+ // wall clock time
+ stats->metadata.start_time_epoch_ms = systemTime(SYSTEM_TIME_REALTIME) / 1'000'000;
+
+ // monotonic time follows boot time, but does not include any time suspended.
+ stats->metadata.start_time_monotonic_ms = systemTime() / 1'000'000;
+ return 0;
+}
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/PowerStatsProvider.cpp b/media/psh_utils/PowerStatsProvider.cpp
new file mode 100644
index 0000000..71a39e1
--- /dev/null
+++ b/media/psh_utils/PowerStatsProvider.cpp
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "PowerStatsProvider.h"
+#include <android-base/logging.h>
+#include <android/binder_manager.h>
+#include <unordered_map>
+#include <aidl/android/hardware/power/stats/IPowerStats.h>
+
+using ::aidl::android::hardware::power::stats::IPowerStats;
+
+namespace android::media::psh_utils {
+
+static auto getPowerStatsService() {
+ [[clang::no_destroy]] static constinit std::mutex m;
+ [[clang::no_destroy]] static constinit
+ std::shared_ptr<IPowerStats> powerStats;
+
+ std::lock_guard l(m);
+ if (powerStats) {
+ return powerStats;
+ }
+ const auto serviceName =
+ std::string(IPowerStats::descriptor)
+ .append("/default");
+ powerStats = IPowerStats::fromBinder(
+ ::ndk::SpAIBinder(AServiceManager_checkService(serviceName.c_str())));
+ return powerStats;
+}
+
+int RailEnergyDataProvider::fill(PowerStats *stat) const {
+ auto powerStatsService = getPowerStatsService();
+ if (powerStatsService == nullptr) {
+ LOG(ERROR) << "unable to get power.stats AIDL service";
+ return 1;
+ }
+
+ std::unordered_map<int32_t, ::aidl::android::hardware::power::stats::Channel> channelMap;
+ {
+ std::vector<::aidl::android::hardware::power::stats::Channel> channels;
+ if (!powerStatsService->getEnergyMeterInfo(&channels).isOk()) {
+ LOG(ERROR) << "unable to get energy meter info";
+ return 1;
+ }
+ for (auto& channel : channels) {
+ channelMap.emplace(channel.id, std::move(channel));
+ }
+ }
+
+ std::vector<::aidl::android::hardware::power::stats::EnergyMeasurement> measurements;
+ if (!powerStatsService->readEnergyMeter({}, &measurements).isOk()) {
+ LOG(ERROR) << "unable to get energy measurements";
+ return 1;
+ }
+
+ for (const auto& measurement : measurements) {
+ stat->rail_energy.emplace_back(
+ channelMap.at(measurement.id).subsystem,
+ channelMap.at(measurement.id).name,
+ measurement.energyUWs);
+ }
+
+ // Sort entries first by subsystem_name, then by rail_name.
+ // Sorting is needed to make interval processing efficient.
+ std::sort(stat->rail_energy.begin(), stat->rail_energy.end(),
+ [](const auto& a, const auto& b) {
+ if (a.subsystem_name != b.subsystem_name) {
+ return a.subsystem_name < b.subsystem_name;
+ }
+ return a.rail_name < b.rail_name;
+ });
+
+ return 0;
+}
+
+int PowerEntityResidencyDataProvider::fill(PowerStats* stat) const {
+ auto powerStatsService = getPowerStatsService();
+ if (powerStatsService == nullptr) {
+ LOG(ERROR) << "unable to get power.stats AIDL service";
+ return 1;
+ }
+
+ // these are based on entityId
+ std::unordered_map<int32_t, std::string> entityNames;
+ std::unordered_map<int32_t, std::unordered_map<int32_t, std::string>> stateNames;
+ std::vector<int32_t> powerEntityIds; // ids to use
+
+ {
+ std::vector<::aidl::android::hardware::power::stats::PowerEntity> entities;
+ if (!powerStatsService->getPowerEntityInfo(&entities).isOk()) {
+ LOG(ERROR) << __func__ << ": unable to get entity info";
+ return 1;
+ }
+
+ std::vector<std::string> powerEntityNames;
+ for (const auto& entity : entities) {
+ std::unordered_map<int32_t, std::string> states;
+ for (const auto& state : entity.states) {
+ states.emplace(state.id, state.name);
+ }
+
+ if (std::find(powerEntityNames.begin(), powerEntityNames.end(), entity.name) !=
+ powerEntityNames.end()) {
+ powerEntityIds.emplace_back(entity.id);
+ }
+ entityNames.emplace(entity.id, std::move(entity.name));
+ stateNames.emplace(entity.id, std::move(states));
+ }
+ }
+
+ std::vector<::aidl::android::hardware::power::stats::StateResidencyResult> results;
+ if (!powerStatsService->getStateResidency(powerEntityIds, &results).isOk()) {
+ LOG(ERROR) << __func__ << ": Unable to get state residency";
+ return 1;
+ }
+
+ for (const auto& result : results) {
+ for (const auto& curStateResidency : result.stateResidencyData) {
+ stat->power_entity_state_residency.emplace_back(
+ entityNames.at(result.id),
+ stateNames.at(result.id).at(curStateResidency.id),
+ static_cast<uint64_t>(curStateResidency.totalTimeInStateMs),
+ static_cast<uint64_t>(curStateResidency.totalStateEntryCount));
+ }
+ }
+
+ // Sort entries first by entity_name, then by state_name.
+ // Sorting is needed to make interval processing efficient.
+ std::sort(stat->power_entity_state_residency.begin(),
+ stat->power_entity_state_residency.end(),
+ [](const auto& a, const auto& b) {
+ if (a.entity_name != b.entity_name) {
+ return a.entity_name < b.entity_name;
+ }
+ return a.state_name < b.state_name;
+ });
+ return 0;
+}
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/PowerStatsProvider.h b/media/psh_utils/PowerStatsProvider.h
new file mode 100644
index 0000000..367e684
--- /dev/null
+++ b/media/psh_utils/PowerStatsProvider.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <psh_utils/PowerStatsCollector.h>
+#include <iostream>
+
+namespace android::media::psh_utils {
+
+class RailEnergyDataProvider : public PowerStatsProvider {
+public:
+ int fill(PowerStats* stat) const override;
+};
+
+class PowerEntityResidencyDataProvider : public PowerStatsProvider {
+public:
+ int fill(PowerStats* stat) const override;
+};
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/benchmarks/Android.bp b/media/psh_utils/benchmarks/Android.bp
new file mode 100644
index 0000000..505ebba
--- /dev/null
+++ b/media/psh_utils/benchmarks/Android.bp
@@ -0,0 +1,30 @@
+package {
+ default_team: "trendy_team_android_media_audio_framework",
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_av_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_av_license"],
+}
+
+cc_benchmark {
+ name: "audio_powerstats_benchmark",
+
+ srcs: ["audio_powerstats_benchmark.cpp"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ static_libs: [
+ "libaudioutils",
+ "libpshutils",
+ ],
+ shared_libs: [
+ "libbase",
+ "libbinder_ndk",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+}
diff --git a/media/psh_utils/benchmarks/audio_powerstats_benchmark.cpp b/media/psh_utils/benchmarks/audio_powerstats_benchmark.cpp
new file mode 100644
index 0000000..d3f815c
--- /dev/null
+++ b/media/psh_utils/benchmarks/audio_powerstats_benchmark.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "audio_powerstat_benchmark"
+#include <cutils/properties.h>
+#include <utils/Log.h>
+
+#include <psh_utils/PerformanceFixture.h>
+
+#include <algorithm>
+#include <android-base/strings.h>
+#include <random>
+#include <thread>
+#include <vector>
+
+float result = 0;
+
+using android::media::psh_utils::CoreClass;
+using android::media::psh_utils::CORE_LITTLE;
+using android::media::psh_utils::CORE_MID;
+using android::media::psh_utils::CORE_BIG;
+
+enum Direction {
+ DIRECTION_FORWARD,
+ DIRECTION_BACKWARD,
+ DIRECTION_RANDOM,
+};
+
+std::string toString(Direction direction) {
+ switch (direction) {
+ case DIRECTION_FORWARD: return "DIRECTION_FORWARD";
+ case DIRECTION_BACKWARD: return "DIRECTION_BACKWARD";
+ case DIRECTION_RANDOM: return "DIRECTION_RANDOM";
+ default: return "DIRECTION_UNKNOWN";
+ }
+}
+
+enum Content {
+ CONTENT_ZERO,
+ CONTENT_RANDOM,
+};
+
+std::string toString(Content content) {
+ switch (content) {
+ case CONTENT_ZERO: return "CONTENT_ZERO";
+ case CONTENT_RANDOM: return "CONTENT_RANDOM";
+ default: return "CONTENT_UNKNOWN";
+ }
+}
+
+class MemoryFixture : public android::media::psh_utils::PerformanceFixture {
+public:
+ void SetUp(benchmark::State& state) override {
+ mCount = state.range(0) / (sizeof(uint32_t) + sizeof(float));
+ state.SetComplexityN(mCount * 2); // 2 access per iteration.
+
+ // create src distribution
+ mSource.resize(mCount);
+ const auto content = static_cast<Content>(state.range(3));
+ if (content == CONTENT_RANDOM) {
+ std::minstd_rand gen(mCount);
+ std::uniform_real_distribution<float> dis(-1.f, 1.f);
+ for (size_t i = 0; i < mCount; i++) {
+ mSource[i] = dis(gen);
+ }
+ }
+
+ // create direction
+ mIndex.resize(mCount);
+ const auto direction = static_cast<Direction>(state.range(2));
+ switch (direction) {
+ case DIRECTION_BACKWARD:
+ for (size_t i = 0; i < mCount; i++) {
+ mIndex[i] = i; // it is also possible to go in the reverse direction
+ }
+ break;
+ case DIRECTION_FORWARD:
+ case DIRECTION_RANDOM:
+ for (size_t i = 0; i < mCount; i++) {
+ mIndex[i] = i; // it is also possible to go in the reverse direction
+ }
+ if (direction == DIRECTION_RANDOM) {
+ std::random_device rd;
+ std::mt19937 g(rd());
+ std::shuffle(mIndex.begin(), mIndex.end(), g);
+ }
+ break;
+ }
+
+ // set up the profiler
+ const auto coreClass = static_cast<CoreClass>(state.range(1));
+
+ // It would be best if we could override SetName() but it is too late at this point,
+ // so we set the state label here for clarity.
+ state.SetLabel(toString(coreClass).append("/")
+ .append(toString(direction)).append("/")
+ .append(toString(content)));
+
+ if (property_get_bool("persist.audio.benchmark_profile", false)) {
+ startProfiler(coreClass);
+ }
+ }
+ size_t mCount = 0;
+ std::vector<uint32_t> mIndex;
+ std::vector<float> mSource;
+};
+
+BENCHMARK_DEFINE_F(MemoryFixture, CacheAccess)(benchmark::State &state) {
+ float accum = 0;
+ while (state.KeepRunning()) {
+ for (size_t i = 0; i < mCount; ++i) {
+ accum += mSource[mIndex[i]];
+ }
+ benchmark::ClobberMemory();
+ }
+ result += accum; // not optimized
+}
+
+BENCHMARK_REGISTER_F(MemoryFixture, CacheAccess)->ArgsProduct({
+ benchmark::CreateRange(64, 64<<20, /* multi = */2),
+ {CORE_LITTLE, CORE_MID, CORE_BIG},
+ {DIRECTION_FORWARD, DIRECTION_RANDOM},
+ {CONTENT_RANDOM},
+});
+
+BENCHMARK_MAIN();
diff --git a/media/psh_utils/include/psh_utils/PerformanceFixture.h b/media/psh_utils/include/psh_utils/PerformanceFixture.h
new file mode 100644
index 0000000..2d8b38a
--- /dev/null
+++ b/media/psh_utils/include/psh_utils/PerformanceFixture.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <audio_utils/threads.h>
+#include <benchmark/benchmark.h>
+#include <psh_utils/PowerStats.h>
+#include <psh_utils/PowerStatsCollector.h>
+
+#include <future>
+
+namespace android::media::psh_utils {
+
+enum CoreClass {
+ CORE_LITTLE,
+ CORE_MID,
+ CORE_BIG,
+};
+
+inline std::string toString(CoreClass coreClass) {
+ switch (coreClass) {
+ case CORE_LITTLE: return "LITTLE";
+ case CORE_MID: return "MID";
+ case CORE_BIG: return "BIG";
+ default: return "UNKNOWN";
+ }
+}
+
+/**
+ * A benchmark fixture is used to specify benchmarks that have a custom SetUp() and
+ * TearDown(). This is **required** for performance testing, as a typical benchmark
+ * method **may be called several times** during a run.
+ *
+ * A fixture ensures that SetUp() and TearDown() and the resulting statistics accumulation
+ * is done only once. Note: BENCHMARK(BM_func)->Setup(DoSetup)->Teardown(DoTeardown)
+ * does something similar, but it requires some singleton to contain the state properly.
+ */
+class PerformanceFixture : public benchmark::Fixture {
+public:
+ // call this to start the profiling
+ virtual void startProfiler(CoreClass coreClass) {
+ mCores = android::audio_utils::get_number_cpus();
+ if (mCores == 0) return;
+ mCoreClass = coreClass;
+ std::array<unsigned, 3> coreSelection{0U, mCores / 2 + 1, mCores - 1};
+ mCore = coreSelection[std::min((size_t)coreClass, std::size(coreSelection) - 1)];
+
+ const auto& collector = android::media::psh_utils::PowerStatsCollector::getCollector();
+ mStartStats = collector.getStats();
+
+ const pid_t tid = gettid(); // us.
+
+ // Possibly change priority to improve benchmarking
+ // android::audio_utils::set_thread_priority(gettid(), 98);
+
+ android::audio_utils::set_thread_affinity(0 /* pid */, 1 << mCore);
+ }
+
+ void TearDown(benchmark::State &state) override {
+ const auto N = state.complexity_length_n();
+ state.counters["N"] = benchmark::Counter(N,
+ benchmark::Counter::kIsIterationInvariantRate, benchmark::Counter::OneK::kIs1024);
+ if (mStartStats) {
+ auto& collector = android::media::psh_utils::PowerStatsCollector::getCollector();
+ const auto stopStats = collector.getStats();
+ android::media::psh_utils::PowerStats diff = *stopStats - *mStartStats;
+ auto cpuEnergy = diff.energyFrom("CPU");
+ auto memEnergy = diff.energyFrom("MEM");
+
+ constexpr float kMwToW = 1e-3;
+ state.counters["WCPU"] = benchmark::Counter(std::get<2>(cpuEnergy) * kMwToW,
+ benchmark::Counter::kDefaults,
+ benchmark::Counter::OneK::kIs1000);
+ state.counters["WMem"] = benchmark::Counter(std::get<2>(memEnergy) * kMwToW,
+ benchmark::Counter::kDefaults,
+ benchmark::Counter::OneK::kIs1000);
+ state.counters["JCPU"] = benchmark::Counter(
+ std::get<1>(cpuEnergy) / N / state.iterations(), benchmark::Counter::kDefaults,
+ benchmark::Counter::OneK::kIs1000);
+ state.counters["JMem"] = benchmark::Counter(
+ std::get<1>(memEnergy) / N / state.iterations(), benchmark::Counter::kDefaults,
+ benchmark::Counter::OneK::kIs1000);
+ }
+ }
+
+protected:
+ // these are only initialized upon startProfiler.
+ unsigned mCores = 0;
+ int mCore = 0;
+ CoreClass mCoreClass = CORE_LITTLE;
+ std::shared_ptr<android::media::psh_utils::PowerStats> mStartStats;
+};
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/include/psh_utils/PowerStats.h b/media/psh_utils/include/psh_utils/PowerStats.h
new file mode 100644
index 0000000..e8652d5
--- /dev/null
+++ b/media/psh_utils/include/psh_utils/PowerStats.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android::media::psh_utils {
+
+// See powerstats_util.proto and powerstats_util.pb.h
+
+struct PowerStats {
+ struct Metadata {
+ // Represents the start time measured in milliseconds since boot of the
+ // interval or point in time when stats were gathered.
+ uint64_t start_time_since_boot_ms;
+
+ // Represents the start time measured in milliseconds since epoch of the
+ // interval or point in time when stats were gathered.
+ uint64_t start_time_epoch_ms;
+
+ // In monotonic clock.
+ uint64_t start_time_monotonic_ms;
+
+ // If PowerStats represent an interval, the duration field will be set will
+ // the millisecond duration of stats collection. It will be unset for point
+ // stats.
+ // This is in boottime.
+ uint64_t duration_ms;
+
+ // This is in monotonic time, and does not include suspend.
+ uint64_t duration_monotonic_ms;
+
+ std::string toString() const;
+
+ Metadata operator+=(const Metadata& other);
+ Metadata operator-=(const Metadata& other);
+ Metadata operator+(const Metadata& other) const;
+ Metadata operator-(const Metadata& other) const;
+ bool operator==(const Metadata& other) const = default;
+ };
+
+ struct StateResidency {
+ std::string entity_name;
+ std::string state_name;
+ uint64_t time_ms;
+ uint64_t entry_count;
+
+ std::string toString() const;
+
+ StateResidency operator+=(const StateResidency& other);
+ StateResidency operator-=(const StateResidency& other);
+ StateResidency operator+(const StateResidency& other) const;
+ StateResidency operator-(const StateResidency& other) const;
+ bool operator==(const StateResidency& other) const = default;
+ };
+
+ struct RailEnergy {
+ std::string subsystem_name;
+ std::string rail_name;
+ uint64_t energy_uws;
+
+ std::string toString() const;
+ RailEnergy operator+=(const RailEnergy& other);
+ RailEnergy operator-=(const RailEnergy& other);
+ RailEnergy operator+(const RailEnergy& other) const;
+ RailEnergy operator-(const RailEnergy& other) const;
+ bool operator==(const RailEnergy& other) const = default;
+ };
+
+ std::string normalizedEnergy() const;
+
+ // Returns {seconds, joules, watts} from all rails containing a matching string.
+ std::tuple<float, float, float> energyFrom(const std::string& railMatcher) const;
+ std::string toString() const;
+
+ PowerStats operator+=(const PowerStats& other);
+ PowerStats operator-=(const PowerStats& other);
+ PowerStats operator+(const PowerStats& other) const;
+ PowerStats operator-(const PowerStats& other) const;
+ bool operator==(const PowerStats& other) const = default;
+
+ Metadata metadata{};
+ // These are sorted by name.
+ std::vector<StateResidency> power_entity_state_residency;
+ std::vector<RailEnergy> rail_energy;
+};
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/include/psh_utils/PowerStatsCollector.h b/media/psh_utils/include/psh_utils/PowerStatsCollector.h
new file mode 100644
index 0000000..a3ff53c
--- /dev/null
+++ b/media/psh_utils/include/psh_utils/PowerStatsCollector.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "PowerStats.h"
+#include <memory>
+
+namespace android::media::psh_utils {
+
+// Internal providers that fill up the PowerStats state object.
+class PowerStatsProvider {
+public:
+ virtual ~PowerStatsProvider() = default;
+ virtual int fill(PowerStats* stat) const = 0;
+};
+
+class PowerStatsCollector {
+public:
+ // singleton getter
+ static PowerStatsCollector& getCollector();
+
+ // get a snapshot of the state.
+ std::shared_ptr<PowerStats> getStats() const;
+
+private:
+ PowerStatsCollector(); // use the singleton getter
+
+ int fill(PowerStats* stats) const;
+ void addProvider(std::unique_ptr<PowerStatsProvider>&& powerStatsProvider);
+
+ // addProvider is called in the ctor, so effectively const.
+ std::vector<std::unique_ptr<PowerStatsProvider>> mPowerStatsProviders;
+};
+
+} // namespace android::media::psh_utils
diff --git a/media/psh_utils/tests/Android.bp b/media/psh_utils/tests/Android.bp
new file mode 100644
index 0000000..74589f8
--- /dev/null
+++ b/media/psh_utils/tests/Android.bp
@@ -0,0 +1,26 @@
+package {
+ default_team: "trendy_team_media_framework_audio",
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_av_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_av_license"],
+}
+
+cc_test {
+ name: "powerstats_collector_tests",
+ srcs: [
+ "powerstats_collector_tests.cpp",
+ ],
+ shared_libs: [
+ "libbase",
+ "libbinder_ndk",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+ static_libs: [
+ "libpshutils",
+ ],
+}
diff --git a/media/psh_utils/tests/powerstats_collector_tests.cpp b/media/psh_utils/tests/powerstats_collector_tests.cpp
new file mode 100644
index 0000000..6c83978
--- /dev/null
+++ b/media/psh_utils/tests/powerstats_collector_tests.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <psh_utils/PowerStatsCollector.h>
+#include <gtest/gtest.h>
+#include <utils/Log.h>
+
+TEST(powerstat_collector_tests, basic) {
+ const auto& psc = android::media::psh_utils::PowerStatsCollector::getCollector();
+
+ // This test is used for debugging the string through logcat, we validate a non-empty string.
+ auto powerStats = psc.getStats();
+ ALOGD("%s: %s", __func__, powerStats->toString().c_str());
+ EXPECT_FALSE(powerStats->toString().empty());
+}
+
+TEST(powerstat_collector_tests, arithmetic) {
+ android::media::psh_utils::PowerStats ps1, ps2;
+ ps1.metadata.duration_ms = 5;
+ ps2.metadata.duration_ms = 10;
+
+ // duration follows add / subtract rules.
+ android::media::psh_utils::PowerStats ps3 = ps1 + ps2;
+ android::media::psh_utils::PowerStats ps4 = ps2 + ps1;
+ EXPECT_EQ(ps3, ps4);
+ EXPECT_EQ(15, ps3.metadata.duration_ms);
+
+ android::media::psh_utils::PowerStats ps5 = ps2 - ps1;
+ android::media::psh_utils::PowerStats ps6 = ps5 + ps1;
+ EXPECT_EQ(ps6, ps2);
+ EXPECT_EQ(5, ps5.metadata.duration_ms);
+}