psh_utils: Internally cache power stats

To avoid frequent (expensive) collection of power stats
allow the use of cached values with a certain time
tolerance.

Flag: com.android.media.audioserver.power_stats
Test: atest powerstats_collector_tests
Test: atest audio_powerstatscollector_benchmark
Bug: 350114693
Change-Id: I952950e5373e3c4b8ebc765bf6fb2cf8def0962e
diff --git a/media/psh_utils/PowerStatsCollector.cpp b/media/psh_utils/PowerStatsCollector.cpp
index 7bee3f8..6e02993 100644
--- a/media/psh_utils/PowerStatsCollector.cpp
+++ b/media/psh_utils/PowerStatsCollector.cpp
@@ -32,12 +32,43 @@
     return psc;
 }
 
-std::shared_ptr<PowerStats> PowerStatsCollector::getStats() const {
+std::shared_ptr<const PowerStats> PowerStatsCollector::getStats(int64_t toleranceNs) {
+    // Check if there is a cached PowerStats result available.
+    // As toleranceNs may be different between callers, it may be that some callers
+    // are blocked on mMutexExclusiveFill for a new stats result, while other callers
+    // may find the current cached result acceptable (within toleranceNs).
+    if (toleranceNs > 0) {
+        auto result = checkLastStats(toleranceNs);
+        if (result) return result;
+    }
+
+    // Take the mMutexExclusiveFill to ensure only one thread is filling.
+    std::lock_guard lg1(mMutexExclusiveFill);
+    // As obtaining a new PowerStats snapshot might take some time,
+    // check again to see if another waiting thread filled the cached result for us.
+    if (toleranceNs > 0) {
+        auto result = checkLastStats(toleranceNs);
+        if (result) return result;
+    }
     auto result = std::make_shared<PowerStats>();
     (void)fill(result.get());
+    std::lock_guard lg2(mMutex);
+    mLastFetchNs = systemTime(SYSTEM_TIME_BOOTTIME);
+    mLastFetchStats = result;
     return result;
 }
 
+std::shared_ptr<const PowerStats> PowerStatsCollector::checkLastStats(int64_t toleranceNs) const {
+    if (toleranceNs > 0) {
+        // see if we can return an old result.
+        std::lock_guard lg(mMutex);
+        if (mLastFetchStats && systemTime(SYSTEM_TIME_BOOTTIME) - mLastFetchNs < toleranceNs) {
+            return mLastFetchStats;
+        }
+    }
+    return {};
+}
+
 void PowerStatsCollector::addProvider(std::unique_ptr<PowerStatsProvider>&& powerStatsProvider) {
     mPowerStatsProviders.emplace_back(std::move(powerStatsProvider));
 }
diff --git a/media/psh_utils/benchmarks/Android.bp b/media/psh_utils/benchmarks/Android.bp
index 505ebba..20efaa9 100644
--- a/media/psh_utils/benchmarks/Android.bp
+++ b/media/psh_utils/benchmarks/Android.bp
@@ -28,3 +28,24 @@
         "libutils",
     ],
 }
+
+cc_benchmark {
+    name: "audio_powerstatscollector_benchmark",
+
+    srcs: ["audio_powerstatscollector_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_powerstatscollector_benchmark.cpp b/media/psh_utils/benchmarks/audio_powerstatscollector_benchmark.cpp
new file mode 100644
index 0000000..9e581bc
--- /dev/null
+++ b/media/psh_utils/benchmarks/audio_powerstatscollector_benchmark.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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_token_benchmark"
+#include <utils/Log.h>
+
+#include <psh_utils/PowerStatsCollector.h>
+
+#include <benchmark/benchmark.h>
+
+/*
+ Pixel 8 Pro
+------------------------------------------------------------------------------------------
+ Benchmark                            Time                      CPU             Iteration
+------------------------------------------------------------------------------------------
+audio_powerstatscollector_benchmark:
+  #BM_StatsToleranceMs/0      1.2005660120994434E8 ns            2532739.72 ns          100
+  #BM_StatsToleranceMs/50        1281.095987079007 ns     346.0322183913503 ns      2022168
+  #BM_StatsToleranceMs/100       459.9668862534226 ns    189.47902626735942 ns      2891307
+  #BM_StatsToleranceMs/200       233.8438662484292 ns    149.84041813854736 ns      4407343
+  #BM_StatsToleranceMs/500      184.42197142314103 ns    144.86896036787098 ns      7295167
+*/
+
+// We check how expensive it is to query stats depending
+// on the tolerance to reuse the cached values.
+// A tolerance of 0 means we always fetch stats.
+static void BM_StatsToleranceMs(benchmark::State& state) {
+    auto& collector = android::media::psh_utils::PowerStatsCollector::getCollector();
+    const int64_t toleranceNs = state.range(0) * 1'000'000;
+    while (state.KeepRunning()) {
+        collector.getStats(toleranceNs);
+        benchmark::ClobberMemory();
+    }
+}
+
+// Here we test various time tolerances (given in milliseconds here)
+BENCHMARK(BM_StatsToleranceMs)->Arg(0)->Arg(50)->Arg(100)->Arg(200)->Arg(500);
+
+BENCHMARK_MAIN();
diff --git a/media/psh_utils/include/psh_utils/PerformanceFixture.h b/media/psh_utils/include/psh_utils/PerformanceFixture.h
index 2d8b38a..092a508 100644
--- a/media/psh_utils/include/psh_utils/PerformanceFixture.h
+++ b/media/psh_utils/include/psh_utils/PerformanceFixture.h
@@ -59,7 +59,7 @@
         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();
+        auto& collector = android::media::psh_utils::PowerStatsCollector::getCollector();
         mStartStats = collector.getStats();
 
         const pid_t tid = gettid(); // us.
@@ -102,7 +102,7 @@
     unsigned mCores = 0;
     int mCore = 0;
     CoreClass mCoreClass = CORE_LITTLE;
-    std::shared_ptr<android::media::psh_utils::PowerStats> mStartStats;
+    std::shared_ptr<const android::media::psh_utils::PowerStats> mStartStats;
 };
 
 } // 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
index 437b93d..e3f8ea8 100644
--- a/media/psh_utils/include/psh_utils/PowerStatsCollector.h
+++ b/media/psh_utils/include/psh_utils/PowerStatsCollector.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include "PowerStats.h"
+#include <android-base/thread_annotations.h>
 #include <memory>
 #include <utils/Errors.h> // status_t
 
@@ -34,17 +35,25 @@
     // singleton getter
     static PowerStatsCollector& getCollector();
 
-    // get a snapshot of the state.
-    std::shared_ptr<PowerStats> getStats() const;
+    // Returns a snapshot of the state.
+    // If toleranceNs > 0, we permit the use of a stale snapshot taken within that tolerance.
+    std::shared_ptr<const PowerStats> getStats(int64_t toleranceNs = 0)
+            EXCLUDES(mMutex, mMutexExclusiveFill);
 
 private:
     PowerStatsCollector();  // use the singleton getter
 
+    // Returns non-empty PowerStats if we have a previous stats snapshot within toleranceNs.
+    std::shared_ptr<const PowerStats> checkLastStats(int64_t toleranceNs) const EXCLUDES(mMutex);
     int fill(PowerStats* stats) const;
     void addProvider(std::unique_ptr<PowerStatsProvider>&& powerStatsProvider);
 
+    mutable std::mutex mMutexExclusiveFill;
+    mutable std::mutex mMutex;
     // addProvider is called in the ctor, so effectively const.
     std::vector<std::unique_ptr<PowerStatsProvider>> mPowerStatsProviders;
+    int64_t mLastFetchNs GUARDED_BY(mMutex) = 0;
+    std::shared_ptr<const PowerStats> mLastFetchStats GUARDED_BY(mMutex);
 };
 
 } // namespace android::media::psh_utils
diff --git a/media/psh_utils/tests/powerstats_collector_tests.cpp b/media/psh_utils/tests/powerstats_collector_tests.cpp
index 5115d09..35c264a 100644
--- a/media/psh_utils/tests/powerstats_collector_tests.cpp
+++ b/media/psh_utils/tests/powerstats_collector_tests.cpp
@@ -27,7 +27,7 @@
 }
 
 TEST(powerstat_collector_tests, basic) {
-    const auto& psc = PowerStatsCollector::getCollector();
+    auto& psc = PowerStatsCollector::getCollector();
 
     // This test is used for debugging the string through logcat, we validate a non-empty string.
     auto powerStats = psc.getStats();