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);
+}