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