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