1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.power.stats;
18 
19 import android.util.Slog;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 import com.android.internal.os.LongArrayMultiStateCounter;
23 import com.android.internal.util.Preconditions;
24 import com.android.modules.utils.TypedXmlPullParser;
25 import com.android.modules.utils.TypedXmlSerializer;
26 
27 import org.xmlpull.v1.XmlPullParser;
28 import org.xmlpull.v1.XmlPullParserException;
29 
30 import java.io.IOException;
31 import java.util.Arrays;
32 import java.util.function.Consumer;
33 
34 /**
35  * Maintains multidimensional multi-state stats.  States could be something like on-battery (0,1),
36  * screen-on (0,1), process state etc.  Dimensions refer to the metrics themselves, e.g.
37  * CPU residency, Network packet counts etc.  All metrics must be represented as <code>long</code>
38  * values;
39  */
40 public class MultiStateStats {
41     private static final String TAG = "MultiStateStats";
42 
43     private static final String XML_TAG_STATS = "stats";
44     public static final int STATE_DOES_NOT_EXIST = -1;
45 
46     /**
47      * A set of states, e.g. on-battery, screen-on, procstate.  The state values are integers
48      * from 0 to States.mLabels.length
49      */
50     public static class States {
51         final String mName;
52         final boolean mTracked;
53         final String[] mLabels;
54 
States(String name, boolean tracked, String... labels)55         public States(String name, boolean tracked, String... labels) {
56             mName = name;
57             mTracked = tracked;
58             mLabels = labels;
59         }
60 
isTracked()61         public boolean isTracked() {
62             return mTracked;
63         }
64 
getName()65         public String getName() {
66             return mName;
67         }
68 
getLabels()69         public String[] getLabels() {
70             return mLabels;
71         }
72 
73         /**
74          * Finds state by name in the provided array. If not found, returns STATE_DOES_NOT_EXIST.
75          */
findTrackedStateByName(MultiStateStats.States[] states, String name)76         public static int findTrackedStateByName(MultiStateStats.States[] states, String name) {
77             for (int i = 0; i < states.length; i++) {
78                 if (states[i].getName().equals(name)) {
79                     return i;
80                 }
81             }
82             return STATE_DOES_NOT_EXIST;
83         }
84 
85         /**
86          * Iterates over all combinations of tracked states and invokes <code>consumer</code>
87          * for each of them.
88          */
forEachTrackedStateCombination(States[] states, Consumer<int[]> consumer)89         public static void forEachTrackedStateCombination(States[] states,
90                 Consumer<int[]> consumer) {
91             forEachTrackedStateCombination(consumer, states, new int[states.length], 0);
92         }
93 
94         /**
95          * Recursive function that does a depth-first traversal of the multi-dimensional
96          * state space. Each time the traversal reaches the end of the <code>states</code> array,
97          * <code>statesValues</code> contains a unique combination of values for all tracked states.
98          * For untracked states, the corresponding values are left as 0.  The end result is
99          * that the <code>consumer</code> is invoked for every unique combination of tracked state
100          * values.  For example, it may be a sequence of calls like screen-on/power-on,
101          * screen-on/power-off, screen-off/power-on, screen-off/power-off.
102          */
forEachTrackedStateCombination(Consumer<int[]> consumer, States[] states, int[] statesValues, int stateIndex)103         private static void forEachTrackedStateCombination(Consumer<int[]> consumer,
104                 States[] states, int[] statesValues, int stateIndex) {
105             if (stateIndex < statesValues.length) {
106                 if (!states[stateIndex].mTracked) {
107                     forEachTrackedStateCombination(consumer, states, statesValues, stateIndex + 1);
108                     return;
109                 }
110                 for (int i = 0; i < states[stateIndex].mLabels.length; i++) {
111                     statesValues[stateIndex] = i;
112                     forEachTrackedStateCombination(consumer, states, statesValues, stateIndex + 1);
113                 }
114                 return;
115             }
116             consumer.accept(statesValues);
117         }
118     }
119 
120     /**
121      * Factory for MultiStateStats containers. All generated containers retain their connection
122      * to the Factory and the corresponding configuration.
123      */
124     public static class Factory {
125         private static final int INVALID_SERIAL_STATE = -1;
126         final int mDimensionCount;
127         final States[] mStates;
128         /*
129          * The LongArrayMultiStateCounter object that is used for accumulation of per-state
130          * stats thinks of "state" as a simple 0-based index. This Factory object's job is to
131          * map a combination of individual states (e.g. on-battery, process state etc) to
132          * such a simple index.
133          *
134          * This task is performed in two steps:
135          * 1) We define "composite state" as an integer that combines all constituent States
136          * into one integer as bit fields. This gives us a convenient mechanism for updating a
137          * single constituent State at a time.  We maintain an array of bit field masks
138          * corresponding to each constituent State.
139          *
140          * 2) We map composite states to "serial states", i.e. simple integer indexes, taking
141          * into account which constituent states are configured as tracked.  If a state is not
142          * tracked, there is no need to maintain separate counts for its values, thus
143          * all values of that constituent state can be mapped to the same serial state.
144          */
145         private final int[] mStateBitFieldMasks;
146         private final short[] mStateBitFieldShifts;
147         final int[] mCompositeToSerialState;
148         final int mSerialStateCount;
149 
Factory(int dimensionCount, States... states)150         public Factory(int dimensionCount, States... states) {
151             mDimensionCount = dimensionCount;
152             mStates = states;
153 
154             int serialStateCount = 1;
155             for (States state : mStates) {
156                 if (state.mTracked) {
157                     serialStateCount *= state.mLabels.length;
158                 }
159             }
160             mSerialStateCount = serialStateCount;
161 
162             mStateBitFieldMasks = new int[mStates.length];
163             mStateBitFieldShifts = new short[mStates.length];
164 
165             int shift = 0;
166             for (int i = 0; i < mStates.length; i++) {
167                 mStateBitFieldShifts[i] = (short) shift;
168                 if (mStates[i].mLabels.length < 2) {
169                     throw new IllegalArgumentException("Invalid state: " + Arrays.toString(
170                             mStates[i].mLabels) + ". Should have at least two values.");
171                 }
172                 int max = mStates[i].mLabels.length - 1;
173                 int bitcount = Integer.SIZE - Integer.numberOfLeadingZeros(max);
174                 mStateBitFieldMasks[i] = ((1 << bitcount) - 1) << shift;
175                 shift = shift + bitcount;
176             }
177 
178             if (shift >= Integer.SIZE - 1) {
179                 throw new IllegalArgumentException("Too many states: " + shift
180                         + " bits are required to represent the composite state, but only "
181                         + (Integer.SIZE - 1) + " are available");
182             }
183 
184             // Create a mask that filters out all non tracked states
185             int trackedMask = 0xFFFFFFFF;
186             for (int state = 0; state < mStates.length; state++) {
187                 if (!mStates[state].mTracked) {
188                     trackedMask &= ~mStateBitFieldMasks[state];
189                 }
190             }
191 
192             mCompositeToSerialState = new int[1 << shift];
193             Arrays.fill(mCompositeToSerialState, INVALID_SERIAL_STATE);
194 
195             int nextSerialState = 0;
196             for (int composite = 0; composite < mCompositeToSerialState.length; composite++) {
197                 if (!isValidCompositeState(composite)) continue;
198 
199                 // Values of an untracked State map to different composite states, but must map to
200                 // the same serial state. Achieve that by computing a "base composite", which
201                 // is equivalent to the current composite, but has 0 for all untracked States.
202                 // See if the base composite already has a serial state assigned.  If so, just use
203                 // the same one for the current composite.
204                 int baseComposite = composite & trackedMask;
205                 if (mCompositeToSerialState[baseComposite] != INVALID_SERIAL_STATE) {
206                     mCompositeToSerialState[composite] = mCompositeToSerialState[baseComposite];
207                 } else {
208                     mCompositeToSerialState[composite] = nextSerialState++;
209                 }
210             }
211         }
212 
isValidCompositeState(int composite)213         private boolean isValidCompositeState(int composite) {
214             for (int stateIndex = 0; stateIndex < mStates.length; stateIndex++) {
215                 int state = extractStateFromComposite(composite, stateIndex);
216                 if (state >= mStates[stateIndex].mLabels.length) {
217                     return false;
218                 }
219             }
220             return true;
221         }
222 
extractStateFromComposite(int compositeState, int stateIndex)223         private int extractStateFromComposite(int compositeState, int stateIndex) {
224             return (compositeState & mStateBitFieldMasks[stateIndex])
225                    >>> mStateBitFieldShifts[stateIndex];
226         }
227 
setStateInComposite(int baseCompositeState, int stateIndex, int value)228         int setStateInComposite(int baseCompositeState, int stateIndex, int value) {
229             return (baseCompositeState & ~mStateBitFieldMasks[stateIndex])
230                     | (value << mStateBitFieldShifts[stateIndex]);
231         }
232 
setStateInComposite(int compositeState, String stateName, String stateLabel)233         int setStateInComposite(int compositeState, String stateName, String stateLabel) {
234             for (int stateIndex = 0; stateIndex < mStates.length; stateIndex++) {
235                 States stateConfig = mStates[stateIndex];
236                 if (stateConfig.mName.equals(stateName)) {
237                     for (int state = 0; state < stateConfig.mLabels.length; state++) {
238                         if (stateConfig.mLabels[state].equals(stateLabel)) {
239                             return setStateInComposite(compositeState, stateIndex, state);
240                         }
241                     }
242                     Slog.e(TAG, "Unexpected label '" + stateLabel + "' for state: " + stateName);
243                     return -1;
244                 }
245             }
246             Slog.e(TAG, "Unsupported state: " + stateName);
247             return -1;
248         }
249 
250         /**
251          * Allocates a new stats container using this Factory's configuration.
252          */
create()253         public MultiStateStats create() {
254             return new MultiStateStats(this, mDimensionCount);
255         }
256 
257         /**
258          * Returns the total number of composite states handled by this container. For example,
259          * if there are two states: on-battery (0,1) and screen-on (0,1), both tracked, then the
260          * serial state count will be 2 * 2 = 4
261          */
262         @VisibleForTesting
getSerialStateCount()263         public int getSerialStateCount() {
264             return mSerialStateCount;
265         }
266 
267         /**
268          * Returns the integer index used by this container to represent the supplied composite
269          * state.
270          */
271         @VisibleForTesting
getSerialState(int[] states)272         public int getSerialState(int[] states) {
273             Preconditions.checkArgument(states.length == mStates.length);
274             int compositeState = 0;
275             for (int i = 0; i < states.length; i++) {
276                 compositeState = setStateInComposite(compositeState, i, states[i]);
277             }
278             int serialState = mCompositeToSerialState[compositeState];
279             if (serialState == INVALID_SERIAL_STATE) {
280                 throw new IllegalArgumentException("State values out of bounds: "
281                                                    + Arrays.toString(states));
282             }
283             return serialState;
284         }
285 
getSerialState(int compositeState)286         int getSerialState(int compositeState) {
287             return mCompositeToSerialState[compositeState];
288         }
289     }
290 
291     private final Factory mFactory;
292     private final LongArrayMultiStateCounter mCounter;
293     private int mCompositeState;
294     private boolean mTracking;
295 
MultiStateStats(Factory factory, int dimensionCount)296     public MultiStateStats(Factory factory, int dimensionCount) {
297         this.mFactory = factory;
298         mCounter = new LongArrayMultiStateCounter(factory.mSerialStateCount, dimensionCount);
299     }
300 
getDimensionCount()301     public int getDimensionCount() {
302         return mFactory.mDimensionCount;
303     }
304 
getStates()305     public States[] getStates() {
306         return mFactory.mStates;
307     }
308 
309     /**
310      * Copies time-in-state and timestamps from the supplied prototype. Does not
311      * copy accumulated counts.
312      */
copyStatesFrom(MultiStateStats otherStats)313     public void copyStatesFrom(MultiStateStats otherStats) {
314         mCounter.copyStatesFrom(otherStats.mCounter);
315     }
316 
317     /**
318      * Updates the current composite state by changing one of the States supplied to the Factory
319      * constructor.
320      *
321      * @param stateIndex  Corresponds to the index of the States supplied to the Factory constructor
322      * @param state       The new value of the state (e.g. 0 or 1 for "on-battery")
323      * @param timestampMs The time when the state change occurred
324      */
setState(int stateIndex, int state, long timestampMs)325     public void setState(int stateIndex, int state, long timestampMs) {
326         if (!mTracking) {
327             mCounter.updateValues(new long[mCounter.getArrayLength()], timestampMs);
328             mTracking = true;
329         }
330         mCompositeState = mFactory.setStateInComposite(mCompositeState, stateIndex, state);
331         mCounter.setState(mFactory.mCompositeToSerialState[mCompositeState], timestampMs);
332     }
333 
334     /**
335      * Adds the delta to the metrics.  The number of values must correspond to the dimension count
336      * supplied to the Factory constructor
337      */
increment(long[] values, long timestampMs)338     public void increment(long[] values, long timestampMs) {
339         mCounter.incrementValues(values, timestampMs);
340         mTracking = true;
341     }
342 
343     /**
344      * Returns accumulated stats for the specified composite state.
345      */
getStats(long[] outValues, int[] states)346     public void getStats(long[] outValues, int[] states) {
347         mCounter.getCounts(outValues, mFactory.getSerialState(states));
348     }
349 
350     /**
351      * Updates the stats values for the provided combination of states.
352      */
setStats(int[] states, long[] values)353     public void setStats(int[] states, long[] values) {
354         mCounter.setValues(mFactory.getSerialState(states), values);
355     }
356 
357     /**
358      * Resets the counters.
359      */
reset()360     public void reset() {
361         mCounter.reset();
362         mTracking = false;
363     }
364 
365     /**
366      * Stores contents in an XML doc.
367      */
writeXml(TypedXmlSerializer serializer)368     public void writeXml(TypedXmlSerializer serializer) throws IOException {
369         long[] tmpArray = new long[mCounter.getArrayLength()];
370 
371         try {
372             States.forEachTrackedStateCombination(mFactory.mStates,
373                     states -> {
374                         try {
375                             writeXmlForStates(serializer, states, tmpArray);
376                         } catch (IOException e) {
377                             throw new RuntimeException(e);
378                         }
379                     });
380         } catch (RuntimeException e) {
381             if (e.getCause() instanceof IOException) {
382                 throw (IOException) e.getCause();
383             } else {
384                 throw e;
385             }
386         }
387     }
388 
writeXmlForStates(TypedXmlSerializer serializer, int[] states, long[] values)389     private void writeXmlForStates(TypedXmlSerializer serializer, int[] states, long[] values)
390             throws IOException {
391         mCounter.getCounts(values, mFactory.getSerialState(states));
392         boolean nonZero = false;
393         for (long value : values) {
394             if (value != 0) {
395                 nonZero = true;
396                 break;
397             }
398         }
399         if (!nonZero) {
400             return;
401         }
402 
403         serializer.startTag(null, XML_TAG_STATS);
404 
405         for (int i = 0; i < states.length; i++) {
406             if (mFactory.mStates[i].mTracked && states[i] != 0) {
407                 serializer.attribute(null, mFactory.mStates[i].mName,
408                         mFactory.mStates[i].mLabels[states[i]]);
409             }
410         }
411         for (int i = 0; i < values.length; i++) {
412             if (values[i] != 0) {
413                 serializer.attributeLong(null, "_" + i, values[i]);
414             }
415         }
416         serializer.endTag(null, XML_TAG_STATS);
417     }
418 
419     /**
420      * Populates the object with contents in an XML doc. The parser is expected to be
421      * positioned on the opening tag of the corresponding element.
422      */
readFromXml(TypedXmlPullParser parser)423     public boolean readFromXml(TypedXmlPullParser parser) throws XmlPullParserException,
424             IOException {
425         String outerTag = parser.getName();
426         long[] tmpArray = new long[mCounter.getArrayLength()];
427         int eventType = parser.getEventType();
428         while (eventType != XmlPullParser.END_DOCUMENT
429                && !(eventType == XmlPullParser.END_TAG && parser.getName().equals(outerTag))) {
430             if (eventType == XmlPullParser.START_TAG) {
431                 if (parser.getName().equals(XML_TAG_STATS)) {
432                     Arrays.fill(tmpArray, 0);
433                     int compositeState = 0;
434                     int attributeCount = parser.getAttributeCount();
435                     for (int i = 0; i < attributeCount; i++) {
436                         String attributeName = parser.getAttributeName(i);
437                         if (attributeName.startsWith("_")) {
438                             int index;
439                             try {
440                                 index = Integer.parseInt(attributeName.substring(1));
441                             } catch (NumberFormatException e) {
442                                 throw new XmlPullParserException(
443                                         "Unexpected index syntax: " + attributeName, parser, e);
444                             }
445                             if (index < 0 || index >= tmpArray.length) {
446                                 Slog.e(TAG, "State index out of bounds: " + index
447                                             + " length: " + tmpArray.length);
448                                 return false;
449                             }
450                             tmpArray[index] = parser.getAttributeLong(i);
451                         } else {
452                             String attributeValue = parser.getAttributeValue(i);
453                             compositeState = mFactory.setStateInComposite(compositeState,
454                                     attributeName, attributeValue);
455                             if (compositeState == -1) {
456                                 return false;
457                             }
458                         }
459                     }
460                     mCounter.setValues(mFactory.getSerialState(compositeState), tmpArray);
461                 }
462             }
463             eventType = parser.next();
464         }
465         return true;
466     }
467 
468     @Override
toString()469     public String toString() {
470         StringBuilder sb = new StringBuilder();
471         long[] values = new long[mCounter.getArrayLength()];
472         States.forEachTrackedStateCombination(mFactory.mStates, states -> {
473             mCounter.getCounts(values, mFactory.getSerialState(states));
474             boolean nonZero = false;
475             for (long value : values) {
476                 if (value != 0) {
477                     nonZero = true;
478                     break;
479                 }
480             }
481             if (!nonZero) {
482                 return;
483             }
484 
485             if (!sb.isEmpty()) {
486                 sb.append("\n");
487             }
488 
489             sb.append("(");
490             boolean first = true;
491             for (int i = 0; i < states.length; i++) {
492                 if (mFactory.mStates[i].mTracked) {
493                     if (!first) {
494                         sb.append(" ");
495                     }
496                     first = false;
497                     sb.append(mFactory.mStates[i].mLabels[states[i]]);
498                 }
499             }
500             sb.append(") ");
501             sb.append(Arrays.toString(values));
502         });
503         return sb.toString();
504     }
505 }
506