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