Add CpuPowerStatsCollector for reading power-related CPU stats from kernel

Bug: 285646152
Bug: 285042200
Test: atest FrameworksServicesTests:BatteryStatsTests
Change-Id: I1cd64b8b1c5e1ffd58bc475507393c21f13f32fd
diff --git a/core/java/com/android/internal/os/OWNERS b/core/java/com/android/internal/os/OWNERS
index 0b9773e..e35b7f1 100644
--- a/core/java/com/android/internal/os/OWNERS
+++ b/core/java/com/android/internal/os/OWNERS
@@ -11,6 +11,7 @@
 per-file *ChargeCalculator* = file:/BATTERY_STATS_OWNERS
 per-file *PowerCalculator* = file:/BATTERY_STATS_OWNERS
 per-file *PowerEstimator* = file:/BATTERY_STATS_OWNERS
+per-file *PowerStats* = file:/BATTERY_STATS_OWNERS
 per-file *Kernel* = file:/BATTERY_STATS_OWNERS
 per-file *MultiState* = file:/BATTERY_STATS_OWNERS
 per-file *PowerProfile* = file:/BATTERY_STATS_OWNERS
diff --git a/core/java/com/android/internal/os/PowerStats.java b/core/java/com/android/internal/os/PowerStats.java
new file mode 100644
index 0000000..1169552
--- /dev/null
+++ b/core/java/com/android/internal/os/PowerStats.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.internal.os;
+
+import android.os.BatteryConsumer;
+import android.util.IndentingPrintWriter;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+
+/**
+ * Container for power stats, acquired by various PowerStatsCollector classes. See subclasses for
+ * details.
+ */
+public final class PowerStats {
+    /**
+     * Power component (e.g. CPU, WIFI etc) that this snapshot relates to.
+     */
+    public @BatteryConsumer.PowerComponent int powerComponentId;
+
+    /**
+     * Duration, in milliseconds, covered by this snapshot.
+     */
+    public long durationMs;
+
+    /**
+     * Device-wide stats.
+     */
+    public long[] stats;
+
+    /**
+     * Per-UID CPU stats.
+     */
+    public final SparseArray<long[]> uidStats = new SparseArray<>();
+
+    /**
+     * Prints the contents of the stats snapshot.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        pw.print("PowerStats: ");
+        pw.println(BatteryConsumer.powerComponentIdToString(powerComponentId));
+        pw.increaseIndent();
+        pw.print("duration", durationMs).println();
+        for (int i = 0; i < uidStats.size(); i++) {
+            pw.print("UID ");
+            pw.print(uidStats.keyAt(i));
+            pw.print(": ");
+            pw.print(Arrays.toString(uidStats.valueAt(i)));
+            pw.println();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 72333fb..a5ee4b5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6559,11 +6559,6 @@
     <!-- Whether to show weather on the lock screen by default. -->
     <bool name="config_lockscreenWeatherEnabledByDefault">false</bool>
 
-    <!-- Whether to reset Battery Stats on unplug when the battery level is high. -->
-    <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool>
-    <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged -->
-    <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool>
-
     <!-- Whether we should persist the brightness value in nits for the default display even if
          the underlying display device changes. -->
     <bool name="config_persistBrightnessNitsForDefaultDisplay">false</bool>
diff --git a/core/res/res/values/config_battery_stats.xml b/core/res/res/values/config_battery_stats.xml
new file mode 100644
index 0000000..8fb48bc
--- /dev/null
+++ b/core/res/res/values/config_battery_stats.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2023 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.
+  -->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds.  Do not translate.
+
+     NOTE: The naming convention is "config_camelCaseValue". -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Whether to reset Battery Stats on unplug when the battery level is high. -->
+    <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool>
+    <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged -->
+    <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool>
+
+    <!-- CPU power stats collection throttle period in milliseconds.  Since power stats collection
+    is a relatively expensive operation, this throttle period may need to be adjusted for low-power
+    devices-->
+    <integer name="config_defaultPowerStatsThrottlePeriodCpu">60000</integer>
+</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 851c0c2..9f37a6e5 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5074,6 +5074,7 @@
 
   <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" />
   <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" />
+  <java-symbol type="integer" name="config_defaultPowerStatsThrottlePeriodCpu" />
 
   <java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/>
   <java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/>
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 04ebb2b..b0f30d6 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -156,6 +156,7 @@
 
     private final PowerProfile mPowerProfile;
     private final CpuScalingPolicies mCpuScalingPolicies;
+    private final BatteryStatsImpl.BatteryStatsConfig mBatteryStatsConfig;
     final BatteryStatsImpl mStats;
     final CpuWakeupStats mCpuWakeupStats;
     private final BatteryUsageStatsStore mBatteryUsageStatsStore;
@@ -374,22 +375,24 @@
         mPowerProfile = new PowerProfile(context);
         mCpuScalingPolicies = new CpuScalingPolicyReader().read();
 
-        mStats = new BatteryStatsImpl(systemDir, handler, this,
+        final boolean resetOnUnplugHighBatteryLevel = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel);
+        final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean(
+                com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge);
+        final long powerStatsThrottlePeriodCpu = context.getResources().getInteger(
+                com.android.internal.R.integer.config_defaultPowerStatsThrottlePeriodCpu);
+        mBatteryStatsConfig =
+                new BatteryStatsImpl.BatteryStatsConfig.Builder()
+                        .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
+                        .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
+                        .setPowerStatsThrottlePeriodCpu(powerStatsThrottlePeriodCpu)
+                        .build();
+        mStats = new BatteryStatsImpl(mBatteryStatsConfig, systemDir, handler, this,
                 this, mUserManagerUserInfoProvider, mPowerProfile, mCpuScalingPolicies);
         mWorker = new BatteryExternalStatsWorker(context, mStats);
         mStats.setExternalStatsSyncLocked(mWorker);
         mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
-
-        final boolean resetOnUnplugHighBatteryLevel = context.getResources().getBoolean(
-                com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel);
-        final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean(
-                com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge);
-        mStats.setBatteryStatsConfig(
-                new BatteryStatsImpl.BatteryStatsConfig.Builder()
-                        .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
-                        .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
-                        .build());
         mStats.startTrackingSystemServerCpuTime();
 
         if (BATTERY_USAGE_STORE_ENABLED) {
@@ -2591,6 +2594,7 @@
         pw.println("     --proto: output as a binary protobuffer");
         pw.println("     --model power-profile: use the power profile model"
                 + " even if measured energy is available");
+        pw.println("  --sample: collect and dump a sample of stats for debugging purpose");
         pw.println("  <package.name>: optional name of package to filter output by.");
         pw.println("  -h: print this help text.");
         pw.println("Battery stats (batterystats) commands:");
@@ -2623,6 +2627,10 @@
         }
     }
 
+    private void dumpStatsSample(PrintWriter pw) {
+        mStats.dumpStatsSample(pw);
+    }
+
     private void dumpMeasuredEnergyStats(PrintWriter pw) {
         // Wait for the completion of pending works if there is any
         awaitCompletion();
@@ -2864,6 +2872,9 @@
                     mCpuWakeupStats.dump(new IndentingPrintWriter(pw, "  "),
                             SystemClock.elapsedRealtime());
                     return;
+                } else if ("--sample".equals(arg)) {
+                    dumpStatsSample(pw);
+                    return;
                 } else if ("-a".equals(arg)) {
                     flags |= BatteryStats.DUMP_VERBOSE;
                 } else if (arg.length() > 0 && arg.charAt(0) == '-'){
@@ -2924,6 +2935,7 @@
                                 in.unmarshall(raw, 0, raw.length);
                                 in.setDataPosition(0);
                                 BatteryStatsImpl checkinStats = new BatteryStatsImpl(
+                                        mBatteryStatsConfig,
                                         null, mStats.mHandler, null, null,
                                         mUserManagerUserInfoProvider, mPowerProfile,
                                         mCpuScalingPolicies);
@@ -2965,6 +2977,7 @@
                                 in.unmarshall(raw, 0, raw.length);
                                 in.setDataPosition(0);
                                 BatteryStatsImpl checkinStats = new BatteryStatsImpl(
+                                        mBatteryStatsConfig,
                                         null, mStats.mHandler, null, null,
                                         mUserManagerUserInfoProvider, mPowerProfile,
                                         mCpuScalingPolicies);
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index cf4e845..5b10afa 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -281,6 +281,7 @@
     private final LongSparseArray<SamplingTimer> mKernelMemoryStats = new LongSparseArray<>();
     private int[] mCpuPowerBracketMap;
     private final CpuUsageDetails mCpuUsageDetails = new CpuUsageDetails();
+    private final CpuPowerStatsCollector mCpuPowerStatsCollector;
 
     public LongSparseArray<SamplingTimer> getKernelMemoryStats() {
         return mKernelMemoryStats;
@@ -439,6 +440,7 @@
         static final int RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG = 1 << 1;
 
         private final int mFlags;
+        private final long mPowerStatsThrottlePeriodCpu;
 
         private BatteryStatsConfig(Builder builder) {
             int flags = 0;
@@ -449,6 +451,7 @@
                 flags |= RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
             }
             mFlags = flags;
+            mPowerStatsThrottlePeriodCpu = builder.mPowerStatsThrottlePeriodCpu;
         }
 
         /**
@@ -469,15 +472,22 @@
                     == RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
         }
 
+        long getPowerStatsThrottlePeriodCpu() {
+            return mPowerStatsThrottlePeriodCpu;
+        }
+
         /**
          * Builder for BatteryStatsConfig
          */
         public static class Builder {
             private boolean mResetOnUnplugHighBatteryLevel;
             private boolean mResetOnUnplugAfterSignificantCharge;
+            private long mPowerStatsThrottlePeriodCpu;
+
             public Builder() {
                 mResetOnUnplugHighBatteryLevel = true;
                 mResetOnUnplugAfterSignificantCharge = true;
+                mPowerStatsThrottlePeriodCpu = 60000;
             }
 
             /**
@@ -504,8 +514,16 @@
                 mResetOnUnplugAfterSignificantCharge = reset;
                 return this;
             }
-        }
 
+            /**
+             * Sets the minimum amount of time (in millis) to wait between passes
+             * of CPU power stats collection.
+             */
+            public Builder setPowerStatsThrottlePeriodCpu(long periodMs) {
+                mPowerStatsThrottlePeriodCpu = periodMs;
+                return this;
+            }
+        }
     }
 
     private final PlatformIdleStateCallback mPlatformIdleStateCallback;
@@ -1556,7 +1574,7 @@
 
     @VisibleForTesting
     @GuardedBy("this")
-    protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
+    protected BatteryStatsConfig mBatteryStatsConfig;
 
     @GuardedBy("this")
     private AlarmManager mAlarmManager = null;
@@ -1733,6 +1751,7 @@
 
     public BatteryStatsImpl(Clock clock, File historyDirectory) {
         init(clock);
+        mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
         mHandler = null;
         mConstants = new Constants(mHandler);
         mStartClockTimeMs = clock.currentTimeMillis();
@@ -1751,6 +1770,7 @@
         mPlatformIdleStateCallback = null;
         mEnergyConsumerRetriever = null;
         mUserInfoProvider = null;
+        mCpuPowerStatsCollector = null;
     }
 
     private void init(Clock clock) {
@@ -10911,21 +10931,23 @@
         return mTmpCpuTimeInFreq;
     }
 
-    public BatteryStatsImpl(@Nullable File systemDir, @NonNull Handler handler,
-            @Nullable PlatformIdleStateCallback cb, @Nullable EnergyStatsRetriever energyStatsCb,
-            @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile,
-            @NonNull CpuScalingPolicies cpuScalingPolicies) {
-        this(Clock.SYSTEM_CLOCK, systemDir, handler, cb, energyStatsCb, userInfoProvider,
-                powerProfile, cpuScalingPolicies);
-    }
-
-    private BatteryStatsImpl(@NonNull Clock clock, @Nullable File systemDir,
+    public BatteryStatsImpl(@NonNull BatteryStatsConfig config, @Nullable File systemDir,
             @NonNull Handler handler, @Nullable PlatformIdleStateCallback cb,
             @Nullable EnergyStatsRetriever energyStatsCb,
             @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile,
             @NonNull CpuScalingPolicies cpuScalingPolicies) {
+        this(config, Clock.SYSTEM_CLOCK, systemDir, handler, cb, energyStatsCb, userInfoProvider,
+                powerProfile, cpuScalingPolicies);
+    }
+
+    private BatteryStatsImpl(@NonNull BatteryStatsConfig config, @NonNull Clock clock,
+            @Nullable File systemDir, @NonNull Handler handler,
+            @Nullable PlatformIdleStateCallback cb, @Nullable EnergyStatsRetriever energyStatsCb,
+            @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile,
+            @NonNull CpuScalingPolicies cpuScalingPolicies) {
         init(clock);
 
+        mBatteryStatsConfig = config;
         mHandler = new MyHandler(handler.getLooper());
         mConstants = new Constants(mHandler);
 
@@ -10947,6 +10969,10 @@
             mHistory = new BatteryStatsHistory(systemDir, mConstants.MAX_HISTORY_FILES,
                     mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock);
         }
+
+        mCpuPowerStatsCollector = new CpuPowerStatsCollector(mCpuScalingPolicies, mPowerProfile,
+                mHandler, mBatteryStatsConfig.getPowerStatsThrottlePeriodCpu());
+
         mStartCount++;
         initTimersAndCounters();
         mOnBattery = mOnBatteryInternal = false;
@@ -11095,15 +11121,6 @@
     }
 
     /**
-     * Injects BatteryStatsConfig
-     */
-    public void setBatteryStatsConfig(BatteryStatsConfig config) {
-        synchronized (this) {
-            mBatteryStatsConfig = config;
-        }
-    }
-
-    /**
      * Starts tracking CPU time-in-state for threads of the system server process,
      * keeping a separate account of threads receiving incoming binder calls.
      */
@@ -14197,6 +14214,9 @@
         if (mCpuUidFreqTimeReader != null) {
             mCpuUidFreqTimeReader.onSystemReady();
         }
+        if (mCpuPowerStatsCollector != null) {
+            mCpuPowerStatsCollector.onSystemReady();
+        }
         mSystemReady = true;
     }
 
@@ -15705,6 +15725,13 @@
         iPw.decreaseIndent();
     }
 
+    /**
+     * Grabs one sample of PowerStats and prints it.
+     */
+    public void dumpStatsSample(PrintWriter pw) {
+        mCpuPowerStatsCollector.collectAndDump(pw);
+    }
+
     private final Runnable mWriteAsyncRunnable = () -> {
         synchronized (BatteryStatsImpl.this) {
             writeSyncLocked();
diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
new file mode 100644
index 0000000..1401746
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.server.power.stats;
+
+import android.os.Handler;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.Keep;
+import com.android.internal.annotations.VisibleForNative;
+import com.android.internal.os.Clock;
+import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.PowerProfile;
+import com.android.internal.os.PowerStats;
+import com.android.server.power.optimization.Flags;
+
+/**
+ * Collects snapshots of power-related system statistics.
+ * <p>
+ * The class is intended to be used in a serialized fashion using the handler supplied in the
+ * constructor. Thus the object is not thread-safe except where noted.
+ */
+public class CpuPowerStatsCollector extends PowerStatsCollector {
+    private static final long NANOS_PER_MILLIS = 1000000;
+
+    private final KernelCpuStatsReader mKernelCpuStatsReader;
+    private final int[] mScalingStepToPowerBracketMap;
+    private final long[] mTempUidStats;
+    private final SparseArray<UidStats> mUidStats = new SparseArray<>();
+    private final int mUidStatsSize;
+    // Reusable instance
+    private final PowerStats mCpuPowerStats = new PowerStats();
+    private long mLastUpdateTimestampNanos;
+
+    public CpuPowerStatsCollector(CpuScalingPolicies cpuScalingPolicies, PowerProfile powerProfile,
+                                  Handler handler, long throttlePeriodMs) {
+        this(cpuScalingPolicies, powerProfile, handler, new KernelCpuStatsReader(),
+                throttlePeriodMs, Clock.SYSTEM_CLOCK);
+    }
+
+    public CpuPowerStatsCollector(CpuScalingPolicies cpuScalingPolicies, PowerProfile powerProfile,
+                                  Handler handler, KernelCpuStatsReader kernelCpuStatsReader,
+                                  long throttlePeriodMs, Clock clock) {
+        super(handler, throttlePeriodMs, clock);
+        mKernelCpuStatsReader = kernelCpuStatsReader;
+
+        int scalingStepCount = cpuScalingPolicies.getScalingStepCount();
+        mScalingStepToPowerBracketMap = new int[scalingStepCount];
+        int index = 0;
+        for (int policy : cpuScalingPolicies.getPolicies()) {
+            int[] frequencies = cpuScalingPolicies.getFrequencies(policy);
+            for (int step = 0; step < frequencies.length; step++) {
+                int bracket = powerProfile.getCpuPowerBracketForScalingStep(policy, step);
+                mScalingStepToPowerBracketMap[index++] = bracket;
+            }
+        }
+        mUidStatsSize = powerProfile.getCpuPowerBracketCount();
+        mTempUidStats = new long[mUidStatsSize];
+    }
+
+    /**
+     * Initializes the collector during the boot sequence.
+     */
+    public void onSystemReady() {
+        setEnabled(Flags.streamlinedBatteryStats());
+    }
+
+    @Override
+    protected PowerStats collectStats() {
+        mCpuPowerStats.uidStats.clear();
+        long newTimestampNanos = mKernelCpuStatsReader.nativeReadCpuStats(
+                this::processUidStats, mScalingStepToPowerBracketMap, mLastUpdateTimestampNanos,
+                mTempUidStats);
+        mCpuPowerStats.durationMs =
+                (newTimestampNanos - mLastUpdateTimestampNanos) / NANOS_PER_MILLIS;
+        mLastUpdateTimestampNanos = newTimestampNanos;
+        return mCpuPowerStats;
+    }
+
+    @VisibleForNative
+    interface KernelCpuStatsCallback {
+        @Keep // Called from native
+        void processUidStats(int uid, long[] stats);
+    }
+
+    private void processUidStats(int uid, long[] stats) {
+        UidStats uidStats = mUidStats.get(uid);
+        if (uidStats == null) {
+            uidStats = new UidStats();
+            uidStats.stats = new long[mUidStatsSize];
+            uidStats.delta = new long[mUidStatsSize];
+            mUidStats.put(uid, uidStats);
+        }
+
+        boolean nonzero = false;
+        for (int i = mUidStatsSize - 1; i >= 0; i--) {
+            long delta = uidStats.delta[i] = stats[i] - uidStats.stats[i];
+            if (delta != 0) {
+                nonzero = true;
+            }
+            uidStats.stats[i] = stats[i];
+        }
+        if (nonzero) {
+            mCpuPowerStats.uidStats.put(uid, uidStats.delta);
+        }
+    }
+
+    /**
+     * Native class that retrieves CPU stats from the kernel.
+     */
+    public static class KernelCpuStatsReader {
+        protected native long nativeReadCpuStats(KernelCpuStatsCallback callback,
+                int[] scalingStepToPowerBracketMap, long lastUpdateTimestampNanos,
+                long[] tempForUidStats);
+    }
+
+    private static class UidStats {
+        public long[] stats;
+        public long[] delta;
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsCollector.java b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
new file mode 100644
index 0000000..b49c89f
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/PowerStatsCollector.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.server.power.stats;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.util.FastImmutableArraySet;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.Clock;
+import com.android.internal.os.PowerStats;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+/**
+ * Collects snapshots of power-related system statistics.
+ * <p>
+ * Instances of this class are intended to be used in a serialized fashion using
+ * the handler supplied in the constructor. Thus these objects are not thread-safe
+ * except where noted.
+ */
+public abstract class PowerStatsCollector {
+    private final Handler mHandler;
+    private final Clock mClock;
+    private final long mThrottlePeriodMs;
+    private final Runnable mCollectAndDeliverStats = this::collectAndDeliverStats;
+    private boolean mEnabled;
+    private long mLastScheduledUpdateMs = -1;
+
+    @GuardedBy("this")
+    @SuppressWarnings("unchecked")
+    private volatile FastImmutableArraySet<Consumer<PowerStats>> mConsumerList =
+            new FastImmutableArraySet<Consumer<PowerStats>>(new Consumer[0]);
+
+    public PowerStatsCollector(Handler handler, long throttlePeriodMs, Clock clock) {
+        mHandler = handler;
+        mThrottlePeriodMs = throttlePeriodMs;
+        mClock = clock;
+    }
+
+    /**
+     * Adds a consumer that will receive a callback every time a snapshot of stats is collected.
+     * The method is thread safe.
+     */
+    @SuppressWarnings("unchecked")
+    public void addConsumer(Consumer<PowerStats> consumer) {
+        synchronized (this) {
+            mConsumerList = new FastImmutableArraySet<Consumer<PowerStats>>(
+                    Stream.concat(mConsumerList.stream(), Stream.of(consumer))
+                            .toArray(Consumer[]::new));
+        }
+    }
+
+    /**
+     * Removes a consumer.
+     * The method is thread safe.
+     */
+    @SuppressWarnings("unchecked")
+    public void removeConsumer(Consumer<PowerStats> consumer) {
+        synchronized (this) {
+            mConsumerList = new FastImmutableArraySet<Consumer<PowerStats>>(
+                    mConsumerList.stream().filter(c -> c != consumer)
+                            .toArray(Consumer[]::new));
+        }
+    }
+
+    /**
+     * Should be called at most once, before the first invocation of {@link #schedule} or
+     * {@link #forceSchedule}
+     */
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    /**
+     * Returns true if the collector is enabled.
+     */
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    @SuppressWarnings("GuardedBy")  // Field is volatile
+    private void collectAndDeliverStats() {
+        PowerStats stats = collectStats();
+        for (Consumer<PowerStats> consumer : mConsumerList) {
+            consumer.accept(stats);
+        }
+    }
+
+    /**
+     * Schedules a stats snapshot collection, throttled in accordance with the
+     * {@link #mThrottlePeriodMs} parameter.
+     */
+    public boolean schedule() {
+        if (!mEnabled) {
+            return false;
+        }
+
+        long uptimeMillis = mClock.uptimeMillis();
+        if (uptimeMillis - mLastScheduledUpdateMs < mThrottlePeriodMs
+                && mLastScheduledUpdateMs >= 0) {
+            return false;
+        }
+        mLastScheduledUpdateMs = uptimeMillis;
+        mHandler.post(mCollectAndDeliverStats);
+        return true;
+    }
+
+    /**
+     * Schedules an immediate snapshot collection, foregoing throttling.
+     */
+    public boolean forceSchedule() {
+        if (!mEnabled) {
+            return false;
+        }
+
+        mHandler.removeCallbacks(mCollectAndDeliverStats);
+        mHandler.postAtFrontOfQueue(mCollectAndDeliverStats);
+        return true;
+    }
+
+    protected abstract PowerStats collectStats();
+
+    /**
+     * Collects a fresh stats snapshot and prints it to the supplied printer.
+     */
+    public void collectAndDump(PrintWriter pw) {
+        if (Thread.currentThread() == mHandler.getLooper().getThread()) {
+            throw new RuntimeException(
+                    "Calling this method from the handler thread would cause a deadlock");
+        }
+
+        IndentingPrintWriter out = new IndentingPrintWriter(pw);
+        out.print(getClass().getSimpleName());
+        if (!isEnabled()) {
+            out.println(": disabled");
+            return;
+        }
+        out.println();
+
+        ArrayList<PowerStats> collected = new ArrayList<>();
+        Consumer<PowerStats> consumer = collected::add;
+        addConsumer(consumer);
+
+        try {
+            if (forceSchedule()) {
+                awaitCompletion();
+            }
+        } finally {
+            removeConsumer(consumer);
+        }
+
+        out.increaseIndent();
+        for (PowerStats stats : collected) {
+            stats.dump(out);
+        }
+        out.decreaseIndent();
+    }
+
+    private void awaitCompletion() {
+        ConditionVariable done = new ConditionVariable();
+        mHandler.post(done::open);
+        done.block();
+    }
+}
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index 405b133..2f150a1 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -50,6 +50,7 @@
         "com_android_server_locksettings_SyntheticPasswordManager.cpp",
         "com_android_server_power_PowerManagerService.cpp",
         "com_android_server_powerstats_PowerStatsService.cpp",
+        "com_android_server_power_stats_CpuPowerStatsCollector.cpp",
         "com_android_server_hint_HintManagerService.cpp",
         "com_android_server_SerialService.cpp",
         "com_android_server_soundtrigger_middleware_AudioSessionProviderImpl.cpp",
@@ -144,6 +145,7 @@
         "libsensorservicehidl",
         "libsensorserviceaidl",
         "libgui",
+        "libtimeinstate",
         "libtimestats_atoms_proto",
         "libusbhost",
         "libtinyalsa",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index d9acf41..7e8ce60 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -23,6 +23,7 @@
 per-file com_android_server_pm_* = file:/services/core/java/com/android/server/pm/OWNERS
 per-file com_android_server_power_* = file:/services/core/java/com/android/server/power/OWNERS
 per-file com_android_server_powerstats_* = file:/services/core/java/com/android/server/powerstats/OWNERS
+per-file com_android_server_power_stats_* = file:/BATTERY_STATS_OWNERS
 per-file com_android_server_security_* = file:/core/java/android/security/OWNERS
 per-file com_android_server_tv_* = file:/media/java/android/media/tv/OWNERS
 per-file com_android_server_vibrator_* = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp b/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp
new file mode 100644
index 0000000..a6084ea
--- /dev/null
+++ b/services/core/jni/com_android_server_power_stats_CpuPowerStatsCollector.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 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 "CpuPowerStatsCollector"
+
+#include <cputimeinstate.h>
+#include <log/log.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+
+#include "core_jni_helpers.h"
+
+#define EXCEPTION (-1)
+
+namespace android {
+
+#define JAVA_CLASS_CPU_POWER_STATS_COLLECTOR "com/android/server/power/stats/CpuPowerStatsCollector"
+#define JAVA_CLASS_KERNEL_CPU_STATS_READER \
+    JAVA_CLASS_CPU_POWER_STATS_COLLECTOR "$KernelCpuStatsReader"
+#define JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK \
+    JAVA_CLASS_CPU_POWER_STATS_COLLECTOR "$KernelCpuStatsCallback"
+
+static constexpr uint64_t NSEC_PER_MSEC = 1000000;
+
+static int extractUidStats(JNIEnv *env, std::vector<std::vector<uint64_t>> &times,
+                           ScopedIntArrayRO &scopedScalingStepToPowerBracketMap,
+                           jlongArray tempForUidStats);
+
+static bool initialized = false;
+static jclass class_KernelCpuStatsCallback;
+static jmethodID method_KernelCpuStatsCallback_processUidStats;
+
+static int init(JNIEnv *env) {
+    jclass temp = env->FindClass(JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK);
+    class_KernelCpuStatsCallback = (jclass)env->NewGlobalRef(temp);
+    if (!class_KernelCpuStatsCallback) {
+        jniThrowExceptionFmt(env, "java/lang/ClassNotFoundException",
+                             "Class not found: " JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK);
+        return EXCEPTION;
+    }
+    method_KernelCpuStatsCallback_processUidStats =
+            env->GetMethodID(class_KernelCpuStatsCallback, "processUidStats", "(I[J)V");
+    if (!method_KernelCpuStatsCallback_processUidStats) {
+        jniThrowExceptionFmt(env, "java/lang/NoSuchMethodException",
+                             "Method not found: " JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK
+                             ".processUidStats");
+        return EXCEPTION;
+    }
+    initialized = true;
+    return OK;
+}
+
+static jlong nativeReadCpuStats(JNIEnv *env, [[maybe_unused]] jobject zis, jobject callback,
+                                jintArray scalingStepToPowerBracketMap,
+                                jlong lastUpdateTimestampNanos, jlongArray tempForUidStats) {
+    if (!initialized) {
+        if (init(env) == EXCEPTION) {
+            return 0L;
+        }
+    }
+
+    uint64_t newLastUpdate = lastUpdateTimestampNanos;
+    auto data = android::bpf::getUidsUpdatedCpuFreqTimes(&newLastUpdate);
+    if (!data.has_value()) return lastUpdateTimestampNanos;
+
+    ScopedIntArrayRO scopedScalingStepToPowerBracketMap(env, scalingStepToPowerBracketMap);
+
+    for (auto &[uid, times] : *data) {
+        int status =
+                extractUidStats(env, times, scopedScalingStepToPowerBracketMap, tempForUidStats);
+        if (status == EXCEPTION) {
+            return 0L;
+        }
+        env->CallVoidMethod(callback, method_KernelCpuStatsCallback_processUidStats, (jint)uid,
+                            tempForUidStats);
+    }
+    return newLastUpdate;
+}
+
+static int extractUidStats(JNIEnv *env, std::vector<std::vector<uint64_t>> &times,
+                           ScopedIntArrayRO &scopedScalingStepToPowerBracketMap,
+                           jlongArray tempForUidStats) {
+    ScopedLongArrayRW scopedTempForStats(env, tempForUidStats);
+    uint64_t *arrayForStats = reinterpret_cast<uint64_t *>(scopedTempForStats.get());
+    const uint8_t statsSize = scopedTempForStats.size();
+    memset(arrayForStats, 0, statsSize * sizeof(uint64_t));
+    const uint8_t scalingStepCount = scopedScalingStepToPowerBracketMap.size();
+
+    uint32_t scalingStep = 0;
+    for (const auto &subVec : times) {
+        for (uint32_t i = 0; i < subVec.size(); ++i) {
+            if (scalingStep >= scalingStepCount) {
+                jniThrowExceptionFmt(env, "java/lang/IndexOutOfBoundsException",
+                                     "scalingStepToPowerBracketMap is too short, "
+                                     "size=%u, scalingStep=%u",
+                                     scalingStepCount, scalingStep);
+                return EXCEPTION;
+            }
+            uint32_t bucket = scopedScalingStepToPowerBracketMap[scalingStep];
+            if (bucket >= statsSize) {
+                jniThrowExceptionFmt(env, "java/lang/IndexOutOfBoundsException",
+                                     "UidStats array is too short, length=%u, bucket[%u]=%u",
+                                     statsSize, scalingStep, bucket);
+                return EXCEPTION;
+            }
+            arrayForStats[bucket] += subVec[i] / NSEC_PER_MSEC;
+            scalingStep++;
+        }
+    }
+    return OK;
+}
+
+static const JNINativeMethod method_table[] = {
+        {"nativeReadCpuStats", "(L" JAVA_CLASS_KERNEL_CPU_STATS_CALLBACK ";[IJ[J)J",
+         (void *)nativeReadCpuStats},
+};
+
+int register_android_server_power_stats_CpuPowerStatsCollector(JNIEnv *env) {
+    return jniRegisterNativeMethods(env, JAVA_CLASS_KERNEL_CPU_STATS_READER, method_table,
+                                    NELEM(method_table));
+}
+
+} // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 290ad8d..97d7be6 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -30,6 +30,7 @@
 int register_android_server_LightsService(JNIEnv* env);
 int register_android_server_PowerManagerService(JNIEnv* env);
 int register_android_server_PowerStatsService(JNIEnv* env);
+int register_android_server_power_stats_CpuPowerStatsCollector(JNIEnv* env);
 int register_android_server_HintManagerService(JNIEnv* env);
 int register_android_server_storage_AppFuse(JNIEnv* env);
 int register_android_server_SerialService(JNIEnv* env);
@@ -85,6 +86,7 @@
     register_android_server_broadcastradio_Tuner(vm, env);
     register_android_server_PowerManagerService(env);
     register_android_server_PowerStatsService(env);
+    register_android_server_power_stats_CpuPowerStatsCollector(env);
     register_android_server_HintManagerService(env);
     register_android_server_SerialService(env);
     register_android_server_InputManager(env);
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index 05acd9b..5ea1929 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -23,6 +23,7 @@
         "androidx.test.uiautomator_uiautomator",
         "mockito-target-minus-junit4",
         "servicestests-utils",
+        "flag-junit",
     ],
 
     libs: [
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java
new file mode 100644
index 0000000..f2ee6db
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.server.power.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.when;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.PowerProfile;
+import com.android.internal.os.PowerStats;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CpuPowerStatsCollectorTest {
+    private final MockClock mMockClock = new MockClock();
+    private final HandlerThread mHandlerThread = new HandlerThread("test");
+    private Handler mHandler;
+    private CpuPowerStatsCollector mCollector;
+    private PowerStats mCollectedStats;
+    @Mock
+    private PowerProfile mPowerProfile;
+    @Mock
+    private CpuPowerStatsCollector.KernelCpuStatsReader mMockKernelCpuStatsReader;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread.start();
+        mHandler = mHandlerThread.getThreadHandler();
+        when(mPowerProfile.getCpuPowerBracketCount()).thenReturn(2);
+        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 0)).thenReturn(0);
+        when(mPowerProfile.getCpuPowerBracketForScalingStep(0, 1)).thenReturn(1);
+        mCollector = new CpuPowerStatsCollector(new CpuScalingPolicies(
+                new SparseArray<>() {{
+                    put(0, new int[]{0});
+                }},
+                new SparseArray<>() {{
+                    put(0, new int[]{1, 12});
+                }}),
+                mPowerProfile, mHandler, mMockKernelCpuStatsReader, 60_000, mMockClock);
+        mCollector.addConsumer(stats -> mCollectedStats = stats);
+        mCollector.setEnabled(true);
+    }
+
+    @Test
+    public void collectStats() {
+        mockKernelCpuStats(new SparseArray<>() {{
+                put(42, new long[]{100, 200});
+                put(99, new long[]{300, 600});
+            }}, 0, 1234);
+
+        mMockClock.uptime = 1000;
+        mCollector.forceSchedule();
+        waitForIdle();
+
+        assertThat(mCollectedStats.durationMs).isEqualTo(1234);
+        assertThat(mCollectedStats.uidStats.get(42)).isEqualTo(new long[]{100, 200});
+        assertThat(mCollectedStats.uidStats.get(99)).isEqualTo(new long[]{300, 600});
+
+        mockKernelCpuStats(new SparseArray<>() {{
+                put(42, new long[]{123, 234});
+                put(99, new long[]{345, 678});
+            }}, 1234, 3421);
+
+        mMockClock.uptime = 2000;
+        mCollector.forceSchedule();
+        waitForIdle();
+
+        assertThat(mCollectedStats.durationMs).isEqualTo(3421 - 1234);
+        assertThat(mCollectedStats.uidStats.get(42)).isEqualTo(new long[]{23, 34});
+        assertThat(mCollectedStats.uidStats.get(99)).isEqualTo(new long[]{45, 78});
+    }
+
+    private void mockKernelCpuStats(SparseArray<long[]> uidToCpuStats,
+            long expectedLastUpdateTimestampMs, long newLastUpdateTimestampMs) {
+        when(mMockKernelCpuStatsReader.nativeReadCpuStats(
+                any(CpuPowerStatsCollector.KernelCpuStatsCallback.class),
+                any(int[].class), anyLong(), any(long[].class)))
+                .thenAnswer(invocation -> {
+                    CpuPowerStatsCollector.KernelCpuStatsCallback callback =
+                            invocation.getArgument(0);
+                    int[] powerBucketIndexes = invocation.getArgument(1);
+                    long lastTimestamp = invocation.getArgument(2);
+                    long[] tempStats = invocation.getArgument(3);
+
+                    assertThat(powerBucketIndexes).isEqualTo(new int[]{0, 1});
+                    assertThat(lastTimestamp / 1000000L).isEqualTo(expectedLastUpdateTimestampMs);
+                    assertThat(tempStats).hasLength(2);
+
+                    for (int i = 0; i < uidToCpuStats.size(); i++) {
+                        int uid = uidToCpuStats.keyAt(i);
+                        long[] cpuStats = uidToCpuStats.valueAt(i);
+                        System.arraycopy(cpuStats, 0, tempStats, 0, tempStats.length);
+                        callback.processUidStats(uid, tempStats);
+                    }
+                    return newLastUpdateTimestampMs * 1000000L; // Nanoseconds
+                });
+    }
+
+    private void waitForIdle() {
+        ConditionVariable done = new ConditionVariable();
+        mHandler.post(done::open);
+        done.block();
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
new file mode 100644
index 0000000..38a5d19
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.server.power.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.DeviceConfig;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiDevice;
+
+import com.android.frameworks.coretests.aidl.ICmdCallback;
+import com.android.frameworks.coretests.aidl.ICmdReceiver;
+import com.android.server.power.optimization.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class CpuPowerStatsCollectorValidationTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final int WORK_DURATION_MS = 2000;
+    private static final String TEST_PKG = "com.android.coretests.apps.bstatstestapp";
+    private static final String TEST_ACTIVITY = TEST_PKG + ".TestActivity";
+    private static final String EXTRA_KEY_CMD_RECEIVER = "cmd_receiver";
+    private static final int START_ACTIVITY_TIMEOUT_MS = 2000;
+
+    private Context mContext;
+    private UiDevice mUiDevice;
+    private DeviceConfig.Properties mBackupFlags;
+    private int mTestPkgUid;
+
+    @Before
+    public void setup() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mTestPkgUid = mContext.getPackageManager().getPackageUid(TEST_PKG, 0);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_STREAMLINED_BATTERY_STATS)
+    public void totalTimeInPowerBrackets() throws Exception {
+        dumpCpuStats();     // For the side effect of capturing the baseline.
+
+        doSomeWork();
+
+        long duration = 0;
+        long[] stats = null;
+
+        String[] cpuStatsDump = dumpCpuStats();
+        Pattern durationPattern = Pattern.compile("duration=([0-9]*)");
+        Pattern uidPattern = Pattern.compile("UID " + mTestPkgUid + ": \\[([0-9,\\s]*)]");
+        for (String line : cpuStatsDump) {
+            Matcher durationMatcher = durationPattern.matcher(line);
+            if (durationMatcher.find()) {
+                duration = Long.parseLong(durationMatcher.group(1));
+            }
+            Matcher uidMatcher = uidPattern.matcher(line);
+            if (uidMatcher.find()) {
+                String[] strings = uidMatcher.group(1).split(", ");
+                stats = new long[strings.length];
+                for (int i = 0; i < strings.length; i++) {
+                    stats[i] = Long.parseLong(strings[i]);
+                }
+            }
+        }
+        if (stats == null) {
+            fail("No CPU stats for " + mTestPkgUid + " (" + TEST_PKG + ")");
+        }
+
+        assertThat(duration).isAtLeast(WORK_DURATION_MS);
+
+        long total = Arrays.stream(stats).sum();
+        assertThat(total).isAtLeast((long) (WORK_DURATION_MS * 0.8));
+    }
+
+    private String[] dumpCpuStats() throws Exception {
+        String dump = executeCmdSilent("dumpsys batterystats --sample");
+        String[] lines = dump.split("\n");
+        for (int i = 0; i < lines.length; i++) {
+            if (lines[i].startsWith("CpuPowerStatsCollector")) {
+                return Arrays.copyOfRange(lines, i + 1, lines.length);
+            }
+        }
+        return new String[0];
+    }
+
+    private void doSomeWork() throws Exception {
+        final ICmdReceiver receiver;
+        receiver = ICmdReceiver.Stub.asInterface(startActivity());
+        try {
+            receiver.doSomeWork(WORK_DURATION_MS);
+        } finally {
+            receiver.finishHost();
+        }
+    }
+
+    private IBinder startActivity() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final Intent launchIntent = new Intent().setComponent(
+                new ComponentName(TEST_PKG, TEST_ACTIVITY));
+        final Bundle extras = new Bundle();
+        final IBinder[] binders = new IBinder[1];
+        extras.putBinder(EXTRA_KEY_CMD_RECEIVER, new ICmdCallback.Stub() {
+            @Override
+            public void onLaunched(IBinder receiver) {
+                binders[0] = receiver;
+                latch.countDown();
+            }
+        });
+        launchIntent.putExtras(extras).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(launchIntent);
+        if (latch.await(START_ACTIVITY_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            if (binders[0] == null) {
+                fail("Receiver binder should not be null");
+            }
+            return binders[0];
+        } else {
+            fail("Timed out waiting for the test activity to start; testUid=" + mTestPkgUid);
+        }
+        return null;
+    }
+
+    private String executeCmdSilent(String cmd) throws Exception {
+        return mUiDevice.executeShellCommand(cmd).trim();
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 6d3f1f2..4150972a 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -119,6 +119,13 @@
         return MOBILE_RADIO_POWER_STATE_UPDATE_FREQ_MS;
     }
 
+    public MockBatteryStatsImpl setBatteryStatsConfig(BatteryStatsConfig config) {
+        synchronized (this) {
+            mBatteryStatsConfig = config;
+        }
+        return this;
+    }
+
     public MockBatteryStatsImpl setNetworkStats(NetworkStats networkStats) {
         mNetworkStats = networkStats;
         return this;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java
new file mode 100644
index 0000000..08c8213
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsCollectorTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+package com.android.server.power.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.PowerStats;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PowerStatsCollectorTest {
+    private final MockClock mMockClock = new MockClock();
+    private final HandlerThread mHandlerThread = new HandlerThread("test");
+    private Handler mHandler;
+    private PowerStatsCollector mCollector;
+    private PowerStats mCollectedStats;
+
+    @Before
+    public void setup() {
+        mHandlerThread.start();
+        mHandler = mHandlerThread.getThreadHandler();
+        mCollector = new PowerStatsCollector(mHandler,
+                60000,
+                mMockClock) {
+            @Override
+            protected PowerStats collectStats() {
+                return new PowerStats();
+            }
+        };
+        mCollector.addConsumer(stats -> mCollectedStats = stats);
+        mCollector.setEnabled(true);
+    }
+
+    @Test
+    public void throttlePeriod() {
+        mMockClock.uptime = 1000;
+        mCollector.schedule();
+        waitForIdle();
+
+        assertThat(mCollectedStats).isNotNull();
+
+        mMockClock.uptime += 1000;
+        mCollectedStats = null;
+        mCollector.schedule();      // Should be throttled
+        waitForIdle();
+
+        assertThat(mCollectedStats).isNull();
+
+        // Should be allowed to run
+        mMockClock.uptime += 100_000;
+        mCollector.schedule();
+        waitForIdle();
+
+        assertThat(mCollectedStats).isNotNull();
+    }
+
+    private void waitForIdle() {
+        ConditionVariable done = new ConditionVariable();
+        mHandler.post(done::open);
+        done.block();
+    }
+}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 92ff7ab..20d8a5d 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -68,6 +68,7 @@
         "ActivityContext",
         "coretests-aidl",
         "securebox",
+        "flag-junit",
     ],
 
     libs: [